From b5ad65fa3271775e0a5733efde9e0b6703595f09 Mon Sep 17 00:00:00 2001 From: Conor Broderick Date: Tue, 20 Jun 2017 18:09:14 +0100 Subject: [PATCH] Omit empty config fields and show regex upon re-marshalling to elide secrets (#864) * Omit empty config fields upon remarshalling to elide secrets * added test checking for empty or null fields and blank regexps --- config/config.go | 40 ++++----- config/config_test.go | 87 ++++++++++++++++++-- config/notifiers.go | 114 +++++++++++++------------- config/testdata/conf.empty-fields.yml | 27 ++++++ 4 files changed, 186 insertions(+), 82 deletions(-) create mode 100644 config/testdata/conf.empty-fields.yml diff --git a/config/config.go b/config/config.go index fb3af5bb..1429cf10 100644 --- a/config/config.go +++ b/config/config.go @@ -303,19 +303,19 @@ type GlobalConfig struct { // if it has not been updated. ResolveTimeout model.Duration `yaml:"resolve_timeout" json:"resolve_timeout"` - SMTPFrom string `yaml:"smtp_from" json:"smtp_from"` - SMTPSmarthost string `yaml:"smtp_smarthost" json:"smtp_smarthost"` - SMTPAuthUsername string `yaml:"smtp_auth_username" json:"smtp_auth_username"` - SMTPAuthPassword Secret `yaml:"smtp_auth_password" json:"smtp_auth_password"` - SMTPAuthSecret Secret `yaml:"smtp_auth_secret" json:"smtp_auth_secret"` - SMTPAuthIdentity string `yaml:"smtp_auth_identity" json:"smtp_auth_identity"` - SMTPRequireTLS bool `yaml:"smtp_require_tls" json:"smtp_require_tls"` - SlackAPIURL Secret `yaml:"slack_api_url" json:"slack_api_url"` - PagerdutyURL string `yaml:"pagerduty_url" json:"pagerduty_url"` - HipchatURL string `yaml:"hipchat_url" json:"hipchat_url"` - HipchatAuthToken Secret `yaml:"hipchat_auth_token" json:"hipchat_auth_token"` - OpsGenieAPIHost string `yaml:"opsgenie_api_host" json:"opsgenie_api_host"` - VictorOpsAPIURL string `yaml:"victorops_api_url" json:"victorops_api_url"` + SMTPFrom string `yaml:"smtp_from,omitempty" json:"smtp_from,omitempty"` + SMTPSmarthost string `yaml:"smtp_smarthost,omitempty" json:"smtp_smarthost,omitempty"` + SMTPAuthUsername string `yaml:"smtp_auth_username,omitempty" json:"smtp_auth_username,omitempty"` + SMTPAuthPassword Secret `yaml:"smtp_auth_password,omitempty" json:"smtp_auth_password,omitempty"` + SMTPAuthSecret Secret `yaml:"smtp_auth_secret,omitempty" json:"smtp_auth_secret,omitempty"` + SMTPAuthIdentity string `yaml:"smtp_auth_identity,omitempty" json:"smtp_auth_identity,omitempty"` + SMTPRequireTLS bool `yaml:"smtp_require_tls,omitempty" json:"smtp_require_tls,omitempty"` + SlackAPIURL Secret `yaml:"slack_api_url,omitempty" json:"slack_api_url,omitempty"` + PagerdutyURL string `yaml:"pagerduty_url,omitempty" json:"pagerduty_url,omitempty"` + HipchatURL string `yaml:"hipchat_url,omitempty" json:"hipchat_url,omitempty"` + HipchatAuthToken Secret `yaml:"hipchat_auth_token,omitempty" json:"hipchat_auth_token,omitempty"` + OpsGenieAPIHost string `yaml:"opsgenie_api_host,omitempty" json:"opsgenie_api_host,omitempty"` + VictorOpsAPIURL string `yaml:"victorops_api_url,omitempty" json:"victorops_api_url,omitempty"` // Catches all undefined fields and must be empty after parsing. XXX map[string]interface{} `yaml:",inline" json:"-"` @@ -386,19 +386,19 @@ func (r *Route) UnmarshalYAML(unmarshal func(interface{}) error) error { type InhibitRule struct { // SourceMatch defines a set of labels that have to equal the given // value for source alerts. - SourceMatch map[string]string `yaml:"source_match" json:"source_match"` + SourceMatch map[string]string `yaml:"source_match,omitempty" json:"source_match,omitempty"` // SourceMatchRE defines pairs like SourceMatch but does regular expression // matching. - SourceMatchRE map[string]Regexp `yaml:"source_match_re" json:"source_match_re"` + SourceMatchRE map[string]Regexp `yaml:"source_match_re,omitempty" json:"source_match_re,omitempty"` // TargetMatch defines a set of labels that have to equal the given // value for target alerts. - TargetMatch map[string]string `yaml:"target_match" json:"target_match"` + TargetMatch map[string]string `yaml:"target_match,omitempty" json:"target_match,omitempty"` // TargetMatchRE defines pairs like TargetMatch but does regular expression // matching. - TargetMatchRE map[string]Regexp `yaml:"target_match_re" json:"target_match_re"` + TargetMatchRE map[string]Regexp `yaml:"target_match_re,omitempty" json:"target_match_re,omitempty"` // A set of labels that must be equal between the source and target alert // for them to be a match. - Equal model.LabelNames `yaml:"equal" json:"equal"` + Equal model.LabelNames `yaml:"equal,omitempty" json:"equal,omitempty"` // Catches all undefined fields and must be empty after parsing. XXX map[string]interface{} `yaml:",inline" json:"-"` @@ -488,8 +488,8 @@ func (re *Regexp) UnmarshalYAML(unmarshal func(interface{}) error) error { } // MarshalYAML implements the yaml.Marshaler interface. -func (re *Regexp) MarshalYAML() (interface{}, error) { - if re != nil { +func (re Regexp) MarshalYAML() (interface{}, error) { + if re.Regexp != nil { return re.String(), nil } return nil, nil diff --git a/config/config_test.go b/config/config_test.go index c99e5f4f..b284dcf9 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -15,11 +15,13 @@ package config import ( "encoding/json" - "fmt" + "reflect" "regexp" "strings" "testing" + "time" + "github.com/prometheus/common/model" "github.com/stretchr/testify/require" "gopkg.in/yaml.v2" ) @@ -69,14 +71,13 @@ receivers: func TestHideConfigSecrets(t *testing.T) { c, _, err := LoadFile("testdata/conf.good.yml") if err != nil { - t.Errorf("Error parsing %s: %s", "testdata/good.yml", err) + t.Errorf("Error parsing %s: %s", "testdata/conf.good.yml", err) } // String method must not reveal authentication credentials. s := c.String() secretRe := regexp.MustCompile("") matches := secretRe.FindAllStringIndex(s, -1) - fmt.Println(len(matches)) if len(matches) != 14 || strings.Contains(s, "mysecret") { t.Fatal("config's String method reveals authentication credentials.") } @@ -85,7 +86,7 @@ func TestHideConfigSecrets(t *testing.T) { func TestJSONMarshal(t *testing.T) { c, _, err := LoadFile("testdata/conf.good.yml") if err != nil { - t.Errorf("Error parsing %s: %s", "testdata/good.yml", err) + t.Errorf("Error parsing %s: %s", "testdata/conf.good.yml", err) } _, err = json.Marshal(c) @@ -114,7 +115,7 @@ func TestJSONMarshalSecret(t *testing.T) { func TestJSONUnmarshalMarshaled(t *testing.T) { c, _, err := LoadFile("testdata/conf.good.yml") if err != nil { - t.Errorf("Error parsing %s: %s", "testdata/good.yml", err) + t.Errorf("Error parsing %s: %s", "testdata/conf.good.yml", err) } plainCfg, err := json.Marshal(c) @@ -128,3 +129,79 @@ func TestJSONUnmarshalMarshaled(t *testing.T) { t.Fatal("JSON Unmarshaling failed:", err) } } + +func TestEmptyFieldsAndRegex(t *testing.T) { + + boolFoo := true + var regexpFoo Regexp + regexpFoo.Regexp, _ = regexp.Compile("^(?:^(foo1|foo2|baz)$)$") + + var expectedConf = Config{ + + Global: &GlobalConfig{ + ResolveTimeout: model.Duration(5 * time.Minute), + SMTPSmarthost: "localhost:25", + SMTPFrom: "alertmanager@example.org", + HipchatAuthToken: "mysecret", + HipchatURL: "https://hipchat.foobar.org/", + SlackAPIURL: "mysecret", + SMTPRequireTLS: true, + PagerdutyURL: "https://events.pagerduty.com/generic/2010-04-15/create_event.json", + OpsGenieAPIHost: "https://api.opsgenie.com/", + VictorOpsAPIURL: "https://alert.victorops.com/integrations/generic/20131114/alert/", + }, + + Templates: []string{ + "/etc/alertmanager/template/*.tmpl", + }, + Route: &Route{ + Receiver: "team-X-mails", + GroupBy: []model.LabelName{ + "alertname", + "cluster", + "service", + }, + Routes: []*Route{ + { + Receiver: "team-X-mails", + MatchRE: map[string]Regexp{ + "service": regexpFoo, + }, + }, + }, + }, + Receivers: []*Receiver{ + { + Name: "team-X-mails", + EmailConfigs: []*EmailConfig{ + { + To: "team-X+alerts@example.org", + From: "alertmanager@example.org", + Smarthost: "localhost:25", + HTML: "{{ template \"email.default.html\" . }}", + RequireTLS: &boolFoo, + }, + }, + }, + }, + } + + config, _, err := LoadFile("testdata/conf.empty-fields.yml") + if err != nil { + t.Errorf("Error parsing %s: %s", "testdata/conf.empty-fields.yml", err) + } + + configGot, err := yaml.Marshal(config) + if err != nil { + t.Fatal("YAML Marshaling failed:", err) + } + + configExp, err := yaml.Marshal(expectedConf) + if err != nil { + t.Fatalf("%s", err) + } + + if !reflect.DeepEqual(configGot, configExp) { + t.Fatalf("%s: unexpected config result: \n\n%s\n expected\n\n%s", "testdata/conf.empty-fields.yml", configGot, configExp) + } +} diff --git a/config/notifiers.go b/config/notifiers.go index 53cc3d43..4eb21df2 100644 --- a/config/notifiers.go +++ b/config/notifiers.go @@ -131,15 +131,15 @@ type EmailConfig struct { NotifierConfig `yaml:",inline" json:",inline"` // Email address to notify. - To string `yaml:"to" json:"to"` - From string `yaml:"from" json:"from"` + To string `yaml:"to,omitempty" json:"to,omitempty"` + From string `yaml:"from,omitempty" json:"from,omitempty"` Smarthost string `yaml:"smarthost,omitempty" json:"smarthost,omitempty"` - AuthUsername string `yaml:"auth_username" json:"auth_username"` - AuthPassword Secret `yaml:"auth_password" json:"auth_password"` - AuthSecret Secret `yaml:"auth_secret" json:"auth_secret"` - AuthIdentity string `yaml:"auth_identity" json:"auth_identity"` - Headers map[string]string `yaml:"headers" json:"headers"` - HTML string `yaml:"html" json:"html"` + AuthUsername string `yaml:"auth_username,omitempty" json:"auth_username,omitempty"` + AuthPassword Secret `yaml:"auth_password,omitempty" json:"auth_password,omitempty"` + AuthSecret Secret `yaml:"auth_secret,omitempty" json:"auth_secret,omitempty"` + AuthIdentity string `yaml:"auth_identity,omitempty" json:"auth_identity,omitempty"` + Headers map[string]string `yaml:"headers,omitempty" json:"headers,omitempty"` + HTML string `yaml:"html,omitempty" json:"html,omitempty"` RequireTLS *bool `yaml:"require_tls,omitempty" json:"require_tls,omitempty"` // Catches all undefined fields and must be empty after parsing. @@ -174,12 +174,12 @@ func (c *EmailConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { type PagerdutyConfig struct { NotifierConfig `yaml:",inline" json:",inline"` - ServiceKey Secret `yaml:"service_key" json:"service_key"` - URL string `yaml:"url" json:"url"` - Client string `yaml:"client" json:"client"` - ClientURL string `yaml:"client_url" json:"client_url"` - Description string `yaml:"description" json:"description"` - Details map[string]string `yaml:"details" json:"details"` + ServiceKey Secret `yaml:"service_key,omitempty" json:"service_key,omitempty"` + URL string `yaml:"url,omitempty" json:"url,omitempty"` + Client string `yaml:"client,omitempty" json:"client,omitempty"` + ClientURL string `yaml:"client_url,omitempty" json:"client_url,omitempty"` + Description string `yaml:"description,omitempty" json:"description,omitempty"` + Details map[string]string `yaml:"details,omitempty" json:"details,omitempty"` // Catches all undefined fields and must be empty after parsing. XXX map[string]interface{} `yaml:",inline" json:"-"` @@ -202,20 +202,20 @@ func (c *PagerdutyConfig) UnmarshalYAML(unmarshal func(interface{}) error) error type SlackConfig struct { NotifierConfig `yaml:",inline" json:",inline"` - APIURL Secret `yaml:"api_url" json:"api_url"` + APIURL Secret `yaml:"api_url,omitempty" json:"api_url,omitempty"` // Slack channel override, (like #other-channel or @username). - Channel string `yaml:"channel" json:"channel"` - Username string `yaml:"username" json:"username"` - Color string `yaml:"color" json:"color"` + Channel string `yaml:"channel,omitempty" json:"channel,omitempty"` + Username string `yaml:"username,omitempty" json:"username,omitempty"` + Color string `yaml:"color,omitempty" json:"color,omitempty"` - Title string `yaml:"title" json:"title"` - TitleLink string `yaml:"title_link" json:"title_link"` - Pretext string `yaml:"pretext" json:"pretext"` - Text string `yaml:"text" json:"text"` - Fallback string `yaml:"fallback" json:"fallback"` - IconEmoji string `yaml:"icon_emoji" json:"icon_emoji"` - IconURL string `yaml:"icon_url" json:"icon_url"` + Title string `yaml:"title,omitempty" json:"title,omitempty"` + TitleLink string `yaml:"title_link,omitempty" json:"title_link,omitempty"` + Pretext string `yaml:"pretext,omitempty" json:"pretext,omitempty"` + Text string `yaml:"text,omitempty" json:"text,omitempty"` + Fallback string `yaml:"fallback,omitempty" json:"fallback,omitempty"` + IconEmoji string `yaml:"icon_emoji,omitempty" json:"icon_emoji,omitempty"` + IconURL string `yaml:"icon_url,omitempty" json:"icon_url,omitempty"` // Catches all undefined fields and must be empty after parsing. XXX map[string]interface{} `yaml:",inline" json:"-"` @@ -235,17 +235,17 @@ func (c *SlackConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { type HipchatConfig struct { NotifierConfig `yaml:",inline" json:",inline"` - APIURL string `yaml:"api_url" json:"api_url"` - AuthToken Secret `yaml:"auth_token" json:"auth_token"` - RoomID string `yaml:"room_id" json:"room_id"` - From string `yaml:"from" json:"from"` - Notify bool `yaml:"notify" json:"notify"` - Message string `yaml:"message" json:"message"` - MessageFormat string `yaml:"message_format" json:"message_format"` - Color string `yaml:"color" json:"color"` + APIURL string `yaml:"api_url,omitempty" json:"api_url,omitempty"` + AuthToken Secret `yaml:"auth_token,omitempty" json:"auth_token,omitempty"` + RoomID string `yaml:"room_id,omitempty" json:"room_id,omitempty"` + From string `yaml:"from,omitempty" json:"from,omitempty"` + Notify bool `yaml:"notify,omitempty" json:"notify,omitempty"` + Message string `yaml:"message,omitempty" json:"message,omitempty"` + MessageFormat string `yaml:"message_format,omitempty" json:"message_format,omitempty"` + Color string `yaml:"color,omitempty" json:"color,omitempty"` // Catches all undefined fields and must be empty after parsing. - XXX map[string]interface{} `yaml:",inline" json:"-"` + XXX map[string]interface{} `yaml:",inline" ,json:"-"` } // UnmarshalYAML implements the yaml.Unmarshaler interface. @@ -290,15 +290,15 @@ func (c *WebhookConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { type OpsGenieConfig struct { NotifierConfig `yaml:",inline" json:",inline"` - APIKey Secret `yaml:"api_key" json:"api_key"` - APIHost string `yaml:"api_host" json:"api_host"` - Message string `yaml:"message" json:"message"` - Description string `yaml:"description" json:"description"` - Source string `yaml:"source" json:"source"` - Details map[string]string `yaml:"details" json:"details"` - Teams string `yaml:"teams" json:"teams"` - Tags string `yaml:"tags" json:"tags"` - Note string `yaml:"note" json:"note"` + APIKey Secret `yaml:"api_key,omitempty" json:"api_key,omitempty"` + APIHost string `yaml:"api_host,omitempty" json:"api_host,omitempty"` + Message string `yaml:"message,omitempty" json:"message,omitempty"` + Description string `yaml:"description,omitempty" json:"description,omitempty"` + Source string `yaml:"source,omitempty" json:"source,omitempty"` + Details map[string]string `yaml:"details,omitempty" json:"details,omitempty"` + Teams string `yaml:"teams,omitempty" json:"teams,omitempty"` + Tags string `yaml:"tags,omitempty" json:"tags,omitempty"` + Note string `yaml:"note,omitempty" json:"note,omitempty"` // Catches all undefined fields and must be empty after parsing. XXX map[string]interface{} `yaml:",inline" json:"-"` @@ -321,12 +321,12 @@ func (c *OpsGenieConfig) UnmarshalYAML(unmarshal func(interface{}) error) error type VictorOpsConfig struct { NotifierConfig `yaml:",inline" json:",inline"` - APIKey Secret `yaml:"api_key" json:"api_key"` - APIURL string `yaml:"api_url" json:"api_url"` - RoutingKey string `yaml:"routing_key" json:"routing_key"` - MessageType string `yaml:"message_type" json:"message_type"` - StateMessage string `yaml:"state_message" json:"state_message"` - MonitoringTool string `yaml:"monitoring_tool" json:"monitoring_tool"` + APIKey Secret `yaml:"api_key,omitempty" json:"api_key,omitempty"` + APIURL string `yaml:"api_url,omitempty" json:"api_url,omitempty"` + RoutingKey string `yaml:"routing_key,omitempty" json:"routing_key,omitempty"` + MessageType string `yaml:"message_type,omitempty" json:"message_type,omitempty"` + StateMessage string `yaml:"state_message,omitempty" json:"state_message,omitempty"` + MonitoringTool string `yaml:"monitoring_tool,omitempty" json:"monitoring_tool,omitempty"` XXX map[string]interface{} `yaml:",inline" json:"-"` } @@ -364,14 +364,14 @@ func (d duration) MarshalText() ([]byte, error) { type PushoverConfig struct { NotifierConfig `yaml:",inline" json:",inline"` - UserKey Secret `yaml:"user_key" json:"user_key"` - Token Secret `yaml:"token" json:"token"` - Title string `yaml:"title" json:"title"` - Message string `yaml:"message" json:"message"` - URL string `yaml:"url" json:"url"` - Priority string `yaml:"priority" json:"priority"` - Retry duration `yaml:"retry" json:"retry"` - Expire duration `yaml:"expire" json:"expire"` + UserKey Secret `yaml:"user_key,omitempty" json:"user_key,omitempty"` + Token Secret `yaml:"token,omitempty" json:"token,omitempty"` + Title string `yaml:"title,omitempty" json:"title,omitempty"` + Message string `yaml:"message,omitempty" json:"message,omitempty"` + URL string `yaml:"url,omitempty" json:"url,omitempty"` + Priority string `yaml:"priority,omitempty" json:"priority,omitempty"` + Retry duration `yaml:"retry,omitempty" json:"retry,omitempty"` + Expire duration `yaml:"expire,omitempty" json:"expire,omitempty"` // Catches all undefined fields and must be empty after parsing. XXX map[string]interface{} `yaml:",inline" json:"-"` diff --git a/config/testdata/conf.empty-fields.yml b/config/testdata/conf.empty-fields.yml new file mode 100644 index 00000000..e4fbe01f --- /dev/null +++ b/config/testdata/conf.empty-fields.yml @@ -0,0 +1,27 @@ +global: + smtp_smarthost: 'localhost:25' + smtp_from: 'alertmanager@example.org' + smtp_auth_username: '' + smtp_auth_password: '' + hipchat_auth_token: 'mysecret' + hipchat_url: 'https://hipchat.foobar.org/' + slack_api_url: 'mysecret' + + + +templates: +- '/etc/alertmanager/template/*.tmpl' + +route: + group_by: ['alertname', 'cluster', 'service'] + + receiver: team-X-mails + routes: + - match_re: + service: ^(foo1|foo2|baz)$ + receiver: team-X-mails + +receivers: +- name: 'team-X-mails' + email_configs: + - to: 'team-X+alerts@example.org'