From de6f428f1080ba4d2033d8f94f9c3817d3acba12 Mon Sep 17 00:00:00 2001 From: Santiago Date: Mon, 13 May 2024 22:03:56 +0200 Subject: [PATCH] Add YAML unmarshalers for Alertmanager configuration struct --- definition/alertmanager.go | 53 ++++++++++++----------- definition/alertmanager_test.go | 75 +++++++++++++++++++++++++++------ 2 files changed, 90 insertions(+), 38 deletions(-) diff --git a/definition/alertmanager.go b/definition/alertmanager.go index dcf2d27b..61c98940 100644 --- a/definition/alertmanager.go +++ b/definition/alertmanager.go @@ -10,6 +10,7 @@ import ( "github.com/prometheus/alertmanager/config" "github.com/prometheus/alertmanager/pkg/labels" "github.com/prometheus/common/model" + "gopkg.in/yaml.v3" ) type Provenance string @@ -22,7 +23,7 @@ type Config struct { // MuteTimeIntervals is deprecated and will be removed before Alertmanager 1.0. MuteTimeIntervals []config.MuteTimeInterval `yaml:"mute_time_intervals,omitempty" json:"mute_time_intervals,omitempty"` TimeIntervals []config.TimeInterval `yaml:"time_intervals,omitempty" json:"time_intervals,omitempty"` - Templates []string `yaml:"templates" json:"templates"` + Templates []string `yaml:"templates,omitempty" json:"templates,omitempty"` } // A Route is a node that contains definitions of how to handle alerts. This is modified @@ -125,17 +126,19 @@ func (r *Route) ResourceID() string { // post-validation is included in the UnmarshalYAML method. Here we simply run this with // a noop unmarshaling function in order to benefit from said validation. func (c *Config) UnmarshalJSON(b []byte) error { + return yaml.Unmarshal(b, c) +} + +func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error { type plain Config - if err := json.Unmarshal(b, (*plain)(c)); err != nil { + if err := unmarshal((*plain)(c)); err != nil { return err } - noopUnmarshal := func(_ interface{}) error { return nil } - - if c.Global != nil { - if err := c.Global.UnmarshalYAML(noopUnmarshal); err != nil { - return err - } + // Having a nil global config causes panics in the Alertmanager codebase. + if c.Global == nil { + c.Global = &config.GlobalConfig{} + *c.Global = config.DefaultGlobalConfig() } if c.Route == nil { @@ -148,7 +151,7 @@ func (c *Config) UnmarshalJSON(b []byte) error { } for _, r := range c.InhibitRules { - if err := r.UnmarshalYAML(noopUnmarshal); err != nil { + if err := r.UnmarshalYAML(unmarshal); err != nil { return err } } @@ -215,19 +218,23 @@ func (c *PostableApiAlertingConfig) GetRoute() *Route { } func (c *PostableApiAlertingConfig) UnmarshalJSON(b []byte) error { + return yaml.Unmarshal(b, c) +} + +func (c *PostableApiAlertingConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { type plain PostableApiAlertingConfig - if err := json.Unmarshal(b, (*plain)(c)); err != nil { + if err := unmarshal((*plain)(c)); err != nil { return err } - // Since Config implements json.Unmarshaler, we must handle _all_ other fields independently. + // Since Config implements yaml.Unmarshaler, we must handle _all_ other fields independently. // Otherwise, the json decoder will detect this and only use the embedded type. // Additionally, we'll use pointers to slices in order to reference the intended target. type overrides struct { - Receivers *[]*PostableApiReceiver `yaml:"receivers,omitempty" json:"receivers,omitempty"` + Receivers *[]*PostableApiReceiver `yaml:"receivers" json:"receivers,omitempty"` } - if err := json.Unmarshal(b, &overrides{Receivers: &c.Receivers}); err != nil { + if err := unmarshal(&overrides{Receivers: &c.Receivers}); err != nil { return err } @@ -371,8 +378,8 @@ type PostableGrafanaReceiver struct { Name string `json:"name"` Type string `json:"type"` DisableResolveMessage bool `json:"disableResolveMessage"` - Settings RawMessage `json:"settings,omitempty"` - SecureSettings map[string]string `json:"secureSettings"` + Settings RawMessage `json:"settings,omitempty" yaml:"settings,omitempty"` + SecureSettings map[string]string `json:"secureSettings,omitempty" yaml:"secureSettings,omitempty"` } type ReceiverType int @@ -522,21 +529,17 @@ type PostableApiReceiver struct { PostableGrafanaReceivers `yaml:",inline"` } +func (r *PostableApiReceiver) UnmarshalJSON(b []byte) error { + return yaml.Unmarshal(b, r) +} + func (r *PostableApiReceiver) UnmarshalYAML(unmarshal func(interface{}) error) error { if err := unmarshal(&r.PostableGrafanaReceivers); err != nil { return err } - if err := unmarshal(&r.Receiver); err != nil { - return err - } - - return nil -} - -func (r *PostableApiReceiver) UnmarshalJSON(b []byte) error { - type plain PostableApiReceiver - if err := json.Unmarshal(b, (*plain)(r)); err != nil { + type plain config.Receiver + if err := unmarshal((*plain)(&r.Receiver)); err != nil { return err } diff --git a/definition/alertmanager_test.go b/definition/alertmanager_test.go index 59f8e290..27d1bc04 100644 --- a/definition/alertmanager_test.go +++ b/definition/alertmanager_test.go @@ -22,8 +22,12 @@ func Test_ApiReceiver_Marshaling(t *testing.T) { desc: "success AM", input: PostableApiReceiver{ Receiver: config.Receiver{ - Name: "foo", - EmailConfigs: []*config.EmailConfig{{}}, + Name: "foo", + EmailConfigs: []*config.EmailConfig{{ + To: "test@test.com", + HTML: config.DefaultEmailConfig.HTML, + Headers: map[string]string{}, + }}, }, }, }, @@ -42,8 +46,12 @@ func Test_ApiReceiver_Marshaling(t *testing.T) { desc: "failure mixed", input: PostableApiReceiver{ Receiver: config.Receiver{ - Name: "foo", - EmailConfigs: []*config.EmailConfig{{}}, + Name: "foo", + EmailConfigs: []*config.EmailConfig{{ + To: "test@test.com", + HTML: config.DefaultEmailConfig.HTML, + Headers: map[string]string{}, + }}, }, PostableGrafanaReceivers: PostableGrafanaReceivers{ GrafanaManagedReceivers: []*PostableGrafanaReceiver{{}}, @@ -88,8 +96,12 @@ func Test_APIReceiverType(t *testing.T) { desc: "am", input: PostableApiReceiver{ Receiver: config.Receiver{ - Name: "foo", - EmailConfigs: []*config.EmailConfig{{}}, + Name: "foo", + EmailConfigs: []*config.EmailConfig{{ + To: "test@test.com", + HTML: config.DefaultEmailConfig.HTML, + Headers: map[string]string{}, + }}, }, }, expected: AlertmanagerReceiverType, @@ -140,6 +152,7 @@ func Test_AllReceivers(t *testing.T) { } func Test_ApiAlertingConfig_Marshaling(t *testing.T) { + defaultGlobalConfig := config.DefaultGlobalConfig() for _, tc := range []struct { desc string input PostableApiAlertingConfig @@ -149,6 +162,7 @@ func Test_ApiAlertingConfig_Marshaling(t *testing.T) { desc: "success am", input: PostableApiAlertingConfig{ Config: Config{ + Global: &defaultGlobalConfig, Route: &Route{ Receiver: "am", Routes: []*Route{ @@ -161,8 +175,12 @@ func Test_ApiAlertingConfig_Marshaling(t *testing.T) { Receivers: []*PostableApiReceiver{ { Receiver: config.Receiver{ - Name: "am", - EmailConfigs: []*config.EmailConfig{{}}, + Name: "am", + EmailConfigs: []*config.EmailConfig{{ + To: "test@test.com", + HTML: config.DefaultEmailConfig.HTML, + Headers: map[string]string{}, + }}, }, }, }, @@ -172,6 +190,7 @@ func Test_ApiAlertingConfig_Marshaling(t *testing.T) { desc: "success graf", input: PostableApiAlertingConfig{ Config: Config{ + Global: &defaultGlobalConfig, Route: &Route{ Receiver: "graf", Routes: []*Route{ @@ -197,6 +216,7 @@ func Test_ApiAlertingConfig_Marshaling(t *testing.T) { desc: "failure undefined am receiver", input: PostableApiAlertingConfig{ Config: Config{ + Global: &defaultGlobalConfig, Route: &Route{ Receiver: "am", Routes: []*Route{ @@ -209,8 +229,12 @@ func Test_ApiAlertingConfig_Marshaling(t *testing.T) { Receivers: []*PostableApiReceiver{ { Receiver: config.Receiver{ - Name: "am", - EmailConfigs: []*config.EmailConfig{{}}, + Name: "am", + EmailConfigs: []*config.EmailConfig{{ + To: "test@test.com", + HTML: config.DefaultEmailConfig.HTML, + Headers: map[string]string{}, + }}, }, }, }, @@ -221,6 +245,7 @@ func Test_ApiAlertingConfig_Marshaling(t *testing.T) { desc: "failure undefined graf receiver", input: PostableApiAlertingConfig{ Config: Config{ + Global: &defaultGlobalConfig, Route: &Route{ Receiver: "graf", Routes: []*Route{ @@ -263,6 +288,7 @@ func Test_ApiAlertingConfig_Marshaling(t *testing.T) { desc: "failure graf no default receiver", input: PostableApiAlertingConfig{ Config: Config{ + Global: &defaultGlobalConfig, Route: &Route{ Routes: []*Route{ { @@ -288,6 +314,7 @@ func Test_ApiAlertingConfig_Marshaling(t *testing.T) { desc: "failure graf root route with matchers", input: PostableApiAlertingConfig{ Config: Config{ + Global: &defaultGlobalConfig, Route: &Route{ Receiver: "graf", Routes: []*Route{ @@ -315,6 +342,7 @@ func Test_ApiAlertingConfig_Marshaling(t *testing.T) { desc: "failure graf nested route duplicate group by labels", input: PostableApiAlertingConfig{ Config: Config{ + Global: &defaultGlobalConfig, Route: &Route{ Receiver: "graf", Routes: []*Route{ @@ -342,6 +370,7 @@ func Test_ApiAlertingConfig_Marshaling(t *testing.T) { desc: "success undefined am receiver in autogenerated route is ignored", input: PostableApiAlertingConfig{ Config: Config{ + Global: &defaultGlobalConfig, Route: &Route{ Receiver: "am", Routes: []*Route{ @@ -365,8 +394,12 @@ func Test_ApiAlertingConfig_Marshaling(t *testing.T) { Receivers: []*PostableApiReceiver{ { Receiver: config.Receiver{ - Name: "am", - EmailConfigs: []*config.EmailConfig{{}}, + Name: "am", + EmailConfigs: []*config.EmailConfig{{ + To: "test@test.com", + HTML: config.DefaultEmailConfig.HTML, + Headers: map[string]string{}, + }}, }, }, }, @@ -377,6 +410,7 @@ func Test_ApiAlertingConfig_Marshaling(t *testing.T) { desc: "success undefined graf receiver in autogenerated route is ignored", input: PostableApiAlertingConfig{ Config: Config{ + Global: &defaultGlobalConfig, Route: &Route{ Receiver: "graf", Routes: []*Route{ @@ -411,7 +445,7 @@ func Test_ApiAlertingConfig_Marshaling(t *testing.T) { err: false, }, } { - t.Run(tc.desc, func(t *testing.T) { + t.Run(tc.desc+" (json)", func(t *testing.T) { encoded, err := json.Marshal(tc.input) require.Nil(t, err) @@ -425,6 +459,21 @@ func Test_ApiAlertingConfig_Marshaling(t *testing.T) { require.Equal(t, tc.input, out) } }) + + t.Run(tc.desc+" (yaml)", func(t *testing.T) { + encoded, err := yaml.Marshal(tc.input) + require.Nil(t, err) + + var out PostableApiAlertingConfig + err = yaml.Unmarshal(encoded, &out) + + if tc.err { + require.Error(t, err) + } else { + require.Nil(t, err) + require.Equal(t, tc.input, out) + } + }) } }