diff --git a/pkg/services/ngalert/api/api_provisioning.go b/pkg/services/ngalert/api/api_provisioning.go index bd825349c28..af8feda9eb7 100644 --- a/pkg/services/ngalert/api/api_provisioning.go +++ b/pkg/services/ngalert/api/api_provisioning.go @@ -557,13 +557,14 @@ func exportHcl(download bool, body definitions.AlertingFileExport) response.Resp // Body: &cp, // }) // } - // for idx, cp := range ex.Policies { - // resources = append(resources, resourceBlock{ - // Type: "grafana_notification_policy", - // Name: fmt.Sprintf("notification_policy_%d", idx), - // Body: &cp, - // }) - // + for idx, cp := range body.Policies { + policy := cp.Policy + resources = append(resources, hcl.Resource{ + Type: "grafana_notification_policy", + Name: fmt.Sprintf("notification_policy_%d", idx+1), + Body: policy, + }) + } hclBody, err := hcl.Encode(resources...) if err != nil { diff --git a/pkg/services/ngalert/api/api_provisioning_test.go b/pkg/services/ngalert/api/api_provisioning_test.go index 5128072c039..e067861b5d3 100644 --- a/pkg/services/ngalert/api/api_provisioning_test.go +++ b/pkg/services/ngalert/api/api_provisioning_test.go @@ -1134,6 +1134,21 @@ func TestProvisioningApi(t *testing.T) { require.Equal(t, 200, response.Status()) require.Equal(t, expectedResponse, string(response.Body())) }) + + t.Run("hcl body content is as expected", func(t *testing.T) { + sut := createProvisioningSrvSut(t) + sut.policies = createFakeNotificationPolicyService() + rc := createTestRequestCtx() + + rc.Context.Req.Form.Add("format", "hcl") + expectedResponse := "resource \"grafana_notification_policy\" \"notification_policy_1\" {\n contact_point = \"default-receiver\"\n group_by = [\"g1\", \"g2\"]\n\n policy {\n contact_point = \"nested-receiver\"\n group_by = [\"g3\", \"g4\"]\n\n matcher {\n label = \"foo\"\n match = \"=\"\n value = \"bar\"\n }\n\n mute_timings = [\"interval\"]\n continue = true\n group_wait = \"5m\"\n group_interval = \"5m\"\n repeat_interval = \"5m\"\n }\n\n group_wait = \"30s\"\n group_interval = \"5m\"\n repeat_interval = \"1h\"\n}\n" + + response := sut.RouteGetPolicyTreeExport(&rc) + + t.Log(string(response.Body())) + require.Equal(t, 200, response.Status()) + require.Equal(t, expectedResponse, string(response.Body())) + }) }) }) } diff --git a/pkg/services/ngalert/api/compat.go b/pkg/services/ngalert/api/compat.go index 899609a7dc3..8a901c6de1a 100644 --- a/pkg/services/ngalert/api/compat.go +++ b/pkg/services/ngalert/api/compat.go @@ -281,18 +281,36 @@ func AlertingFileExportFromRoute(orgID int64, route definitions.Route) (definiti // RouteExportFromRoute creates a definitions.RouteExport DTO from definitions.Route. func RouteExportFromRoute(route *definitions.Route) *definitions.RouteExport { + toStringIfNotNil := func(d *model.Duration) *string { + if d == nil { + return nil + } + s := d.String() + return &s + } + + matchers := make([]*definitions.MatcherExport, 0, len(route.ObjectMatchers)) + for _, matcher := range route.ObjectMatchers { + matchers = append(matchers, &definitions.MatcherExport{ + Label: matcher.Name, + Match: matcher.Type.String(), + Value: matcher.Value, + }) + } + export := definitions.RouteExport{ - Receiver: route.Receiver, - GroupByStr: route.GroupByStr, - Match: route.Match, - MatchRE: route.MatchRE, - Matchers: route.Matchers, - ObjectMatchers: route.ObjectMatchers, - MuteTimeIntervals: route.MuteTimeIntervals, - Continue: route.Continue, - GroupWait: route.GroupWait, - GroupInterval: route.GroupInterval, - RepeatInterval: route.RepeatInterval, + Receiver: route.Receiver, + GroupByStr: NilIfEmpty(util.Pointer(route.GroupByStr)), + Match: route.Match, + MatchRE: route.MatchRE, + Matchers: route.Matchers, + ObjectMatchers: route.ObjectMatchers, + ObjectMatchersSlice: matchers, + MuteTimeIntervals: NilIfEmpty(util.Pointer(route.MuteTimeIntervals)), + Continue: OmitDefault(util.Pointer(route.Continue)), + GroupWait: toStringIfNotNil(route.GroupWait), + GroupInterval: toStringIfNotNil(route.GroupInterval), + RepeatInterval: toStringIfNotNil(route.RepeatInterval), } if len(route.Routes) > 0 { @@ -304,3 +322,23 @@ func RouteExportFromRoute(route *definitions.Route) *definitions.RouteExport { return &export } + +// OmitDefault returns nil if the value is the default. +func OmitDefault[T comparable](v *T) *T { + var def T + if v == nil { + return v + } + if *v == def { + return nil + } + return v +} + +// NilIfEmpty returns nil if pointer to slice points to the empty slice. +func NilIfEmpty[T any](v *[]T) *[]T { + if v == nil || len(*v) == 0 { + return nil + } + return v +} diff --git a/pkg/services/ngalert/api/tooling/definitions/provisioning_policies.go b/pkg/services/ngalert/api/tooling/definitions/provisioning_policies.go index 697fa1937a2..480339895ee 100644 --- a/pkg/services/ngalert/api/tooling/definitions/provisioning_policies.go +++ b/pkg/services/ngalert/api/tooling/definitions/provisioning_policies.go @@ -2,7 +2,6 @@ package definitions import ( "github.com/prometheus/alertmanager/config" - "github.com/prometheus/common/model" ) // swagger:route GET /api/v1/provisioning/policies provisioning stable RouteGetPolicyTree @@ -58,20 +57,27 @@ type NotificationPolicyExport struct { // RouteExport is the provisioned file export of definitions.Route. This is needed to hide fields that aren't useable in // provisioning file format. An alternative would be to define a custom MarshalJSON and MarshalYAML that excludes them. type RouteExport struct { - Receiver string `yaml:"receiver,omitempty" json:"receiver,omitempty"` + Receiver string `yaml:"receiver,omitempty" json:"receiver,omitempty" hcl:"contact_point"` - GroupByStr []string `yaml:"group_by,omitempty" json:"group_by,omitempty"` + GroupByStr *[]string `yaml:"group_by,omitempty" json:"group_by,omitempty" hcl:"group_by"` // Deprecated. Remove before v1.0 release. Match map[string]string `yaml:"match,omitempty" json:"match,omitempty"` // Deprecated. Remove before v1.0 release. - MatchRE config.MatchRegexps `yaml:"match_re,omitempty" json:"match_re,omitempty"` - Matchers config.Matchers `yaml:"matchers,omitempty" json:"matchers,omitempty"` - ObjectMatchers ObjectMatchers `yaml:"object_matchers,omitempty" json:"object_matchers,omitempty"` - MuteTimeIntervals []string `yaml:"mute_time_intervals,omitempty" json:"mute_time_intervals,omitempty"` - Continue bool `yaml:"continue,omitempty" json:"continue,omitempty"` // Added omitempty to yaml for a cleaner export. - Routes []*RouteExport `yaml:"routes,omitempty" json:"routes,omitempty"` + MatchRE config.MatchRegexps `yaml:"match_re,omitempty" json:"match_re,omitempty"` + Matchers config.Matchers `yaml:"matchers,omitempty" json:"matchers,omitempty"` + ObjectMatchers ObjectMatchers `yaml:"object_matchers,omitempty" json:"object_matchers,omitempty"` + ObjectMatchersSlice []*MatcherExport `yaml:"-" json:"-" hcl:"matcher,block"` + MuteTimeIntervals *[]string `yaml:"mute_time_intervals,omitempty" json:"mute_time_intervals,omitempty" hcl:"mute_timings"` + Continue *bool `yaml:"continue,omitempty" json:"continue,omitempty" hcl:"continue,optional"` // Added omitempty to yaml for a cleaner export. + Routes []*RouteExport `yaml:"routes,omitempty" json:"routes,omitempty" hcl:"policy,block"` - GroupWait *model.Duration `yaml:"group_wait,omitempty" json:"group_wait,omitempty"` - GroupInterval *model.Duration `yaml:"group_interval,omitempty" json:"group_interval,omitempty"` - RepeatInterval *model.Duration `yaml:"repeat_interval,omitempty" json:"repeat_interval,omitempty"` + 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"` +} + +type MatcherExport struct { + Label string `yaml:"-" json:"-" hcl:"label"` + Match string `yaml:"-" json:"-" hcl:"match"` + Value string `yaml:"-" json:"-" hcl:"value"` } diff --git a/public/app/features/alerting/unified/components/export/GrafanaPoliciesExporter.tsx b/public/app/features/alerting/unified/components/export/GrafanaPoliciesExporter.tsx index 7cf5d82dd35..c0cf2e0083b 100644 --- a/public/app/features/alerting/unified/components/export/GrafanaPoliciesExporter.tsx +++ b/public/app/features/alerting/unified/components/export/GrafanaPoliciesExporter.tsx @@ -6,7 +6,7 @@ import { alertRuleApi } from '../../api/alertRuleApi'; import { FileExportPreview } from './FileExportPreview'; import { GrafanaExportDrawer } from './GrafanaExportDrawer'; -import { ExportFormats, jsonAndYamlGrafanaExportProviders } from './providers'; +import { allGrafanaExportProviders, ExportFormats } from './providers'; interface GrafanaPoliciesPreviewProps { exportFormat: ExportFormats; onClose: () => void; @@ -45,7 +45,7 @@ export const GrafanaPoliciesExporter = ({ onClose }: GrafanaPoliciesExporterProp activeTab={activeTab} onTabChange={setActiveTab} onClose={onClose} - formatProviders={jsonAndYamlGrafanaExportProviders} + formatProviders={Object.values(allGrafanaExportProviders)} >