CloudMigrations: create snapshot for Mute Timings (#94668)

* CloudMigrations: Create snapshot for Mute Timings

* CloudMigrations: add mute timings icon and copies to frontend
This commit is contained in:
Matheus Macabu 2024-10-15 09:44:41 +02:00 committed by GitHub
parent 5fb685dcc6
commit 081ec57443
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 174 additions and 7 deletions

View File

@ -34,6 +34,7 @@ import (
libraryelements "github.com/grafana/grafana/pkg/services/libraryelements/model" libraryelements "github.com/grafana/grafana/pkg/services/libraryelements/model"
"github.com/grafana/grafana/pkg/services/ngalert" "github.com/grafana/grafana/pkg/services/ngalert"
"github.com/grafana/grafana/pkg/services/ngalert/metrics" "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" ngalertstore "github.com/grafana/grafana/pkg/services/ngalert/store"
ngalertfakes "github.com/grafana/grafana/pkg/services/ngalert/tests/fakes" ngalertfakes "github.com/grafana/grafana/pkg/services/ngalert/tests/fakes"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore" "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) kvStore := kvstore.ProvideService(sqlStore)
@ -793,12 +798,37 @@ func setUpServiceTest(t *testing.T, withDashboardMock bool) cloudmigration.Servi
) )
require.NoError(t, err) 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": "<example@email.com>"
}
}]
}]
}
}`
require.NoError(t, ng.Api.AlertingStore.SaveAlertmanagerConfiguration(context.Background(), &models.SaveAlertmanagerConfigurationCmd{
AlertmanagerConfiguration: validConfig,
OrgID: 1,
LastApplied: time.Now().Unix(),
}))
s, err := ProvideService( s, err := ProvideService(
cfg, cfg,
httpclient.NewProvider(), httpclient.NewProvider(),
featuremgmt.WithFeatures( featureToggles,
featuremgmt.FlagOnPremToCloudMigrations,
featuremgmt.FlagDashboardRestore),
sqlStore, sqlStore,
dsService, dsService,
secretskv.NewFakeSQLSecretsKVStore(t, sqlStore), secretskv.NewFakeSQLSecretsKVStore(t, sqlStore),

View File

@ -32,6 +32,7 @@ var currentMigrationTypes = []cloudmigration.MigrateDataType{
cloudmigration.FolderDataType, cloudmigration.FolderDataType,
cloudmigration.LibraryElementDataType, cloudmigration.LibraryElementDataType,
cloudmigration.DashboardDataType, cloudmigration.DashboardDataType,
cloudmigration.MuteTimingType,
} }
func (s *Service) getMigrationDataJSON(ctx context.Context, signedInUser *user.SignedInUser) (*cloudmigration.MigrateDataRequest, error) { 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 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( migrationDataSlice := make(
[]cloudmigration.MigrateDataRequestItem, 0, []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 { 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 // Obtain the names of parent elements for Dashboard and Folders data types
parentNamesByType, err := s.getParentNames(ctx, signedInUser, dashs, folders, libraryElements) parentNamesByType, err := s.getParentNames(ctx, signedInUser, dashs, folders, libraryElements)
if err != nil { if err != nil {

View File

@ -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
}

View File

@ -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
}

View File

@ -219,6 +219,8 @@ function ResourceIcon({ resource }: { resource: ResourceTableItem }) {
return <Icon size="xl" name="database" />; return <Icon size="xl" name="database" />;
case 'LIBRARY_ELEMENT': case 'LIBRARY_ELEMENT':
return <Icon size="xl" name="library-panel" />; return <Icon size="xl" name="library-panel" />;
case 'MUTE_TIMING':
return <Icon size="xl" name="bell-slash" />;
default: default:
return undefined; return undefined;
} }

View File

@ -13,6 +13,8 @@ export function prettyTypeName(type: ResourceTableItem['type']) {
return t('migrate-to-cloud.resource-type.folder', 'Folder'); return t('migrate-to-cloud.resource-type.folder', 'Folder');
case 'LIBRARY_ELEMENT': case 'LIBRARY_ELEMENT':
return t('migrate-to-cloud.resource-type.library_element', '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: default:
return t('migrate-to-cloud.resource-type.unknown', 'Unknown'); return t('migrate-to-cloud.resource-type.unknown', 'Unknown');
} }

View File

@ -52,6 +52,8 @@ function getTranslatedMessage(snapshot: GetSnapshotResponseDto) {
types.push(t('migrate-to-cloud.migrated-counts.folders', 'folders')); types.push(t('migrate-to-cloud.migrated-counts.folders', 'folders'));
} else if (type === 'LIBRARY_ELEMENT') { } else if (type === 'LIBRARY_ELEMENT') {
types.push(t('migrate-to-cloud.migrated-counts.library_elements', 'library elements')); 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; distinctItems += 1;

View File

@ -1408,7 +1408,8 @@
"dashboards": "dashboards", "dashboards": "dashboards",
"datasources": "data sources", "datasources": "data sources",
"folders": "folders", "folders": "folders",
"library_elements": "library elements" "library_elements": "library elements",
"mute_timings": "mute timings"
}, },
"migration-token": { "migration-token": {
"delete-button": "Delete token", "delete-button": "Delete token",
@ -1492,6 +1493,7 @@
"datasource": "Data source", "datasource": "Data source",
"folder": "Folder", "folder": "Folder",
"library_element": "Library Element", "library_element": "Library Element",
"mute_timing": "Mute Timing",
"unknown": "Unknown" "unknown": "Unknown"
}, },
"summary": { "summary": {

View File

@ -1408,7 +1408,8 @@
"dashboards": "đäşĥþőäřđş", "dashboards": "đäşĥþőäřđş",
"datasources": "đäŧä şőūřčęş", "datasources": "đäŧä şőūřčęş",
"folders": "ƒőľđęřş", "folders": "ƒőľđęřş",
"library_elements": "ľįþřäřy ęľęmęʼnŧş" "library_elements": "ľįþřäřy ęľęmęʼnŧş",
"mute_timings": "mūŧę ŧįmįʼnģş"
}, },
"migration-token": { "migration-token": {
"delete-button": "Đęľęŧę ŧőĸęʼn", "delete-button": "Đęľęŧę ŧőĸęʼn",
@ -1492,6 +1493,7 @@
"datasource": "Đäŧä şőūřčę", "datasource": "Đäŧä şőūřčę",
"folder": "Főľđęř", "folder": "Főľđęř",
"library_element": "Ŀįþřäřy Ēľęmęʼnŧ", "library_element": "Ŀįþřäřy Ēľęmęʼnŧ",
"mute_timing": "Mūŧę Ŧįmįʼnģ",
"unknown": "Ůʼnĸʼnőŵʼn" "unknown": "Ůʼnĸʼnőŵʼn"
}, },
"summary": { "summary": {