From 7e63a01a798d0f04e509c656943deca276910e75 Mon Sep 17 00:00:00 2001 From: Tito Lins Date: Mon, 6 Oct 2025 14:23:21 +0200 Subject: [PATCH] alerting: omit optional notification settings fields (#112049) --- pkg/services/ngalert/api/api_provisioning.go | 21 ++++- .../ngalert/api/api_ruler_export_test.go | 27 ++++++ pkg/services/ngalert/api/compat/compat.go | 6 +- ...st-rulegroup-simplified-routing-export.hcl | 45 +++++++++ .../post-rulegroup-simplified-routing.json | 94 +++++++++++++++++++ .../definitions/provisioning_alert_rules.go | 14 +-- 6 files changed, 192 insertions(+), 15 deletions(-) create mode 100644 pkg/services/ngalert/api/test-data/post-rulegroup-simplified-routing-export.hcl create mode 100644 pkg/services/ngalert/api/test-data/post-rulegroup-simplified-routing.json diff --git a/pkg/services/ngalert/api/api_provisioning.go b/pkg/services/ngalert/api/api_provisioning.go index 29f3ca6679b..dc8863e1fb6 100644 --- a/pkg/services/ngalert/api/api_provisioning.go +++ b/pkg/services/ngalert/api/api_provisioning.go @@ -525,7 +525,7 @@ func determineProvenance(ctx *contextmodel.ReqContext) definitions.Provenance { } func extractExportRequest(c *contextmodel.ReqContext) definitions.ExportQueryParams { - var format = "yaml" + format := "yaml" acceptHeader := c.Req.Header.Get("Accept") if strings.Contains(acceptHeader, "yaml") { @@ -673,11 +673,22 @@ func escapeRuleGroup(group definitions.AlertRuleGroupExport) definitions.AlertRu func escapeRuleNotificationSettings(ns definitions.AlertRuleNotificationSettingsExport) definitions.AlertRuleNotificationSettingsExport { ns.Receiver = addEscapeCharactersToString(ns.Receiver) - for j := range ns.GroupBy { - ns.GroupBy[j] = addEscapeCharactersToString(ns.GroupBy[j]) + if ns.GroupBy != nil { + for j := range *ns.GroupBy { + (*ns.GroupBy)[j] = addEscapeCharactersToString((*ns.GroupBy)[j]) + } } - for k := range ns.MuteTimeIntervals { - ns.MuteTimeIntervals[k] = addEscapeCharactersToString(ns.MuteTimeIntervals[k]) + + if ns.MuteTimeIntervals != nil { + for k := range *ns.MuteTimeIntervals { + (*ns.MuteTimeIntervals)[k] = addEscapeCharactersToString((*ns.MuteTimeIntervals)[k]) + } + } + + if ns.ActiveTimeIntervals != nil { + for k := range *ns.ActiveTimeIntervals { + (*ns.ActiveTimeIntervals)[k] = addEscapeCharactersToString((*ns.ActiveTimeIntervals)[k]) + } } return ns } diff --git a/pkg/services/ngalert/api/api_ruler_export_test.go b/pkg/services/ngalert/api/api_ruler_export_test.go index d9b69d9e1d3..dcdeaf0c436 100644 --- a/pkg/services/ngalert/api/api_ruler_export_test.go +++ b/pkg/services/ngalert/api/api_ruler_export_test.go @@ -201,6 +201,33 @@ func TestExportFromPayload(t *testing.T) { require.Equal(t, `attachment;filename=export.tf`, rc.Resp.Header().Get("Content-Disposition")) }) }) + + t.Run("hcl body with simplified routing is as expected", func(t *testing.T) { + requestFile := "post-rulegroup-simplified-routing.json" + + rawBody, err := testData.ReadFile(path.Join("test-data", requestFile)) + require.NoError(t, err) + + var buf bytes.Buffer + require.NoError(t, json.Compact(&buf, rawBody)) + + var body apimodels.PostableRuleGroupConfig + require.NoError(t, json.Unmarshal(buf.Bytes(), &body)) + + expectedResponse, err := testData.ReadFile(path.Join("test-data", strings.Replace(requestFile, ".json", "-export.hcl", 1))) + require.NoError(t, err) + + rc := createRequest() + rc.Req.Form.Set("format", "hcl") + rc.Req.Form.Set("download", "false") + + response := srv.ExportFromPayload(rc, body, folder.UID) + response.WriteTo(rc) + + require.Equal(t, 200, response.Status()) + require.Equal(t, string(expectedResponse), string(response.Body())) + require.Equal(t, "text/hcl", rc.Resp.Header().Get("Content-Type")) + }) } func TestExportRules(t *testing.T) { diff --git a/pkg/services/ngalert/api/compat/compat.go b/pkg/services/ngalert/api/compat/compat.go index 9d1796f78f5..eadf40217af 100644 --- a/pkg/services/ngalert/api/compat/compat.go +++ b/pkg/services/ngalert/api/compat/compat.go @@ -483,12 +483,12 @@ func AlertRuleNotificationSettingsExportFromNotificationSettings(ns []models.Not return &definitions.AlertRuleNotificationSettingsExport{ Receiver: m.Receiver, - GroupBy: m.GroupBy, + GroupBy: NilIfEmpty(util.Pointer(m.GroupBy)), GroupWait: toStringIfNotNil(m.GroupWait), GroupInterval: toStringIfNotNil(m.GroupInterval), RepeatInterval: toStringIfNotNil(m.RepeatInterval), - MuteTimeIntervals: m.MuteTimeIntervals, - ActiveTimeIntervals: m.ActiveTimeIntervals, + MuteTimeIntervals: NilIfEmpty(util.Pointer(m.MuteTimeIntervals)), + ActiveTimeIntervals: NilIfEmpty(util.Pointer(m.ActiveTimeIntervals)), } } diff --git a/pkg/services/ngalert/api/test-data/post-rulegroup-simplified-routing-export.hcl b/pkg/services/ngalert/api/test-data/post-rulegroup-simplified-routing-export.hcl new file mode 100644 index 00000000000..ff3c07ee97e --- /dev/null +++ b/pkg/services/ngalert/api/test-data/post-rulegroup-simplified-routing-export.hcl @@ -0,0 +1,45 @@ +resource "grafana_rule_group" "rule_group_2b12784d0e1454cd" { + org_id = 1 + name = "group_simplified_routing" + folder_uid = "e4584834-1a87-4dff-8913-8a4748dfca79" + interval_seconds = 10 + + rule { + name = "test" + condition = "C" + + data { + ref_id = "A" + + relative_time_range { + from = 600 + to = 0 + } + + datasource_uid = "grafanacloud-prom" + model = "{\"editorMode\":\"code\",\"expr\":\"vector(1)\",\"instant\":true,\"intervalMs\":1000,\"legendFormat\":\"__auto\",\"maxDataPoints\":43200,\"range\":false,\"refId\":\"A\"}" + } + data { + ref_id = "C" + + relative_time_range { + from = 0 + to = 0 + } + + datasource_uid = "__expr__" + model = "{\"conditions\":[{\"evaluator\":{\"params\":[1],\"type\":\"gt\"},\"operator\":{\"type\":\"and\"},\"query\":{\"params\":[\"C\"]},\"reducer\":{\"params\":[],\"type\":\"last\"},\"type\":\"query\"}],\"datasource\":{\"type\":\"__expr__\",\"uid\":\"__expr__\"},\"expression\":\"A\",\"intervalMs\":1000,\"maxDataPoints\":43200,\"refId\":\"C\",\"type\":\"threshold\"}" + } + + no_data_state = "NoData" + exec_err_state = "Error" + for = "1m" + annotations = {} + labels = {} + is_paused = false + + notification_settings { + contact_point = "email" + } + } +} diff --git a/pkg/services/ngalert/api/test-data/post-rulegroup-simplified-routing.json b/pkg/services/ngalert/api/test-data/post-rulegroup-simplified-routing.json new file mode 100644 index 00000000000..7cd163d4d35 --- /dev/null +++ b/pkg/services/ngalert/api/test-data/post-rulegroup-simplified-routing.json @@ -0,0 +1,94 @@ +{ + "name": "group_simplified_routing", + "interval": "10s", + "rules": [ + { + "grafana_alert": { + "title": "test", + "condition": "C", + "data": [ + { + "refId": "A", + "queryType": "", + "relativeTimeRange": { + "from": 600, + "to": 0 + }, + "datasourceUid": "grafanacloud-prom", + "model": { + "editorMode": "code", + "expr": "vector(1)", + "instant": true, + "intervalMs": 1000, + "legendFormat": "__auto", + "maxDataPoints": 43200, + "range": false, + "refId": "A" + } + }, + { + "refId": "C", + "queryType": "", + "relativeTimeRange": { + "from": 0, + "to": 0 + }, + "datasourceUid": "__expr__", + "model": { + "conditions": [ + { + "evaluator": { + "params": [ + 1 + ], + "type": "gt" + }, + "operator": { + "type": "and" + }, + "query": { + "params": [ + "C" + ] + }, + "reducer": { + "params": [], + "type": "last" + }, + "type": "query" + } + ], + "datasource": { + "type": "__expr__", + "uid": "__expr__" + }, + "expression": "A", + "intervalMs": 1000, + "maxDataPoints": 43200, + "refId": "C", + "type": "threshold" + } + } + ], + "is_paused": false, + "no_data_state": "NoData", + "exec_err_state": "Error", + "notification_settings": { + "receiver": "email" + }, + "metadata": { + "editor_settings": { + "simplified_query_and_expressions_section": false, + "simplified_notifications_section": true + } + }, + "missing_series_evals_to_resolve": 0, + "uid": "alert-with-simplified-routing" + }, + "annotations": {}, + "labels": {}, + "for": "1m", + "keep_firing_for": "0s" + } + ] +} diff --git a/pkg/services/ngalert/api/tooling/definitions/provisioning_alert_rules.go b/pkg/services/ngalert/api/tooling/definitions/provisioning_alert_rules.go index 0d91080064b..cbfb7adfd24 100644 --- a/pkg/services/ngalert/api/tooling/definitions/provisioning_alert_rules.go +++ b/pkg/services/ngalert/api/tooling/definitions/provisioning_alert_rules.go @@ -306,13 +306,13 @@ type RelativeTimeRangeExport struct { type AlertRuleNotificationSettingsExport struct { // Field name mismatches with Terraform provider schema are noted where applicable. - Receiver string `yaml:"receiver,omitempty" json:"receiver,omitempty" hcl:"contact_point"` // TF -> `contact_point` - GroupBy []string `yaml:"group_by,omitempty" json:"group_by,omitempty" hcl:"group_by"` - GroupWait *string `yaml:"group_wait,omitempty" json:"group_wait,omitempty" hcl:"group_wait,optional"` - GroupInterval *string `yaml:"group_interval,omitempty" json:"group_interval,omitempty" hcl:"group_interval,optional"` - RepeatInterval *string `yaml:"repeat_interval,omitempty" json:"repeat_interval,omitempty" hcl:"repeat_interval,optional"` - MuteTimeIntervals []string `yaml:"mute_time_intervals,omitempty" json:"mute_time_intervals,omitempty" hcl:"mute_timings"` // TF -> `mute_timings` - ActiveTimeIntervals []string `yaml:"active_time_intervals,omitempty" json:"active_time_intervals,omitempty" hcl:"active_timings"` // TF -> `active_timings` + Receiver string `yaml:"receiver,omitempty" json:"receiver,omitempty" hcl:"contact_point"` // TF -> `contact_point` + GroupBy *[]string `yaml:"group_by,omitempty" json:"group_by,omitempty" hcl:"group_by,optional"` + GroupWait *string `yaml:"group_wait,omitempty" json:"group_wait,omitempty" hcl:"group_wait,optional"` + GroupInterval *string `yaml:"group_interval,omitempty" json:"group_interval,omitempty" hcl:"group_interval,optional"` + RepeatInterval *string `yaml:"repeat_interval,omitempty" json:"repeat_interval,omitempty" hcl:"repeat_interval,optional"` + MuteTimeIntervals *[]string `yaml:"mute_time_intervals,omitempty" json:"mute_time_intervals,omitempty" hcl:"mute_timings,optional"` // TF -> `mute_timings` + ActiveTimeIntervals *[]string `yaml:"active_time_intervals,omitempty" json:"active_time_intervals,omitempty" hcl:"active_timings,optional"` // TF -> `active_timings` } // Record is the provisioned export of models.Record.