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
This commit is contained in:
Matheus Macabu 2024-10-17 14:58:05 +02:00 committed by GitHub
parent 8f5edb09ef
commit 1051561154
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 121 additions and 12 deletions

View File

@ -35,6 +35,7 @@ var currentMigrationTypes = []cloudmigration.MigrateDataType{
cloudmigration.MuteTimingType, cloudmigration.MuteTimingType,
cloudmigration.NotificationTemplateType, cloudmigration.NotificationTemplateType,
cloudmigration.ContactPointType, cloudmigration.ContactPointType,
cloudmigration.NotificationPolicyType,
} }
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) {
@ -82,6 +83,13 @@ func (s *Service) getMigrationDataJSON(ctx context.Context, signedInUser *user.S
return nil, err 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( migrationDataSlice := make(
[]cloudmigration.MigrateDataRequestItem, 0, []cloudmigration.MigrateDataRequestItem, 0,
len(dataSources)+len(dashs)+len(folders)+len(libraryElements)+ 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 { for _, muteTiming := range muteTimings {
migrationDataSlice = append(migrationDataSlice, cloudmigration.MigrateDataRequestItem{ migrationDataSlice = append(migrationDataSlice, cloudmigration.MigrateDataRequestItem{
Type: cloudmigration.MuteTimingType, Type: cloudmigration.MuteTimingType,
RefID: muteTiming.Name, RefID: muteTiming.UID,
Name: muteTiming.Name, Name: muteTiming.Name,
Data: muteTiming, Data: muteTiming,
}) })
@ -144,7 +152,7 @@ func (s *Service) getMigrationDataJSON(ctx context.Context, signedInUser *user.S
for _, notificationTemplate := range notificationTemplates { for _, notificationTemplate := range notificationTemplates {
migrationDataSlice = append(migrationDataSlice, cloudmigration.MigrateDataRequestItem{ migrationDataSlice = append(migrationDataSlice, cloudmigration.MigrateDataRequestItem{
Type: cloudmigration.NotificationTemplateType, Type: cloudmigration.NotificationTemplateType,
RefID: notificationTemplate.Name, RefID: notificationTemplate.UID,
Name: notificationTemplate.Name, Name: notificationTemplate.Name,
Data: notificationTemplate, 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 // 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

@ -8,11 +8,14 @@ import (
"github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/services/featuremgmt" "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/ngalert/provisioning"
"github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/services/user"
) )
type muteTimeInterval struct { type muteTimeInterval struct {
UID string `json:"uid"`
// There is a lot of custom (de)serialization logic from Alertmanager, // 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. // and this is the same type used by the underlying API, hence we can use the type as-is.
config.MuteTimeInterval `json:",inline"` config.MuteTimeInterval `json:",inline"`
@ -32,6 +35,7 @@ func (s *Service) getAlertMuteTimings(ctx context.Context, signedInUser *user.Si
for _, muteTiming := range muteTimings { for _, muteTiming := range muteTimings {
muteTimeIntervals = append(muteTimeIntervals, muteTimeInterval{ muteTimeIntervals = append(muteTimeIntervals, muteTimeInterval{
UID: muteTiming.UID,
MuteTimeInterval: config.MuteTimeInterval{ MuteTimeInterval: config.MuteTimeInterval{
Name: muteTiming.Name, Name: muteTiming.Name,
TimeIntervals: muteTiming.TimeIntervals, TimeIntervals: muteTiming.TimeIntervals,
@ -43,6 +47,7 @@ func (s *Service) getAlertMuteTimings(ctx context.Context, signedInUser *user.Si
} }
type notificationTemplate struct { type notificationTemplate struct {
UID string `json:"uid"`
Name string `json:"name"` Name string `json:"name"`
Template string `json:"template"` Template string `json:"template"`
} }
@ -61,6 +66,7 @@ func (s *Service) getNotificationTemplates(ctx context.Context, signedInUser *us
for _, template := range templates { for _, template := range templates {
notificationTemplates = append(notificationTemplates, notificationTemplate{ notificationTemplates = append(notificationTemplates, notificationTemplate{
UID: template.UID,
Name: template.Name, Name: template.Name,
Template: template.Template, Template: template.Template,
}) })
@ -106,3 +112,24 @@ func (s *Service) getContactPoints(ctx context.Context, signedInUser *user.Signe
return contactPoints, nil 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
}

View File

@ -5,8 +5,10 @@ import (
"encoding/json" "encoding/json"
"testing" "testing"
"github.com/prometheus/alertmanager/pkg/labels"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/grafana/alerting/definition"
"github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/featuremgmt"
@ -32,10 +34,9 @@ func TestGetAlertMuteTimings(t *testing.T) {
s := setUpServiceTest(t, false).(*Service) s := setUpServiceTest(t, false).(*Service)
s.features = featuremgmt.WithFeatures(featuremgmt.FlagOnPremToCloudMigrations, featuremgmt.FlagOnPremToCloudMigrationsAlerts) s.features = featuremgmt.WithFeatures(featuremgmt.FlagOnPremToCloudMigrations, featuremgmt.FlagOnPremToCloudMigrationsAlerts)
var orgID int64 = 1 user := &user.SignedInUser{OrgID: 1}
user := &user.SignedInUser{OrgID: orgID}
createdMuteTiming := createMuteTiming(t, ctx, s, orgID) createdMuteTiming := createMuteTiming(t, ctx, s, user)
muteTimeIntervals, err := s.getAlertMuteTimings(ctx, user) muteTimeIntervals, err := s.getAlertMuteTimings(ctx, user)
require.NoError(t, err) require.NoError(t, err)
@ -62,10 +63,9 @@ func TestGetNotificationTemplates(t *testing.T) {
s := setUpServiceTest(t, false).(*Service) s := setUpServiceTest(t, false).(*Service)
s.features = featuremgmt.WithFeatures(featuremgmt.FlagOnPremToCloudMigrations, featuremgmt.FlagOnPremToCloudMigrationsAlerts) s.features = featuremgmt.WithFeatures(featuremgmt.FlagOnPremToCloudMigrations, featuremgmt.FlagOnPremToCloudMigrationsAlerts)
var orgID int64 = 1 user := &user.SignedInUser{OrgID: 1}
user := &user.SignedInUser{OrgID: orgID}
createdTemplate := createNotificationTemplate(t, ctx, s, orgID) createdTemplate := createNotificationTemplate(t, ctx, s, user)
notificationTemplates, err := s.getNotificationTemplates(ctx, user) notificationTemplates, err := s.getNotificationTemplates(ctx, user)
require.NoError(t, err) 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() t.Helper()
muteTiming := `{ muteTiming := `{
@ -133,13 +167,13 @@ func createMuteTiming(t *testing.T, ctx context.Context, service *Service, orgID
var mt definitions.MuteTimeInterval var mt definitions.MuteTimeInterval
require.NoError(t, json.Unmarshal([]byte(muteTiming), &mt)) 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) require.NoError(t, err)
return createdTiming 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() t.Helper()
tmpl := definitions.NotificationTemplate{ 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 }}", 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) require.NoError(t, err)
return createdTemplate return createdTemplate
@ -205,3 +239,25 @@ func createContactPoints(t *testing.T, ctx context.Context, service *Service, us
createdTelegram, 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)
}

View File

@ -225,6 +225,8 @@ function ResourceIcon({ resource }: { resource: ResourceTableItem }) {
return <Icon size="xl" name="bell" />; return <Icon size="xl" name="bell" />;
case 'CONTACT_POINT': case 'CONTACT_POINT':
return <Icon size="xl" name="bell" />; return <Icon size="xl" name="bell" />;
case 'NOTIFICATION_POLICY':
return <Icon size="xl" name="bell" />;
default: default:
return undefined; return undefined;
} }

View File

@ -19,6 +19,8 @@ export function prettyTypeName(type: ResourceTableItem['type']) {
return t('migrate-to-cloud.resource-type.notification_template', 'Notification Template'); return t('migrate-to-cloud.resource-type.notification_template', 'Notification Template');
case 'CONTACT_POINT': case 'CONTACT_POINT':
return t('migrate-to-cloud.resource-type.contact_point', '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: default:
return t('migrate-to-cloud.resource-type.unknown', 'Unknown'); return t('migrate-to-cloud.resource-type.unknown', 'Unknown');
} }

View File

@ -58,6 +58,8 @@ function getTranslatedMessage(snapshot: GetSnapshotResponseDto) {
types.push(t('migrate-to-cloud.migrated-counts.notification_templates', 'notification templates')); types.push(t('migrate-to-cloud.migrated-counts.notification_templates', 'notification templates'));
} else if (type === 'CONTACT_POINT') { } else if (type === 'CONTACT_POINT') {
types.push(t('migrate-to-cloud.migrated-counts.contact_points', 'contact points')); 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; distinctItems += 1;

View File

@ -1416,6 +1416,7 @@
"folders": "folders", "folders": "folders",
"library_elements": "library elements", "library_elements": "library elements",
"mute_timings": "mute timings", "mute_timings": "mute timings",
"notification_policies": "notification policies",
"notification_templates": "notification templates" "notification_templates": "notification templates"
}, },
"migration-token": { "migration-token": {
@ -1502,6 +1503,7 @@
"folder": "Folder", "folder": "Folder",
"library_element": "Library Element", "library_element": "Library Element",
"mute_timing": "Mute Timing", "mute_timing": "Mute Timing",
"notification_policy": "Notification Policy",
"notification_template": "Notification Template", "notification_template": "Notification Template",
"unknown": "Unknown" "unknown": "Unknown"
}, },

View File

@ -1416,6 +1416,7 @@
"folders": "ƒőľđęřş", "folders": "ƒőľđęřş",
"library_elements": "ľįþřäřy ęľęmęʼnŧş", "library_elements": "ľįþřäřy ęľęmęʼnŧş",
"mute_timings": "mūŧę ŧįmįʼnģş", "mute_timings": "mūŧę ŧįmįʼnģş",
"notification_policies": "ʼnőŧįƒįčäŧįőʼn pőľįčįęş",
"notification_templates": "ʼnőŧįƒįčäŧįőʼn ŧęmpľäŧęş" "notification_templates": "ʼnőŧįƒįčäŧįőʼn ŧęmpľäŧęş"
}, },
"migration-token": { "migration-token": {
@ -1502,6 +1503,7 @@
"folder": "Főľđęř", "folder": "Főľđęř",
"library_element": "Ŀįþřäřy Ēľęmęʼnŧ", "library_element": "Ŀįþřäřy Ēľęmęʼnŧ",
"mute_timing": "Mūŧę Ŧįmįʼnģ", "mute_timing": "Mūŧę Ŧįmįʼnģ",
"notification_policy": "Ńőŧįƒįčäŧįőʼn Pőľįčy",
"notification_template": "Ńőŧįƒįčäŧįőʼn Ŧęmpľäŧę", "notification_template": "Ńőŧįƒįčäŧįőʼn Ŧęmpľäŧę",
"unknown": "Ůʼnĸʼnőŵʼn" "unknown": "Ůʼnĸʼnőŵʼn"
}, },