diff --git a/pkg/services/provisioning/alerting/config_reader_test.go b/pkg/services/provisioning/alerting/config_reader_test.go index 0d2fe73db8a..c5a93f09021 100644 --- a/pkg/services/provisioning/alerting/config_reader_test.go +++ b/pkg/services/provisioning/alerting/config_reader_test.go @@ -23,6 +23,9 @@ const ( testFileMissingUID = "./testdata/contact_points/missing-uid" testFileWhitespaceUID = "./testdata/contact_points/whitespace-uid" testFileMultipleCps = "./testdata/contact_points/multiple-contact-points" + testFileCorrectProperties_np = "./testdata/notificiation_policies/correct-properties" + testFileCorrectPropertiesWithOrg_np = "./testdata/notificiation_policies/correct-properties-with-org" + testFileMultipleNps = "./testdata/notificiation_policies/multiple-policies" ) func TestConfigReader(t *testing.T) { @@ -100,4 +103,20 @@ func TestConfigReader(t *testing.T) { require.NoError(t, err) require.Len(t, file[0].ContactPoints, 2) }) + t.Run("a notification policy file with correct properties and specific org should not error", func(t *testing.T) { + _, err := configReader.readConfig(ctx, testFileCorrectProperties_np) + require.NoError(t, err) + }) + t.Run("a notification policy file with correct properties and specific org should not error", func(t *testing.T) { + file, err := configReader.readConfig(ctx, testFileCorrectPropertiesWithOrg_np) + require.NoError(t, err) + t.Run("when an organization is set it should not overwrite it with the default of 1", func(t *testing.T) { + require.Equal(t, int64(1337), file[0].Policies[0].OrgID) + }) + }) + t.Run("the config reader should be able to read a file with multiple notification policies", func(t *testing.T) { + file, err := configReader.readConfig(ctx, testFileMultipleNps) + require.NoError(t, err) + require.Len(t, file[0].Policies, 2) + }) } diff --git a/pkg/services/provisioning/alerting/notification_policy_provisioner.go b/pkg/services/provisioning/alerting/notification_policy_provisioner.go new file mode 100644 index 00000000000..33e536de04d --- /dev/null +++ b/pkg/services/provisioning/alerting/notification_policy_provisioner.go @@ -0,0 +1,55 @@ +package alerting + +import ( + "context" + "fmt" + + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/services/ngalert/models" + "github.com/grafana/grafana/pkg/services/ngalert/provisioning" +) + +type NotificationPolicyProvisioner interface { + Provision(ctx context.Context, files []*AlertingFile) error + Unprovision(ctx context.Context, files []*AlertingFile) error +} + +type defaultNotificationPolicyProvisioner struct { + logger log.Logger + notificationPolicyService provisioning.NotificationPolicyService +} + +func NewNotificationPolicyProvisoner(logger log.Logger, + notificationPolicyService provisioning.NotificationPolicyService) NotificationPolicyProvisioner { + return &defaultNotificationPolicyProvisioner{ + logger: logger, + notificationPolicyService: notificationPolicyService, + } +} + +func (c *defaultNotificationPolicyProvisioner) Provision(ctx context.Context, + files []*AlertingFile) error { + for _, file := range files { + for _, np := range file.Policies { + err := c.notificationPolicyService.UpdatePolicyTree(ctx, np.OrgID, + np.Policy, models.ProvenanceFile) + if err != nil { + return fmt.Errorf("%s: %w", file.Filename, err) + } + } + } + return nil +} + +func (c *defaultNotificationPolicyProvisioner) Unprovision(ctx context.Context, + files []*AlertingFile) error { + for _, file := range files { + for _, orgID := range file.ResetPolicies { + _, err := c.notificationPolicyService.ResetPolicyTree(ctx, int64(orgID)) + if err != nil { + return fmt.Errorf("%s: %w", file.Filename, err) + } + } + } + return nil +} diff --git a/pkg/services/provisioning/alerting/notification_policy_types.go b/pkg/services/provisioning/alerting/notification_policy_types.go new file mode 100644 index 00000000000..21987888612 --- /dev/null +++ b/pkg/services/provisioning/alerting/notification_policy_types.go @@ -0,0 +1,29 @@ +package alerting + +import ( + "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" + "github.com/grafana/grafana/pkg/services/provisioning/values" +) + +type NotificiationPolicyV1 struct { + OrgID values.Int64Value `json:"orgId" yaml:"orgId"` + Policy definitions.Route `json:",inline" yaml:",inline"` +} + +func (v1 *NotificiationPolicyV1) mapToModel() NotificiationPolicy { + orgID := v1.OrgID.Value() + if orgID < 1 { + orgID = 1 + } + // we don't need any further validation here as it's done by + // the notification policy service + return NotificiationPolicy{ + OrgID: orgID, + Policy: v1.Policy, + } +} + +type NotificiationPolicy struct { + OrgID int64 + Policy definitions.Route +} diff --git a/pkg/services/provisioning/alerting/provisioner.go b/pkg/services/provisioning/alerting/provisioner.go index 36a7689ebcc..70c568a6d10 100644 --- a/pkg/services/provisioning/alerting/provisioner.go +++ b/pkg/services/provisioning/alerting/provisioner.go @@ -9,11 +9,12 @@ import ( ) type ProvisionerConfig struct { - Path string - DashboardService dashboards.DashboardService - DashboardProvService dashboards.DashboardProvisioningService - RuleService provisioning.AlertRuleService - ContactPointService provisioning.ContactPointService + Path string + DashboardService dashboards.DashboardService + DashboardProvService dashboards.DashboardProvisioningService + RuleService provisioning.AlertRuleService + ContactPointService provisioning.ContactPointService + NotificiationPolicyService provisioning.NotificationPolicyService } func Provision(ctx context.Context, cfg ProvisionerConfig) error { @@ -39,8 +40,15 @@ func Provision(ctx context.Context, cfg ProvisionerConfig) error { if err != nil { return err } - // TODO: provision notificiation policy in between so that when applying it - // new objects already exists and old ones are still there + npProvisioner := NewNotificationPolicyProvisoner(logger, cfg.NotificiationPolicyService) + err = npProvisioner.Provision(ctx, files) + if err != nil { + return err + } + err = npProvisioner.Unprovision(ctx, files) + if err != nil { + return err + } err = cpProvisioner.Unprovision(ctx, files) if err != nil { return err diff --git a/pkg/services/provisioning/alerting/testdata/notificiation_policies/correct-properties-with-org/policies.yml b/pkg/services/provisioning/alerting/testdata/notificiation_policies/correct-properties-with-org/policies.yml new file mode 100644 index 00000000000..dbfafe7c0e7 --- /dev/null +++ b/pkg/services/provisioning/alerting/testdata/notificiation_policies/correct-properties-with-org/policies.yml @@ -0,0 +1,7 @@ +apiVersion: 1 +policies: + - orgId: 1337 + receiver: grafana-default-email + group_by: + - grafana_folder + - alertname \ No newline at end of file diff --git a/pkg/services/provisioning/alerting/testdata/notificiation_policies/correct-properties/policies.yml b/pkg/services/provisioning/alerting/testdata/notificiation_policies/correct-properties/policies.yml new file mode 100644 index 00000000000..ad1132333c4 --- /dev/null +++ b/pkg/services/provisioning/alerting/testdata/notificiation_policies/correct-properties/policies.yml @@ -0,0 +1,6 @@ +apiVersion: 1 +policies: + - receiver: grafana-default-email + group_by: + - grafana_folder + - alertname \ No newline at end of file diff --git a/pkg/services/provisioning/alerting/testdata/notificiation_policies/multiple-policies/policies.yml b/pkg/services/provisioning/alerting/testdata/notificiation_policies/multiple-policies/policies.yml new file mode 100644 index 00000000000..71582672105 --- /dev/null +++ b/pkg/services/provisioning/alerting/testdata/notificiation_policies/multiple-policies/policies.yml @@ -0,0 +1,11 @@ +apiVersion: 1 +policies: + - receiver: grafana-default-email + group_by: + - grafana_folder + - alertname + - orgId: 1337 + receiver: grafana-default-email + group_by: + - grafana_folder + - alertname \ No newline at end of file diff --git a/pkg/services/provisioning/alerting/types.go b/pkg/services/provisioning/alerting/types.go index 7edac90ee6d..5dc5fe75bd5 100644 --- a/pkg/services/provisioning/alerting/types.go +++ b/pkg/services/provisioning/alerting/types.go @@ -10,25 +10,33 @@ type configVersion struct { APIVersion values.Int64Value `json:"apiVersion" yaml:"apiVersion"` } +type OrgID int64 + type AlertingFile struct { configVersion + Filename string Groups []AlertRuleGroup DeleteRules []RuleDelete ContactPoints []ContactPoint DeleteContactPoints []DeleteContactPoint + Policies []NotificiationPolicy + ResetPolicies []OrgID } type AlertingFileV1 struct { configVersion Filename string - Groups []AlertRuleGroupV1 `json:"groups" yaml:"groups"` - DeleteRules []RuleDeleteV1 `json:"deleteRules" yaml:"deleteRules"` - ContactPoints []ContactPointV1 `json:"contactPoints" yaml:"contactPoints"` - DeleteContactPoints []DeleteContactPointV1 `json:"deleteContactPoints" yaml:"deleteContactPoints"` + Groups []AlertRuleGroupV1 `json:"groups" yaml:"groups"` + DeleteRules []RuleDeleteV1 `json:"deleteRules" yaml:"deleteRules"` + ContactPoints []ContactPointV1 `json:"contactPoints" yaml:"contactPoints"` + DeleteContactPoints []DeleteContactPointV1 `json:"deleteContactPoints" yaml:"deleteContactPoints"` + Policies []NotificiationPolicyV1 `json:"policies" yaml:"policies"` + ResetPolicies []values.Int64Value `json:"resetPolicies" yaml:"resetPolicies"` } func (fileV1 *AlertingFileV1) MapToModel() (AlertingFile, error) { alertingFile := AlertingFile{} + alertingFile.Filename = fileV1.Filename err := fileV1.mapRules(&alertingFile) if err != nil { return AlertingFile{}, fmt.Errorf("failure parsing rules: %w", err) @@ -37,9 +45,19 @@ func (fileV1 *AlertingFileV1) MapToModel() (AlertingFile, error) { if err != nil { return AlertingFile{}, fmt.Errorf("failure parsing contact points: %w", err) } + fileV1.mapPolicies(&alertingFile) return alertingFile, nil } +func (fileV1 *AlertingFileV1) mapPolicies(alertingFile *AlertingFile) { + for _, npV1 := range fileV1.Policies { + alertingFile.Policies = append(alertingFile.Policies, npV1.mapToModel()) + } + for _, orgIDV1 := range fileV1.ResetPolicies { + alertingFile.ResetPolicies = append(alertingFile.ResetPolicies, OrgID(orgIDV1.Value())) + } +} + func (fileV1 *AlertingFileV1) mapContactPoint(alertingFile *AlertingFile) error { for _, dcp := range fileV1.DeleteContactPoints { alertingFile.DeleteContactPoints = append(alertingFile.DeleteContactPoints, dcp.MapToModel()) diff --git a/pkg/services/provisioning/provisioning.go b/pkg/services/provisioning/provisioning.go index 2ac1e9435f6..120ebaa10a5 100644 --- a/pkg/services/provisioning/provisioning.go +++ b/pkg/services/provisioning/provisioning.go @@ -272,12 +272,15 @@ func (ps *ProvisioningServiceImpl) ProvisionAlerting(ctx context.Context) error ps.log) contactPointService := provisioning.NewContactPointService(&st, ps.secretService, st, ps.SQLStore, ps.log) + notificationPolicyService := provisioning.NewNotificationPolicyService(&st, + st, ps.SQLStore, ps.Cfg.UnifiedAlerting, ps.log) cfg := prov_alerting.ProvisionerConfig{ - Path: alertingPath, - RuleService: *ruleService, - DashboardService: ps.dashboardService, - DashboardProvService: ps.dashboardProvisioningService, - ContactPointService: *contactPointService, + Path: alertingPath, + RuleService: *ruleService, + DashboardService: ps.dashboardService, + DashboardProvService: ps.dashboardProvisioningService, + ContactPointService: *contactPointService, + NotificiationPolicyService: *notificationPolicyService, } return ps.provisionAlerting(ctx, cfg) }