Alerting: Support for renaming receivers (#93349)

* update RenameReceiverInNotificationSettings in DbStore to check for provisioning

* implement renaming in receiver service and provisioning

* do not patch route when stitching

* fix bug in stitching because it returned new name but the old one was expected

* update receiver service to always return result converted from storage model this makes sure that UID and version are consistent with GET\LIST operations

* use provided metadata.name for UID of domain model because rename changes UID and request fails

* remove rename guard

* update UI to not disable receiver name when k8s api enabled

* create should calculate uid from name because new receiver does not have UID yet.
This commit is contained in:
Yuri Tseretyan
2024-09-17 19:07:31 +03:00
committed by GitHub
parent fb0d3b944a
commit 0f788d8d83
14 changed files with 488 additions and 254 deletions
@@ -12,7 +12,6 @@ import (
model "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1"
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/ngalert/notifier/legacy_storage"
)
func convertToK8sResources(
@@ -115,7 +114,7 @@ var permissionMapper = map[ngmodels.ReceiverPermission]string{
func convertToDomainModel(receiver *model.Receiver) (*ngmodels.Receiver, map[string][]string, error) {
domain := &ngmodels.Receiver{
UID: legacy_storage.NameToUid(receiver.Spec.Title),
UID: receiver.Name,
Name: receiver.Spec.Title,
Integrations: make([]*ngmodels.Integration, 0, len(receiver.Spec.Integrations)),
Version: receiver.ResourceVersion,
@@ -231,10 +231,6 @@ func (s *legacyStorage) Update(ctx context.Context,
return old, false, err
}
if p.ObjectMeta.Name != model.GetUID() {
return nil, false, errors.NewBadRequest("title cannot be changed. Consider creating a new resource.")
}
updated, err := s.service.UpdateReceiver(ctx, model, storedSecureFields, info.OrgID, user)
if err != nil {
return nil, false, err
@@ -17,57 +17,57 @@ func (rev *ConfigRevision) DeleteReceiver(uid string) {
})
}
func (rev *ConfigRevision) CreateReceiver(receiver *models.Receiver) error {
func (rev *ConfigRevision) CreateReceiver(receiver *models.Receiver) (*definitions.PostableApiReceiver, error) {
// Check if the receiver already exists.
_, err := rev.GetReceiver(receiver.GetUID())
_, err := rev.GetReceiver(NameToUid(receiver.Name)) // get UID from name because the new receiver does not have UID yet.
if err == nil {
return ErrReceiverExists.Errorf("")
return nil, ErrReceiverExists.Errorf("")
}
if !errors.Is(err, ErrReceiverNotFound) {
return err
return nil, err
}
if err := validateAndSetIntegrationUIDs(receiver); err != nil {
return err
return nil, err
}
postable, err := ReceiverToPostableApiReceiver(receiver)
if err != nil {
return err
return nil, err
}
rev.Config.AlertmanagerConfig.Receivers = append(rev.Config.AlertmanagerConfig.Receivers, postable)
if err := rev.ValidateReceiver(postable); err != nil {
return err
return nil, err
}
return nil
return postable, nil
}
func (rev *ConfigRevision) UpdateReceiver(receiver *models.Receiver) error {
func (rev *ConfigRevision) UpdateReceiver(receiver *models.Receiver) (*definitions.PostableApiReceiver, error) {
existing, err := rev.GetReceiver(receiver.GetUID())
if err != nil {
return err
return nil, err
}
if err := validateAndSetIntegrationUIDs(receiver); err != nil {
return err
return nil, err
}
postable, err := ReceiverToPostableApiReceiver(receiver)
if err != nil {
return err
return nil, err
}
// Update receiver in the configuration.
*existing = *postable
if err := rev.ValidateReceiver(existing); err != nil {
return err
return nil, err
}
return nil
return postable, nil
}
// ReceiverNameUsedByRoutes checks if a receiver name is used in any routes.
@@ -101,9 +101,9 @@ func (rev *ConfigRevision) GetReceivers(uids []string) []*definitions.PostableAp
return receivers
}
// RenameReceiverInRoutes renames all references to a receiver in routes.
func (rev *ConfigRevision) RenameReceiverInRoutes(oldName, newName string) {
RenameReceiverInRoute(oldName, newName, rev.Config.AlertmanagerConfig.Route)
// RenameReceiverInRoutes renames all references to a receiver in routes. Returns number of routes that were updated
func (rev *ConfigRevision) RenameReceiverInRoutes(oldName, newName string) int {
return RenameReceiverInRoute(oldName, newName, rev.Config.AlertmanagerConfig.Route)
}
// ValidateReceiver checks if the given receiver conflicts in name or integration UID with existing receivers.
@@ -135,16 +135,19 @@ func (rev *ConfigRevision) ValidateReceiver(p *definitions.PostableApiReceiver)
return nil
}
func RenameReceiverInRoute(oldName, newName string, routes ...*definitions.Route) {
func RenameReceiverInRoute(oldName, newName string, routes ...*definitions.Route) int {
if len(routes) == 0 {
return
return 0
}
updated := 0
for _, route := range routes {
if route.Receiver == oldName {
route.Receiver = newName
updated++
}
RenameReceiverInRoute(oldName, newName, route.Routes...)
updated += RenameReceiverInRoute(oldName, newName, route.Routes...)
}
return updated
}
// isReceiverInUse checks if a receiver is used in a route or any of its sub-routes.
+66 -14
View File
@@ -28,6 +28,11 @@ var (
"Provided version '{{ .Public.Version }}' of receiver '{{ .Public.Name }}' does not match current version '{{ .Public.CurrentVersion }}'",
errutil.WithPublic("Provided version '{{ .Public.Version }}' of receiver '{{ .Public.Name }}' does not match current version '{{ .Public.CurrentVersion }}'"),
)
ErrReceiverDependentResourcesProvenance = errutil.Conflict("alerting.notifications.receivers.usedProvisioned").MustTemplate(
"Receiver cannot be renamed because it is used by provisioned {{ if .Public.UsedByRules }}alert rules{{ end }}{{ if .Public.UsedByRoutes }}{{ if .Public.UsedByRules }} and {{ end }}notification policies{{ end }}",
errutil.WithPublic(`Receiver cannot be renamed because it is used by provisioned {{ if .Public.UsedByRules }}alert rules{{ end }}{{ if .Public.UsedByRoutes }}{{ if .Public.UsedByRules }} and {{ end }}notification policies{{ end }}. You must update those resources first using the original provision method.`),
)
)
// ReceiverService is the service for managing alertmanager receivers.
@@ -43,7 +48,7 @@ type ReceiverService struct {
}
type alertRuleNotificationSettingsStore interface {
RenameReceiverInNotificationSettings(ctx context.Context, orgID int64, oldReceiver, newReceiver string) (int, error)
RenameReceiverInNotificationSettings(ctx context.Context, orgID int64, oldReceiver, newReceiver string, validateProvenance func(models.Provenance) bool, dryRun bool) ([]models.AlertRuleKey, []models.AlertRuleKey, error)
ListNotificationSettings(ctx context.Context, q models.ListNotificationSettingsQuery) (map[models.AlertRuleKey][]models.NotificationSettings, error)
}
@@ -73,6 +78,7 @@ type alertmanagerConfigStore interface {
}
type provisoningStore interface {
GetProvenance(ctx context.Context, o models.Provisionable, org int64) (models.Provenance, error)
GetProvenances(ctx context.Context, org int64, resourceType string) (map[string]models.Provenance, error)
SetProvenance(ctx context.Context, o models.Provisionable, org int64, p models.Provenance) error
DeleteProvenance(ctx context.Context, o models.Provisionable, org int64) error
@@ -330,11 +336,10 @@ func (rs *ReceiverService) CreateReceiver(ctx context.Context, r *models.Receive
return nil, legacy_storage.MakeErrReceiverInvalid(err)
}
err = revision.CreateReceiver(&createdReceiver)
created, err := revision.CreateReceiver(&createdReceiver)
if err != nil {
return nil, err
}
createdReceiver.Version = createdReceiver.Fingerprint()
err = rs.xact.InTransaction(ctx, func(ctx context.Context) error {
err = rs.cfgStore.Save(ctx, revision, orgID)
@@ -346,7 +351,12 @@ func (rs *ReceiverService) CreateReceiver(ctx context.Context, r *models.Receive
if err != nil {
return nil, err
}
return &createdReceiver, nil
result, err := PostableApiReceiverToReceiver(created, createdReceiver.Provenance)
if err != nil {
return nil, err
}
return result, nil
}
func (rs *ReceiverService) UpdateReceiver(ctx context.Context, r *models.Receiver, storedSecureFields map[string][]string, orgID int64, user identity.Requester) (*models.Receiver, error) {
@@ -400,26 +410,19 @@ func (rs *ReceiverService) UpdateReceiver(ctx context.Context, r *models.Receive
return nil, legacy_storage.MakeErrReceiverInvalid(err)
}
err = revision.UpdateReceiver(&updatedReceiver)
updated, err := revision.UpdateReceiver(&updatedReceiver)
if err != nil {
return nil, err
}
updatedReceiver.Version = updatedReceiver.Fingerprint()
err = rs.xact.InTransaction(ctx, func(ctx context.Context) error {
// If the name of the receiver changed, we must update references to it in both routes and notification settings.
// TODO: Needs to check provenance status compatibility: For example, if we rename a receiver via UI but rules are provisioned, this call should be rejected.
if existing.Name != r.Name {
affected, err := rs.ruleNotificationsStore.RenameReceiverInNotificationSettings(ctx, orgID, existing.Name, r.Name)
err := rs.RenameReceiverInDependentResources(ctx, orgID, revision.Config.AlertmanagerConfig.Route, existing.Name, r.Name, r.Provenance)
if err != nil {
return err
}
if affected > 0 {
rs.log.Info("Renamed receiver in notification settings", "oldName", existing.Name, "newName", r.Name, "affectedSettings", affected)
}
revision.RenameReceiverInRoutes(existing.Name, r.Name)
}
err = rs.cfgStore.Save(ctx, revision, orgID)
if err != nil {
return err
@@ -434,7 +437,12 @@ func (rs *ReceiverService) UpdateReceiver(ctx context.Context, r *models.Receive
if err != nil {
return nil, err
}
return &updatedReceiver, nil
result, err := PostableApiReceiverToReceiver(updated, updatedReceiver.Provenance)
if err != nil {
return nil, err
}
return result, nil
}
func (rs *ReceiverService) UsedByRules(ctx context.Context, orgID int64, name string) ([]models.AlertRuleKey, error) {
@@ -611,3 +619,47 @@ func makeErrReceiverVersionConflict(current *models.Receiver, desiredVersion str
}
return ErrReceiverVersionConflict.Build(data)
}
func makeErrReceiverDependentResourcesProvenance(usedByRoutes bool, rules []models.AlertRuleKey) error {
uids := make([]string, 0, len(rules))
for _, key := range rules {
uids = append(uids, key.UID)
}
data := make(map[string]any, 2)
if len(uids) > 0 {
data["UsedByRules"] = uids
}
if usedByRoutes {
data["UsedByRoutes"] = true
}
return ErrReceiverDependentResourcesProvenance.Build(errutil.TemplateData{
Public: data,
})
}
func (rs *ReceiverService) RenameReceiverInDependentResources(ctx context.Context, orgID int64, route *definitions.Route, oldName, newName string, receiverProvenance models.Provenance) error {
validate := validation.ValidateProvenanceOfDependentResources(receiverProvenance)
// if there are no references to the old time interval, exit
updatedRoutes := legacy_storage.RenameReceiverInRoute(oldName, newName, route)
canUpdate := true
if updatedRoutes > 0 {
routeProvenance, err := rs.provisioningStore.GetProvenance(ctx, route, orgID)
if err != nil {
return err
}
canUpdate = validate(routeProvenance)
}
dryRun := !canUpdate
affected, invalidProvenance, err := rs.ruleNotificationsStore.RenameReceiverInNotificationSettings(ctx, orgID, oldName, newName, validate, dryRun)
if err != nil {
return err
}
if !canUpdate || len(invalidProvenance) > 0 {
return makeErrReceiverDependentResourcesProvenance(updatedRoutes > 0, invalidProvenance)
}
if len(affected) > 0 || updatedRoutes > 0 {
rs.log.FromContext(ctx).Info("Updated rules and routes that use renamed receiver", "oldName", oldName, "newName", newName, "rules", len(affected), "routes", updatedRoutes)
}
return nil
}
@@ -287,16 +287,12 @@ func TestReceiverService_Delete(t *testing.T) {
},
} {
t.Run(tc.name, func(t *testing.T) {
store := &fakeAlertRuleNotificationStore{}
store.ListNotificationSettingsFn = func(ctx context.Context, q models.ListNotificationSettingsQuery) (map[models.AlertRuleKey][]models.NotificationSettings, error) {
return tc.storeSettings, nil
}
sut := createReceiverServiceSut(t, &secretsService)
store := sut.ruleNotificationsStore.(*fakeConfigStore)
store.notificationSettings = map[int64]map[models.AlertRuleKey][]models.NotificationSettings{
1: make(map[models.AlertRuleKey][]models.NotificationSettings),
}
for key, settings := range tc.storeSettings {
store.notificationSettings[tc.user.GetOrgID()][key] = settings
}
sut.ruleNotificationsStore = store
if tc.existing != nil {
created, err := sut.CreateReceiver(context.Background(), tc.existing, tc.user.GetOrgID(), tc.user)
@@ -758,8 +754,6 @@ func TestReceiverService_UpdateReceiverName(t *testing.T) {
}}
secretsService := fake_secrets.NewFakeSecretsService()
sut := createReceiverServiceSut(t, &secretsService)
receiverName := "grafana-default-email"
newReceiverName := "new-name"
slackIntegration := models.IntegrationGen(models.IntegrationMuts.WithName(receiverName), models.IntegrationMuts.WithValidConfig("slack"))()
@@ -767,38 +761,78 @@ func TestReceiverService_UpdateReceiverName(t *testing.T) {
baseReceiver.Version = "cd95627c75892a39" // Correct version for grafana-default-email.
baseReceiver.Name = newReceiverName // Done here instead of in a mutator so we keep the same uid.
store := sut.ruleNotificationsStore.(*fakeConfigStore)
ns := models.NotificationSettingsGen(models.NSMuts.WithReceiver(receiverName))()
store.notificationSettings = map[int64]map[models.AlertRuleKey][]models.NotificationSettings{
1: {
{OrgID: 1, UID: "rule1"}: {ns},
},
}
t.Run("renames receiver and all its dependencies", func(t *testing.T) {
ruleStore := &fakeAlertRuleNotificationStore{}
sut := createReceiverServiceSut(t, &secretsService)
sut.ruleNotificationsStore = ruleStore
_, err := sut.UpdateReceiver(context.Background(), &baseReceiver, nil, writer.GetOrgID(), writer)
require.NoError(t, err)
_, err := sut.UpdateReceiver(context.Background(), &baseReceiver, nil, writer.GetOrgID(), writer)
require.NoError(t, err)
// Ensure receiver name is updated in notification settings.
oldSettings, err := sut.ruleNotificationsStore.ListNotificationSettings(context.Background(), models.ListNotificationSettingsQuery{
OrgID: writer.GetOrgID(),
ReceiverName: receiverName,
assert.Equal(t, "RenameReceiverInNotificationSettings", ruleStore.Calls[0].Method)
assert.Equal(t, writer.OrgID, ruleStore.Calls[0].Args[1])
assert.Equal(t, receiverName, ruleStore.Calls[0].Args[2])
assert.Equal(t, newReceiverName, ruleStore.Calls[0].Args[3])
assert.NotNil(t, ruleStore.Calls[0].Args[4])
assert.Falsef(t, ruleStore.Calls[0].Args[5].(bool), "dryrun expected to be false")
// Ensure receiver name is updated in routes.
revision, err := sut.cfgStore.Get(context.Background(), writer.GetOrgID())
require.NoError(t, err)
assert.Falsef(t, revision.ReceiverNameUsedByRoutes(receiverName), "old receiver name '%s' should not be used by routes", receiverName)
assert.Truef(t, revision.ReceiverNameUsedByRoutes(newReceiverName), "new receiver name '%s' should be used by routes", newReceiverName)
})
require.NoError(t, err)
assert.Equal(t, 0, len(oldSettings))
newSettings, err := sut.ruleNotificationsStore.ListNotificationSettings(context.Background(), models.ListNotificationSettingsQuery{
OrgID: writer.GetOrgID(),
ReceiverName: baseReceiver.Name,
t.Run("returns ErrReceiverDependentResourcesProvenance if route has different provenance status", func(t *testing.T) {
sut := createReceiverServiceSut(t, &secretsService)
provenanceStore := sut.provisioningStore.(*fakes.FakeProvisioningStore)
provenanceStore.Records[1] = map[string]models.Provenance{
(&definitions.Route{}).ResourceType(): models.ProvenanceFile,
}
ruleStore := &fakeAlertRuleNotificationStore{
RenameReceiverInNotificationSettingsFn: func(ctx context.Context, orgID int64, old, new string, validate func(models.Provenance) bool, dryRun bool) ([]models.AlertRuleKey, []models.AlertRuleKey, error) {
assertInTransaction(t, ctx)
return nil, nil, nil
},
}
sut.ruleNotificationsStore = ruleStore
_, err := sut.UpdateReceiver(context.Background(), &baseReceiver, nil, writer.GetOrgID(), writer)
require.ErrorIs(t, err, ErrReceiverDependentResourcesProvenance)
require.Len(t, ruleStore.Calls, 1)
assert.Equal(t, "RenameReceiverInNotificationSettings", ruleStore.Calls[0].Method)
assert.Equal(t, writer.OrgID, ruleStore.Calls[0].Args[1])
assert.Equal(t, receiverName, ruleStore.Calls[0].Args[2])
assert.Equal(t, newReceiverName, ruleStore.Calls[0].Args[3])
assert.NotNil(t, ruleStore.Calls[0].Args[4])
assert.True(t, ruleStore.Calls[0].Args[5].(bool)) // still check if there are rules that have incompatible provenance
})
require.NoError(t, err)
assert.Equal(t, 1, len(newSettings))
assert.Equal(t, newReceiverName, newSettings[models.AlertRuleKey{OrgID: 1, UID: "rule1"}][0].Receiver)
// Ensure receiver name is updated in routes.
revision, err := sut.cfgStore.Get(context.Background(), writer.GetOrgID())
require.NoError(t, err)
t.Run("returns ErrReceiverDependentResourcesProvenance if rules have different provenance status", func(t *testing.T) {
sut := createReceiverServiceSut(t, &secretsService)
assert.Falsef(t, revision.ReceiverNameUsedByRoutes(receiverName), "old receiver name '%s' should not be used by routes", receiverName)
assert.Truef(t, revision.ReceiverNameUsedByRoutes(newReceiverName), "new receiver name '%s' should be used by routes", newReceiverName)
ruleStore := &fakeAlertRuleNotificationStore{
RenameReceiverInNotificationSettingsFn: func(ctx context.Context, orgID int64, old, new string, validate func(models.Provenance) bool, dryRun bool) ([]models.AlertRuleKey, []models.AlertRuleKey, error) {
assertInTransaction(t, ctx)
return nil, []models.AlertRuleKey{models.GenerateRuleKey(orgID)}, nil
},
}
sut.ruleNotificationsStore = ruleStore
_, err := sut.UpdateReceiver(context.Background(), &baseReceiver, nil, writer.GetOrgID(), writer)
require.ErrorIs(t, err, ErrReceiverDependentResourcesProvenance)
require.Len(t, ruleStore.Calls, 1)
assert.Equal(t, "RenameReceiverInNotificationSettings", ruleStore.Calls[0].Method)
assert.Equal(t, writer.OrgID, ruleStore.Calls[0].Args[1])
assert.Equal(t, receiverName, ruleStore.Calls[0].Args[2])
assert.Equal(t, newReceiverName, ruleStore.Calls[0].Args[3])
assert.NotNil(t, ruleStore.Calls[0].Args[4])
assert.Falsef(t, ruleStore.Calls[0].Args[5].(bool), "dryrun expected to be false")
})
}
func TestReceiverServiceAC_Read(t *testing.T) {
@@ -1401,16 +1435,13 @@ func TestReceiverService_InUseMetadata(t *testing.T) {
},
} {
t.Run(tc.name, func(t *testing.T) {
store := &fakeAlertRuleNotificationStore{}
store.ListNotificationSettingsFn = func(ctx context.Context, q models.ListNotificationSettingsQuery) (map[models.AlertRuleKey][]models.NotificationSettings, error) {
return tc.storeSettings, nil
}
sut := createReceiverServiceSut(t, &secretsService)
store := sut.ruleNotificationsStore.(*fakeConfigStore)
store.notificationSettings = map[int64]map[models.AlertRuleKey][]models.NotificationSettings{
1: make(map[models.AlertRuleKey][]models.NotificationSettings),
}
for key, settings := range tc.storeSettings {
store.notificationSettings[tc.user.GetOrgID()][key] = settings
}
sut.ruleNotificationsStore = store
for _, recv := range tc.existing {
_, err := sut.CreateReceiver(context.Background(), recv, tc.user.GetOrgID(), tc.user)
@@ -1448,7 +1479,7 @@ func createReceiverServiceSut(t *testing.T, encryptSvc secretService) *ReceiverS
ac.NewReceiverAccess[*models.Receiver](acimpl.ProvideAccessControl(featuremgmt.WithFeatures(), zanzana.NewNoopClient()), false),
legacy_storage.NewAlertmanagerConfigStore(store),
provisioningStore,
NewFakeConfigStore(t, nil),
&fakeAlertRuleNotificationStore{},
encryptSvc,
xact,
log.NewNopLogger(),
@@ -1533,3 +1564,7 @@ func newNopTransactionManager() *NopTransactionManager {
func (n *NopTransactionManager) InTransaction(ctx context.Context, work func(ctx context.Context) error) error {
return work(context.WithValue(ctx, NopTransactionManager{}, struct{}{}))
}
func assertInTransaction(t *testing.T, ctx context.Context) {
assert.Truef(t, ctx.Value(NopTransactionManager{}) != nil, "Expected to be executed in transaction but there is none")
}
+42 -22
View File
@@ -57,28 +57,6 @@ func (f *fakeConfigStore) ListNotificationSettings(ctx context.Context, q models
return settings, nil
}
func (f *fakeConfigStore) RenameReceiverInNotificationSettings(ctx context.Context, orgID int64, oldReceiver, newReceiver string) (int, error) {
if oldReceiver == newReceiver {
return 0, nil
}
settings, ok := f.notificationSettings[orgID]
if !ok {
return 0, nil
}
var updated int
for _, notificationSettings := range settings {
for i, setting := range notificationSettings {
if setting.Receiver == oldReceiver {
updated++
notificationSettings[i].Receiver = newReceiver
}
}
}
return updated, nil
}
// Saves the image or returns an error.
func (f *fakeConfigStore) SaveImage(ctx context.Context, img *models.Image) error {
return alertingImages.ErrImageNotFound
@@ -379,3 +357,45 @@ func createNotificationLog(groupKey string, receiverName string, sentAt, expires
ExpiresAt: expiresAt,
}
}
type call struct {
Method string
Args []interface{}
}
type fakeAlertRuleNotificationStore struct {
Calls []call
RenameReceiverInNotificationSettingsFn func(ctx context.Context, orgID int64, oldReceiver, newReceiver string, validateProvenance func(models.Provenance) bool, dryRun bool) ([]models.AlertRuleKey, []models.AlertRuleKey, error)
ListNotificationSettingsFn func(ctx context.Context, q models.ListNotificationSettingsQuery) (map[models.AlertRuleKey][]models.NotificationSettings, error)
}
func (f *fakeAlertRuleNotificationStore) RenameReceiverInNotificationSettings(ctx context.Context, orgID int64, oldReceiver, newReceiver string, validateProvenance func(models.Provenance) bool, dryRun bool) ([]models.AlertRuleKey, []models.AlertRuleKey, error) {
call := call{
Method: "RenameReceiverInNotificationSettings",
Args: []interface{}{ctx, orgID, oldReceiver, newReceiver, validateProvenance, dryRun},
}
f.Calls = append(f.Calls, call)
if f.RenameReceiverInNotificationSettingsFn != nil {
return f.RenameReceiverInNotificationSettingsFn(ctx, orgID, oldReceiver, newReceiver, validateProvenance, dryRun)
}
// Default values when no function hook is provided
return nil, nil, nil
}
func (f *fakeAlertRuleNotificationStore) ListNotificationSettings(ctx context.Context, q models.ListNotificationSettingsQuery) (map[models.AlertRuleKey][]models.NotificationSettings, error) {
call := call{
Method: "ListNotificationSettings",
Args: []interface{}{ctx, q},
}
f.Calls = append(f.Calls, call)
if f.ListNotificationSettingsFn != nil {
return f.ListNotificationSettingsFn(ctx, q)
}
// Default values when no function hook is provided
return nil, nil
}
@@ -24,7 +24,7 @@ import (
)
type AlertRuleNotificationSettingsStore interface {
RenameReceiverInNotificationSettings(ctx context.Context, orgID int64, oldReceiver, newReceiver string) (int, error)
RenameReceiverInNotificationSettings(ctx context.Context, orgID int64, oldReceiver, newReceiver string, validateProvenance func(models.Provenance) bool, dryRun bool) ([]models.AlertRuleKey, []models.AlertRuleKey, error)
RenameTimeIntervalInNotificationSettings(ctx context.Context, orgID int64, oldTimeInterval, newTimeInterval string, validateProvenance func(models.Provenance) bool, dryRun bool) ([]models.AlertRuleKey, []models.AlertRuleKey, error)
ListNotificationSettings(ctx context.Context, q models.ListNotificationSettingsQuery) (map[models.AlertRuleKey][]models.NotificationSettings, error)
}
@@ -41,6 +41,7 @@ type ContactPointService struct {
type receiverService interface {
GetReceivers(ctx context.Context, query models.GetReceiversQuery, user identity.Requester) ([]*models.Receiver, error)
RenameReceiverInDependentResources(ctx context.Context, orgID int64, route *apimodels.Route, oldName, newName string, receiverProvenance models.Provenance) error
}
func NewContactPointService(store alertmanagerConfigStore, encryptionService secrets.Service,
@@ -290,17 +291,14 @@ func (ecp *ContactPointService) UpdateContactPoint(ctx context.Context, orgID in
}
err = ecp.xact.InTransaction(ctx, func(ctx context.Context) error {
if err := ecp.configStore.Save(ctx, revision, orgID); err != nil {
return err
}
if renamedReceiver != "" && renamedReceiver != mergedReceiver.Name {
affected, err := ecp.notificationSettingsStore.RenameReceiverInNotificationSettings(ctx, orgID, renamedReceiver, mergedReceiver.Name)
err := ecp.receiverService.RenameReceiverInDependentResources(ctx, orgID, revision.Config.AlertmanagerConfig.Route, renamedReceiver, mergedReceiver.Name, provenance)
if err != nil {
return err
}
if affected > 0 {
ecp.log.Info("Renamed receiver in notification settings", "oldName", renamedReceiver, "newName", mergedReceiver.Name, "affectedSettings", affected)
}
}
if err := ecp.configStore.Save(ctx, revision, orgID); err != nil {
return err
}
return ecp.provenanceStore.SetProvenance(ctx, &contactPoint, orgID, provenance)
})
@@ -426,10 +424,9 @@ groupLoop:
// If we're renaming, we'll need to fix up the macro receiver group for consistency.
// Firstly, if we're the only receiver in the group, simply rename the group to match. Done!
if len(receiverGroup.GrafanaManagedReceivers) == 1 {
legacy_storage.RenameReceiverInRoute(receiverGroup.Name, target.Name, cfg.AlertmanagerConfig.Route)
renamedReceiver = receiverGroup.Name // remember the old name of the receiver.
receiverGroup.Name = target.Name
receiverGroup.GrafanaManagedReceivers[i] = target
renamedReceiver = receiverGroup.Name
}
// Otherwise, we only want to rename the receiver we are touching... NOT all of them.
@@ -178,6 +178,44 @@ func TestContactPointService(t *testing.T) {
require.ErrorIs(t, err, ErrValidation)
})
t.Run("update renames references when group is renamed", func(t *testing.T) {
cfg := createEncryptedConfig(t, secretsService)
store := fakes.NewFakeAlertmanagerConfigStore(cfg)
sut := createContactPointServiceSutWithConfigStore(t, secretsService, store)
svc := &fakeReceiverService{}
sut.receiverService = svc
newCp := createTestContactPoint()
oldName := newCp.Name
newName := "new-name"
newCp, err := sut.CreateContactPoint(context.Background(), 1, newCp, models.ProvenanceAPI)
require.NoError(t, err)
newCp.Name = newName
svc.RenameReceiverInDependentResourcesFunc = func(ctx context.Context, orgID int64, route *definitions.Route, oldName, newName string, receiverProvenance models.Provenance) error {
legacy_storage.RenameReceiverInRoute(oldName, newName, route)
return nil
}
err = sut.UpdateContactPoint(context.Background(), 1, newCp, models.ProvenanceAPI)
require.NoError(t, err)
parsed, err := legacy_storage.DeserializeAlertmanagerConfig([]byte(store.LastSaveCommand.AlertmanagerConfiguration))
require.NoError(t, err)
require.Lenf(t, svc.Calls, 1, "service was supposed to be called once")
assert.Equal(t, "RenameReceiverInDependentResources", svc.Calls[0].Method)
assertInTransaction(t, svc.Calls[0].Args[0].(context.Context))
assert.Equal(t, int64(1), svc.Calls[0].Args[1])
assert.EqualValues(t, parsed.AlertmanagerConfig.Route, svc.Calls[0].Args[2])
assert.Equal(t, oldName, svc.Calls[0].Args[3])
assert.Equal(t, newName, svc.Calls[0].Args[4])
assert.Equal(t, models.ProvenanceAPI, svc.Calls[0].Args[5])
})
t.Run("default provenance of contact points is none", func(t *testing.T) {
sut := createContactPointServiceSut(t, secretsService)
@@ -442,6 +480,7 @@ func createContactPointServiceSut(t *testing.T, secretService secrets.Service) *
}
func createContactPointServiceSutWithConfigStore(t *testing.T, secretService secrets.Service, configStore legacy_storage.AMConfigStore) *ContactPointService {
t.Helper()
// Encrypt secure settings.
xact := newNopTransactionManager()
provisioningStore := fakes.NewFakeProvisioningStore()
@@ -450,7 +489,7 @@ func createContactPointServiceSutWithConfigStore(t *testing.T, secretService sec
ac.NewReceiverAccess[*models.Receiver](acimpl.ProvideAccessControl(featuremgmt.WithFeatures(), zanzana.NewNoopClient()), true),
legacy_storage.NewAlertmanagerConfigStore(configStore),
provisioningStore,
notifier.NewFakeConfigStore(t, nil),
&fakeAlertRuleNotificationStore{},
secretService,
xact,
log.NewNopLogger(),
@@ -589,14 +628,14 @@ func TestStitchReceivers(t *testing.T) {
Type: "slack",
},
expModified: true,
expRenamedReceiver: "new-receiver",
expRenamedReceiver: "receiver-1",
expCfg: definitions.PostableApiAlertingConfig{
Config: definitions.Config{
Route: &definitions.Route{
Receiver: "new-receiver",
Receiver: "receiver-1",
Routes: []*definitions.Route{
{
Receiver: "new-receiver",
Receiver: "receiver-1",
},
},
},
@@ -1191,7 +1230,7 @@ func TestStitchReceivers(t *testing.T) {
Type: "slack",
},
expModified: true,
expRenamedReceiver: "receiver-1",
expRenamedReceiver: "receiver-2",
expCfg: definitions.PostableApiAlertingConfig{
Config: definitions.Config{
Route: &definitions.Route{
@@ -1201,7 +1240,7 @@ func TestStitchReceivers(t *testing.T) {
Receiver: "receiver-1",
},
{
Receiver: "receiver-1",
Receiver: "receiver-2",
},
},
},
+28 -5
View File
@@ -9,6 +9,7 @@ import (
mock "github.com/stretchr/testify/mock"
"github.com/grafana/grafana/pkg/apimachinery/identity"
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
"github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/ngalert/notifier"
"github.com/grafana/grafana/pkg/services/ngalert/store"
@@ -169,24 +170,24 @@ func (s *fakeRuleAccessControlService) CanWriteAllRules(ctx context.Context, use
type fakeAlertRuleNotificationStore struct {
Calls []call
RenameReceiverInNotificationSettingsFn func(ctx context.Context, orgID int64, oldReceiver, newReceiver string) (int, error)
RenameReceiverInNotificationSettingsFn func(ctx context.Context, orgID int64, oldReceiver, newReceiver string, validateProvenance func(models.Provenance) bool, dryRun bool) ([]models.AlertRuleKey, []models.AlertRuleKey, error)
RenameTimeIntervalInNotificationSettingsFn func(ctx context.Context, orgID int64, old, new string, validate func(models.Provenance) bool, dryRun bool) ([]models.AlertRuleKey, []models.AlertRuleKey, error)
ListNotificationSettingsFn func(ctx context.Context, q models.ListNotificationSettingsQuery) (map[models.AlertRuleKey][]models.NotificationSettings, error)
}
func (f *fakeAlertRuleNotificationStore) RenameReceiverInNotificationSettings(ctx context.Context, orgID int64, oldReceiver, newReceiver string) (int, error) {
func (f *fakeAlertRuleNotificationStore) RenameReceiverInNotificationSettings(ctx context.Context, orgID int64, oldReceiver, newReceiver string, validateProvenance func(models.Provenance) bool, dryRun bool) ([]models.AlertRuleKey, []models.AlertRuleKey, error) {
call := call{
Method: "RenameReceiverInNotificationSettings",
Args: []interface{}{ctx, orgID, oldReceiver, newReceiver},
Args: []interface{}{ctx, orgID, oldReceiver, newReceiver, validateProvenance, dryRun},
}
f.Calls = append(f.Calls, call)
if f.RenameReceiverInNotificationSettingsFn != nil {
return f.RenameReceiverInNotificationSettingsFn(ctx, orgID, oldReceiver, newReceiver)
return f.RenameReceiverInNotificationSettingsFn(ctx, orgID, oldReceiver, newReceiver, validateProvenance, dryRun)
}
// Default values when no function hook is provided
return 0, nil
return nil, nil, nil
}
func (f *fakeAlertRuleNotificationStore) RenameTimeIntervalInNotificationSettings(ctx context.Context, orgID int64, oldTimeInterval, newTimeInterval string, validate func(models.Provenance) bool, dryRun bool) ([]models.AlertRuleKey, []models.AlertRuleKey, error) {
@@ -218,3 +219,25 @@ func (f *fakeAlertRuleNotificationStore) ListNotificationSettings(ctx context.Co
// Default values when no function hook is provided
return nil, nil
}
type fakeReceiverService struct {
Calls []call
GetReceiversFunc func(ctx context.Context, query models.GetReceiversQuery, user identity.Requester) ([]*models.Receiver, error)
RenameReceiverInDependentResourcesFunc func(ctx context.Context, orgID int64, route *apimodels.Route, oldName, newName string, receiverProvenance models.Provenance) error
}
func (f *fakeReceiverService) GetReceivers(ctx context.Context, query models.GetReceiversQuery, user identity.Requester) ([]*models.Receiver, error) {
f.Calls = append(f.Calls, call{Method: "GetReceivers", Args: []interface{}{ctx, query, user}})
if f.GetReceiversFunc != nil {
return f.GetReceiversFunc(ctx, query, user)
}
return nil, nil
}
func (f *fakeReceiverService) RenameReceiverInDependentResources(ctx context.Context, orgID int64, route *apimodels.Route, oldName, newName string, receiverProvenance models.Provenance) error {
f.Calls = append(f.Calls, call{Method: "RenameReceiverInDependentResources", Args: []interface{}{ctx, orgID, route, oldName, newName, receiverProvenance}})
if f.RenameReceiverInDependentResourcesFunc != nil {
return f.RenameReceiverInDependentResourcesFunc(ctx, orgID, route, oldName, newName, receiverProvenance)
}
return nil
}
+34 -4
View File
@@ -824,21 +824,45 @@ func (st DBstore) filterByContentInNotificationSettings(value string, sess *xorm
return sess.And(fmt.Sprintf("notification_settings %s ?", st.SQLStore.GetDialect().LikeStr()), "%"+search+"%"), nil
}
func (st DBstore) RenameReceiverInNotificationSettings(ctx context.Context, orgID int64, oldReceiver, newReceiver string) (int, error) {
func (st DBstore) RenameReceiverInNotificationSettings(ctx context.Context, orgID int64, oldReceiver, newReceiver string, validateProvenance func(ngmodels.Provenance) bool, dryRun bool) ([]ngmodels.AlertRuleKey, []ngmodels.AlertRuleKey, error) {
// fetch entire rules because Update method requires it because it copies rules to version table
rules, err := st.ListAlertRules(ctx, &ngmodels.ListAlertRulesQuery{
OrgID: orgID,
ReceiverName: oldReceiver,
})
if err != nil {
return 0, err
return nil, nil, err
}
if len(rules) == 0 {
return 0, nil
return nil, nil, nil
}
provenances, err := st.GetProvenances(ctx, orgID, (&ngmodels.AlertRule{}).ResourceType())
if err != nil {
return nil, nil, err
}
var invalidProvenance []ngmodels.AlertRuleKey
result := make([]ngmodels.AlertRuleKey, 0, len(rules))
updates := make([]ngmodels.UpdateRule, 0, len(rules))
for _, rule := range rules {
provenance, ok := provenances[rule.UID]
if !ok {
provenance = ngmodels.ProvenanceNone
}
if !validateProvenance(provenance) {
invalidProvenance = append(invalidProvenance, rule.GetKey())
}
if len(invalidProvenance) > 0 { // do not do any fixes if there is at least one rule with not allowed provenance
continue
}
result = append(result, rule.GetKey())
if dryRun {
continue
}
r := ngmodels.CopyRule(rule)
for idx := range r.NotificationSettings {
if r.NotificationSettings[idx].Receiver == oldReceiver {
@@ -851,7 +875,13 @@ func (st DBstore) RenameReceiverInNotificationSettings(ctx context.Context, orgI
New: *r,
})
}
return len(updates), st.UpdateAlertRules(ctx, updates)
if len(invalidProvenance) > 0 {
return nil, invalidProvenance, nil
}
if dryRun {
return result, nil, nil
}
return result, nil, st.UpdateAlertRules(ctx, updates)
}
// RenameTimeIntervalInNotificationSettings renames all rules that use old time interval name to the new name.
+78 -22
View File
@@ -880,35 +880,91 @@ func TestIntegrationAlertRulesNotificationSettings(t *testing.T) {
}
})
t.Run("RenameReceiverInNotificationSettings should update all rules that refer to the old receiver", func(t *testing.T) {
t.Run("RenameReceiverInNotificationSettings", func(t *testing.T) {
newName := "new-receiver"
affected, err := store.RenameReceiverInNotificationSettings(context.Background(), 1, receiverName, newName)
require.NoError(t, err)
require.Equal(t, len(receiveRules), affected)
expected := getKeyMap(receiveRules)
actual, err := store.ListAlertRules(context.Background(), &models.ListAlertRulesQuery{
OrgID: 1,
ReceiverName: newName,
})
require.NoError(t, err)
assert.Len(t, actual, len(expected))
for _, rule := range actual {
assert.Contains(t, expected, rule.GetKey())
alwaysTrue := func(p models.Provenance) bool {
return true
}
actual, err = store.ListAlertRules(context.Background(), &models.ListAlertRulesQuery{
OrgID: 1,
ReceiverName: receiverName,
})
require.NoError(t, err)
require.Empty(t, actual)
t.Run("should do nothing if no rules that match the filter", func(t *testing.T) {
affected, err := store.RenameReceiverInNotificationSettings(context.Background(), 1, receiverName, util.GenerateShortUID())
affected, invalidProvenance, err := store.RenameReceiverInNotificationSettings(context.Background(), 1, "not-found", timeIntervalName, alwaysTrue, false)
require.NoError(t, err)
require.Empty(t, affected)
require.Empty(t, invalidProvenance)
})
t.Run("should do nothing if at least one rule has provenance that is not allowed", func(t *testing.T) {
calledTimes := 0
alwaysFalse := func(p models.Provenance) bool {
calledTimes++
return false
}
affected, invalidProvenance, err := store.RenameReceiverInNotificationSettings(context.Background(), 1, receiverName, newName, alwaysFalse, false)
var expected []models.AlertRuleKey
for _, rule := range receiveRules {
expected = append(expected, rule.GetKey())
}
require.NoError(t, err)
require.Empty(t, affected)
require.ElementsMatch(t, expected, invalidProvenance)
assert.Equal(t, len(expected), calledTimes)
actual, err := store.ListAlertRules(context.Background(), &models.ListAlertRulesQuery{
OrgID: 1,
ReceiverName: receiverName,
})
require.NoError(t, err)
assert.Len(t, actual, len(receiveRules))
})
t.Run("should do nothing if dry run is set to true", func(t *testing.T) {
affected, invalidProvenance, err := store.RenameReceiverInNotificationSettings(context.Background(), 1, receiverName, newName, alwaysTrue, true)
require.NoError(t, err)
require.Empty(t, invalidProvenance)
assert.Len(t, affected, len(receiveRules))
expected := getKeyMap(receiveRules)
for _, key := range affected {
assert.Contains(t, expected, key)
}
actual, err := store.ListAlertRules(context.Background(), &models.ListAlertRulesQuery{
OrgID: 1,
ReceiverName: receiverName,
})
require.NoError(t, err)
assert.Len(t, actual, len(receiveRules))
})
t.Run("should update all rules that refer to the old receiver", func(t *testing.T) {
affected, invalidProvenance, err := store.RenameReceiverInNotificationSettings(context.Background(), 1, receiverName, newName, alwaysTrue, false)
require.NoError(t, err)
require.Empty(t, invalidProvenance)
assert.Len(t, affected, len(receiveRules))
expected := getKeyMap(receiveRules)
for _, key := range affected {
assert.Contains(t, expected, key)
}
actual, err := store.ListAlertRules(context.Background(), &models.ListAlertRulesQuery{
OrgID: 1,
ReceiverName: newName,
})
require.NoError(t, err)
assert.Len(t, actual, len(expected))
for _, rule := range actual {
assert.Contains(t, expected, rule.GetKey())
}
actual, err = store.ListAlertRules(context.Background(), &models.ListAlertRulesQuery{
OrgID: 1,
ReceiverName: receiverName,
})
require.NoError(t, err)
require.Empty(t, actual)
})
})
@@ -34,6 +34,7 @@ import (
"github.com/grafana/grafana/pkg/services/folder/foldertest"
"github.com/grafana/grafana/pkg/services/ngalert/api"
"github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/ngalert/notifier/channels_config"
"github.com/grafana/grafana/pkg/services/ngalert/store"
"github.com/grafana/grafana/pkg/services/org"
@@ -103,24 +104,25 @@ func TestIntegrationResourceIdentifier(t *testing.T) {
require.Equal(t, newResource.Spec, actual.Spec)
})
// TODO uncomment when renaming is supported
// t.Run("update should rename receiver if name in the specification changes", func(t *testing.T) {
// existing, err := client.Get(ctx, resourceID, v1.GetOptions{})
// require.NoError(t, err)
//
// updated := existing.DeepCopy()
// updated.Spec.Title = "another-newReceiver"
//
// actual, err := client.Update(ctx, updated, v1.UpdateOptions{})
// require.NoError(t, err)
// require.Equal(t, updated.Spec, actual.Spec)
// require.NotEqualf(t, updated.Name, actual.Name, "Update should change the resource name but it didn't")
// require.NotEqualf(t, updated.ResourceVersion, actual.ResourceVersion, "Update should change the resource version but it didn't")
//
// resource, err := client.Get(ctx, actual.Name, v1.GetOptions{})
// require.NoError(t, err)
// require.Equal(t, actual, resource)
// })
t.Run("update should rename receiver if name in the specification changes", func(t *testing.T) {
existing, err := client.Get(ctx, resourceID, v1.GetOptions{})
require.NoError(t, err)
updated := existing.DeepCopy()
updated.Spec.Title = "another-newReceiver"
actual, err := client.Update(ctx, updated, v1.UpdateOptions{})
require.NoError(t, err)
require.Equal(t, updated.Spec, actual.Spec)
require.NotEqualf(t, updated.Name, actual.Name, "Update should change the resource name but it didn't")
require.NotEqualf(t, updated.ResourceVersion, actual.ResourceVersion, "Update should change the resource version but it didn't")
resource, err := client.Get(ctx, actual.Name, v1.GetOptions{})
require.NoError(t, err)
require.Equal(t, actual.Spec, resource.Spec)
require.Equal(t, actual.Name, resource.Name)
require.Equal(t, actual.ResourceVersion, resource.ResourceVersion)
})
}
func TestIntegrationAccessControl(t *testing.T) {
@@ -813,11 +815,11 @@ func TestIntegrationReferentialIntegrity(t *testing.T) {
ctx := context.Background()
helper := getTestHelper(t)
// env := helper.GetEnv()
// ac := acimpl.ProvideAccessControl(env.FeatureToggles, zanzana.NewNoopClient())
// db, err := store.ProvideDBStore(env.Cfg, env.FeatureToggles, env.SQLStore, &foldertest.FakeService{}, &dashboards.FakeDashboardService{}, ac)
// require.NoError(t, err)
// orgID := helper.Org1.Admin.Identity.GetOrgID()
env := helper.GetEnv()
ac := acimpl.ProvideAccessControl(env.FeatureToggles, zanzana.NewNoopClient())
db, err := store.ProvideDBStore(env.Cfg, env.FeatureToggles, env.SQLStore, &foldertest.FakeService{}, &dashboards.FakeDashboardService{}, ac)
require.NoError(t, err)
orgID := helper.Org1.Admin.Identity.GetOrgID()
cliCfg := helper.Org1.Admin.NewRestConfig()
legacyCli := alerting.NewAlertingLegacyAPIClient(helper.GetEnv().Server.HTTPServer.Listener.Addr().String(), cliCfg.Username, cliCfg.Password)
@@ -852,71 +854,63 @@ func TestIntegrationReferentialIntegrity(t *testing.T) {
})
receiver := receivers.Items[idx]
// TODO uncomment when renaming is enabled
// currentRoute := legacyCli.GetRoute(t)
// currentRuleGroup := legacyCli.GetRulesGroup(t, folderUID, ruleGroup.Name)
// replace := func(input []string, oldName, newName string) []string {
// result := make([]string, 0, len(input))
// for _, s := range input {
// if s == oldName {
// result = append(result, newName)
// continue
// }
// result = append(result, s)
// }
// return result
// }
//
// t.Run("Update", func(t *testing.T) {
// t.Run("should rename all references if name changes", func(t *testing.T) {
// renamed := receiver.DeepCopy()
// expectedTitle := renamed.Spec.Title + "-new"
// renamed.Spec.Title += expectedTitle
//
// actual, err := adminClient.Update(ctx, renamed, v1.UpdateOptions{})
// require.NoError(t, err)
//
// updatedRuleGroup := legacyCli.GetRulesGroup(t, folderUID, ruleGroup.Name)
// for idx, rule := range updatedRuleGroup.Rules {
// assert.Equalf(t, expectedTitle, rule.GrafanaManagedAlert.NotificationSettings.Receiver, "receiver in rule %d should have been renamed but it did not", idx)
// }
//
// updatedRoute := legacyCli.GetRoute(t)
// for _, route := range updatedRoute.Routes {
// assert.Equalf(t, expectedTitle, route.Receiver, "time receiver in routes should have been renamed but it did not")
// }
// receiver = *actual
// })
//
// t.Run("should fail if at least one resource is provisioned", func(t *testing.T) {
// require.NoError(t, err)
// renamed := receiver.DeepCopy()
// renamed.Spec.Title += util.GenerateShortUID()
//
// t.Run("provisioned route", func(t *testing.T) {
// require.NoError(t, db.SetProvenance(ctx, &currentRoute, orgID, "API"))
// t.Cleanup(func() {
// require.NoError(t, db.DeleteProvenance(ctx, &currentRoute, orgID))
// })
// actual, err := adminClient.Update(ctx, renamed, v1.UpdateOptions{})
// require.Errorf(t, err, "Expected error but got successful result: %v", actual)
// require.Truef(t, errors.IsConflict(err), "Expected Conflict, got: %s", err)
// })
//
// t.Run("provisioned rules", func(t *testing.T) {
// ruleUid := currentRuleGroup.Rules[0].GrafanaManagedAlert.UID
// resource := &ngmodels.AlertRule{UID: ruleUid}
// require.NoError(t, db.SetProvenance(ctx, resource, orgID, "API"))
// t.Cleanup(func() {
// require.NoError(t, db.DeleteProvenance(ctx, resource, orgID))
// })
//
// actual, err := adminClient.Update(ctx, renamed, v1.UpdateOptions{})
// require.Errorf(t, err, "Expected error but got successful result: %v", actual)
// require.Truef(t, errors.IsConflict(err), "Expected Conflict, got: %s", err)
// })
// })
// })
currentRoute := legacyCli.GetRoute(t)
currentRuleGroup := legacyCli.GetRulesGroup(t, folderUID, ruleGroup.Name)
t.Run("Update", func(t *testing.T) {
t.Run("should rename all references if name changes", func(t *testing.T) {
renamed := receiver.DeepCopy()
expectedTitle := renamed.Spec.Title + "-new"
renamed.Spec.Title = expectedTitle
actual, err := adminClient.Update(ctx, renamed, v1.UpdateOptions{})
require.NoError(t, err)
updatedRuleGroup := legacyCli.GetRulesGroup(t, folderUID, ruleGroup.Name)
for idx, rule := range updatedRuleGroup.Rules {
assert.Equalf(t, expectedTitle, rule.GrafanaManagedAlert.NotificationSettings.Receiver, "receiver in rule %d should have been renamed but it did not", idx)
}
updatedRoute := legacyCli.GetRoute(t)
for _, route := range updatedRoute.Routes {
assert.Equalf(t, expectedTitle, route.Receiver, "time receiver in routes should have been renamed but it did not")
}
actual, err = adminClient.Get(ctx, actual.Name, v1.GetOptions{})
require.NoError(t, err)
receiver = *actual
})
t.Run("should fail if at least one resource is provisioned", func(t *testing.T) {
require.NoError(t, err)
renamed := receiver.DeepCopy()
renamed.Spec.Title += util.GenerateShortUID()
t.Run("provisioned route", func(t *testing.T) {
require.NoError(t, db.SetProvenance(ctx, &currentRoute, orgID, "API"))
t.Cleanup(func() {
require.NoError(t, db.DeleteProvenance(ctx, &currentRoute, orgID))
})
actual, err := adminClient.Update(ctx, renamed, v1.UpdateOptions{})
require.Errorf(t, err, "Expected error but got successful result: %v", actual)
require.Truef(t, errors.IsConflict(err), "Expected Conflict, got: %s", err)
})
t.Run("provisioned rules", func(t *testing.T) {
ruleUid := currentRuleGroup.Rules[0].GrafanaManagedAlert.UID
resource := &ngmodels.AlertRule{UID: ruleUid}
require.NoError(t, db.SetProvenance(ctx, resource, orgID, "API"))
t.Cleanup(func() {
require.NoError(t, db.DeleteProvenance(ctx, resource, orgID))
})
actual, err := adminClient.Update(ctx, renamed, v1.UpdateOptions{})
require.Errorf(t, err, "Expected error but got successful result: %v", actual)
require.Truef(t, errors.IsConflict(err), "Expected Conflict, got: %s", err)
})
})
})
t.Run("Delete", func(t *testing.T) {
t.Run("should fail to delete if receiver is used in rule and routes", func(t *testing.T) {
@@ -7,7 +7,6 @@ import {
useUpdateContactPoint,
} from 'app/features/alerting/unified/components/contact-points/useContactPoints';
import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource';
import { shouldUseK8sApi } from 'app/features/alerting/unified/utils/k8s/utils';
import {
GrafanaManagedContactPoint,
GrafanaManagedReceiverConfig,
@@ -139,7 +138,6 @@ export const GrafanaReceiverForm = ({ contactPoint, readOnly = false, editMode }
return { dto: n };
});
const disableEditTitle = editMode && shouldUseK8sApi(GRAFANA_RULES_SOURCE_NAME);
return (
<>
{hasOnCallError && (
@@ -152,7 +150,6 @@ export const GrafanaReceiverForm = ({ contactPoint, readOnly = false, editMode }
{contactPoint?.provisioned && <ProvisioningAlert resource={ProvisionedResource.ContactPoint} />}
<ReceiverForm<GrafanaChannelValues>
disableEditTitle={disableEditTitle}
isEditable={isEditable}
isTestable={isTestable}
onSubmit={onSubmit}
@@ -38,11 +38,6 @@ interface Props<R extends ChannelValues> {
* and that contact point being created will be set as the default?
*/
showDefaultRouteWarning?: boolean;
/**
* Should we disable the title editing? Required when we're using the API server,
* as at the time of writing this is not possible
*/
disableEditTitle?: boolean;
}
export function ReceiverForm<R extends ChannelValues>({
@@ -57,7 +52,6 @@ export function ReceiverForm<R extends ChannelValues>({
isTestable,
customValidators,
showDefaultRouteWarning,
disableEditTitle,
}: Props<R>) {
const notifyApp = useAppNotification();
const styles = useStyles2(getStyles);
@@ -138,7 +132,6 @@ export function ReceiverForm<R extends ChannelValues>({
</h4>
<Field label="Name" invalid={!!errors.name} error={errors.name && errors.name.message} required>
<Input
disabled={disableEditTitle}
readOnly={!isEditable}
id="name"
{...register('name', {