Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
aos
GitHub Repository: aos/grafana-agent
Path: blob/main/pkg/config/config.go
4094 views
1
package config
2
3
import (
4
"bytes"
5
"flag"
6
"fmt"
7
"os"
8
"strings"
9
"testing"
10
"unicode"
11
12
"github.com/drone/envsubst/v2"
13
"github.com/go-kit/log"
14
"github.com/go-kit/log/level"
15
"github.com/grafana/agent/pkg/build"
16
"github.com/grafana/agent/pkg/config/features"
17
"github.com/grafana/agent/pkg/config/instrumentation"
18
"github.com/grafana/agent/pkg/logs"
19
"github.com/grafana/agent/pkg/metrics"
20
"github.com/grafana/agent/pkg/server"
21
"github.com/grafana/agent/pkg/traces"
22
"github.com/grafana/agent/pkg/util"
23
"github.com/prometheus/common/config"
24
"github.com/stretchr/testify/require"
25
"gopkg.in/yaml.v2"
26
)
27
28
var (
29
featRemoteConfigs = features.Feature("remote-configs")
30
featIntegrationsNext = features.Feature("integrations-next")
31
featExtraMetrics = features.Feature("extra-scrape-metrics")
32
featAgentManagement = features.Feature("agent-management")
33
34
allFeatures = []features.Feature{
35
featRemoteConfigs,
36
featIntegrationsNext,
37
featExtraMetrics,
38
featAgentManagement,
39
}
40
)
41
42
var (
43
fileTypeYAML = "yaml"
44
fileTypeDynamic = "dynamic"
45
46
fileTypes = []string{fileTypeYAML, fileTypeDynamic}
47
)
48
49
// DefaultConfig holds default settings for all the subsystems.
50
func DefaultConfig() Config {
51
defaultServerCfg := server.DefaultConfig()
52
return Config{
53
// All subsystems with a DefaultConfig should be listed here.
54
Server: &defaultServerCfg,
55
ServerFlags: server.DefaultFlags,
56
Metrics: metrics.DefaultConfig,
57
Integrations: DefaultVersionedIntegrations(),
58
DisableSupportBundle: false,
59
EnableConfigEndpoints: false,
60
EnableUsageReport: true,
61
}
62
}
63
64
// Config contains underlying configurations for the agent
65
type Config struct {
66
Server *server.Config `yaml:"server,omitempty"`
67
Metrics metrics.Config `yaml:"metrics,omitempty"`
68
Integrations VersionedIntegrations `yaml:"integrations,omitempty"`
69
Traces traces.Config `yaml:"traces,omitempty"`
70
Logs *logs.Config `yaml:"logs,omitempty"`
71
AgentManagement AgentManagementConfig `yaml:"agent_management,omitempty"`
72
73
// Flag-only fields
74
ServerFlags server.Flags `yaml:"-"`
75
76
// Deprecated fields user has used. Generated during UnmarshalYAML.
77
Deprecations []string `yaml:"-"`
78
79
// Remote config options
80
BasicAuthUser string `yaml:"-"`
81
BasicAuthPassFile string `yaml:"-"`
82
83
// Toggle for config endpoint(s)
84
EnableConfigEndpoints bool `yaml:"-"`
85
86
// Toggle for support bundle generation.
87
DisableSupportBundle bool `yaml:"-"`
88
89
// Report enabled features options
90
EnableUsageReport bool `yaml:"-"`
91
EnabledFeatures []string `yaml:"-"`
92
}
93
94
// UnmarshalYAML implements yaml.Unmarshaler.
95
func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error {
96
// Apply defaults to the config from our struct and any defaults inherited
97
// from flags before unmarshaling.
98
*c = DefaultConfig()
99
util.DefaultConfigFromFlags(c)
100
101
type baseConfig Config
102
103
type config struct {
104
baseConfig `yaml:",inline"`
105
106
// Deprecated field names:
107
Prometheus *metrics.Config `yaml:"prometheus,omitempty"`
108
Loki *logs.Config `yaml:"loki,omitempty"`
109
Tempo *traces.Config `yaml:"tempo,omitempty"`
110
}
111
112
var fc config
113
fc.baseConfig = baseConfig(*c)
114
115
if err := unmarshal(&fc); err != nil {
116
return err
117
}
118
119
// Migrate old fields to the new name
120
if fc.Prometheus != nil && fc.Metrics.Unmarshaled && fc.Prometheus.Unmarshaled {
121
return fmt.Errorf("at most one of prometheus and metrics should be specified")
122
} else if fc.Prometheus != nil && fc.Prometheus.Unmarshaled {
123
fc.Deprecations = append(fc.Deprecations, "`prometheus` has been deprecated in favor of `metrics`")
124
fc.Metrics = *fc.Prometheus
125
fc.Prometheus = nil
126
}
127
128
if fc.Logs != nil && fc.Loki != nil {
129
return fmt.Errorf("at most one of loki and logs should be specified")
130
} else if fc.Logs == nil && fc.Loki != nil {
131
fc.Deprecations = append(fc.Deprecations, "`loki` has been deprecated in favor of `logs`")
132
fc.Logs = fc.Loki
133
fc.Loki = nil
134
}
135
136
if fc.Tempo != nil && fc.Traces.Unmarshaled {
137
return fmt.Errorf("at most one of tempo and traces should be specified")
138
} else if fc.Tempo != nil && fc.Tempo.Unmarshaled {
139
fc.Deprecations = append(fc.Deprecations, "`tempo` has been deprecated in favor of `traces`")
140
fc.Traces = *fc.Tempo
141
fc.Tempo = nil
142
}
143
144
*c = Config(fc.baseConfig)
145
return nil
146
}
147
148
// MarshalYAML implements yaml.Marshaler.
149
func (c Config) MarshalYAML() (interface{}, error) {
150
var buf bytes.Buffer
151
152
enc := yaml.NewEncoder(&buf)
153
154
type config Config
155
if err := enc.Encode((config)(c)); err != nil {
156
return nil, err
157
}
158
159
// Use a yaml.MapSlice rather than a map[string]interface{} so
160
// order of keys is retained compared to just calling MarshalConfig.
161
var m yaml.MapSlice
162
if err := yaml.Unmarshal(buf.Bytes(), &m); err != nil {
163
return nil, err
164
}
165
return m, nil
166
}
167
168
// LogDeprecations will log use of any deprecated fields to l as warn-level
169
// messages.
170
func (c *Config) LogDeprecations(l log.Logger) {
171
for _, d := range c.Deprecations {
172
level.Warn(l).Log("msg", fmt.Sprintf("DEPRECATION NOTICE: %s", d))
173
}
174
}
175
176
// Validate validates the config, flags, and sets default values.
177
func (c *Config) Validate(fs *flag.FlagSet) error {
178
if c.Server == nil {
179
return fmt.Errorf("an empty server config is invalid")
180
}
181
182
if err := c.Metrics.ApplyDefaults(); err != nil {
183
return err
184
}
185
186
if c.Logs != nil {
187
if err := c.Logs.ApplyDefaults(); err != nil {
188
return err
189
}
190
}
191
192
// Need to propagate the listen address to the host and grpcPort
193
_, grpcPort, err := c.ServerFlags.GRPC.ListenHostPort()
194
if err != nil {
195
return err
196
}
197
c.Metrics.ServiceConfig.Lifecycler.ListenPort = grpcPort
198
199
// TODO(jcreixell): Make this method side-effect free and, if necessary, implement a
200
// method bundling defaults application and validation. Rationale: sometimes (for example
201
// in tests) we want to validate a config without mutating it, or apply all defaults
202
// for comparison.
203
if err := c.Integrations.ApplyDefaults(&c.ServerFlags, &c.Metrics); err != nil {
204
return err
205
}
206
207
// since the Traces config might rely on an existing Loki config
208
// this check is made here to look for cross config issues before we attempt to load
209
if err := c.Traces.Validate(c.Logs); err != nil {
210
return err
211
}
212
213
if c.AgentManagement.Enabled {
214
if err := c.AgentManagement.Validate(); err != nil {
215
return fmt.Errorf("invalid agent management config: %w", err)
216
}
217
}
218
219
c.Metrics.ServiceConfig.APIEnableGetConfiguration = c.EnableConfigEndpoints
220
221
// Don't validate flags if there's no FlagSet. Used for testing.
222
if fs == nil {
223
return nil
224
}
225
deps := []features.Dependency{
226
{Flag: "config.url.basic-auth-user", Feature: featRemoteConfigs},
227
{Flag: "config.url.basic-auth-password-file", Feature: featRemoteConfigs},
228
}
229
return features.Validate(fs, deps)
230
}
231
232
// RegisterFlags registers flags in underlying configs
233
func (c *Config) RegisterFlags(f *flag.FlagSet) {
234
c.Metrics.RegisterFlags(f)
235
c.ServerFlags.RegisterFlags(f)
236
237
f.StringVar(&c.BasicAuthUser, "config.url.basic-auth-user", "",
238
"basic auth username for fetching remote config. (requires remote-configs experiment to be enabled")
239
f.StringVar(&c.BasicAuthPassFile, "config.url.basic-auth-password-file", "",
240
"path to file containing basic auth password for fetching remote config. (requires remote-configs experiment to be enabled")
241
242
f.BoolVar(&c.EnableConfigEndpoints, "config.enable-read-api", false, "Enables the /-/config and /agent/api/v1/configs/{name} APIs. Be aware that secrets could be exposed by enabling these endpoints!")
243
}
244
245
// LoadFile reads a file and passes the contents to Load
246
func LoadFile(filename string, expandEnvVars bool, c *Config) error {
247
buf, err := os.ReadFile(filename)
248
249
if err != nil {
250
return fmt.Errorf("error reading config file %w", err)
251
}
252
253
instrumentation.InstrumentConfig(buf)
254
255
return LoadBytes(buf, expandEnvVars, c)
256
}
257
258
// loadFromAgentManagementAPI loads and merges a config from an Agent Management API.
259
// 1. Read local initial config.
260
// 2. Get the remote config.
261
// a) Fetch from remote. If this fails or is invalid:
262
// b) Read the remote config from cache. If this fails, return an error.
263
// 4. Merge the initial and remote config into c.
264
func loadFromAgentManagementAPI(path string, expandEnvVars bool, c *Config, log *server.Logger, fs *flag.FlagSet) error {
265
// Load the initial config from disk without instrumenting the config hash
266
buf, err := os.ReadFile(path)
267
if err != nil {
268
return fmt.Errorf("error reading initial config file %w", err)
269
}
270
271
err = LoadBytes(buf, expandEnvVars, c)
272
if err != nil {
273
return fmt.Errorf("failed to load initial config: %w", err)
274
}
275
276
configProvider, err := newRemoteConfigProvider(c)
277
if err != nil {
278
return err
279
}
280
remoteConfig, err := getRemoteConfig(expandEnvVars, configProvider, log, fs, true)
281
if err != nil {
282
return err
283
}
284
mergeEffectiveConfig(c, remoteConfig)
285
286
effectiveConfigBytes, err := yaml.Marshal(c)
287
if err != nil {
288
level.Warn(log).Log("msg", "error marshalling config for instrumenting config version", "err", err)
289
} else {
290
instrumentation.InstrumentConfig(effectiveConfigBytes)
291
}
292
293
return nil
294
}
295
296
// mergeEffectiveConfig overwrites any values in initialConfig with those in remoteConfig
297
func mergeEffectiveConfig(initialConfig *Config, remoteConfig *Config) {
298
initialConfig.Server = remoteConfig.Server
299
initialConfig.Metrics = remoteConfig.Metrics
300
initialConfig.Integrations = remoteConfig.Integrations
301
initialConfig.Traces = remoteConfig.Traces
302
initialConfig.Logs = remoteConfig.Logs
303
}
304
305
// LoadRemote reads a config from url
306
func LoadRemote(url string, expandEnvVars bool, c *Config) error {
307
remoteOpts := &remoteOpts{}
308
if c.BasicAuthUser != "" && c.BasicAuthPassFile != "" {
309
remoteOpts.HTTPClientConfig = &config.HTTPClientConfig{
310
BasicAuth: &config.BasicAuth{
311
Username: c.BasicAuthUser,
312
PasswordFile: c.BasicAuthPassFile,
313
},
314
}
315
}
316
317
if remoteOpts.HTTPClientConfig != nil {
318
dir, err := os.Getwd()
319
if err != nil {
320
return fmt.Errorf("failed to get current working directory: %w", err)
321
}
322
remoteOpts.HTTPClientConfig.SetDirectory(dir)
323
}
324
325
rc, err := newRemoteProvider(url, remoteOpts)
326
if err != nil {
327
return fmt.Errorf("error reading remote config: %w", err)
328
}
329
// fall back to file if no scheme is passed
330
if rc == nil {
331
return LoadFile(url, expandEnvVars, c)
332
}
333
bb, err := rc.retrieve()
334
if err != nil {
335
return fmt.Errorf("error retrieving remote config: %w", err)
336
}
337
338
instrumentation.InstrumentConfig(bb)
339
340
return LoadBytes(bb, expandEnvVars, c)
341
}
342
343
func performEnvVarExpansion(buf []byte, expandEnvVars bool) ([]byte, error) {
344
// (Optionally) expand with environment variables
345
if expandEnvVars {
346
s, err := envsubst.Eval(string(buf), getenv)
347
if err != nil {
348
return nil, fmt.Errorf("unable to substitute config with environment variables: %w", err)
349
}
350
return []byte(s), nil
351
}
352
return buf, nil
353
}
354
355
// LoadBytes unmarshals a config from a buffer. Defaults are not
356
// applied to the file and must be done manually if LoadBytes
357
// is called directly.
358
func LoadBytes(buf []byte, expandEnvVars bool, c *Config) error {
359
expandedBuf, err := performEnvVarExpansion(buf, expandEnvVars)
360
if err != nil {
361
return err
362
}
363
// Unmarshal yaml config
364
return yaml.UnmarshalStrict(expandedBuf, c)
365
}
366
367
// getenv is a wrapper around os.Getenv that ignores patterns that are numeric
368
// regex capture groups (ie "${1}").
369
func getenv(name string) string {
370
numericName := true
371
372
for _, r := range name {
373
if !unicode.IsDigit(r) {
374
numericName = false
375
break
376
}
377
}
378
379
if numericName {
380
// We need to add ${} back in since envsubst removes it.
381
return fmt.Sprintf("${%s}", name)
382
}
383
return os.Getenv(name)
384
}
385
386
// Load loads a config file from a flagset. Flags will be registered
387
// to the flagset before parsing them with the values specified by
388
// args.
389
func Load(fs *flag.FlagSet, args []string, log *server.Logger) (*Config, error) {
390
cfg, error := load(fs, args, func(path, fileType string, expandArgs bool, c *Config) error {
391
switch fileType {
392
case fileTypeYAML:
393
if features.Enabled(fs, featRemoteConfigs) {
394
return LoadRemote(path, expandArgs, c)
395
}
396
if features.Enabled(fs, featAgentManagement) {
397
return loadFromAgentManagementAPI(path, expandArgs, c, log, fs)
398
}
399
return LoadFile(path, expandArgs, c)
400
default:
401
return fmt.Errorf("unknown file type %q. accepted values: %s", fileType, strings.Join(fileTypes, ", "))
402
}
403
})
404
405
instrumentation.InstrumentLoad(error == nil)
406
return cfg, error
407
}
408
409
type loaderFunc func(path string, fileType string, expandArgs bool, target *Config) error
410
411
func applyIntegrationValuesFromFlagset(fs *flag.FlagSet, args []string, path string, cfg *Config) error {
412
// Parse the flags again to override any YAML values with command line flag
413
// values.
414
if err := fs.Parse(args); err != nil {
415
return fmt.Errorf("error parsing flags: %w", err)
416
}
417
418
// Complete unmarshaling integrations using the version from the flag. This
419
// MUST be called before ApplyDefaults.
420
version := integrationsVersion1
421
if features.Enabled(fs, featIntegrationsNext) {
422
version = integrationsVersion2
423
}
424
425
if err := cfg.Integrations.setVersion(version); err != nil {
426
return fmt.Errorf("error loading config file %s: %w", path, err)
427
}
428
return nil
429
}
430
431
// load allows for tests to inject a function for retrieving the config file that
432
// doesn't require having a literal file on disk.
433
func load(fs *flag.FlagSet, args []string, loader loaderFunc) (*Config, error) {
434
var (
435
cfg = DefaultConfig()
436
437
printVersion bool
438
file string
439
fileType string
440
configExpandEnv bool
441
disableReporting bool
442
disableSupportBundles bool
443
)
444
445
fs.StringVar(&file, "config.file", "", "configuration file to load")
446
fs.StringVar(&fileType, "config.file.type", "yaml", fmt.Sprintf("Type of file pointed to by -config.file flag. Supported values: %s. %s requires dynamic-config and integrations-next features to be enabled.", strings.Join(fileTypes, ", "), fileTypeDynamic))
447
fs.BoolVar(&printVersion, "version", false, "Print this build's version information.")
448
fs.BoolVar(&configExpandEnv, "config.expand-env", false, "Expands ${var} in config according to the values of the environment variables.")
449
fs.BoolVar(&disableReporting, "disable-reporting", false, "Disable reporting of enabled feature flags to Grafana.")
450
fs.BoolVar(&disableSupportBundles, "disable-support-bundle", false, "Disable functionality for generating support bundles.")
451
cfg.RegisterFlags(fs)
452
453
features.Register(fs, allFeatures)
454
455
if err := fs.Parse(args); err != nil {
456
return nil, fmt.Errorf("error parsing flags: %w", err)
457
}
458
459
if printVersion {
460
fmt.Println(build.Print("agent"))
461
os.Exit(0)
462
}
463
464
if file == "" {
465
return nil, fmt.Errorf("-config.file flag required")
466
} else if err := loader(file, fileType, configExpandEnv, &cfg); err != nil {
467
return nil, fmt.Errorf("error loading config file %s: %w", file, err)
468
}
469
470
if err := applyIntegrationValuesFromFlagset(fs, args, file, &cfg); err != nil {
471
return nil, err
472
}
473
474
if features.Enabled(fs, featExtraMetrics) {
475
cfg.Metrics.Global.ExtraMetrics = true
476
}
477
478
if disableReporting {
479
cfg.EnableUsageReport = false
480
} else {
481
cfg.EnabledFeatures = features.GetAllEnabled(fs)
482
}
483
484
cfg.AgentManagement.Enabled = features.Enabled(fs, featAgentManagement)
485
486
if disableSupportBundles {
487
cfg.DisableSupportBundle = true
488
}
489
490
// Finally, apply defaults to config that wasn't specified by file or flag
491
if err := cfg.Validate(fs); err != nil {
492
return nil, fmt.Errorf("error in config file: %w", err)
493
}
494
return &cfg, nil
495
}
496
497
// CheckSecret is a helper function to ensure the original value is overwritten with <secret>
498
func CheckSecret(t *testing.T, rawCfg string, originalValue string) {
499
var cfg Config
500
err := LoadBytes([]byte(rawCfg), false, &cfg)
501
require.NoError(t, err)
502
503
// Set integrations version to make sure our marshal function goes through
504
// the custom marshaling code.
505
err = cfg.Integrations.setVersion(integrationsVersion1)
506
require.NoError(t, err)
507
508
bb, err := yaml.Marshal(&cfg)
509
require.NoError(t, err)
510
511
require.True(t, strings.Contains(string(bb), "<secret>"))
512
require.False(t, strings.Contains(string(bb), originalValue))
513
}
514
515