From fde475e3d9a28b19fc6a1decd7fa911123c00a2e Mon Sep 17 00:00:00 2001 From: Matheus Macabu Date: Thu, 6 Feb 2025 12:35:59 +0100 Subject: [PATCH] CloudMigrations: save snapshot of alert rule groups (#100109) --- pkg/services/cloudmigration/api/dtos.go | 1 + .../cloudmigrationimpl/cloudmigration_test.go | 5 +- .../cloudmigrationimpl/snapshot_mgmt.go | 17 +++ .../snapshot_mgmt_alerts.go | 58 ++++++++++ .../snapshot_mgmt_alerts_test.go | 105 +++++++++++++++++- pkg/services/cloudmigration/model.go | 1 + public/api-enterprise-spec.json | 1 + public/api-merged.json | 1 + .../migrate-to-cloud/api/endpoints.gen.ts | 1 + .../migrate-to-cloud/onprem/NameCell.tsx | 2 + .../migrate-to-cloud/onprem/TypeCell.tsx | 2 + .../onprem/useNotifyOnSuccess.tsx | 2 + public/locales/en-US/grafana.json | 2 + public/locales/pseudo-LOCALE/grafana.json | 2 + public/openapi3.json | 1 + 15 files changed, 194 insertions(+), 7 deletions(-) diff --git a/pkg/services/cloudmigration/api/dtos.go b/pkg/services/cloudmigration/api/dtos.go index 6e1154489bf..1efa21fc77c 100644 --- a/pkg/services/cloudmigration/api/dtos.go +++ b/pkg/services/cloudmigration/api/dtos.go @@ -127,6 +127,7 @@ const ( FolderDataType MigrateDataType = "FOLDER" LibraryElementDataType MigrateDataType = "LIBRARY_ELEMENT" AlertRuleType MigrateDataType = "ALERT_RULE" + AlertRuleGroupType MigrateDataType = "ALERT_RULE_GROUP" ContactPointType MigrateDataType = "CONTACT_POINT" NotificationPolicyType MigrateDataType = "NOTIFICATION_POLICY" NotificationTemplateType MigrateDataType = "NOTIFICATION_TEMPLATE" diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go b/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go index 257d2e23a07..348bf8b1e98 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go @@ -821,8 +821,11 @@ func setUpServiceTest(t *testing.T, withDashboardMock bool, cfgOverrides ...conf secretsService := secretsfakes.NewFakeSecretsService() rr := routing.NewRouteRegister() tracer := tracing.InitializeTracerForTest() + + fakeFolder := &folder.Folder{UID: "folderUID", Title: "Folder"} mockFolder := &foldertest.FakeService{ - ExpectedFolder: &folder.Folder{UID: "folderUID", Title: "Folder"}, + ExpectedFolders: []*folder.Folder{fakeFolder}, + ExpectedFolder: fakeFolder, } cfg := setting.NewCfg() diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt.go b/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt.go index 870c4949533..034c601f510 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt.go @@ -43,6 +43,7 @@ var currentMigrationTypes = []cloudmigration.MigrateDataType{ cloudmigration.NotificationTemplateType, cloudmigration.ContactPointType, cloudmigration.NotificationPolicyType, + cloudmigration.AlertRuleGroupType, cloudmigration.AlertRuleType, cloudmigration.PluginDataType, } @@ -106,6 +107,13 @@ func (s *Service) getMigrationDataJSON(ctx context.Context, signedInUser *user.S return nil, err } + // Alerts: Alert Rule Groups + alertRuleGroups, err := s.getAlertRuleGroups(ctx, signedInUser) + if err != nil { + s.log.Error("Failed to get alert rule groups", "err", err) + return nil, err + } + // Alerts: Alert Rules alertRules, err := s.getAlertRules(ctx, signedInUser) if err != nil { @@ -209,6 +217,15 @@ func (s *Service) getMigrationDataJSON(ctx context.Context, signedInUser *user.S }) } + for _, alertRuleGroup := range alertRuleGroups { + migrationDataSlice = append(migrationDataSlice, cloudmigration.MigrateDataRequestItem{ + Type: cloudmigration.AlertRuleGroupType, + RefID: alertRuleGroup.Title, // no UID available + Name: alertRuleGroup.Title, + Data: alertRuleGroup, + }) + } + for _, alertRule := range alertRules { migrationDataSlice = append(migrationDataSlice, cloudmigration.MigrateDataRequestItem{ Type: cloudmigration.AlertRuleType, diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt_alerts.go b/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt_alerts.go index a848508b7eb..c75d8a35df0 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt_alerts.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt_alerts.go @@ -180,3 +180,61 @@ func (s *Service) getAlertRules(ctx context.Context, signedInUser *user.SignedIn return provisionedAlertRules, nil } + +type alertRuleGroup struct { + Title string `json:"title"` + FolderUID string `json:"folderUid"` + Interval int64 `json:"interval"` + Rules []alertRule `json:"rules"` +} + +func (s *Service) getAlertRuleGroups(ctx context.Context, signedInUser *user.SignedInUser) ([]alertRuleGroup, error) { + alertRuleGroupsWithFolder, err := s.ngAlert.Api.AlertRules.GetAlertGroupsWithFolderFullpath(ctx, signedInUser, nil) + if err != nil { + return nil, fmt.Errorf("fetching alert rule groups with folders: %w", err) + } + + settingAlertRulesPaused := s.cfg.CloudMigration.AlertRulesState == setting.GMSAlertRulesPaused + + alertRuleGroups := make([]alertRuleGroup, 0, len(alertRuleGroupsWithFolder)) + + for _, ruleGroup := range alertRuleGroupsWithFolder { + provisionedAlertRules := make([]alertRule, 0, len(ruleGroup.Rules)) + + for _, rule := range ruleGroup.Rules { + isPaused := rule.IsPaused + if settingAlertRulesPaused { + isPaused = true + } + + provisionedAlertRules = append(provisionedAlertRules, alertRule{ + ID: rule.ID, + UID: rule.UID, + OrgID: rule.OrgID, + FolderUID: rule.NamespaceUID, + RuleGroup: rule.RuleGroup, + Title: rule.Title, + For: model.Duration(rule.For), + Condition: rule.Condition, + Data: ngalertapi.ApiAlertQueriesFromAlertQueries(rule.Data), + Updated: rule.Updated, + NoDataState: rule.NoDataState.String(), + ExecErrState: rule.ExecErrState.String(), + Annotations: rule.Annotations, + Labels: rule.Labels, + IsPaused: isPaused, + NotificationSettings: ngalertapi.AlertRuleNotificationSettingsFromNotificationSettings(rule.NotificationSettings), + Record: ngalertapi.ApiRecordFromModelRecord(rule.Record), + }) + } + + alertRuleGroups = append(alertRuleGroups, alertRuleGroup{ + Title: ruleGroup.Title, + FolderUID: ruleGroup.FolderUID, + Interval: ruleGroup.Interval, + Rules: provisionedAlertRules, + }) + } + + return alertRuleGroups, nil +} diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt_alerts_test.go b/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt_alerts_test.go index f8e99aed021..91d7ecc5cd7 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt_alerts_test.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt_alerts_test.go @@ -121,7 +121,7 @@ func TestGetAlertRules(t *testing.T) { user := &user.SignedInUser{OrgID: 1} - alertRule := createAlertRule(t, ctx, s, user, false) + alertRule := createAlertRule(t, ctx, s, user, false, "") alertRules, err := s.getAlertRules(ctx, user) require.NoError(t, err) @@ -138,10 +138,10 @@ func TestGetAlertRules(t *testing.T) { user := &user.SignedInUser{OrgID: 1} - alertRulePaused := createAlertRule(t, ctx, s, user, true) + alertRulePaused := createAlertRule(t, ctx, s, user, true, "") require.True(t, alertRulePaused.IsPaused) - alertRuleUnpaused := createAlertRule(t, ctx, s, user, false) + alertRuleUnpaused := createAlertRule(t, ctx, s, user, false, "") require.False(t, alertRuleUnpaused.IsPaused) alertRules, err := s.getAlertRules(ctx, user) @@ -152,6 +152,83 @@ func TestGetAlertRules(t *testing.T) { }) } +func TestGetAlertRuleGroups(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + t.Run("it returns the alert rule groups", func(t *testing.T) { + s := setUpServiceTest(t, false).(*Service) + + user := &user.SignedInUser{OrgID: 1} + + ruleGroupTitle := "ruleGroupTitle" + + alertRule1 := createAlertRule(t, ctx, s, user, true, ruleGroupTitle) + alertRule2 := createAlertRule(t, ctx, s, user, false, ruleGroupTitle) + alertRule3 := createAlertRule(t, ctx, s, user, false, "anotherRuleGroup") + + createAlertRuleGroup(t, ctx, s, user, ruleGroupTitle, []models.AlertRule{alertRule1, alertRule2}) + + ruleGroups, err := s.getAlertRuleGroups(ctx, user) + require.NoError(t, err) + require.Len(t, ruleGroups, 2) + + for _, ruleGroup := range ruleGroups { + alertRuleUIDs := make([]string, 0) + for _, alertRule := range ruleGroup.Rules { + alertRuleUIDs = append(alertRuleUIDs, alertRule.UID) + } + + if ruleGroup.Title == ruleGroupTitle { + require.Len(t, ruleGroup.Rules, 2) + require.ElementsMatch(t, []string{alertRule1.UID, alertRule2.UID}, alertRuleUIDs) + } else { + require.Len(t, ruleGroup.Rules, 1) + require.ElementsMatch(t, []string{alertRule3.UID}, alertRuleUIDs) + } + } + }) + + t.Run("with the alert rules state set to paused, it returns the alert rule groups with alert rules paused", func(t *testing.T) { + alertRulesState := func(c *setting.Cfg) { + c.CloudMigration.AlertRulesState = setting.GMSAlertRulesPaused + } + + s := setUpServiceTest(t, false, alertRulesState).(*Service) + + user := &user.SignedInUser{OrgID: 1} + + ruleGroupTitle := "ruleGroupTitle" + + alertRule1 := createAlertRule(t, ctx, s, user, true, ruleGroupTitle) + alertRule2 := createAlertRule(t, ctx, s, user, false, ruleGroupTitle) + alertRule3 := createAlertRule(t, ctx, s, user, false, "anotherRuleGroup") + + createAlertRuleGroup(t, ctx, s, user, ruleGroupTitle, []models.AlertRule{alertRule1, alertRule2}) + + ruleGroups, err := s.getAlertRuleGroups(ctx, user) + require.NoError(t, err) + require.Len(t, ruleGroups, 2) + + for _, ruleGroup := range ruleGroups { + alertRuleUIDs := make([]string, 0) + for _, alertRule := range ruleGroup.Rules { + alertRuleUIDs = append(alertRuleUIDs, alertRule.UID) + + require.True(t, alertRule.IsPaused) + } + + if ruleGroup.Title == ruleGroupTitle { + require.Len(t, ruleGroup.Rules, 2) + require.ElementsMatch(t, []string{alertRule1.UID, alertRule2.UID}, alertRuleUIDs) + } else { + require.Len(t, ruleGroup.Rules, 1) + require.ElementsMatch(t, []string{alertRule3.UID}, alertRuleUIDs) + } + } + }) +} + func createMuteTiming(t *testing.T, ctx context.Context, service *Service, user *user.SignedInUser) definitions.MuteTimeInterval { t.Helper() @@ -267,12 +344,12 @@ func updateNotificationPolicyTree(t *testing.T, ctx context.Context, service *Se require.NoError(t, err) } -func createAlertRule(t *testing.T, ctx context.Context, service *Service, user *user.SignedInUser, isPaused bool) models.AlertRule { +func createAlertRule(t *testing.T, ctx context.Context, service *Service, user *user.SignedInUser, isPaused bool, ruleGroup string) models.AlertRule { t.Helper() rule := models.AlertRule{ OrgID: user.GetOrgID(), - Title: fmt.Sprintf("Alert Rule SLO (Paused: %v)", isPaused), + Title: fmt.Sprintf("Alert Rule SLO (Paused: %v) - %v", isPaused, ruleGroup), NamespaceUID: "folderUID", Condition: "A", Data: []models.AlertQuery{ @@ -286,7 +363,7 @@ func createAlertRule(t *testing.T, ctx context.Context, service *Service, user * }, }, IsPaused: isPaused, - RuleGroup: "ruleGroup", + RuleGroup: ruleGroup, For: time.Minute, IntervalSeconds: 60, NoDataState: models.OK, @@ -298,3 +375,19 @@ func createAlertRule(t *testing.T, ctx context.Context, service *Service, user * return createdRule } + +func createAlertRuleGroup(t *testing.T, ctx context.Context, service *Service, user *user.SignedInUser, title string, rules []models.AlertRule) models.AlertRuleGroup { + t.Helper() + + group := models.AlertRuleGroup{ + Title: title, + FolderUID: "folderUID", + Interval: 300, + Rules: rules, + } + + err := service.ngAlert.Api.AlertRules.ReplaceRuleGroup(ctx, user, group, "") + require.NoError(t, err) + + return group +} diff --git a/pkg/services/cloudmigration/model.go b/pkg/services/cloudmigration/model.go index 7d0b4b58f49..a46a3b46910 100644 --- a/pkg/services/cloudmigration/model.go +++ b/pkg/services/cloudmigration/model.go @@ -88,6 +88,7 @@ const ( FolderDataType MigrateDataType = "FOLDER" LibraryElementDataType MigrateDataType = "LIBRARY_ELEMENT" AlertRuleType MigrateDataType = "ALERT_RULE" + AlertRuleGroupType MigrateDataType = "ALERT_RULE_GROUP" ContactPointType MigrateDataType = "CONTACT_POINT" NotificationPolicyType MigrateDataType = "NOTIFICATION_POLICY" NotificationTemplateType MigrateDataType = "NOTIFICATION_TEMPLATE" diff --git a/public/api-enterprise-spec.json b/public/api-enterprise-spec.json index 7667a82756c..e2e8df6926b 100644 --- a/public/api-enterprise-spec.json +++ b/public/api-enterprise-spec.json @@ -5670,6 +5670,7 @@ "FOLDER", "LIBRARY_ELEMENT", "ALERT_RULE", + "ALERT_RULE_GROUP", "CONTACT_POINT", "NOTIFICATION_POLICY", "NOTIFICATION_TEMPLATE", diff --git a/public/api-merged.json b/public/api-merged.json index e063c8c959c..3e2239131d4 100644 --- a/public/api-merged.json +++ b/public/api-merged.json @@ -17182,6 +17182,7 @@ "FOLDER", "LIBRARY_ELEMENT", "ALERT_RULE", + "ALERT_RULE_GROUP", "CONTACT_POINT", "NOTIFICATION_POLICY", "NOTIFICATION_TEMPLATE", diff --git a/public/app/features/migrate-to-cloud/api/endpoints.gen.ts b/public/app/features/migrate-to-cloud/api/endpoints.gen.ts index e24b6b424f0..98e962b61e0 100644 --- a/public/app/features/migrate-to-cloud/api/endpoints.gen.ts +++ b/public/app/features/migrate-to-cloud/api/endpoints.gen.ts @@ -194,6 +194,7 @@ export type MigrateDataResponseItemDto = { | 'FOLDER' | 'LIBRARY_ELEMENT' | 'ALERT_RULE' + | 'ALERT_RULE_GROUP' | 'CONTACT_POINT' | 'NOTIFICATION_POLICY' | 'NOTIFICATION_TEMPLATE' diff --git a/public/app/features/migrate-to-cloud/onprem/NameCell.tsx b/public/app/features/migrate-to-cloud/onprem/NameCell.tsx index c6b8f78f04b..3080a26fdd9 100644 --- a/public/app/features/migrate-to-cloud/onprem/NameCell.tsx +++ b/public/app/features/migrate-to-cloud/onprem/NameCell.tsx @@ -231,6 +231,8 @@ function ResourceIcon({ resource }: { resource: ResourceTableItem }) { return ; case 'ALERT_RULE': return ; + case 'ALERT_RULE_GROUP': + return ; case 'PLUGIN': if (pluginLogo) { return ; diff --git a/public/app/features/migrate-to-cloud/onprem/TypeCell.tsx b/public/app/features/migrate-to-cloud/onprem/TypeCell.tsx index 399945cdaba..00d576db9c5 100644 --- a/public/app/features/migrate-to-cloud/onprem/TypeCell.tsx +++ b/public/app/features/migrate-to-cloud/onprem/TypeCell.tsx @@ -23,6 +23,8 @@ export function prettyTypeName(type: ResourceTableItem['type']) { return t('migrate-to-cloud.resource-type.notification_policy', 'Notification Policy'); case 'ALERT_RULE': return t('migrate-to-cloud.resource-type.alert_rule', 'Alert Rule'); + case 'ALERT_RULE_GROUP': + return t('migrate-to-cloud.resource-type.alert_rule_group', 'Alert Rule Group'); case 'PLUGIN': return t('migrate-to-cloud.resource-type.plugin', 'Plugin'); default: diff --git a/public/app/features/migrate-to-cloud/onprem/useNotifyOnSuccess.tsx b/public/app/features/migrate-to-cloud/onprem/useNotifyOnSuccess.tsx index e21fd83e815..e98a8221bb6 100644 --- a/public/app/features/migrate-to-cloud/onprem/useNotifyOnSuccess.tsx +++ b/public/app/features/migrate-to-cloud/onprem/useNotifyOnSuccess.tsx @@ -62,6 +62,8 @@ function getTranslatedMessage(snapshot: GetSnapshotResponseDto) { types.push(t('migrate-to-cloud.migrated-counts.notification_policies', 'notification policies')); } else if (type === 'ALERT_RULE') { types.push(t('migrate-to-cloud.migrated-counts.alert_rules', 'alert rules')); + } else if (type === 'ALERT_RULE_GROUP') { + types.push(t('migrate-to-cloud.migrated-counts.alert_rule_groups', 'alert rule groups')); } else if (type === 'PLUGIN') { types.push(t('migrate-to-cloud.migrated-counts.plugins', 'plugins')); } diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index f1b5d7ba851..66492eac6b1 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -2117,6 +2117,7 @@ "title": "Let us help you migrate to this stack" }, "migrated-counts": { + "alert_rule_groups": "alert rule groups", "alert_rules": "alert rules", "contact_points": "contact points", "dashboards": "dashboards", @@ -2221,6 +2222,7 @@ }, "resource-type": { "alert_rule": "Alert Rule", + "alert_rule_group": "Alert Rule Group", "contact_point": "Contact Point", "dashboard": "Dashboard", "datasource": "Data source", diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index ad00f61eef6..193643a5111 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -2117,6 +2117,7 @@ "title": "Ŀęŧ ūş ĥęľp yőū mįģřäŧę ŧő ŧĥįş şŧäčĸ" }, "migrated-counts": { + "alert_rule_groups": "äľęřŧ řūľę ģřőūpş", "alert_rules": "äľęřŧ řūľęş", "contact_points": "čőʼnŧäčŧ pőįʼnŧş", "dashboards": "đäşĥþőäřđş", @@ -2221,6 +2222,7 @@ }, "resource-type": { "alert_rule": "Åľęřŧ Ŗūľę", + "alert_rule_group": "Åľęřŧ Ŗūľę Ğřőūp", "contact_point": "Cőʼnŧäčŧ Pőįʼnŧ", "dashboard": "Đäşĥþőäřđ", "datasource": "Đäŧä şőūřčę", diff --git a/public/openapi3.json b/public/openapi3.json index f326616d62d..89612da9cf1 100644 --- a/public/openapi3.json +++ b/public/openapi3.json @@ -7250,6 +7250,7 @@ "FOLDER", "LIBRARY_ELEMENT", "ALERT_RULE", + "ALERT_RULE_GROUP", "CONTACT_POINT", "NOTIFICATION_POLICY", "NOTIFICATION_TEMPLATE",