master
go 433 lines 13.5 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package azure_monitor
4
5 import (
6 "errors"
7 "fmt"
8 "strings"
9 "time"
10
11 "github.com/netdata/netdata/go/plugins/pkg/confopt"
12 "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/cloudauth"
13 )
14
15 const (
16 defaultUpdateEvery = 60
17 defaultAutoDetectRetry = 0
18 defaultCloud = cloudPublic
19 defaultDiscoveryEvery = 300
20 defaultDiscoveryMode = discoveryModeFilters
21 defaultProfilesMode = profilesModeAuto
22 defaultQueryOffset = 180
23 defaultTimeout = confopt.Duration(30 * time.Second)
24 defaultMaxConcurrency = 4
25 defaultMaxBatchResource = 50
26 defaultMaxMetricsQuery = 20
27 )
28
29 const (
30 discoveryModeFilters = "filters"
31 discoveryModeQuery = "query"
32 )
33
34 const (
35 profilesModeAuto = "auto"
36 profilesModeExact = "exact"
37 profilesModeCombined = "combined"
38 )
39
40 const (
41 cloudPublic = "public"
42 cloudGovernment = "government"
43 cloudChina = "china"
44 )
45
46 type Config struct {
47 Vnode string `yaml:"vnode,omitempty" json:"vnode,omitempty"`
48 VirtualNodes *VirtualNodesConfig `yaml:"virtual_nodes,omitempty" json:"virtual_nodes,omitempty"`
49 UpdateEvery int `yaml:"update_every,omitempty" json:"update_every,omitempty"`
50 AutoDetectionRetry int `yaml:"autodetection_retry,omitempty" json:"autodetection_retry,omitempty"`
51 SubscriptionIDs []string `yaml:"subscription_ids" json:"subscription_ids"`
52 Cloud string `yaml:"cloud,omitempty" json:"cloud"`
53 Discovery DiscoveryConfig `yaml:"discovery" json:"discovery"`
54 Profiles ProfilesConfig `yaml:"profiles" json:"profiles"`
55 QueryOffset int `yaml:"query_offset,omitempty" json:"query_offset"`
56 Timeout confopt.Duration `yaml:"timeout,omitempty" json:"timeout"`
57 Limits LimitsConfig `yaml:"limits" json:"limits"`
58 Auth cloudauth.AzureADAuthConfig `yaml:"auth" json:"auth"`
59 }
60
61 type VirtualNodesConfig struct {
62 ByResourceTag string `yaml:"by_resource_tag,omitempty" json:"by_resource_tag,omitempty"`
63 }
64
65 type DiscoveryConfig struct {
66 RefreshEvery int `yaml:"refresh_every,omitempty" json:"refresh_every"`
67 Mode string `yaml:"mode,omitempty" json:"mode"`
68 ModeFilters *ResourceFiltersConfig `yaml:"mode_filters,omitempty" json:"mode_filters,omitempty"`
69 ModeQuery *DiscoveryQueryConfig `yaml:"mode_query,omitempty" json:"mode_query,omitempty"`
70 }
71
72 type ResourceFiltersConfig struct {
73 ResourceGroups []string `yaml:"resource_groups,omitempty" json:"resource_groups,omitempty"`
74 Regions []string `yaml:"regions,omitempty" json:"regions,omitempty"`
75 Tags map[string][]string `yaml:"tags,omitempty" json:"tags,omitempty"`
76 }
77
78 type DiscoveryQueryConfig struct {
79 KQL string `yaml:"kql" json:"kql"`
80 }
81
82 type ProfileEntryConfig struct {
83 Name string `yaml:"name" json:"name"`
84 Filters *ResourceFiltersConfig `yaml:"filters,omitempty" json:"filters,omitempty"`
85 }
86
87 type ProfilesModeConfig struct {
88 Entries []ProfileEntryConfig `yaml:"entries,omitempty" json:"entries,omitempty"`
89 }
90
91 type ProfilesConfig struct {
92 Mode string `yaml:"mode,omitempty" json:"mode"`
93 ModeAuto *ProfilesModeConfig `yaml:"mode_auto,omitempty" json:"mode_auto,omitempty"`
94 ModeExact *ProfilesModeConfig `yaml:"mode_exact,omitempty" json:"mode_exact,omitempty"`
95 ModeCombined *ProfilesModeConfig `yaml:"mode_combined,omitempty" json:"mode_combined,omitempty"`
96 }
97
98 type LimitsConfig struct {
99 MaxConcurrency int `yaml:"max_concurrency,omitempty" json:"max_concurrency"`
100 MaxBatchResources int `yaml:"max_batch_resources,omitempty" json:"max_batch_resources"`
101 MaxMetricsPerQuery int `yaml:"max_metrics_per_query,omitempty" json:"max_metrics_per_query"`
102 }
103
104 func (c *Config) applyDefaults() {
105 if c.UpdateEvery <= 0 {
106 c.UpdateEvery = defaultUpdateEvery
107 }
108 if c.AutoDetectionRetry < 0 {
109 c.AutoDetectionRetry = defaultAutoDetectRetry
110 }
111 if strings.TrimSpace(c.Cloud) == "" {
112 c.Cloud = defaultCloud
113 }
114 if c.Discovery.RefreshEvery < 0 {
115 c.Discovery.RefreshEvery = defaultDiscoveryEvery
116 }
117 if strings.TrimSpace(c.Discovery.Mode) == "" {
118 c.Discovery.Mode = defaultDiscoveryMode
119 }
120 if strings.TrimSpace(c.Profiles.Mode) == "" {
121 c.Profiles.Mode = defaultProfilesMode
122 }
123 if c.QueryOffset <= 0 {
124 c.QueryOffset = defaultQueryOffset
125 }
126 if c.Timeout.Duration() == 0 {
127 c.Timeout = defaultTimeout
128 }
129 if c.Limits.MaxConcurrency <= 0 {
130 c.Limits.MaxConcurrency = defaultMaxConcurrency
131 }
132 if c.Limits.MaxBatchResources <= 0 {
133 c.Limits.MaxBatchResources = defaultMaxBatchResource
134 }
135 if c.Limits.MaxMetricsPerQuery <= 0 {
136 c.Limits.MaxMetricsPerQuery = defaultMaxMetricsQuery
137 }
138 if c.VirtualNodes != nil {
139 tagKey := stringsLowerTrim(c.VirtualNodes.ByResourceTag)
140 if tagKey == "" {
141 c.VirtualNodes = nil
142 } else {
143 c.VirtualNodes = &VirtualNodesConfig{ByResourceTag: tagKey}
144 }
145 }
146 }
147
148 func (c Config) validate() error {
149 var errs []error
150
151 if len(c.SubscriptionIDs) == 0 {
152 errs = append(errs, errors.New("'subscription_ids' must contain at least one value"))
153 } else {
154 for i, v := range c.SubscriptionIDs {
155 if strings.TrimSpace(v) == "" {
156 errs = append(errs, fmt.Errorf("'subscription_ids[%d]' must not be empty", i))
157 }
158 }
159 }
160 if c.UpdateEvery < 60 {
161 errs = append(errs, errors.New("'update_every' must be >= 60 seconds"))
162 }
163 if c.Discovery.RefreshEvery < 0 || (c.Discovery.RefreshEvery > 0 && c.Discovery.RefreshEvery < 60) {
164 errs = append(errs, errors.New("'discovery.refresh_every' must be 0 or >= 60 seconds"))
165 }
166 if c.QueryOffset < 60 {
167 errs = append(errs, errors.New("'query_offset' must be >= 60 seconds"))
168 }
169 if c.Timeout.Duration() < 0 {
170 errs = append(errs, errors.New("'timeout' cannot be negative"))
171 }
172 if c.Limits.MaxConcurrency < 1 || c.Limits.MaxConcurrency > 64 {
173 errs = append(errs, errors.New("'limits.max_concurrency' must be between 1 and 64"))
174 }
175 if c.Limits.MaxBatchResources < 1 || c.Limits.MaxBatchResources > 50 {
176 errs = append(errs, errors.New("'limits.max_batch_resources' must be between 1 and 50"))
177 }
178 if c.Limits.MaxMetricsPerQuery < 1 || c.Limits.MaxMetricsPerQuery > 20 {
179 errs = append(errs, errors.New("'limits.max_metrics_per_query' must be between 1 and 20"))
180 }
181
182 switch stringsLowerTrim(c.Cloud) {
183 case cloudPublic, cloudGovernment, cloudChina:
184 default:
185 errs = append(errs, fmt.Errorf("'cloud' must be one of: %s, %s, %s", cloudPublic, cloudGovernment, cloudChina))
186 }
187
188 if err := c.Auth.ValidateWithPath("auth"); err != nil {
189 errs = append(errs, err)
190 }
191
192 validateProfileTags := false
193 switch stringsLowerTrim(c.Discovery.Mode) {
194 case discoveryModeFilters:
195 validateProfileTags = true
196 errs = append(errs, validateResourceFilters("discovery.mode_filters", c.Discovery.ModeFilters, true)...)
197 case discoveryModeQuery:
198 if c.Discovery.ModeQuery == nil || strings.TrimSpace(c.Discovery.ModeQuery.KQL) == "" {
199 errs = append(errs, errors.New("'discovery.mode_query.kql' must not be empty when discovery.mode is 'query'"))
200 }
201 default:
202 errs = append(errs, fmt.Errorf("'discovery.mode' must be one of: %s, %s", discoveryModeFilters, discoveryModeQuery))
203 }
204
205 switch stringsLowerTrim(c.Profiles.Mode) {
206 case profilesModeAuto:
207 errs = append(errs, validateProfileEntries("profiles.mode_auto.entries", modeEntries(c.Profiles.ModeAuto), validateProfileTags)...)
208 case profilesModeExact:
209 entries := modeEntries(c.Profiles.ModeExact)
210 if len(entries) == 0 {
211 errs = append(errs, fmt.Errorf("'profiles.mode_exact.entries' must not be empty when profiles.mode is '%s'", c.Profiles.Mode))
212 } else {
213 errs = append(errs, validateProfileEntries("profiles.mode_exact.entries", entries, validateProfileTags)...)
214 }
215 case profilesModeCombined:
216 entries := modeEntries(c.Profiles.ModeCombined)
217 if len(entries) == 0 {
218 errs = append(errs, fmt.Errorf("'profiles.mode_combined.entries' must not be empty when profiles.mode is '%s'", c.Profiles.Mode))
219 } else {
220 errs = append(errs, validateProfileEntries("profiles.mode_combined.entries", entries, validateProfileTags)...)
221 }
222 default:
223 errs = append(errs, fmt.Errorf("'profiles.mode' must be one of: %s, %s, %s",
224 profilesModeAuto, profilesModeExact, profilesModeCombined))
225 }
226
227 return errors.Join(errs...)
228 }
229
230 func validateProfileEntries(path string, entries []ProfileEntryConfig, validateTags bool) []error {
231 if len(entries) == 0 {
232 return nil
233 }
234
235 var errs []error
236 seen := map[string]struct{}{}
237 for i, entry := range entries {
238 entryPath := fmt.Sprintf("%s[%d]", path, i)
239 name := stringsTrim(entry.Name)
240 if !isValidProfileName(name) {
241 errs = append(errs, fmt.Errorf("'%s.name' must match %q", entryPath, profileNamePattern))
242 } else {
243 if _, ok := seen[name]; ok {
244 errs = append(errs, fmt.Errorf("'%s' contains duplicate entry name '%s'", path, name))
245 }
246 seen[name] = struct{}{}
247 }
248 errs = append(errs, validateResourceFilters(entryPath+".filters", entry.Filters, validateTags)...)
249 }
250 return errs
251 }
252
253 func validateResourceFilters(path string, filters *ResourceFiltersConfig, validateTags bool) []error {
254 if filters == nil {
255 return nil
256 }
257
258 var errs []error
259 for i, v := range filters.ResourceGroups {
260 if stringsTrim(v) == "" {
261 errs = append(errs, fmt.Errorf("'%s.resource_groups[%d]' must not be empty", path, i))
262 }
263 }
264 for i, v := range filters.Regions {
265 if stringsTrim(v) == "" {
266 errs = append(errs, fmt.Errorf("'%s.regions[%d]' must not be empty", path, i))
267 }
268 }
269 if !validateTags && len(filters.Tags) > 0 {
270 return errs
271 }
272 for key, values := range filters.Tags {
273 if stringsTrim(key) == "" {
274 errs = append(errs, fmt.Errorf("'%s.tags' contains an empty key", path))
275 continue
276 }
277 if len(values) == 0 {
278 errs = append(errs, fmt.Errorf("'%s.tags.%s' must contain at least one value", path, key))
279 continue
280 }
281 for i, v := range values {
282 if stringsTrim(v) == "" {
283 errs = append(errs, fmt.Errorf("'%s.tags.%s[%d]' must not be empty", path, key, i))
284 }
285 }
286 }
287 return errs
288 }
289
290 func sanitizeIgnoredProfileTagFilters(cfg Config) (Config, []string) {
291 if stringsLowerTrim(cfg.Discovery.Mode) != discoveryModeQuery {
292 return cfg, nil
293 }
294
295 switch stringsLowerTrim(cfg.Profiles.Mode) {
296 case profilesModeAuto:
297 cfg.Profiles.ModeAuto = cloneProfilesModeConfig(cfg.Profiles.ModeAuto)
298 warnings := stripIgnoredProfileTagFilters("profiles.mode_auto.entries", cfg.Profiles.ModeAuto)
299 return cfg, warnings
300 case profilesModeExact:
301 cfg.Profiles.ModeExact = cloneProfilesModeConfig(cfg.Profiles.ModeExact)
302 warnings := stripIgnoredProfileTagFilters("profiles.mode_exact.entries", cfg.Profiles.ModeExact)
303 return cfg, warnings
304 case profilesModeCombined:
305 cfg.Profiles.ModeCombined = cloneProfilesModeConfig(cfg.Profiles.ModeCombined)
306 warnings := stripIgnoredProfileTagFilters("profiles.mode_combined.entries", cfg.Profiles.ModeCombined)
307 return cfg, warnings
308 default:
309 return cfg, nil
310 }
311 }
312
313 func cloneProfilesModeConfig(src *ProfilesModeConfig) *ProfilesModeConfig {
314 if src == nil {
315 return nil
316 }
317
318 out := &ProfilesModeConfig{
319 Entries: make([]ProfileEntryConfig, len(src.Entries)),
320 }
321 for i, entry := range src.Entries {
322 out.Entries[i] = ProfileEntryConfig{
323 Name: entry.Name,
324 Filters: cloneResourceFilters(entry.Filters),
325 }
326 }
327 return out
328 }
329
330 func stripIgnoredProfileTagFilters(path string, cfg *ProfilesModeConfig) []string {
331 if cfg == nil {
332 return nil
333 }
334
335 var warnings []string
336 for i := range cfg.Entries {
337 filters := cfg.Entries[i].Filters
338 if filters == nil || len(filters.Tags) == 0 {
339 continue
340 }
341
342 warnings = append(warnings, fmt.Sprintf("%s[%d].filters.tags", path, i))
343 filters.Tags = nil
344 if len(filters.ResourceGroups) == 0 && len(filters.Regions) == 0 {
345 cfg.Entries[i].Filters = nil
346 }
347 }
348 return warnings
349 }
350
351 func modeEntries(cfg *ProfilesModeConfig) []ProfileEntryConfig {
352 if cfg == nil {
353 return nil
354 }
355 return cfg.Entries
356 }
357
358 func entryNames(entries []ProfileEntryConfig) []string {
359 if len(entries) == 0 {
360 return nil
361 }
362
363 names := make([]string, 0, len(entries))
364 for _, entry := range entries {
365 if name := stringsTrim(entry.Name); name != "" {
366 names = append(names, name)
367 }
368 }
369 return names
370 }
371
372 func entryMap(entries []ProfileEntryConfig) map[string]ProfileEntryConfig {
373 if len(entries) == 0 {
374 return nil
375 }
376
377 out := make(map[string]ProfileEntryConfig, len(entries))
378 for _, entry := range entries {
379 name := stringsTrim(entry.Name)
380 if name == "" {
381 continue
382 }
383 out[name] = ProfileEntryConfig{
384 Name: name,
385 Filters: cloneResourceFilters(entry.Filters),
386 }
387 }
388 return out
389 }
390
391 func cloneResourceFilters(src *ResourceFiltersConfig) *ResourceFiltersConfig {
392 if src == nil {
393 return nil
394 }
395
396 dst := &ResourceFiltersConfig{
397 ResourceGroups: append([]string(nil), src.ResourceGroups...),
398 Regions: append([]string(nil), src.Regions...),
399 }
400 if len(src.Tags) > 0 {
401 dst.Tags = make(map[string][]string, len(src.Tags))
402 for key, values := range src.Tags {
403 dst.Tags[key] = append([]string(nil), values...)
404 }
405 }
406 return dst
407 }
408
409 func (c Config) primarySubscriptionID() string {
410 for _, id := range c.SubscriptionIDs {
411 if v := stringsTrim(id); v != "" {
412 return v
413 }
414 }
415 return ""
416 }
417
418 func (c Config) subscriptionIDs() []string {
419 out := make([]string, 0, len(c.SubscriptionIDs))
420 for _, id := range c.SubscriptionIDs {
421 if v := stringsTrim(id); v != "" {
422 out = append(out, v)
423 }
424 }
425 return out
426 }
427
428 func (c Config) workloadResourceTagKey() string {
429 if c.VirtualNodes == nil {
430 return ""
431 }
432 return stringsLowerTrim(c.VirtualNodes.ByResourceTag)
433 }