From 3d39594d75a2e11f04fcf7d8d06148287b11f042 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Philippe=20Qu=C3=A9m=C3=A9ner?= Date: Mon, 1 Aug 2022 20:19:33 +0200 Subject: [PATCH] Alerting: Add file provisioning for mute timings (#52936) --- .../alerting/config_reader_test.go | 19 +++++ .../alerting/mute_times_provisioner.go | 73 +++++++++++++++++++ .../provisioning/alerting/mute_times_types.go | 55 ++++++++++++++ .../provisioning/alerting/provisioner.go | 21 ++++-- .../mute_times.yml | 10 +++ .../correct-properties/mute_times.yml | 11 +++ .../multiple-mute-times/mute_times.yml | 17 +++++ pkg/services/provisioning/alerting/types.go | 27 ++++++- pkg/services/provisioning/provisioning.go | 2 + 9 files changed, 226 insertions(+), 9 deletions(-) create mode 100644 pkg/services/provisioning/alerting/mute_times_provisioner.go create mode 100644 pkg/services/provisioning/alerting/mute_times_types.go create mode 100644 pkg/services/provisioning/alerting/testdata/mute_times/correct-properties-with-org/mute_times.yml create mode 100644 pkg/services/provisioning/alerting/testdata/mute_times/correct-properties/mute_times.yml create mode 100644 pkg/services/provisioning/alerting/testdata/mute_times/multiple-mute-times/mute_times.yml diff --git a/pkg/services/provisioning/alerting/config_reader_test.go b/pkg/services/provisioning/alerting/config_reader_test.go index c5a93f09021..e43ce9f64e1 100644 --- a/pkg/services/provisioning/alerting/config_reader_test.go +++ b/pkg/services/provisioning/alerting/config_reader_test.go @@ -26,6 +26,9 @@ const ( testFileCorrectProperties_np = "./testdata/notificiation_policies/correct-properties" testFileCorrectPropertiesWithOrg_np = "./testdata/notificiation_policies/correct-properties-with-org" testFileMultipleNps = "./testdata/notificiation_policies/multiple-policies" + testFileCorrectProperties_mt = "./testdata/mute_times/correct-properties" + testFileCorrectPropertiesWithOrg_mt = "./testdata/mute_times/correct-properties-with-org" + testFileMultipleMts = "./testdata/mute_times/multiple-mute-times" ) func TestConfigReader(t *testing.T) { @@ -119,4 +122,20 @@ func TestConfigReader(t *testing.T) { require.NoError(t, err) require.Len(t, file[0].Policies, 2) }) + t.Run("a mute times file with correct properties and specific org should not error", func(t *testing.T) { + _, err := configReader.readConfig(ctx, testFileCorrectProperties_mt) + require.NoError(t, err) + }) + t.Run("a mute times file with correct properties and specific org should not error", func(t *testing.T) { + file, err := configReader.readConfig(ctx, testFileCorrectPropertiesWithOrg_mt) + 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].MuteTimes[0].OrgID) + }) + }) + t.Run("the config reader should be able to read a file with multiple mute times", func(t *testing.T) { + file, err := configReader.readConfig(ctx, testFileMultipleMts) + require.NoError(t, err) + require.Len(t, file[0].MuteTimes, 2) + }) } diff --git a/pkg/services/provisioning/alerting/mute_times_provisioner.go b/pkg/services/provisioning/alerting/mute_times_provisioner.go new file mode 100644 index 00000000000..d83f94d5e69 --- /dev/null +++ b/pkg/services/provisioning/alerting/mute_times_provisioner.go @@ -0,0 +1,73 @@ +package alerting + +import ( + "context" + + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" + "github.com/grafana/grafana/pkg/services/ngalert/models" + "github.com/grafana/grafana/pkg/services/ngalert/provisioning" +) + +type MuteTimesProvisioner interface { + Provision(ctx context.Context, files []*AlertingFile) error + Unprovision(ctx context.Context, files []*AlertingFile) error +} + +type defaultMuteTimesProvisioner struct { + logger log.Logger + muteTimingService provisioning.MuteTimingService +} + +func NewMuteTimesProvisioner(logger log.Logger, + muteTimingService provisioning.MuteTimingService) MuteTimesProvisioner { + return &defaultMuteTimesProvisioner{ + logger: logger, + muteTimingService: muteTimingService, + } +} + +func (c *defaultMuteTimesProvisioner) Provision(ctx context.Context, + files []*AlertingFile) error { + cache := map[int64]map[string]definitions.MuteTimeInterval{} + for _, file := range files { + for _, muteTiming := range file.MuteTimes { + if _, exists := cache[muteTiming.OrgID]; !exists { + intervals, err := c.muteTimingService.GetMuteTimings(ctx, muteTiming.OrgID) + if err != nil { + return err + } + cache[muteTiming.OrgID] = make(map[string]definitions.MuteTimeInterval, len(intervals)) + for _, interval := range intervals { + cache[muteTiming.OrgID][interval.Name] = interval + } + } + muteTiming.MuteTime.Provenance = models.ProvenanceFile + if _, exists := cache[muteTiming.OrgID][muteTiming.MuteTime.Name]; exists { + _, err := c.muteTimingService.UpdateMuteTiming(ctx, muteTiming.MuteTime, muteTiming.OrgID) + if err != nil { + return err + } + continue + } + _, err := c.muteTimingService.CreateMuteTiming(ctx, muteTiming.MuteTime, muteTiming.OrgID) + if err != nil { + return err + } + } + } + return nil +} + +func (c *defaultMuteTimesProvisioner) Unprovision(ctx context.Context, + files []*AlertingFile) error { + for _, file := range files { + for _, deleteMuteTime := range file.DeleteMuteTimes { + err := c.muteTimingService.DeleteMuteTiming(ctx, deleteMuteTime.Name, deleteMuteTime.OrgID) + if err != nil { + return err + } + } + } + return nil +} diff --git a/pkg/services/provisioning/alerting/mute_times_types.go b/pkg/services/provisioning/alerting/mute_times_types.go new file mode 100644 index 00000000000..40dfe24261e --- /dev/null +++ b/pkg/services/provisioning/alerting/mute_times_types.go @@ -0,0 +1,55 @@ +package alerting + +import ( + "errors" + "strings" + + "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" + "github.com/grafana/grafana/pkg/services/provisioning/values" +) + +type MuteTimeV1 struct { + OrgID values.Int64Value `json:"orgId" yaml:"orgId"` + MuteTime definitions.MuteTimeInterval `json:",inline" yaml:",inline"` +} + +func (v1 *MuteTimeV1) mapToModel() MuteTime { + orgID := v1.OrgID.Value() + if orgID < 1 { + orgID = 1 + } + return MuteTime{ + OrgID: orgID, + MuteTime: v1.MuteTime, + } +} + +type MuteTime struct { + OrgID int64 + MuteTime definitions.MuteTimeInterval +} + +type DeleteMuteTimeV1 struct { + OrgID values.Int64Value `json:"orgId" yaml:"orgId"` + Name values.StringValue `json:"name" yaml:"name"` +} + +func (v1 *DeleteMuteTimeV1) mapToModel() (DeleteMuteTime, error) { + name := strings.TrimSpace(v1.Name.Value()) + if name == "" { + return DeleteMuteTime{}, errors.New("delete mute time missing name") + } + orgID := v1.OrgID.Value() + if orgID < 1 { + orgID = 1 + } + return DeleteMuteTime{ + OrgID: orgID, + Name: name, + }, nil +} + +type DeleteMuteTime struct { + OrgID int64 + Name string +} diff --git a/pkg/services/provisioning/alerting/provisioner.go b/pkg/services/provisioning/alerting/provisioner.go index 70c568a6d10..d8af9f3bf29 100644 --- a/pkg/services/provisioning/alerting/provisioner.go +++ b/pkg/services/provisioning/alerting/provisioner.go @@ -2,6 +2,7 @@ package alerting import ( "context" + "fmt" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/dashboards" @@ -15,6 +16,7 @@ type ProvisionerConfig struct { RuleService provisioning.AlertRuleService ContactPointService provisioning.ContactPointService NotificiationPolicyService provisioning.NotificationPolicyService + MuteTimingService provisioning.MuteTimingService } func Provision(ctx context.Context, cfg ProvisionerConfig) error { @@ -33,25 +35,34 @@ func Provision(ctx context.Context, cfg ProvisionerConfig) error { cfg.RuleService) err = ruleProvisioner.Provision(ctx, files) if err != nil { - return err + return fmt.Errorf("alert rules: %w", err) } cpProvisioner := NewContactPointProvisoner(logger, cfg.ContactPointService) err = cpProvisioner.Provision(ctx, files) if err != nil { - return err + return fmt.Errorf("contact points: %w", err) + } + mtProvisioner := NewMuteTimesProvisioner(logger, cfg.MuteTimingService) + err = mtProvisioner.Provision(ctx, files) + if err != nil { + return fmt.Errorf("mute times: %w", err) } npProvisioner := NewNotificationPolicyProvisoner(logger, cfg.NotificiationPolicyService) err = npProvisioner.Provision(ctx, files) if err != nil { - return err + return fmt.Errorf("notification policies: %w", err) } err = npProvisioner.Unprovision(ctx, files) if err != nil { - return err + return fmt.Errorf("notification policies: %w", err) } err = cpProvisioner.Unprovision(ctx, files) if err != nil { - return err + return fmt.Errorf("contact points: %w", err) + } + err = mtProvisioner.Unprovision(ctx, files) + if err != nil { + return fmt.Errorf("mute times: %w", err) } logger.Info("finished to provision alerting") return nil diff --git a/pkg/services/provisioning/alerting/testdata/mute_times/correct-properties-with-org/mute_times.yml b/pkg/services/provisioning/alerting/testdata/mute_times/correct-properties-with-org/mute_times.yml new file mode 100644 index 00000000000..27bd67f0b46 --- /dev/null +++ b/pkg/services/provisioning/alerting/testdata/mute_times/correct-properties-with-org/mute_times.yml @@ -0,0 +1,10 @@ +apiVersion: 1 +muteTimes: + - orgId: 1337 + name: test + time_intervals: + - times: + - start_time: '06:00' + end_time: '23:59' + weekdays: + - monday \ No newline at end of file diff --git a/pkg/services/provisioning/alerting/testdata/mute_times/correct-properties/mute_times.yml b/pkg/services/provisioning/alerting/testdata/mute_times/correct-properties/mute_times.yml new file mode 100644 index 00000000000..c4390b44533 --- /dev/null +++ b/pkg/services/provisioning/alerting/testdata/mute_times/correct-properties/mute_times.yml @@ -0,0 +1,11 @@ +apiVersion: 1 +muteTimes: + - name: test + time_intervals: + - times: + - start_time: '06:00' + end_time: '23:59' + weekdays: ['monday:wednesday','saturday', 'sunday'] + months: ['1:3', 'may:august', 'december'] + years: ['2020:2022', '2030'] + days_of_month: ['1:5', '-3:-1'] diff --git a/pkg/services/provisioning/alerting/testdata/mute_times/multiple-mute-times/mute_times.yml b/pkg/services/provisioning/alerting/testdata/mute_times/multiple-mute-times/mute_times.yml new file mode 100644 index 00000000000..6f5c03a5b91 --- /dev/null +++ b/pkg/services/provisioning/alerting/testdata/mute_times/multiple-mute-times/mute_times.yml @@ -0,0 +1,17 @@ +apiVersion: 1 +muteTimes: + - name: test + time_intervals: + - times: + - start_time: '06:00' + end_time: '23:59' + weekdays: + - monday + - orgId: 1337 + name: test + time_intervals: + - times: + - start_time: '06:00' + end_time: '23:59' + weekdays: + - monday \ No newline at end of file diff --git a/pkg/services/provisioning/alerting/types.go b/pkg/services/provisioning/alerting/types.go index 5dc5fe75bd5..b934ef8fe45 100644 --- a/pkg/services/provisioning/alerting/types.go +++ b/pkg/services/provisioning/alerting/types.go @@ -21,6 +21,8 @@ type AlertingFile struct { DeleteContactPoints []DeleteContactPoint Policies []NotificiationPolicy ResetPolicies []OrgID + MuteTimes []MuteTime + DeleteMuteTimes []DeleteMuteTime } type AlertingFileV1 struct { @@ -32,23 +34,40 @@ type AlertingFileV1 struct { DeleteContactPoints []DeleteContactPointV1 `json:"deleteContactPoints" yaml:"deleteContactPoints"` Policies []NotificiationPolicyV1 `json:"policies" yaml:"policies"` ResetPolicies []values.Int64Value `json:"resetPolicies" yaml:"resetPolicies"` + MuteTimes []MuteTimeV1 `json:"muteTimes" yaml:"muteTimes"` + DeleteMuteTimes []DeleteMuteTimeV1 `json:"deleteMuteTimes" yaml:"deleteMuteTimes"` } func (fileV1 *AlertingFileV1) MapToModel() (AlertingFile, error) { alertingFile := AlertingFile{} alertingFile.Filename = fileV1.Filename - err := fileV1.mapRules(&alertingFile) - if err != nil { + if err := fileV1.mapRules(&alertingFile); err != nil { return AlertingFile{}, fmt.Errorf("failure parsing rules: %w", err) } - err = fileV1.mapContactPoint(&alertingFile) - if err != nil { + if err := fileV1.mapContactPoint(&alertingFile); err != nil { return AlertingFile{}, fmt.Errorf("failure parsing contact points: %w", err) } fileV1.mapPolicies(&alertingFile) + if err := fileV1.mapMuteTimes(&alertingFile); err != nil { + return AlertingFile{}, fmt.Errorf("failure parsing mute times: %w", err) + } return alertingFile, nil } +func (fileV1 *AlertingFileV1) mapMuteTimes(alertingFile *AlertingFile) error { + for _, mtV1 := range fileV1.MuteTimes { + alertingFile.MuteTimes = append(alertingFile.MuteTimes, mtV1.mapToModel()) + } + for _, deleteV1 := range fileV1.DeleteMuteTimes { + delReq, err := deleteV1.mapToModel() + if err != nil { + return err + } + alertingFile.DeleteMuteTimes = append(alertingFile.DeleteMuteTimes, delReq) + } + return nil +} + func (fileV1 *AlertingFileV1) mapPolicies(alertingFile *AlertingFile) { for _, npV1 := range fileV1.Policies { alertingFile.Policies = append(alertingFile.Policies, npV1.mapToModel()) diff --git a/pkg/services/provisioning/provisioning.go b/pkg/services/provisioning/provisioning.go index 120ebaa10a5..6a10a567b29 100644 --- a/pkg/services/provisioning/provisioning.go +++ b/pkg/services/provisioning/provisioning.go @@ -274,6 +274,7 @@ func (ps *ProvisioningServiceImpl) ProvisionAlerting(ctx context.Context) error st, ps.SQLStore, ps.log) notificationPolicyService := provisioning.NewNotificationPolicyService(&st, st, ps.SQLStore, ps.Cfg.UnifiedAlerting, ps.log) + mutetimingsService := provisioning.NewMuteTimingService(&st, st, &st, ps.log) cfg := prov_alerting.ProvisionerConfig{ Path: alertingPath, RuleService: *ruleService, @@ -281,6 +282,7 @@ func (ps *ProvisioningServiceImpl) ProvisionAlerting(ctx context.Context) error DashboardProvService: ps.dashboardProvisioningService, ContactPointService: *contactPointService, NotificiationPolicyService: *notificationPolicyService, + MuteTimingService: *mutetimingsService, } return ps.provisionAlerting(ctx, cfg) }