Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
aos
GitHub Repository: aos/grafana-agent
Path: blob/main/pkg/config/agentmanagement_test.go
4094 views
1
package config
2
3
import (
4
"crypto/sha256"
5
"encoding/hex"
6
"errors"
7
"flag"
8
"testing"
9
"time"
10
11
"github.com/grafana/agent/pkg/config/features"
12
"github.com/grafana/agent/pkg/server"
13
"github.com/grafana/agent/pkg/util"
14
"github.com/prometheus/common/config"
15
"github.com/stretchr/testify/assert"
16
"github.com/stretchr/testify/require"
17
"gopkg.in/yaml.v2"
18
)
19
20
// testRemoteConfigProvider is an implementation of remoteConfigProvider that can be
21
// used for testing. It allows setting the values to return for both fetching the
22
// remote config bytes & errors as well as the cached config & errors.
23
type testRemoteConfigProvider struct {
24
InitialConfig *AgentManagementConfig
25
26
fetchedConfigBytesToReturn []byte
27
fetchedConfigErrorToReturn error
28
fetchRemoteConfigCallCount int
29
30
cachedConfigToReturn []byte
31
cachedConfigErrorToReturn error
32
getCachedConfigCallCount int
33
didCacheRemoteConfig bool
34
}
35
36
func (t *testRemoteConfigProvider) GetCachedRemoteConfig() ([]byte, error) {
37
t.getCachedConfigCallCount += 1
38
return t.cachedConfigToReturn, t.cachedConfigErrorToReturn
39
}
40
41
func (t *testRemoteConfigProvider) FetchRemoteConfig() ([]byte, error) {
42
t.fetchRemoteConfigCallCount += 1
43
return t.fetchedConfigBytesToReturn, t.fetchedConfigErrorToReturn
44
}
45
46
func (t *testRemoteConfigProvider) CacheRemoteConfig(r []byte) error {
47
t.didCacheRemoteConfig = true
48
return nil
49
}
50
51
var validAgentManagementConfig = AgentManagementConfig{
52
Enabled: true,
53
Host: "localhost:1234",
54
BasicAuth: config.BasicAuth{
55
Username: "test",
56
PasswordFile: "/test/path",
57
},
58
Protocol: "https",
59
PollingInterval: time.Minute,
60
RemoteConfiguration: RemoteConfiguration{
61
Labels: labelMap{"b": "B", "a": "A"},
62
Namespace: "test_namespace",
63
CacheLocation: "/test/path/",
64
},
65
}
66
67
var cachedConfig = []byte(`{"base_config":"","snippets":[]}`)
68
69
func TestValidateValidConfig(t *testing.T) {
70
assert.NoError(t, validAgentManagementConfig.Validate())
71
}
72
73
func TestValidateInvalidBasicAuth(t *testing.T) {
74
invalidConfig := &AgentManagementConfig{
75
Enabled: true,
76
Host: "localhost:1234",
77
BasicAuth: config.BasicAuth{},
78
Protocol: "https",
79
PollingInterval: time.Minute,
80
RemoteConfiguration: RemoteConfiguration{
81
Namespace: "test_namespace",
82
CacheLocation: "/test/path/",
83
},
84
}
85
assert.Error(t, invalidConfig.Validate())
86
87
invalidConfig.BasicAuth.Username = "test"
88
assert.Error(t, invalidConfig.Validate()) // Should still error as there is no password file set
89
90
invalidConfig.BasicAuth.Username = ""
91
invalidConfig.BasicAuth.PasswordFile = "/test/path"
92
assert.Error(t, invalidConfig.Validate()) // Should still error as there is no username set
93
}
94
95
func TestMissingCacheLocation(t *testing.T) {
96
invalidConfig := &AgentManagementConfig{
97
Enabled: true,
98
Host: "localhost:1234",
99
BasicAuth: config.BasicAuth{
100
Username: "test",
101
PasswordFile: "/test/path",
102
},
103
Protocol: "https",
104
PollingInterval: 1 * time.Minute,
105
RemoteConfiguration: RemoteConfiguration{
106
Namespace: "test_namespace",
107
},
108
}
109
assert.Error(t, invalidConfig.Validate())
110
}
111
112
func TestSleepTime(t *testing.T) {
113
cfg := `
114
api_url: "http://localhost"
115
basic_auth:
116
username: "initial_user"
117
protocol: "http"
118
polling_interval: "1m"
119
remote_configuration:
120
namespace: "new_namespace"
121
cache_location: "/etc"`
122
123
var am AgentManagementConfig
124
yaml.Unmarshal([]byte(cfg), &am)
125
assert.Equal(t, time.Minute, am.SleepTime())
126
}
127
128
func TestFuzzJitterTime(t *testing.T) {
129
am := validAgentManagementConfig
130
pollingInterval := 2 * time.Minute
131
am.PollingInterval = pollingInterval
132
133
zero := time.Duration(0)
134
135
for i := 0; i < 10_000; i++ {
136
j := am.JitterTime()
137
assert.GreaterOrEqual(t, j, zero)
138
assert.Less(t, j, pollingInterval)
139
}
140
}
141
142
func TestFullUrl(t *testing.T) {
143
c := validAgentManagementConfig
144
actual, err := c.fullUrl()
145
assert.NoError(t, err)
146
assert.Equal(t, "https://localhost:1234/agent-management/api/agent/v2/namespace/test_namespace/remote_config?a=A&b=B", actual)
147
}
148
149
func TestRemoteConfigHashCheck(t *testing.T) {
150
// not a truly valid Agent Management config, but used for testing against
151
// precomputed sha256 hash
152
ic := AgentManagementConfig{
153
Protocol: "http",
154
}
155
marshalled, err := yaml.Marshal(ic)
156
require.NoError(t, err)
157
icHashBytes := sha256.Sum256(marshalled)
158
icHash := hex.EncodeToString(icHashBytes[:])
159
160
rcCache := remoteConfigCache{
161
InitialConfigHash: icHash,
162
Config: "server:\\n log_level: debug",
163
}
164
165
require.NoError(t, initialConfigHashCheck(ic, rcCache))
166
rcCache.InitialConfigHash = "abc"
167
require.Error(t, initialConfigHashCheck(ic, rcCache))
168
169
differentIc := validAgentManagementConfig
170
require.Error(t, initialConfigHashCheck(differentIc, rcCache))
171
}
172
173
func TestNewRemoteConfigProvider_ValidInitialConfig(t *testing.T) {
174
invalidAgentManagementConfig := &AgentManagementConfig{
175
Enabled: true,
176
Host: "localhost:1234",
177
BasicAuth: config.BasicAuth{
178
Username: "test",
179
PasswordFile: "/test/path",
180
},
181
Protocol: "https",
182
PollingInterval: time.Minute,
183
RemoteConfiguration: RemoteConfiguration{
184
Labels: labelMap{"b": "B", "a": "A"},
185
Namespace: "test_namespace",
186
CacheLocation: "/test/path/",
187
},
188
}
189
190
cfg := Config{
191
AgentManagement: *invalidAgentManagementConfig,
192
}
193
_, err := newRemoteConfigProvider(&cfg)
194
assert.NoError(t, err)
195
}
196
197
func TestNewRemoteConfigProvider_InvalidProtocol(t *testing.T) {
198
invalidAgentManagementConfig := &AgentManagementConfig{
199
Enabled: true,
200
Host: "localhost:1234",
201
BasicAuth: config.BasicAuth{
202
Username: "test",
203
PasswordFile: "/test/path",
204
},
205
Protocol: "ws",
206
PollingInterval: time.Minute,
207
RemoteConfiguration: RemoteConfiguration{
208
Labels: labelMap{"b": "B", "a": "A"},
209
Namespace: "test_namespace",
210
CacheLocation: "/test/path/",
211
},
212
}
213
214
cfg := Config{
215
AgentManagement: *invalidAgentManagementConfig,
216
}
217
_, err := newRemoteConfigProvider(&cfg)
218
assert.Error(t, err)
219
}
220
221
func TestNewRemoteConfigHTTPProvider_InvalidInitialConfig(t *testing.T) {
222
// this is invalid because it is missing the password file
223
invalidAgentManagementConfig := &AgentManagementConfig{
224
Enabled: true,
225
Host: "localhost:1234",
226
BasicAuth: config.BasicAuth{
227
Username: "test",
228
},
229
Protocol: "https",
230
PollingInterval: time.Minute,
231
RemoteConfiguration: RemoteConfiguration{
232
Labels: labelMap{"b": "B", "a": "A"},
233
Namespace: "test_namespace",
234
CacheLocation: "/test/path/",
235
},
236
}
237
238
cfg := Config{
239
AgentManagement: *invalidAgentManagementConfig,
240
}
241
_, err := newRemoteConfigHTTPProvider(&cfg)
242
assert.Error(t, err)
243
}
244
245
func TestGetRemoteConfig_UnmarshallableRemoteConfig(t *testing.T) {
246
defaultCfg := DefaultConfig()
247
brokenCfg := `completely invalid config (maybe it got corrupted, maybe it was somehow set this way)`
248
249
invalidCfgBytes := []byte(brokenCfg)
250
251
am := validAgentManagementConfig
252
logger := server.NewLogger(defaultCfg.Server)
253
testProvider := testRemoteConfigProvider{InitialConfig: &am}
254
testProvider.fetchedConfigBytesToReturn = invalidCfgBytes
255
testProvider.cachedConfigToReturn = cachedConfig
256
257
// flagset is required because some default values are extracted from it.
258
// In addition, some flags are defined as dependencies for validation
259
fs := flag.NewFlagSet("test", flag.ExitOnError)
260
features.Register(fs, allFeatures)
261
defaultCfg.RegisterFlags(fs)
262
263
cfg, err := getRemoteConfig(true, &testProvider, logger, fs, false)
264
assert.NoError(t, err)
265
assert.False(t, testProvider.didCacheRemoteConfig)
266
267
// check that the returned config is the cached one
268
// Note: Validate is required for the comparison as it mutates the config
269
expected := defaultCfg
270
expected.Validate(fs)
271
assert.True(t, util.CompareYAML(*cfg, expected))
272
}
273
274
func TestGetRemoteConfig_RemoteFetchFails(t *testing.T) {
275
defaultCfg := DefaultConfig()
276
277
am := validAgentManagementConfig
278
logger := server.NewLogger(defaultCfg.Server)
279
testProvider := testRemoteConfigProvider{InitialConfig: &am}
280
testProvider.fetchedConfigErrorToReturn = errors.New("connection refused")
281
testProvider.cachedConfigToReturn = cachedConfig
282
283
// flagset is required because some default values are extracted from it.
284
// In addition, some flags are defined as dependencies for validation
285
fs := flag.NewFlagSet("test", flag.ExitOnError)
286
features.Register(fs, allFeatures)
287
defaultCfg.RegisterFlags(fs)
288
289
cfg, err := getRemoteConfig(true, &testProvider, logger, fs, false)
290
assert.NoError(t, err)
291
assert.False(t, testProvider.didCacheRemoteConfig)
292
293
// check that the returned config is the cached one
294
// Note: Validate is required for the comparison as it mutates the config
295
expected := defaultCfg
296
expected.Validate(fs)
297
assert.True(t, util.CompareYAML(*cfg, expected))
298
}
299
300
func TestGetRemoteConfig_SemanticallyInvalidBaseConfig(t *testing.T) {
301
defaultCfg := DefaultConfig()
302
303
// this is semantically invalid because it has two scrape_configs with
304
// the same job_name
305
invalidConfig := `
306
{
307
"base_config": "metrics:\n configs:\n - name: Metrics Snippets\n scrape_configs:\n - job_name: 'prometheus'\n scrape_interval: 15s\n static_configs:\n - targets: ['localhost:12345']\n - job_name: 'prometheus'\n scrape_interval: 15s\n static_configs:\n - targets: ['localhost:12345']\n",
308
"snippets": []
309
}`
310
invalidCfgBytes := []byte(invalidConfig)
311
312
am := validAgentManagementConfig
313
logger := server.NewLogger(defaultCfg.Server)
314
testProvider := testRemoteConfigProvider{InitialConfig: &am}
315
testProvider.fetchedConfigBytesToReturn = invalidCfgBytes
316
testProvider.cachedConfigToReturn = cachedConfig
317
318
// flagset is required because some default values are extracted from it.
319
// In addition, some flags are defined as dependencies for validation
320
fs := flag.NewFlagSet("test", flag.ExitOnError)
321
features.Register(fs, allFeatures)
322
defaultCfg.RegisterFlags(fs)
323
324
cfg, err := getRemoteConfig(true, &testProvider, logger, fs, false)
325
assert.NoError(t, err)
326
assert.False(t, testProvider.didCacheRemoteConfig)
327
328
// check that the returned config is the cached one
329
// Note: Validate is required for the comparison as it mutates the config
330
expected := defaultCfg
331
expected.Validate(fs)
332
assert.True(t, util.CompareYAML(*cfg, expected))
333
}
334
335
func TestGetRemoteConfig_InvalidSnippet(t *testing.T) {
336
defaultCfg := DefaultConfig()
337
338
// this is semantically invalid because it has two scrape_configs with
339
// the same job_name
340
invalidConfig := `
341
{
342
"base_config": "server:\n log_level: info\n log_format: logfmt\n",
343
"snippets": [
344
{
345
"config": "metrics_scrape_configs:\n- job_name: 'prometheus'\n- job_name: 'prometheus'\n"
346
}
347
]
348
}`
349
invalidCfgBytes := []byte(invalidConfig)
350
351
am := validAgentManagementConfig
352
logger := server.NewLogger(defaultCfg.Server)
353
testProvider := testRemoteConfigProvider{InitialConfig: &am}
354
testProvider.fetchedConfigBytesToReturn = invalidCfgBytes
355
testProvider.cachedConfigToReturn = cachedConfig
356
357
// flagset is required because some default values are extracted from it.
358
// In addition, some flags are defined as dependencies for validation
359
fs := flag.NewFlagSet("test", flag.ExitOnError)
360
features.Register(fs, allFeatures)
361
defaultCfg.RegisterFlags(fs)
362
363
cfg, err := getRemoteConfig(true, &testProvider, logger, fs, false)
364
assert.NoError(t, err)
365
assert.False(t, testProvider.didCacheRemoteConfig)
366
367
// check that the returned config is the cached one
368
// Note: Validate is required for the comparison as it mutates the config
369
expected := defaultCfg
370
expected.Validate(fs)
371
assert.True(t, util.CompareYAML(*cfg, expected))
372
}
373
374
func TestGetRemoteConfig_EmptyBaseConfig(t *testing.T) {
375
defaultCfg := DefaultConfig()
376
377
validConfig := `
378
{
379
"base_config": "",
380
"snippets": []
381
}`
382
cfgBytes := []byte(validConfig)
383
am := validAgentManagementConfig
384
logger := server.NewLogger(defaultCfg.Server)
385
testProvider := testRemoteConfigProvider{InitialConfig: &am}
386
testProvider.fetchedConfigBytesToReturn = cfgBytes
387
testProvider.cachedConfigToReturn = cachedConfig
388
389
fs := flag.NewFlagSet("test", flag.ExitOnError)
390
features.Register(fs, allFeatures)
391
defaultCfg.RegisterFlags(fs)
392
393
cfg, err := getRemoteConfig(true, &testProvider, logger, fs, false)
394
assert.NoError(t, err)
395
assert.True(t, testProvider.didCacheRemoteConfig)
396
397
// check that the returned config is not the cached one
398
assert.NotEqual(t, "debug", cfg.Server.LogLevel.String())
399
}
400
401
func TestGetRemoteConfig_ValidBaseConfig(t *testing.T) {
402
defaultCfg := DefaultConfig()
403
validConfig := `
404
{
405
"base_config": "server:\n log_level: debug\n log_format: logfmt\nlogs:\n positions_directory: /tmp\n global:\n clients:\n - basic_auth:\n password_file: key.txt\n username: 278220\n url: https://logs-prod-eu-west-0.grafana.net/loki/api/v1/push\nintegrations:\n agent:\n enabled: false\n",
406
"snippets": [
407
{
408
"config": "metrics_scrape_configs:\n- job_name: 'prometheus'\n scrape_interval: 15s\n static_configs:\n - targets: ['localhost:12345']\nlogs_scrape_configs:\n- job_name: yologs\n static_configs:\n - targets: [localhost]\n labels:\n job: yologs\n __path__: /tmp/yo.log\n",
409
"selector": {
410
"hostname": "machine-1",
411
"team": "team-a"
412
}
413
}
414
]
415
}`
416
cfgBytes := []byte(validConfig)
417
am := validAgentManagementConfig
418
logger := server.NewLogger(defaultCfg.Server)
419
testProvider := testRemoteConfigProvider{InitialConfig: &am}
420
testProvider.fetchedConfigBytesToReturn = cfgBytes
421
testProvider.cachedConfigToReturn = cachedConfig
422
423
fs := flag.NewFlagSet("test", flag.ExitOnError)
424
features.Register(fs, allFeatures)
425
defaultCfg.RegisterFlags(fs)
426
427
cfg, err := getRemoteConfig(true, &testProvider, logger, fs, false)
428
assert.NoError(t, err)
429
assert.True(t, testProvider.didCacheRemoteConfig)
430
431
// check that the returned config is not the cached one
432
assert.False(t, util.CompareYAML(*cfg, defaultCfg))
433
434
// check some fields to make sure the config was parsed correctly
435
assert.Equal(t, "debug", cfg.Server.LogLevel.String())
436
assert.Equal(t, "278220", cfg.Logs.Global.ClientConfigs[0].Client.BasicAuth.Username)
437
assert.Equal(t, "prometheus", cfg.Metrics.Configs[0].ScrapeConfigs[0].JobName)
438
assert.Equal(t, "yologs", cfg.Logs.Configs[0].ScrapeConfig[0].JobName)
439
assert.Equal(t, 1, len(cfg.Integrations.configV1.Integrations))
440
}
441
442
func TestGetRemoteConfig_ExpandsEnvVars(t *testing.T) {
443
defaultCfg := DefaultConfig()
444
validConfig := `
445
{
446
"base_config": "server:\n log_level: info\n log_format: ${LOG_FORMAT}\nlogs:\n positions_directory: /tmp\n global:\n clients:\n - basic_auth:\n password_file: key.txt\n username: 278220\n url: https://logs-prod-eu-west-0.grafana.net/loki/api/v1/push\nintegrations:\n agent:\n enabled: false\n",
447
"snippets": [
448
{
449
"config": "metrics_scrape_configs:\n- job_name: 'prometheus'\n scrape_interval: ${SCRAPE_INTERVAL}\n static_configs:\n - targets: ['localhost:12345']\n",
450
"selector": {
451
"hostname": "machine-1",
452
"team": "team-a"
453
}
454
}
455
]
456
}`
457
t.Setenv("SCRAPE_INTERVAL", "15s")
458
t.Setenv("LOG_FORMAT", "json")
459
460
cfgBytes := []byte(validConfig)
461
am := validAgentManagementConfig
462
logger := server.NewLogger(defaultCfg.Server)
463
testProvider := testRemoteConfigProvider{InitialConfig: &am}
464
testProvider.fetchedConfigBytesToReturn = cfgBytes
465
testProvider.cachedConfigToReturn = cachedConfig
466
467
fs := flag.NewFlagSet("test", flag.ExitOnError)
468
var configExpandEnv bool
469
fs.BoolVar(&configExpandEnv, "config.expand-env", false, "")
470
features.Register(fs, allFeatures)
471
defaultCfg.RegisterFlags(fs)
472
473
cfg, err := getRemoteConfig(true, &testProvider, logger, fs, false)
474
assert.NoError(t, err)
475
assert.Equal(t, "15s", cfg.Metrics.Configs[0].ScrapeConfigs[0].ScrapeInterval.String())
476
assert.Equal(t, "json", cfg.Server.LogFormat.String())
477
}
478
479
func TestGetCachedConfig_DefaultConfigFallback(t *testing.T) {
480
defaultCfg := DefaultConfig()
481
am := validAgentManagementConfig
482
logger := server.NewLogger(defaultCfg.Server)
483
testProvider := testRemoteConfigProvider{InitialConfig: &am}
484
testProvider.cachedConfigErrorToReturn = errors.New("no cached config")
485
486
fs := flag.NewFlagSet("test", flag.ExitOnError)
487
features.Register(fs, allFeatures)
488
defaultCfg.RegisterFlags(fs)
489
490
cfg, err := getCachedRemoteConfig(true, &testProvider, fs, logger)
491
assert.NoError(t, err)
492
493
// check that the returned config is the default one
494
assert.True(t, util.CompareYAML(*cfg, defaultCfg))
495
}
496
497
func TestGetCachedConfig_RetryAfter(t *testing.T) {
498
defaultCfg := DefaultConfig()
499
am := validAgentManagementConfig
500
logger := server.NewLogger(defaultCfg.Server)
501
testProvider := testRemoteConfigProvider{InitialConfig: &am}
502
testProvider.fetchedConfigErrorToReturn = retryAfterError{retryAfter: time.Duration(0)}
503
testProvider.cachedConfigToReturn = cachedConfig
504
505
fs := flag.NewFlagSet("test", flag.ExitOnError)
506
features.Register(fs, allFeatures)
507
defaultCfg.RegisterFlags(fs)
508
509
_, err := getRemoteConfig(true, &testProvider, logger, fs, true)
510
assert.NoError(t, err)
511
assert.False(t, testProvider.didCacheRemoteConfig)
512
513
// check that FetchRemoteConfig was called twice on the TestProvider:
514
// 1 call for the initial attempt, a second for the retry
515
assert.Equal(t, 2, testProvider.fetchRemoteConfigCallCount)
516
517
// the cached config should have been retrieved once, on the second
518
// attempt to fetch the remote config
519
assert.Equal(t, 1, testProvider.getCachedConfigCallCount)
520
}
521
522