diff --git a/config/config.go b/config/config.go index 77da619c28..5b2356cff5 100644 --- a/config/config.go +++ b/config/config.go @@ -93,10 +93,24 @@ type Config struct { RuleFiles []string `yaml:"rule_files,omitempty"` ScrapeConfigs []*ScrapeConfig `yaml:"scrape_configs,omitempty"` + // Catches all undefined fields and must be empty after parsing. + XXX map[string]interface{} `yaml:",inline"` + // original is the input from which the config was parsed. original string } +func checkOverflow(m map[string]interface{}, ctx string) error { + if len(m) > 0 { + var keys []string + for k := range m { + keys = append(keys, k) + } + return fmt.Errorf("unknown fields in %s: %s", ctx, strings.Join(keys, ", ")) + } + return nil +} + func (c Config) String() string { if c.original != "" { return c.original @@ -138,7 +152,7 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error { } jobNames[scfg.JobName] = struct{}{} } - return nil + return checkOverflow(c.XXX, "config") } // GlobalConfig configures values that are used across other configuration @@ -150,9 +164,11 @@ type GlobalConfig struct { ScrapeTimeout Duration `yaml:"scrape_timeout,omitempty"` // How frequently to evaluate rules by default. EvaluationInterval Duration `yaml:"evaluation_interval,omitempty"` - // The labels to add to any timeseries that this Prometheus instance scrapes. Labels clientmodel.LabelSet `yaml:"labels,omitempty"` + + // Catches all undefined fields and must be empty after parsing. + XXX map[string]interface{} `yaml:",inline"` } // UnmarshalYAML implements the yaml.Unmarshaler interface. @@ -162,7 +178,7 @@ func (c *GlobalConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { if err := unmarshal((*plain)(c)); err != nil { return err } - return nil + return checkOverflow(c.XXX, "global config") } // ScrapeConfig configures a scraping unit for Prometheus. @@ -178,7 +194,7 @@ type ScrapeConfig struct { // The URL scheme with which to fetch metrics from targets. Scheme string `yaml:"scheme,omitempty"` // The HTTP basic authentication credentials for the targets. - BasicAuth *BasicAuth `yaml:"basic_auth"` + BasicAuth *BasicAuth `yaml:"basic_auth,omitempty"` // List of labeled target groups for this job. TargetGroups []*TargetGroup `yaml:"target_groups,omitempty"` @@ -190,6 +206,9 @@ type ScrapeConfig struct { ConsulSDConfigs []*ConsulSDConfig `yaml:"consul_sd_configs,omitempty"` // List of relabel configurations. RelabelConfigs []*RelabelConfig `yaml:"relabel_configs,omitempty"` + + // Catches all undefined fields and must be empty after parsing. + XXX map[string]interface{} `yaml:",inline"` } // UnmarshalYAML implements the yaml.Unmarshaler interface. @@ -203,13 +222,26 @@ func (c *ScrapeConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { if !patJobName.MatchString(c.JobName) { return fmt.Errorf("%q is not a valid job name", c.JobName) } - return nil + return checkOverflow(c.XXX, "scrape_config") } // BasicAuth contains basic HTTP authentication credentials. type BasicAuth struct { Username string `yaml:"username"` Password string `yaml:"password"` + + // Catches all undefined fields and must be empty after parsing. + XXX map[string]interface{} `yaml:",inline"` +} + +// UnmarshalYAML implements the yaml.Unmarshaler interface. +func (a *BasicAuth) UnmarshalYAML(unmarshal func(interface{}) error) error { + type plain BasicAuth + err := unmarshal((*plain)(a)) + if err != nil { + return err + } + return checkOverflow(a.XXX, "basic_auth") } // TargetGroup is a set of targets with a common label set. @@ -231,8 +263,9 @@ func (tg TargetGroup) String() string { // UnmarshalYAML implements the yaml.Unmarshaler interface. func (tg *TargetGroup) UnmarshalYAML(unmarshal func(interface{}) error) error { g := struct { - Targets []string `yaml:"targets"` - Labels clientmodel.LabelSet `yaml:"labels"` + Targets []string `yaml:"targets"` + Labels clientmodel.LabelSet `yaml:"labels"` + XXX map[string]interface{} `yaml:",inline"` }{} if err := unmarshal(&g); err != nil { return err @@ -247,7 +280,7 @@ func (tg *TargetGroup) UnmarshalYAML(unmarshal func(interface{}) error) error { }) } tg.Labels = g.Labels - return nil + return checkOverflow(g.XXX, "target_group") } // MarshalYAML implements the yaml.Marshaler interface. @@ -291,6 +324,9 @@ func (tg *TargetGroup) UnmarshalJSON(b []byte) error { type DNSSDConfig struct { Names []string `yaml:"names"` RefreshInterval Duration `yaml:"refresh_interval,omitempty"` + + // Catches all undefined fields and must be empty after parsing. + XXX map[string]interface{} `yaml:",inline"` } // UnmarshalYAML implements the yaml.Unmarshaler interface. @@ -304,13 +340,16 @@ func (c *DNSSDConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { if len(c.Names) == 0 { return fmt.Errorf("DNS-SD config must contain at least one SRV record name") } - return nil + return checkOverflow(c.XXX, "dns_sd_config") } // FileSDConfig is the configuration for file based discovery. type FileSDConfig struct { Names []string `yaml:"names"` RefreshInterval Duration `yaml:"refresh_interval,omitempty"` + + // Catches all undefined fields and must be empty after parsing. + XXX map[string]interface{} `yaml:",inline"` } // UnmarshalYAML implements the yaml.Unmarshaler interface. @@ -329,7 +368,7 @@ func (c *FileSDConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { return fmt.Errorf("path name %q is not valid for file discovery", name) } } - return nil + return checkOverflow(c.XXX, "file_sd_config") } // ConsulSDConfig is the configuration for Consul service discovery. @@ -343,6 +382,9 @@ type ConsulSDConfig struct { Password string `yaml:"password"` // The list of services for which targets are discovered. Services []string `yaml:"services"` + + // Catches all undefined fields and must be empty after parsing. + XXX map[string]interface{} `yaml:",inline"` } // UnmarshalYAML implements the yaml.Unmarshaler interface. @@ -359,7 +401,7 @@ func (c *ConsulSDConfig) UnmarshalYAML(unmarshal func(interface{}) error) error if len(c.Services) == 0 { return fmt.Errorf("Consul SD configuration requires at least one service name") } - return nil + return checkOverflow(c.XXX, "consul_sd_config") } // RelabelAction is the action to be performed on relabeling. @@ -403,6 +445,9 @@ type RelabelConfig struct { Replacement string `yaml:"replacement,omitempty"` // Action is the action to be performed for the relabeling. Action RelabelAction `yaml:"action,omitempty"` + + // Catches all undefined fields and must be empty after parsing. + XXX map[string]interface{} `yaml:",inline"` } // UnmarshalYAML implements the yaml.Unmarshaler interface. @@ -415,7 +460,7 @@ func (c *RelabelConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { if c.Regex == nil { return fmt.Errorf("relabel configuration requires a regular expression") } - return nil + return checkOverflow(c.XXX, "relabel_config") } // Regexp encapsulates a regexp.Regexp and makes it YAML marshallable. diff --git a/config/config_test.go b/config/config_test.go index ebabe72f65..c65e5e7152 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -115,6 +115,24 @@ var expectedConf = &Config{ }, }, }, + { + JobName: "service-y", + + ScrapeInterval: Duration(15 * time.Second), + ScrapeTimeout: DefaultGlobalConfig.ScrapeTimeout, + + MetricsPath: DefaultScrapeConfig.MetricsPath, + Scheme: DefaultScrapeConfig.Scheme, + + ConsulSDConfigs: []*ConsulSDConfig{ + { + Server: "localhost:1234", + Services: []string{"nginx", "cache", "mysql"}, + TagSeparator: DefaultConsulSDConfig.TagSeparator, + Scheme: DefaultConsulSDConfig.Scheme, + }, + }, + }, }, original: "", } @@ -128,20 +146,20 @@ func TestLoadConfig(t *testing.T) { c, err := LoadFromFile("testdata/conf.good.yml") if err != nil { - t.Errorf("Error parsing %s: %s", "testdata/conf.good.yml", err) + t.Fatalf("Error parsing %s: %s", "testdata/conf.good.yml", err) } bgot, err := yaml.Marshal(c) if err != nil { - t.Errorf("%s", err) + t.Fatalf("%s", err) } bexp, err := yaml.Marshal(expectedConf) if err != nil { - t.Errorf("%s", err) + t.Fatalf("%s", err) } expectedConf.original = c.original if !reflect.DeepEqual(c, expectedConf) { - t.Errorf("%s: unexpected config result: \n\n%s\n expected\n\n%s", "testdata/conf.good.yml", bgot, bexp) + t.Fatalf("%s: unexpected config result: \n\n%s\n expected\n\n%s", "testdata/conf.good.yml", bgot, bexp) } } @@ -170,6 +188,9 @@ var expectedErrors = []struct { }, { filename: "rules.bad.yml", errMsg: "invalid rule file path", + }, { + filename: "unknown_attr.bad.yml", + errMsg: "unknown fields in scrape_config: consult_sd_configs", }, } diff --git a/config/testdata/conf.good.yml b/config/testdata/conf.good.yml index 831006e2e1..112540ec03 100644 --- a/config/testdata/conf.good.yml +++ b/config/testdata/conf.good.yml @@ -5,71 +5,75 @@ global: # scrape_timeout is set to the global default (10s). labels: - monitor: codelab - foo: bar + monitor: codelab + foo: bar rule_files: - - "first.rules" - - "second.rules" - - "my/*.rules" +- "first.rules" +- "second.rules" +- "my/*.rules" scrape_configs: - - job_name: prometheus +- job_name: prometheus - # scrape_interval is defined by the configured global (15s). - # scrape_timeout is defined by the global default (10s). + # scrape_interval is defined by the configured global (15s). + # scrape_timeout is defined by the global default (10s). - # metrics_path defaults to '/metrics' - # scheme defaults to 'http'. + # metrics_path defaults to '/metrics' + # scheme defaults to 'http'. + file_sd_configs: + - names: + - foo/*.slow.json + - foo/*.slow.yml + - single/file.yml + refresh_interval: 10m + - names: + - bar/*.yaml + + target_groups: + - targets: ['localhost:9090', 'localhost:9191'] labels: - foo: baz + my: label + your: label - file_sd_configs: - - names: - - foo/*.slow.json - - foo/*.slow.yml - - single/file.yml - refresh_interval: 10m - - names: - - bar/*.yaml - - target_groups: - - targets: ['localhost:9090', 'localhost:9191'] - labels: - my: label - your: label - - relabel_configs: - - source_labels: [job, __meta_dns_srv_name] - regex: (.*)some-[regex]$ - target_label: job - replacement: foo-${1} - # action defaults to 'replace' + relabel_configs: + - source_labels: [job, __meta_dns_srv_name] + regex: (.*)some-[regex]$ + target_label: job + replacement: foo-${1} + # action defaults to 'replace' - - job_name: service-x +- job_name: service-x - basic_auth: - username: admin - password: password + basic_auth: + username: admin + password: password - scrape_interval: 50s - scrape_timeout: 5s + scrape_interval: 50s + scrape_timeout: 5s - metrics_path: /my_path - scheme: https + metrics_path: /my_path + scheme: https - dns_sd_configs: - - refresh_interval: 15s - names: - - first.dns.address.domain.com - - second.dns.address.domain.com - - names: - - first.dns.address.domain.com - # refresh_interval defaults to 30s. + dns_sd_configs: + - refresh_interval: 15s + names: + - first.dns.address.domain.com + - second.dns.address.domain.com + - names: + - first.dns.address.domain.com + # refresh_interval defaults to 30s. - relabel_configs: - - source_labels: [job] - regex: (.*)some-[regex]$ - action: drop + relabel_configs: + - source_labels: [job] + regex: (.*)some-[regex]$ + action: drop + + +- job_name: service-y + + consul_sd_configs: + - server: 'localhost:1234' + services: ['nginx', 'cache', 'mysql'] \ No newline at end of file diff --git a/config/testdata/unknown_attr.bad.yml b/config/testdata/unknown_attr.bad.yml new file mode 100644 index 0000000000..4baaf565c4 --- /dev/null +++ b/config/testdata/unknown_attr.bad.yml @@ -0,0 +1,20 @@ +# my global config +global: + scrape_interval: 15s + evaluation_interval: 30s + # scrape_timeout is set to the global default (10s). + + labels: + monitor: codelab + foo: bar + +rule_files: + - "first.rules" + - "second.rules" + - "my/*.rules" + +scrape_configs: + - job_name: prometheus + + consult_sd_configs: + - server: 'localhost:1234'