From 47115c714adfcf07c1898021581d5f4a817f9c45 Mon Sep 17 00:00:00 2001 From: Matheus Macabu Date: Tue, 15 Oct 2024 16:27:28 +0200 Subject: [PATCH] CloudMigrations: create snapshot for Contact Points (#94719) * CloudMigrations: create snapshot for Contact Points * CloudMigrations: add contact point copies and components in frontend * CloudMigrations: temporarily use bell for all alerts resources --- .../cloudmigrationimpl/cloudmigration_test.go | 2 +- .../cloudmigrationimpl/snapshot_mgmt.go | 19 +++- .../snapshot_mgmt_alerts.go | 40 ++++++++ .../snapshot_mgmt_alerts_test.go | 96 +++++++++++++++++++ .../migrate-to-cloud/onprem/NameCell.tsx | 6 +- .../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 + 9 files changed, 167 insertions(+), 4 deletions(-) diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go b/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go index cbe6b060754..15438259240 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go @@ -783,7 +783,7 @@ func setUpServiceTest(t *testing.T, withDashboardMock bool) cloudmigration.Servi kvStore := kvstore.ProvideService(sqlStore) bus := bus.ProvideBus(tracer) - fakeAccessControl := actest.FakeAccessControl{} + fakeAccessControl := actest.FakeAccessControl{ExpectedEvaluate: true} fakeAccessControlService := actest.FakeService{} alertMetrics := metrics.NewNGAlert(prometheus.NewRegistry()) diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt.go b/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt.go index 952106e1608..fd977a85be7 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt.go @@ -34,6 +34,7 @@ var currentMigrationTypes = []cloudmigration.MigrateDataType{ cloudmigration.DashboardDataType, cloudmigration.MuteTimingType, cloudmigration.NotificationTemplateType, + cloudmigration.ContactPointType, } func (s *Service) getMigrationDataJSON(ctx context.Context, signedInUser *user.SignedInUser) (*cloudmigration.MigrateDataRequest, error) { @@ -74,10 +75,17 @@ func (s *Service) getMigrationDataJSON(ctx context.Context, signedInUser *user.S return nil, err } + // Alerts: Contact Points + contactPoints, err := s.getContactPoints(ctx, signedInUser) + if err != nil { + s.log.Error("Failed to get alert contact points", "err", err) + return nil, err + } + migrationDataSlice := make( []cloudmigration.MigrateDataRequestItem, 0, len(dataSources)+len(dashs)+len(folders)+len(libraryElements)+ - len(muteTimings)+len(notificationTemplates), + len(muteTimings)+len(notificationTemplates)+len(contactPoints), ) for _, ds := range dataSources { @@ -142,6 +150,15 @@ func (s *Service) getMigrationDataJSON(ctx context.Context, signedInUser *user.S }) } + for _, contactPoint := range contactPoints { + migrationDataSlice = append(migrationDataSlice, cloudmigration.MigrateDataRequestItem{ + Type: cloudmigration.ContactPointType, + RefID: contactPoint.UID, + Name: contactPoint.Name, + Data: contactPoint, + }) + } + // 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 94879e227f2..afc2ca5ec80 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt_alerts.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt_alerts.go @@ -6,7 +6,9 @@ import ( "github.com/prometheus/alertmanager/config" + "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/ngalert/provisioning" "github.com/grafana/grafana/pkg/services/user" ) @@ -66,3 +68,41 @@ func (s *Service) getNotificationTemplates(ctx context.Context, signedInUser *us return notificationTemplates, nil } + +type contactPoint struct { + Settings *simplejson.Json `json:"settings"` + UID string `json:"uid"` + Name string `json:"name"` + Type string `json:"type"` + DisableResolveMessage bool `json:"disableResolveMessage"` +} + +func (s *Service) getContactPoints(ctx context.Context, signedInUser *user.SignedInUser) ([]contactPoint, error) { + if !s.features.IsEnabledGlobally(featuremgmt.FlagOnPremToCloudMigrationsAlerts) { + return nil, nil + } + + query := provisioning.ContactPointQuery{ + OrgID: signedInUser.GetOrgID(), + Decrypt: true, // needed to recreate the settings in the target instance. + } + + embeddedContactPoints, err := s.ngAlert.Api.ContactPointService.GetContactPoints(ctx, query, signedInUser) + if err != nil { + return nil, fmt.Errorf("fetching ngalert contact points: %w", err) + } + + contactPoints := make([]contactPoint, 0, len(embeddedContactPoints)) + + for _, embeddedContactPoint := range embeddedContactPoints { + contactPoints = append(contactPoints, contactPoint{ + UID: embeddedContactPoint.UID, + Name: embeddedContactPoint.Name, + Type: embeddedContactPoint.Type, + Settings: embeddedContactPoint.Settings, + DisableResolveMessage: embeddedContactPoint.DisableResolveMessage, + }) + } + + return contactPoints, nil +} diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt_alerts_test.go b/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt_alerts_test.go index e1fed6720f9..058f7e5b257 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt_alerts_test.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt_alerts_test.go @@ -7,7 +7,10 @@ import ( "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/featuremgmt" + ac "github.com/grafana/grafana/pkg/services/ngalert/accesscontrol" "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/user" ) @@ -72,6 +75,44 @@ func TestGetNotificationTemplates(t *testing.T) { }) } +func TestGetContactPoints(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) + + contactPoints, err := s.getContactPoints(ctx, nil) + require.NoError(t, err) + require.Nil(t, contactPoints) + }) + + 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, + Permissions: map[int64]map[string][]string{ + 1: { + accesscontrol.ActionAlertingNotificationsRead: nil, + accesscontrol.ActionAlertingReceiversReadSecrets: {ac.ScopeReceiversAll}, + }, + }, + } + + defaultEmailContactPointCount := 1 + + createdContactPoints := createContactPoints(t, ctx, s, user) + + contactPoints, err := s.getContactPoints(ctx, user) + require.NoError(t, err) + require.NotNil(t, contactPoints) + require.Len(t, contactPoints, len(createdContactPoints)+defaultEmailContactPointCount) + }) +} + func createMuteTiming(t *testing.T, ctx context.Context, service *Service, orgID int64) definitions.MuteTimeInterval { t.Helper() @@ -99,6 +140,8 @@ func createMuteTiming(t *testing.T, ctx context.Context, service *Service, orgID } func createNotificationTemplate(t *testing.T, ctx context.Context, service *Service, orgID int64) definitions.NotificationTemplate { + t.Helper() + tmpl := definitions.NotificationTemplate{ Name: "MyTestNotificationTemplate", Template: "This is a test template\n{{ .ExternalURL }}", @@ -109,3 +152,56 @@ func createNotificationTemplate(t *testing.T, ctx context.Context, service *Serv return createdTemplate } + +func createContactPoints(t *testing.T, ctx context.Context, service *Service, user *user.SignedInUser) []definitions.EmbeddedContactPoint { + t.Helper() + + slackSettings, err := simplejson.NewJson([]byte(`{ + "icon_emoji":"iconemoji", + "icon_url":"iconurl", + "recipient":"recipient", + "token":"slack-secret", + "username":"user" + }`)) + require.NoError(t, err) + + telegramSettings, err := simplejson.NewJson([]byte(`{ + "bottoken":"telegram-secret", + "chatid":"chat-id", + "disable_notification":true, + "disable_web_page_preview":false, + "message_thread_id":"1234", + "parse_mode":"None", + "protect_content":true + }`)) + require.NoError(t, err) + + nameGroup := "group_1" + + slackContactPoint := definitions.EmbeddedContactPoint{ + Name: nameGroup, + Type: "slack", + Settings: slackSettings, + DisableResolveMessage: false, + Provenance: "", + } + + createdSlack, err := service.ngAlert.Api.ContactPointService.CreateContactPoint(ctx, user.GetOrgID(), user, slackContactPoint, "") + require.NoError(t, err) + + telegramContactPoint := definitions.EmbeddedContactPoint{ + Name: nameGroup, + Type: "telegram", + Settings: telegramSettings, + DisableResolveMessage: false, + Provenance: "", + } + + createdTelegram, err := service.ngAlert.Api.ContactPointService.CreateContactPoint(ctx, user.GetOrgID(), user, telegramContactPoint, "") + require.NoError(t, err) + + return []definitions.EmbeddedContactPoint{ + createdSlack, + createdTelegram, + } +} diff --git a/public/app/features/migrate-to-cloud/onprem/NameCell.tsx b/public/app/features/migrate-to-cloud/onprem/NameCell.tsx index 3f5ea7984ed..62a807c3a4c 100644 --- a/public/app/features/migrate-to-cloud/onprem/NameCell.tsx +++ b/public/app/features/migrate-to-cloud/onprem/NameCell.tsx @@ -220,9 +220,11 @@ function ResourceIcon({ resource }: { resource: ResourceTableItem }) { case 'LIBRARY_ELEMENT': return ; case 'MUTE_TIMING': - return ; + return ; case 'NOTIFICATION_TEMPLATE': - return ; + return ; + case 'CONTACT_POINT': + 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 afecc880de4..50ada9b4d55 100644 --- a/public/app/features/migrate-to-cloud/onprem/TypeCell.tsx +++ b/public/app/features/migrate-to-cloud/onprem/TypeCell.tsx @@ -17,6 +17,8 @@ export function prettyTypeName(type: ResourceTableItem['type']) { return t('migrate-to-cloud.resource-type.mute_timing', 'Mute Timing'); case 'NOTIFICATION_TEMPLATE': 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'); 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 e83c6c5b553..aeeec493993 100644 --- a/public/app/features/migrate-to-cloud/onprem/useNotifyOnSuccess.tsx +++ b/public/app/features/migrate-to-cloud/onprem/useNotifyOnSuccess.tsx @@ -56,6 +56,8 @@ function getTranslatedMessage(snapshot: GetSnapshotResponseDto) { types.push(t('migrate-to-cloud.migrated-counts.mute_timings', 'mute timings')); } else if (type === 'NOTIFICATION_TEMPLATE') { 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')); } distinctItems += 1; diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 09aeebf2b0d..af80b2fbcf9 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -1410,6 +1410,7 @@ "title": "Let us help you migrate to this stack" }, "migrated-counts": { + "contact_points": "contact points", "dashboards": "dashboards", "datasources": "data sources", "folders": "folders", @@ -1495,6 +1496,7 @@ "unknown-datasource-type": "Unknown data source" }, "resource-type": { + "contact_point": "Contact Point", "dashboard": "Dashboard", "datasource": "Data source", "folder": "Folder", diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index 281d8fd3cbf..4e3394f0b41 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -1410,6 +1410,7 @@ "title": "Ŀęŧ ūş ĥęľp yőū mįģřäŧę ŧő ŧĥįş şŧäčĸ" }, "migrated-counts": { + "contact_points": "čőʼnŧäčŧ pőįʼnŧş", "dashboards": "đäşĥþőäřđş", "datasources": "đäŧä şőūřčęş", "folders": "ƒőľđęřş", @@ -1495,6 +1496,7 @@ "unknown-datasource-type": "Ůʼnĸʼnőŵʼn đäŧä şőūřčę" }, "resource-type": { + "contact_point": "Cőʼnŧäčŧ Pőįʼnŧ", "dashboard": "Đäşĥþőäřđ", "datasource": "Đäŧä şőūřčę", "folder": "Főľđęř",