diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go b/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go index d087684946b..3b0825e67ae 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go @@ -34,6 +34,7 @@ import ( libraryelements "github.com/grafana/grafana/pkg/services/libraryelements/model" "github.com/grafana/grafana/pkg/services/ngalert" "github.com/grafana/grafana/pkg/services/ngalert/metrics" + "github.com/grafana/grafana/pkg/services/ngalert/models" ngalertstore "github.com/grafana/grafana/pkg/services/ngalert/store" ngalertfakes "github.com/grafana/grafana/pkg/services/ngalert/tests/fakes" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore" @@ -773,7 +774,11 @@ func setUpServiceTest(t *testing.T, withDashboardMock bool) cloudmigration.Servi }, } - featureToggles := featuremgmt.WithFeatures(featuremgmt.FlagOnPremToCloudMigrations, featuremgmt.FlagDashboardRestore) + featureToggles := featuremgmt.WithFeatures( + featuremgmt.FlagOnPremToCloudMigrations, + featuremgmt.FlagOnPremToCloudMigrationsAlerts, + featuremgmt.FlagDashboardRestore, // needed for skipping creating soft-deleted dashboards in the snapshot. + ) kvStore := kvstore.ProvideService(sqlStore) @@ -793,12 +798,37 @@ func setUpServiceTest(t *testing.T, withDashboardMock bool) cloudmigration.Servi ) require.NoError(t, err) + var validConfig = `{ + "template_files": { + "a": "template" + }, + "alertmanager_config": { + "route": { + "receiver": "grafana-default-email" + }, + "receivers": [{ + "name": "grafana-default-email", + "grafana_managed_receiver_configs": [{ + "uid": "", + "name": "email receiver", + "type": "email", + "settings": { + "addresses": "" + } + }] + }] + } + }` + require.NoError(t, ng.Api.AlertingStore.SaveAlertmanagerConfiguration(context.Background(), &models.SaveAlertmanagerConfigurationCmd{ + AlertmanagerConfiguration: validConfig, + OrgID: 1, + LastApplied: time.Now().Unix(), + })) + s, err := ProvideService( cfg, httpclient.NewProvider(), - featuremgmt.WithFeatures( - featuremgmt.FlagOnPremToCloudMigrations, - featuremgmt.FlagDashboardRestore), + featureToggles, sqlStore, dsService, secretskv.NewFakeSQLSecretsKVStore(t, sqlStore), diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt.go b/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt.go index 5c9df830110..94c82a101da 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt.go @@ -32,6 +32,7 @@ var currentMigrationTypes = []cloudmigration.MigrateDataType{ cloudmigration.FolderDataType, cloudmigration.LibraryElementDataType, cloudmigration.DashboardDataType, + cloudmigration.MuteTimingType, } func (s *Service) getMigrationDataJSON(ctx context.Context, signedInUser *user.SignedInUser) (*cloudmigration.MigrateDataRequest, error) { @@ -58,9 +59,16 @@ func (s *Service) getMigrationDataJSON(ctx context.Context, signedInUser *user.S return nil, err } + // Alerts: Mute Timings + muteTimings, err := s.getAlertMuteTimings(ctx, signedInUser) + if err != nil { + s.log.Error("Failed to get alert mute timings", "err", err) + return nil, err + } + migrationDataSlice := make( []cloudmigration.MigrateDataRequestItem, 0, - len(dataSources)+len(dashs)+len(folders)+len(libraryElements), + len(dataSources)+len(dashs)+len(folders)+len(libraryElements)+len(muteTimings), ) for _, ds := range dataSources { @@ -107,6 +115,15 @@ 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, + Name: muteTiming.Name, + Data: muteTiming, + }) + } + // 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 new file mode 100644 index 00000000000..8869882e8a6 --- /dev/null +++ b/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt_alerts.go @@ -0,0 +1,41 @@ +package cloudmigrationimpl + +import ( + "context" + "fmt" + + "github.com/prometheus/alertmanager/config" + + "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/user" +) + +type muteTimeInterval struct { + // 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"` +} + +func (s *Service) getAlertMuteTimings(ctx context.Context, signedInUser *user.SignedInUser) ([]muteTimeInterval, error) { + if !s.features.IsEnabledGlobally(featuremgmt.FlagOnPremToCloudMigrationsAlerts) { + return nil, nil + } + + muteTimings, err := s.ngAlert.Api.MuteTimings.GetMuteTimings(ctx, signedInUser.OrgID) + if err != nil { + return nil, fmt.Errorf("fetching ngalert mute timings: %w", err) + } + + muteTimeIntervals := make([]muteTimeInterval, 0, len(muteTimings)) + + for _, muteTiming := range muteTimings { + muteTimeIntervals = append(muteTimeIntervals, muteTimeInterval{ + MuteTimeInterval: config.MuteTimeInterval{ + Name: muteTiming.Name, + TimeIntervals: muteTiming.TimeIntervals, + }, + }) + } + + return muteTimeIntervals, nil +} diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt_alerts_test.go b/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt_alerts_test.go new file mode 100644 index 00000000000..7ff8cadc1bc --- /dev/null +++ b/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt_alerts_test.go @@ -0,0 +1,69 @@ +package cloudmigrationimpl + +import ( + "context" + "encoding/json" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" + "github.com/grafana/grafana/pkg/services/user" +) + +func TestGetAlertMuteTimings(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) + + muteTimeIntervals, err := s.getAlertMuteTimings(ctx, nil) + require.NoError(t, err) + require.Nil(t, muteTimeIntervals) + }) + + t.Run("when the feature flag `onPremToCloudMigrationsAlerts` is enabled it returns the mute timings", func(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} + + createdMuteTiming := createMuteTiming(t, ctx, s, orgID) + + muteTimeIntervals, err := s.getAlertMuteTimings(ctx, user) + require.NoError(t, err) + require.NotNil(t, muteTimeIntervals) + require.Len(t, muteTimeIntervals, 1) + require.Equal(t, createdMuteTiming.Name, muteTimeIntervals[0].Name) + }) +} + +func createMuteTiming(t *testing.T, ctx context.Context, service *Service, orgID int64) definitions.MuteTimeInterval { + t.Helper() + + muteTiming := `{ + "name": "My Unique MuteTiming 1", + "time_intervals": [ + { + "times": [{"start_time": "12:12","end_time": "23:23"}], + "weekdays": ["monday","wednesday","friday","sunday"], + "days_of_month": ["10:20","25:-1"], + "months": ["1:6","10:12"], + "years": ["2022:2054"], + "location": "Africa/Douala" + } + ] + }` + + var mt definitions.MuteTimeInterval + require.NoError(t, json.Unmarshal([]byte(muteTiming), &mt)) + + createdTiming, err := service.ngAlert.Api.MuteTimings.CreateMuteTiming(ctx, mt, orgID) + require.NoError(t, err) + + return createdTiming +} diff --git a/public/app/features/migrate-to-cloud/onprem/NameCell.tsx b/public/app/features/migrate-to-cloud/onprem/NameCell.tsx index b978b1d1b27..94fd342ad6c 100644 --- a/public/app/features/migrate-to-cloud/onprem/NameCell.tsx +++ b/public/app/features/migrate-to-cloud/onprem/NameCell.tsx @@ -219,6 +219,8 @@ function ResourceIcon({ resource }: { resource: ResourceTableItem }) { return ; case 'LIBRARY_ELEMENT': return ; + case 'MUTE_TIMING': + 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 37e962d7b53..ca8e29b046a 100644 --- a/public/app/features/migrate-to-cloud/onprem/TypeCell.tsx +++ b/public/app/features/migrate-to-cloud/onprem/TypeCell.tsx @@ -13,6 +13,8 @@ export function prettyTypeName(type: ResourceTableItem['type']) { return t('migrate-to-cloud.resource-type.folder', 'Folder'); case 'LIBRARY_ELEMENT': return t('migrate-to-cloud.resource-type.library_element', 'Library Element'); + case 'MUTE_TIMING': + return t('migrate-to-cloud.resource-type.mute_timing', 'Mute Timing'); 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 705da66b693..0f50d7b6fe0 100644 --- a/public/app/features/migrate-to-cloud/onprem/useNotifyOnSuccess.tsx +++ b/public/app/features/migrate-to-cloud/onprem/useNotifyOnSuccess.tsx @@ -52,6 +52,8 @@ function getTranslatedMessage(snapshot: GetSnapshotResponseDto) { types.push(t('migrate-to-cloud.migrated-counts.folders', 'folders')); } else if (type === 'LIBRARY_ELEMENT') { types.push(t('migrate-to-cloud.migrated-counts.library_elements', 'library elements')); + } else if (type === 'MUTE_TIMING') { + types.push(t('migrate-to-cloud.migrated-counts.mute_timings', 'mute timings')); } distinctItems += 1; diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 376282bc21d..03f75af7079 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -1408,7 +1408,8 @@ "dashboards": "dashboards", "datasources": "data sources", "folders": "folders", - "library_elements": "library elements" + "library_elements": "library elements", + "mute_timings": "mute timings" }, "migration-token": { "delete-button": "Delete token", @@ -1492,6 +1493,7 @@ "datasource": "Data source", "folder": "Folder", "library_element": "Library Element", + "mute_timing": "Mute Timing", "unknown": "Unknown" }, "summary": { diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index 5a68f130569..b65e42aa873 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -1408,7 +1408,8 @@ "dashboards": "đäşĥþőäřđş", "datasources": "đäŧä şőūřčęş", "folders": "ƒőľđęřş", - "library_elements": "ľįþřäřy ęľęmęʼnŧş" + "library_elements": "ľįþřäřy ęľęmęʼnŧş", + "mute_timings": "mūŧę ŧįmįʼnģş" }, "migration-token": { "delete-button": "Đęľęŧę ŧőĸęʼn", @@ -1492,6 +1493,7 @@ "datasource": "Đäŧä şőūřčę", "folder": "Főľđęř", "library_element": "Ŀįþřäřy Ēľęmęʼnŧ", + "mute_timing": "Mūŧę Ŧįmįʼnģ", "unknown": "Ůʼnĸʼnőŵʼn" }, "summary": {