Alerting: Export of notification policies to HCL (#76411)

pull/75849/head^2
Yuri Tseretyan 2 years ago committed by GitHub
parent bdeb829cf6
commit c4ac4eb41b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 15
      pkg/services/ngalert/api/api_provisioning.go
  2. 15
      pkg/services/ngalert/api/api_provisioning_test.go
  3. 60
      pkg/services/ngalert/api/compat.go
  4. 30
      pkg/services/ngalert/api/tooling/definitions/provisioning_policies.go
  5. 4
      public/app/features/alerting/unified/components/export/GrafanaPoliciesExporter.tsx

@ -557,13 +557,14 @@ func exportHcl(download bool, body definitions.AlertingFileExport) response.Resp
// Body: &cp, // Body: &cp,
// }) // })
// } // }
// for idx, cp := range ex.Policies { for idx, cp := range body.Policies {
// resources = append(resources, resourceBlock{ policy := cp.Policy
// Type: "grafana_notification_policy", resources = append(resources, hcl.Resource{
// Name: fmt.Sprintf("notification_policy_%d", idx), Type: "grafana_notification_policy",
// Body: &cp, Name: fmt.Sprintf("notification_policy_%d", idx+1),
// }) Body: policy,
// })
}
hclBody, err := hcl.Encode(resources...) hclBody, err := hcl.Encode(resources...)
if err != nil { if err != nil {

@ -1134,6 +1134,21 @@ func TestProvisioningApi(t *testing.T) {
require.Equal(t, 200, response.Status()) require.Equal(t, 200, response.Status())
require.Equal(t, expectedResponse, string(response.Body())) 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()))
})
}) })
}) })
} }

@ -281,18 +281,36 @@ func AlertingFileExportFromRoute(orgID int64, route definitions.Route) (definiti
// RouteExportFromRoute creates a definitions.RouteExport DTO from definitions.Route. // RouteExportFromRoute creates a definitions.RouteExport DTO from definitions.Route.
func RouteExportFromRoute(route *definitions.Route) *definitions.RouteExport { 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{ export := definitions.RouteExport{
Receiver: route.Receiver, Receiver: route.Receiver,
GroupByStr: route.GroupByStr, GroupByStr: NilIfEmpty(util.Pointer(route.GroupByStr)),
Match: route.Match, Match: route.Match,
MatchRE: route.MatchRE, MatchRE: route.MatchRE,
Matchers: route.Matchers, Matchers: route.Matchers,
ObjectMatchers: route.ObjectMatchers, ObjectMatchers: route.ObjectMatchers,
MuteTimeIntervals: route.MuteTimeIntervals, ObjectMatchersSlice: matchers,
Continue: route.Continue, MuteTimeIntervals: NilIfEmpty(util.Pointer(route.MuteTimeIntervals)),
GroupWait: route.GroupWait, Continue: OmitDefault(util.Pointer(route.Continue)),
GroupInterval: route.GroupInterval, GroupWait: toStringIfNotNil(route.GroupWait),
RepeatInterval: route.RepeatInterval, GroupInterval: toStringIfNotNil(route.GroupInterval),
RepeatInterval: toStringIfNotNil(route.RepeatInterval),
} }
if len(route.Routes) > 0 { if len(route.Routes) > 0 {
@ -304,3 +322,23 @@ func RouteExportFromRoute(route *definitions.Route) *definitions.RouteExport {
return &export 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
}

@ -2,7 +2,6 @@ package definitions
import ( import (
"github.com/prometheus/alertmanager/config" "github.com/prometheus/alertmanager/config"
"github.com/prometheus/common/model"
) )
// swagger:route GET /api/v1/provisioning/policies provisioning stable RouteGetPolicyTree // 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 // 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. // provisioning file format. An alternative would be to define a custom MarshalJSON and MarshalYAML that excludes them.
type RouteExport struct { 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. // Deprecated. Remove before v1.0 release.
Match map[string]string `yaml:"match,omitempty" json:"match,omitempty"` Match map[string]string `yaml:"match,omitempty" json:"match,omitempty"`
// Deprecated. Remove before v1.0 release. // Deprecated. Remove before v1.0 release.
MatchRE config.MatchRegexps `yaml:"match_re,omitempty" json:"match_re,omitempty"` MatchRE config.MatchRegexps `yaml:"match_re,omitempty" json:"match_re,omitempty"`
Matchers config.Matchers `yaml:"matchers,omitempty" json:"matchers,omitempty"` Matchers config.Matchers `yaml:"matchers,omitempty" json:"matchers,omitempty"`
ObjectMatchers ObjectMatchers `yaml:"object_matchers,omitempty" json:"object_matchers,omitempty"` ObjectMatchers ObjectMatchers `yaml:"object_matchers,omitempty" json:"object_matchers,omitempty"`
MuteTimeIntervals []string `yaml:"mute_time_intervals,omitempty" json:"mute_time_intervals,omitempty"` ObjectMatchersSlice []*MatcherExport `yaml:"-" json:"-" hcl:"matcher,block"`
Continue bool `yaml:"continue,omitempty" json:"continue,omitempty"` // Added omitempty to yaml for a cleaner export. MuteTimeIntervals *[]string `yaml:"mute_time_intervals,omitempty" json:"mute_time_intervals,omitempty" hcl:"mute_timings"`
Routes []*RouteExport `yaml:"routes,omitempty" json:"routes,omitempty"` 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"` GroupWait *string `yaml:"group_wait,omitempty" json:"group_wait,omitempty" hcl:"group_wait,optional"`
GroupInterval *model.Duration `yaml:"group_interval,omitempty" json:"group_interval,omitempty"` GroupInterval *string `yaml:"group_interval,omitempty" json:"group_interval,omitempty" hcl:"group_interval,optional"`
RepeatInterval *model.Duration `yaml:"repeat_interval,omitempty" json:"repeat_interval,omitempty"` 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"`
} }

@ -6,7 +6,7 @@ import { alertRuleApi } from '../../api/alertRuleApi';
import { FileExportPreview } from './FileExportPreview'; import { FileExportPreview } from './FileExportPreview';
import { GrafanaExportDrawer } from './GrafanaExportDrawer'; import { GrafanaExportDrawer } from './GrafanaExportDrawer';
import { ExportFormats, jsonAndYamlGrafanaExportProviders } from './providers'; import { allGrafanaExportProviders, ExportFormats } from './providers';
interface GrafanaPoliciesPreviewProps { interface GrafanaPoliciesPreviewProps {
exportFormat: ExportFormats; exportFormat: ExportFormats;
onClose: () => void; onClose: () => void;
@ -45,7 +45,7 @@ export const GrafanaPoliciesExporter = ({ onClose }: GrafanaPoliciesExporterProp
activeTab={activeTab} activeTab={activeTab}
onTabChange={setActiveTab} onTabChange={setActiveTab}
onClose={onClose} onClose={onClose}
formatProviders={jsonAndYamlGrafanaExportProviders} formatProviders={Object.values(allGrafanaExportProviders)}
> >
<GrafanaPoliciesExporterPreview exportFormat={activeTab} onClose={onClose} /> <GrafanaPoliciesExporterPreview exportFormat={activeTab} onClose={onClose} />
</GrafanaExportDrawer> </GrafanaExportDrawer>

Loading…
Cancel
Save