diff --git a/pkg/services/ngalert/api/tooling/definitions/alertmanager.go b/pkg/services/ngalert/api/tooling/definitions/alertmanager.go index 3c3f9b5d4f3..f8fe91444ac 100644 --- a/pkg/services/ngalert/api/tooling/definitions/alertmanager.go +++ b/pkg/services/ngalert/api/tooling/definitions/alertmanager.go @@ -413,6 +413,17 @@ func (c *GettableApiAlertingConfig) UnmarshalJSON(b []byte) error { return err } + // Since Config implements json.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 *[]*GettableApiReceiver `yaml:"receivers,omitempty" json:"receivers,omitempty"` + } + + if err := json.Unmarshal(b, &overrides{Receivers: &c.Receivers}); err != nil { + return err + } + return c.validate() } @@ -452,10 +463,42 @@ type Config struct { Global *config.GlobalConfig `yaml:"global,omitempty" json:"global,omitempty"` Route *config.Route `yaml:"route,omitempty" json:"route,omitempty"` InhibitRules []*config.InhibitRule `yaml:"inhibit_rules,omitempty" json:"inhibit_rules,omitempty"` - Receivers []*config.Receiver `yaml:"-" json:"receivers,omitempty"` Templates []string `yaml:"templates" json:"templates"` } +// Config is the entrypoint for the embedded Alertmanager config with the exception of receivers. +// Prometheus historically uses yaml files as the method of configuration and thus some +// 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 { + type plain Config + if err := json.Unmarshal(b, (*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 + } + } + + if c.Route != nil { + if err := c.Route.UnmarshalYAML(noopUnmarshal); err != nil { + return err + } + } + + for _, r := range c.InhibitRules { + if err := r.UnmarshalYAML(noopUnmarshal); err != nil { + return err + } + } + + return nil +} + type PostableApiAlertingConfig struct { Config `yaml:",inline"` @@ -469,6 +512,17 @@ func (c *PostableApiAlertingConfig) UnmarshalJSON(b []byte) error { return err } + // Since Config implements json.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"` + } + + if err := json.Unmarshal(b, &overrides{Receivers: &c.Receivers}); err != nil { + return err + } + return c.validate() } diff --git a/pkg/services/ngalert/api/tooling/definitions/alertmanager_test.go b/pkg/services/ngalert/api/tooling/definitions/alertmanager_test.go index 529e69c56b2..fcce3364d43 100644 --- a/pkg/services/ngalert/api/tooling/definitions/alertmanager_test.go +++ b/pkg/services/ngalert/api/tooling/definitions/alertmanager_test.go @@ -7,6 +7,7 @@ import ( "testing" "github.com/prometheus/alertmanager/config" + "github.com/prometheus/common/model" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gopkg.in/yaml.v3" @@ -563,3 +564,14 @@ func Test_ReceiverMatchesBackend(t *testing.T) { }) } } + +func Test_Marshaling_Validation(t *testing.T) { + jsonEncoded, err := ioutil.ReadFile("alertmanager_test_artifact.json") + require.Nil(t, err) + + var tmp GettableUserConfig + require.Nil(t, json.Unmarshal(jsonEncoded, &tmp)) + + expected := []model.LabelName{"alertname"} + require.Equal(t, expected, tmp.AlertmanagerConfig.Config.Route.GroupBy) +} diff --git a/pkg/services/ngalert/api/tooling/definitions/alertmanager_test_artifact.json b/pkg/services/ngalert/api/tooling/definitions/alertmanager_test_artifact.json index 42b62fd9f75..cc68eb49359 100644 --- a/pkg/services/ngalert/api/tooling/definitions/alertmanager_test_artifact.json +++ b/pkg/services/ngalert/api/tooling/definitions/alertmanager_test_artifact.json @@ -23,6 +23,9 @@ ], "route": { "continue": false, + "group_by": [ + "alertname" + ], "receiver": "am", "routes": [ { diff --git a/pkg/services/ngalert/api/tooling/definitions/alertmanager_test_artifact.yaml b/pkg/services/ngalert/api/tooling/definitions/alertmanager_test_artifact.yaml index 3b8a30ba18a..0ecbfd8a7e4 100644 --- a/pkg/services/ngalert/api/tooling/definitions/alertmanager_test_artifact.yaml +++ b/pkg/services/ngalert/api/tooling/definitions/alertmanager_test_artifact.yaml @@ -14,6 +14,8 @@ alertmanager_config: | name: am route: continue: false + group_by: + - alertname receiver: am routes: - continue: false diff --git a/pkg/tests/api/alerting/api_notification_channel_test.go b/pkg/tests/api/alerting/api_notification_channel_test.go index 14e2e07e1fd..489adace00d 100644 --- a/pkg/tests/api/alerting/api_notification_channel_test.go +++ b/pkg/tests/api/alerting/api_notification_channel_test.go @@ -748,10 +748,10 @@ var expNotifications = map[string][]string{ "icon_url": "https://awesomeemoji.com/rocket", "attachments": [ { - "title": "Integration Test [FIRING:1] (SlackAlert1 UID_SlackAlert1)", + "title": "Integration Test [FIRING:1] SlackAlert1 (UID_SlackAlert1)", "title_link": "http:/localhost:3000/alerting/list", "text": "Integration Test ", - "fallback": "Integration Test [FIRING:1] (SlackAlert1 UID_SlackAlert1)", + "fallback": "Integration Test [FIRING:1] SlackAlert1 (UID_SlackAlert1)", "footer": "Grafana v", "footer_icon": "https://grafana.com/assets/img/fav32.png", "color": "#D63232", @@ -775,10 +775,10 @@ var expNotifications = map[string][]string{ "username": "Integration Test", "attachments": [ { - "title": "[FIRING:1] (SlackAlert2 UID_SlackAlert2)", + "title": "[FIRING:1] SlackAlert2 (UID_SlackAlert2)", "title_link": "http:/localhost:3000/alerting/list", "text": "\n**Firing**\nLabels:\n - alertname = SlackAlert2\n - __alert_rule_uid__ = UID_SlackAlert2\nAnnotations:\nSource: \n\n\n\n\n", - "fallback": "[FIRING:1] (SlackAlert2 UID_SlackAlert2)", + "fallback": "[FIRING:1] SlackAlert2 (UID_SlackAlert2)", "footer": "Grafana v", "footer_icon": "https://grafana.com/assets/img/fav32.png", "color": "#D63232", @@ -799,11 +799,11 @@ var expNotifications = map[string][]string{ "pagerduty_recvX/pagerduty_testX": { `{ "routing_key": "pagerduty_recv/pagerduty_test", - "dedup_key": "718643b9694d44f7f2b21458afd1b079cb403cf264e51894ff3c9745238bcced", - "description": "[FIRING:1] (PagerdutyAlert UID_PagerdutyAlert)", + "dedup_key": "234edb34441f942f713f3c2ccf58b1d719d921b4cbe34e57a1630f1dee847e3b", + "description": "[FIRING:1] PagerdutyAlert (UID_PagerdutyAlert)", "event_action": "trigger", "payload": { - "summary": "Integration Test [FIRING:1] (PagerdutyAlert UID_PagerdutyAlert)", + "summary": "Integration Test [FIRING:1] PagerdutyAlert (UID_PagerdutyAlert)", "source": "%s", "severity": "warning", "class": "testclass", @@ -831,7 +831,7 @@ var expNotifications = map[string][]string{ "link": { "messageUrl": "dingtalk://dingtalkclient/page/link?pc_slide=false&url=http%3A%2Flocalhost%3A3000%2Falerting%2Flist", "text": "\n**Firing**\nLabels:\n - alertname = DingDingAlert\n - __alert_rule_uid__ = UID_DingDingAlert\nAnnotations:\nSource: \n\n\n\n\n", - "title": "[FIRING:1] (DingDingAlert UID_DingDingAlert)" + "title": "[FIRING:1] DingDingAlert (UID_DingDingAlert)" }, "msgtype": "link" }`, @@ -859,9 +859,9 @@ var expNotifications = map[string][]string{ "title": "Details" } ], - "summary": "[FIRING:1] (TeamsAlert UID_TeamsAlert)", + "summary": "[FIRING:1] TeamsAlert (UID_TeamsAlert)", "themeColor": "#D63232", - "title": "[FIRING:1] (TeamsAlert UID_TeamsAlert)" + "title": "[FIRING:1] TeamsAlert (UID_TeamsAlert)" }`, }, "webhook_recv/webhook_test": { @@ -882,7 +882,7 @@ var expNotifications = map[string][]string{ "fingerprint": "929467973978d053" } ], - "groupLabels": {}, + "groupLabels": {"alertname": "WebhookAlert"}, "commonLabels": { "__alert_rule_uid__": "UID_WebhookAlert", "alertname": "WebhookAlert" @@ -890,9 +890,9 @@ var expNotifications = map[string][]string{ "commonAnnotations": {}, "externalURL": "http://localhost:3000/", "version": "1", - "groupKey": "{}/{alertname=\"WebhookAlert\"}:{}", + "groupKey": "{}/{alertname=\"WebhookAlert\"}:{alertname=\"WebhookAlert\"}", "truncatedAlerts": 0, - "title": "[FIRING:1] (WebhookAlert UID_WebhookAlert)", + "title": "[FIRING:1] WebhookAlert (UID_WebhookAlert)", "state": "alerting", "message": "\n**Firing**\nLabels:\n - alertname = WebhookAlert\n - __alert_rule_uid__ = UID_WebhookAlert\nAnnotations:\nSource: \n\n\n\n\n" }`,