From 1051561154841f3539990032c13168da3cf40180 Mon Sep 17 00:00:00 2001 From: Matheus Macabu Date: Thu, 17 Oct 2024 14:58:05 +0200 Subject: [PATCH] CloudMigrations: create snapshot for Notification Policies (#94852) * CloudMigrations: create snapshot for Notification Policy * CloudMigrations: add notification policy constants and components * CloudMigrations: add uid to resources that have it --- .../cloudmigrationimpl/snapshot_mgmt.go | 20 ++++- .../snapshot_mgmt_alerts.go | 27 +++++++ .../snapshot_mgmt_alerts_test.go | 76 ++++++++++++++++--- .../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 + 8 files changed, 121 insertions(+), 12 deletions(-) diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt.go b/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt.go index fd977a85be7..119c5856539 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt.go @@ -35,6 +35,7 @@ var currentMigrationTypes = []cloudmigration.MigrateDataType{ cloudmigration.MuteTimingType, cloudmigration.NotificationTemplateType, cloudmigration.ContactPointType, + cloudmigration.NotificationPolicyType, } func (s *Service) getMigrationDataJSON(ctx context.Context, signedInUser *user.SignedInUser) (*cloudmigration.MigrateDataRequest, error) { @@ -82,6 +83,13 @@ func (s *Service) getMigrationDataJSON(ctx context.Context, signedInUser *user.S return nil, err } + // Alerts: Notification Policies + notificationPolicies, err := s.getNotificationPolicies(ctx, signedInUser) + if err != nil { + s.log.Error("Failed to get alert notification policies", "err", err) + return nil, err + } + migrationDataSlice := make( []cloudmigration.MigrateDataRequestItem, 0, len(dataSources)+len(dashs)+len(folders)+len(libraryElements)+ @@ -135,7 +143,7 @@ func (s *Service) getMigrationDataJSON(ctx context.Context, signedInUser *user.S for _, muteTiming := range muteTimings { migrationDataSlice = append(migrationDataSlice, cloudmigration.MigrateDataRequestItem{ Type: cloudmigration.MuteTimingType, - RefID: muteTiming.Name, + RefID: muteTiming.UID, Name: muteTiming.Name, Data: muteTiming, }) @@ -144,7 +152,7 @@ func (s *Service) getMigrationDataJSON(ctx context.Context, signedInUser *user.S for _, notificationTemplate := range notificationTemplates { migrationDataSlice = append(migrationDataSlice, cloudmigration.MigrateDataRequestItem{ Type: cloudmigration.NotificationTemplateType, - RefID: notificationTemplate.Name, + RefID: notificationTemplate.UID, Name: notificationTemplate.Name, Data: notificationTemplate, }) @@ -159,6 +167,14 @@ func (s *Service) getMigrationDataJSON(ctx context.Context, signedInUser *user.S }) } + // Notification Policy can only be managed by updating its entire tree, so we send the whole thing as one item. + migrationDataSlice = append(migrationDataSlice, cloudmigration.MigrateDataRequestItem{ + Type: cloudmigration.NotificationPolicyType, + RefID: notificationPolicies.Name, // no UID available + Name: notificationPolicies.Name, + Data: notificationPolicies.Routes, + }) + // Obtain the names of parent elements for Dashboard and Folders data types parentNamesByType, err := s.getParentNames(ctx, signedInUser, dashs, folders, libraryElements) if err != nil { diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt_alerts.go b/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt_alerts.go index afc2ca5ec80..cb9eb9f4330 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt_alerts.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt_alerts.go @@ -8,11 +8,14 @@ import ( "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/provisioning" "github.com/grafana/grafana/pkg/services/user" ) type muteTimeInterval struct { + UID string `json:"uid"` + // There is a lot of custom (de)serialization logic from Alertmanager, // and this is the same type used by the underlying API, hence we can use the type as-is. config.MuteTimeInterval `json:",inline"` @@ -32,6 +35,7 @@ func (s *Service) getAlertMuteTimings(ctx context.Context, signedInUser *user.Si for _, muteTiming := range muteTimings { muteTimeIntervals = append(muteTimeIntervals, muteTimeInterval{ + UID: muteTiming.UID, MuteTimeInterval: config.MuteTimeInterval{ Name: muteTiming.Name, TimeIntervals: muteTiming.TimeIntervals, @@ -43,6 +47,7 @@ func (s *Service) getAlertMuteTimings(ctx context.Context, signedInUser *user.Si } type notificationTemplate struct { + UID string `json:"uid"` Name string `json:"name"` Template string `json:"template"` } @@ -61,6 +66,7 @@ func (s *Service) getNotificationTemplates(ctx context.Context, signedInUser *us for _, template := range templates { notificationTemplates = append(notificationTemplates, notificationTemplate{ + UID: template.UID, Name: template.Name, Template: template.Template, }) @@ -106,3 +112,24 @@ func (s *Service) getContactPoints(ctx context.Context, signedInUser *user.Signe return contactPoints, nil } + +type notificationPolicy struct { + Name string + Routes definitions.Route +} + +func (s *Service) getNotificationPolicies(ctx context.Context, signedInUser *user.SignedInUser) (notificationPolicy, error) { + if !s.features.IsEnabledGlobally(featuremgmt.FlagOnPremToCloudMigrationsAlerts) { + return notificationPolicy{}, nil + } + + policyTree, _, err := s.ngAlert.Api.Policies.GetPolicyTree(ctx, signedInUser.GetOrgID()) + if err != nil { + return notificationPolicy{}, fmt.Errorf("fetching ngalert notification policy tree: %w", err) + } + + return notificationPolicy{ + Name: "Notification Policy Tree", + Routes: policyTree, + }, nil +} diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt_alerts_test.go b/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt_alerts_test.go index 058f7e5b257..55b08fb3cbc 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt_alerts_test.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt_alerts_test.go @@ -5,8 +5,10 @@ import ( "encoding/json" "testing" + "github.com/prometheus/alertmanager/pkg/labels" "github.com/stretchr/testify/require" + "github.com/grafana/alerting/definition" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/featuremgmt" @@ -32,10 +34,9 @@ func TestGetAlertMuteTimings(t *testing.T) { s := setUpServiceTest(t, false).(*Service) s.features = featuremgmt.WithFeatures(featuremgmt.FlagOnPremToCloudMigrations, featuremgmt.FlagOnPremToCloudMigrationsAlerts) - var orgID int64 = 1 - user := &user.SignedInUser{OrgID: orgID} + user := &user.SignedInUser{OrgID: 1} - createdMuteTiming := createMuteTiming(t, ctx, s, orgID) + createdMuteTiming := createMuteTiming(t, ctx, s, user) muteTimeIntervals, err := s.getAlertMuteTimings(ctx, user) require.NoError(t, err) @@ -62,10 +63,9 @@ func TestGetNotificationTemplates(t *testing.T) { s := setUpServiceTest(t, false).(*Service) s.features = featuremgmt.WithFeatures(featuremgmt.FlagOnPremToCloudMigrations, featuremgmt.FlagOnPremToCloudMigrationsAlerts) - var orgID int64 = 1 - user := &user.SignedInUser{OrgID: orgID} + user := &user.SignedInUser{OrgID: 1} - createdTemplate := createNotificationTemplate(t, ctx, s, orgID) + createdTemplate := createNotificationTemplate(t, ctx, s, user) notificationTemplates, err := s.getNotificationTemplates(ctx, user) require.NoError(t, err) @@ -113,7 +113,41 @@ func TestGetContactPoints(t *testing.T) { }) } -func createMuteTiming(t *testing.T, ctx context.Context, service *Service, orgID int64) definitions.MuteTimeInterval { +func TestGetNotificationPolicies(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + t.Run("when the feature flag `onPremToCloudMigrationsAlerts` is not enabled it returns nil", func(t *testing.T) { + s := setUpServiceTest(t, false).(*Service) + s.features = featuremgmt.WithFeatures(featuremgmt.FlagOnPremToCloudMigrations) + + notificationPolicies, err := s.getNotificationPolicies(ctx, nil) + require.NoError(t, err) + require.Empty(t, notificationPolicies) + }) + + t.Run("when the feature flag `onPremToCloudMigrationsAlerts` is enabled it returns the contact points", func(t *testing.T) { + s := setUpServiceTest(t, false).(*Service) + s.features = featuremgmt.WithFeatures(featuremgmt.FlagOnPremToCloudMigrations, featuremgmt.FlagOnPremToCloudMigrationsAlerts) + + user := &user.SignedInUser{OrgID: 1} + + muteTiming := createMuteTiming(t, ctx, s, user) + require.NotEmpty(t, muteTiming.Name) + + contactPoints := createContactPoints(t, ctx, s, user) + require.GreaterOrEqual(t, len(contactPoints), 1) + + updateNotificationPolicyTree(t, ctx, s, user, contactPoints[0].Name, muteTiming.Name) + + notificationPolicies, err := s.getNotificationPolicies(ctx, user) + require.NoError(t, err) + require.NotEmpty(t, notificationPolicies.Routes.Receiver) + require.NotNil(t, notificationPolicies.Routes.Routes) + }) +} + +func createMuteTiming(t *testing.T, ctx context.Context, service *Service, user *user.SignedInUser) definitions.MuteTimeInterval { t.Helper() muteTiming := `{ @@ -133,13 +167,13 @@ func createMuteTiming(t *testing.T, ctx context.Context, service *Service, orgID var mt definitions.MuteTimeInterval require.NoError(t, json.Unmarshal([]byte(muteTiming), &mt)) - createdTiming, err := service.ngAlert.Api.MuteTimings.CreateMuteTiming(ctx, mt, orgID) + createdTiming, err := service.ngAlert.Api.MuteTimings.CreateMuteTiming(ctx, mt, user.GetOrgID()) require.NoError(t, err) return createdTiming } -func createNotificationTemplate(t *testing.T, ctx context.Context, service *Service, orgID int64) definitions.NotificationTemplate { +func createNotificationTemplate(t *testing.T, ctx context.Context, service *Service, user *user.SignedInUser) definitions.NotificationTemplate { t.Helper() tmpl := definitions.NotificationTemplate{ @@ -147,7 +181,7 @@ func createNotificationTemplate(t *testing.T, ctx context.Context, service *Serv Template: "This is a test template\n{{ .ExternalURL }}", } - createdTemplate, err := service.ngAlert.Api.Templates.CreateTemplate(ctx, orgID, tmpl) + createdTemplate, err := service.ngAlert.Api.Templates.CreateTemplate(ctx, user.GetOrgID(), tmpl) require.NoError(t, err) return createdTemplate @@ -205,3 +239,25 @@ func createContactPoints(t *testing.T, ctx context.Context, service *Service, us createdTelegram, } } + +func updateNotificationPolicyTree(t *testing.T, ctx context.Context, service *Service, user *user.SignedInUser, receiverGroup, muteTiming string) { + t.Helper() + + child := definition.Route{ + Continue: true, + MuteTimeIntervals: []string{muteTiming}, + ObjectMatchers: definition.ObjectMatchers{ + {Name: "label1", Type: labels.MatchEqual, Value: "value1"}, + {Name: "label2", Type: labels.MatchNotEqual, Value: "value2"}, + }, + Receiver: receiverGroup, + } + + tree := definition.Route{ + Receiver: "grafana-default-email", + Routes: []*definition.Route{&child}, + } + + err := service.ngAlert.Api.Policies.UpdatePolicyTree(ctx, user.GetOrgID(), tree, "", "") + require.NoError(t, err) +} diff --git a/public/app/features/migrate-to-cloud/onprem/NameCell.tsx b/public/app/features/migrate-to-cloud/onprem/NameCell.tsx index 62a807c3a4c..a9344da0f15 100644 --- a/public/app/features/migrate-to-cloud/onprem/NameCell.tsx +++ b/public/app/features/migrate-to-cloud/onprem/NameCell.tsx @@ -225,6 +225,8 @@ function ResourceIcon({ resource }: { resource: ResourceTableItem }) { return ; case 'CONTACT_POINT': return ; + case 'NOTIFICATION_POLICY': + return ; default: return undefined; } diff --git a/public/app/features/migrate-to-cloud/onprem/TypeCell.tsx b/public/app/features/migrate-to-cloud/onprem/TypeCell.tsx index 50ada9b4d55..8cd71722c00 100644 --- a/public/app/features/migrate-to-cloud/onprem/TypeCell.tsx +++ b/public/app/features/migrate-to-cloud/onprem/TypeCell.tsx @@ -19,6 +19,8 @@ export function prettyTypeName(type: ResourceTableItem['type']) { return t('migrate-to-cloud.resource-type.notification_template', 'Notification Template'); case 'CONTACT_POINT': return t('migrate-to-cloud.resource-type.contact_point', 'Contact Point'); + case 'NOTIFICATION_POLICY': + return t('migrate-to-cloud.resource-type.notification_policy', 'Notification Policy'); default: return t('migrate-to-cloud.resource-type.unknown', 'Unknown'); } diff --git a/public/app/features/migrate-to-cloud/onprem/useNotifyOnSuccess.tsx b/public/app/features/migrate-to-cloud/onprem/useNotifyOnSuccess.tsx index aeeec493993..79e95ddc5f4 100644 --- a/public/app/features/migrate-to-cloud/onprem/useNotifyOnSuccess.tsx +++ b/public/app/features/migrate-to-cloud/onprem/useNotifyOnSuccess.tsx @@ -58,6 +58,8 @@ function getTranslatedMessage(snapshot: GetSnapshotResponseDto) { types.push(t('migrate-to-cloud.migrated-counts.notification_templates', 'notification templates')); } else if (type === 'CONTACT_POINT') { types.push(t('migrate-to-cloud.migrated-counts.contact_points', 'contact points')); + } else if (type === 'NOTIFICATION_POLICY') { + types.push(t('migrate-to-cloud.migrated-counts.notification_policies', 'notification policies')); } distinctItems += 1; diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index af80b2fbcf9..780b37f7ab6 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -1416,6 +1416,7 @@ "folders": "folders", "library_elements": "library elements", "mute_timings": "mute timings", + "notification_policies": "notification policies", "notification_templates": "notification templates" }, "migration-token": { @@ -1502,6 +1503,7 @@ "folder": "Folder", "library_element": "Library Element", "mute_timing": "Mute Timing", + "notification_policy": "Notification Policy", "notification_template": "Notification Template", "unknown": "Unknown" }, diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index 4e3394f0b41..4b10a1e4a89 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -1416,6 +1416,7 @@ "folders": "ƒőľđęřş", "library_elements": "ľįþřäřy ęľęmęʼnŧş", "mute_timings": "mūŧę ŧįmįʼnģş", + "notification_policies": "ʼnőŧįƒįčäŧįőʼn pőľįčįęş", "notification_templates": "ʼnőŧįƒįčäŧįőʼn ŧęmpľäŧęş" }, "migration-token": { @@ -1502,6 +1503,7 @@ "folder": "Főľđęř", "library_element": "Ŀįþřäřy Ēľęmęʼnŧ", "mute_timing": "Mūŧę Ŧįmįʼnģ", + "notification_policy": "Ńőŧįƒįčäŧįőʼn Pőľįčy", "notification_template": "Ńőŧįƒįčäŧįőʼn Ŧęmpľäŧę", "unknown": "Ůʼnĸʼnőŵʼn" },