diff --git a/pkg/services/ngalert/provisioning/config.go b/pkg/services/ngalert/provisioning/config.go index 1c82fba8a6d..90e58819593 100644 --- a/pkg/services/ngalert/provisioning/config.go +++ b/pkg/services/ngalert/provisioning/config.go @@ -3,15 +3,15 @@ package provisioning import ( "context" "encoding/json" - "fmt" "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" + "github.com/grafana/grafana/pkg/services/ngalert/models" ) func deserializeAlertmanagerConfig(config []byte) (*definitions.PostableUserConfig, error) { result := definitions.PostableUserConfig{} if err := json.Unmarshal(config, &result); err != nil { - return nil, fmt.Errorf("failed to deserialize alertmanager configuration: %w", err) + return nil, makeErrBadAlertmanagerConfiguration(err) } return &result, nil } @@ -33,7 +33,7 @@ func getLastConfiguration(ctx context.Context, orgID int64, store AMConfigStore) } if alertManagerConfig == nil { - return nil, fmt.Errorf("no alertmanager configuration present in this org") + return nil, ErrNoAlertmanagerConfiguration.Errorf("") } concurrencyToken := alertManagerConfig.ConfigurationHash @@ -48,3 +48,26 @@ func getLastConfiguration(ctx context.Context, orgID int64, store AMConfigStore) version: alertManagerConfig.ConfigurationVersion, }, nil } + +type alertmanagerConfigStoreImpl struct { + store AMConfigStore +} + +func (a alertmanagerConfigStoreImpl) Get(ctx context.Context, orgID int64) (*cfgRevision, error) { + return getLastConfiguration(ctx, orgID, a.store) +} + +func (a alertmanagerConfigStoreImpl) Save(ctx context.Context, revision *cfgRevision, orgID int64) error { + serialized, err := serializeAlertmanagerConfig(*revision.cfg) + if err != nil { + return err + } + cmd := models.SaveAlertmanagerConfigurationCmd{ + AlertmanagerConfiguration: string(serialized), + ConfigurationVersion: revision.version, + FetchedConfigurationHash: revision.concurrencyToken, + Default: false, + OrgID: orgID, + } + return PersistConfig(ctx, a.store, &cmd) +} diff --git a/pkg/services/ngalert/provisioning/config_test.go b/pkg/services/ngalert/provisioning/config_test.go new file mode 100644 index 00000000000..454f7921098 --- /dev/null +++ b/pkg/services/ngalert/provisioning/config_test.go @@ -0,0 +1,127 @@ +package provisioning + +import ( + "context" + "encoding/json" + "errors" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" + "github.com/grafana/grafana/pkg/services/ngalert/models" +) + +func TestAlertmanagerConfigStoreGet(t *testing.T) { + orgID := int64(1) + + t.Run("should read the latest config for giving organization", func(t *testing.T) { + storeMock := &MockAMConfigStore{} + store := &alertmanagerConfigStoreImpl{store: storeMock} + + expected := models.AlertConfiguration{ + ID: 1, + AlertmanagerConfiguration: defaultConfig, + ConfigurationHash: "config-hash-123", + ConfigurationVersion: "123", + CreatedAt: time.Now().Unix(), + Default: false, + OrgID: orgID, + } + + expectedCfg := definitions.PostableUserConfig{} + require.NoError(t, json.Unmarshal([]byte(defaultConfig), &expectedCfg)) + + storeMock.EXPECT().GetLatestAlertmanagerConfiguration(mock.Anything, mock.Anything).Return(&expected, nil) + + revision, err := store.Get(context.Background(), orgID) + require.NoError(t, err) + + require.Equal(t, expected.ConfigurationVersion, revision.version) + require.Equal(t, expected.ConfigurationHash, revision.concurrencyToken) + require.Equal(t, expectedCfg, *revision.cfg) + + storeMock.AssertCalled(t, "GetLatestAlertmanagerConfiguration", mock.Anything, orgID) + }) + + t.Run("propagate errors", func(t *testing.T) { + t.Run("when underlying store fails", func(t *testing.T) { + storeMock := &MockAMConfigStore{} + store := &alertmanagerConfigStoreImpl{store: storeMock} + expectedErr := errors.New("test=err") + storeMock.EXPECT().GetLatestAlertmanagerConfiguration(mock.Anything, mock.Anything).Return(nil, expectedErr) + + _, err := store.Get(context.Background(), orgID) + require.ErrorIs(t, err, expectedErr) + }) + + t.Run("return ErrNoAlertmanagerConfiguration config does not exist", func(t *testing.T) { + storeMock := &MockAMConfigStore{} + store := &alertmanagerConfigStoreImpl{store: storeMock} + storeMock.EXPECT().GetLatestAlertmanagerConfiguration(mock.Anything, mock.Anything).Return(nil, nil) + + _, err := store.Get(context.Background(), orgID) + require.Truef(t, ErrNoAlertmanagerConfiguration.Is(err), "expected ErrNoAlertmanagerConfiguration but got %s", err.Error()) + }) + + t.Run("when config cannot be unmarshalled", func(t *testing.T) { + storeMock := &MockAMConfigStore{} + store := &alertmanagerConfigStoreImpl{store: storeMock} + storeMock.EXPECT().GetLatestAlertmanagerConfiguration(mock.Anything, mock.Anything).Return(&models.AlertConfiguration{ + AlertmanagerConfiguration: "invalid-json", + }, nil) + + _, err := store.Get(context.Background(), orgID) + require.Truef(t, ErrBadAlertmanagerConfiguration.Base.Is(err), "expected ErrBadAlertmanagerConfiguration but got %s", err.Error()) + }) + }) +} + +func TestAlertmanagerConfigStoreSave(t *testing.T) { + orgID := int64(1) + + cfg := definitions.PostableUserConfig{} + require.NoError(t, json.Unmarshal([]byte(defaultConfig), &cfg)) + expectedCfg, err := serializeAlertmanagerConfig(cfg) + require.NoError(t, err) + + revision := cfgRevision{ + cfg: &cfg, + concurrencyToken: "config-hash-123", + version: "123", + } + + t.Run("should save the config to store", func(t *testing.T) { + storeMock := &MockAMConfigStore{} + store := &alertmanagerConfigStoreImpl{store: storeMock} + + storeMock.EXPECT().UpdateAlertmanagerConfiguration(mock.Anything, mock.Anything).RunAndReturn(func(ctx context.Context, cmd *models.SaveAlertmanagerConfigurationCmd) error { + assert.Equal(t, string(expectedCfg), cmd.AlertmanagerConfiguration) + assert.Equal(t, orgID, cmd.OrgID) + assert.Equal(t, revision.version, cmd.ConfigurationVersion) + assert.Equal(t, false, cmd.Default) + assert.Equal(t, revision.concurrencyToken, cmd.FetchedConfigurationHash) + return nil + }) + + err := store.Save(context.Background(), &revision, orgID) + require.NoError(t, err) + + storeMock.AssertCalled(t, "UpdateAlertmanagerConfiguration", mock.Anything, mock.Anything) + }) + + t.Run("propagates errors when underlying storage returns error", func(t *testing.T) { + storeMock := &MockAMConfigStore{} + store := &alertmanagerConfigStoreImpl{store: storeMock} + + expectedErr := errors.New("test-err") + storeMock.EXPECT().UpdateAlertmanagerConfiguration(mock.Anything, mock.Anything).Return(expectedErr) + + err := store.Save(context.Background(), &revision, orgID) + + require.ErrorIs(t, err, expectedErr) + }) +} diff --git a/pkg/services/ngalert/provisioning/contactpoints.go b/pkg/services/ngalert/provisioning/contactpoints.go index a72bf7d115b..c5dd067682a 100644 --- a/pkg/services/ngalert/provisioning/contactpoints.go +++ b/pkg/services/ngalert/provisioning/contactpoints.go @@ -3,7 +3,6 @@ package provisioning import ( "context" "encoding/base64" - "encoding/json" "errors" "fmt" "sort" @@ -23,7 +22,7 @@ import ( ) type ContactPointService struct { - amStore AMConfigStore + configStore *alertmanagerConfigStoreImpl encryptionService secrets.Service provenanceStore ProvisioningStore xact TransactionManager @@ -34,7 +33,9 @@ type ContactPointService struct { func NewContactPointService(store AMConfigStore, encryptionService secrets.Service, provenanceStore ProvisioningStore, xact TransactionManager, log log.Logger, ac accesscontrol.AccessControl) *ContactPointService { return &ContactPointService{ - amStore: store, + configStore: &alertmanagerConfigStoreImpl{ + store: store, + }, encryptionService: encryptionService, provenanceStore: provenanceStore, xact: xact, @@ -68,7 +69,7 @@ func (ecp *ContactPointService) GetContactPoints(ctx context.Context, q ContactP if q.Decrypt && !ecp.canDecryptSecrets(ctx, u) { return nil, fmt.Errorf("%w: user requires Admin role or alert.provisioning.secrets:read permission to view decrypted secure settings", ErrPermissionDenied) } - revision, err := getLastConfiguration(ctx, q.OrgID, ecp.amStore) + revision, err := ecp.configStore.Get(ctx, q.OrgID) if err != nil { return nil, err } @@ -108,7 +109,7 @@ func (ecp *ContactPointService) GetContactPoints(ctx context.Context, q ContactP // getContactPointDecrypted is an internal-only function that gets full contact point info, included encrypted fields. // nil is returned if no matching contact point exists. func (ecp *ContactPointService) getContactPointDecrypted(ctx context.Context, orgID int64, uid string) (apimodels.EmbeddedContactPoint, error) { - revision, err := getLastConfiguration(ctx, orgID, ecp.amStore) + revision, err := ecp.configStore.Get(ctx, orgID) if err != nil { return apimodels.EmbeddedContactPoint{}, err } @@ -118,7 +119,7 @@ func (ecp *ContactPointService) getContactPointDecrypted(ctx context.Context, or } embeddedContactPoint, err := PostableGrafanaReceiverToEmbeddedContactPoint( receiver, - models.ProvenanceNone, + models.ProvenanceNone, // TODO should be correct provenance? ecp.decryptValueOrRedacted(true, receiver.UID), ) if err != nil { @@ -135,7 +136,7 @@ func (ecp *ContactPointService) CreateContactPoint(ctx context.Context, orgID in return apimodels.EmbeddedContactPoint{}, fmt.Errorf("%w: %s", ErrValidation, err.Error()) } - revision, err := getLastConfiguration(ctx, orgID, ecp.amStore) + revision, err := ecp.configStore.Get(ctx, orgID) if err != nil { return apimodels.EmbeddedContactPoint{}, err } @@ -201,28 +202,11 @@ func (ecp *ContactPointService) CreateContactPoint(ctx context.Context, orgID in }) } - data, err := json.Marshal(revision.cfg) - if err != nil { - return apimodels.EmbeddedContactPoint{}, err - } - err = ecp.xact.InTransaction(ctx, func(ctx context.Context) error { - err = PersistConfig(ctx, ecp.amStore, &models.SaveAlertmanagerConfigurationCmd{ - AlertmanagerConfiguration: string(data), - FetchedConfigurationHash: revision.concurrencyToken, - ConfigurationVersion: revision.version, - Default: false, - OrgID: orgID, - }) - if err != nil { + if err := ecp.configStore.Save(ctx, revision, orgID); err != nil { return err } - err = ecp.provenanceStore.SetProvenance(ctx, &contactPoint, orgID, provenance) - if err != nil { - return err - } - contactPoint.Provenance = string(provenance) - return nil + return ecp.provenanceStore.SetProvenance(ctx, &contactPoint, orgID, provenance) }) if err != nil { return apimodels.EmbeddedContactPoint{}, err @@ -292,7 +276,7 @@ func (ecp *ContactPointService) UpdateContactPoint(ctx context.Context, orgID in SecureSettings: extractedSecrets, } // save to store - revision, err := getLastConfiguration(ctx, orgID, ecp.amStore) + revision, err := ecp.configStore.Get(ctx, orgID) if err != nil { return err } @@ -302,32 +286,20 @@ func (ecp *ContactPointService) UpdateContactPoint(ctx context.Context, orgID in return fmt.Errorf("contact point with uid '%s' not found", mergedReceiver.UID) } - data, err := json.Marshal(revision.cfg) + err = ecp.xact.InTransaction(ctx, func(ctx context.Context) error { + if err := ecp.configStore.Save(ctx, revision, orgID); err != nil { + return err + } + return ecp.provenanceStore.SetProvenance(ctx, &contactPoint, orgID, provenance) + }) if err != nil { return err } - return ecp.xact.InTransaction(ctx, func(ctx context.Context) error { - err = PersistConfig(ctx, ecp.amStore, &models.SaveAlertmanagerConfigurationCmd{ - AlertmanagerConfiguration: string(data), - FetchedConfigurationHash: revision.concurrencyToken, - ConfigurationVersion: revision.version, - Default: false, - OrgID: orgID, - }) - if err != nil { - return err - } - err = ecp.provenanceStore.SetProvenance(ctx, &contactPoint, orgID, provenance) - if err != nil { - return err - } - contactPoint.Provenance = string(provenance) - return nil - }) + return nil } func (ecp *ContactPointService) DeleteContactPoint(ctx context.Context, orgID int64, uid string) error { - revision, err := getLastConfiguration(ctx, orgID, ecp.amStore) + revision, err := ecp.configStore.Get(ctx, orgID) if err != nil { return err } @@ -355,25 +327,15 @@ func (ecp *ContactPointService) DeleteContactPoint(ctx context.Context, orgID in if fullRemoval && isContactPointInUse(name, []*apimodels.Route{revision.cfg.AlertmanagerConfig.Route}) { return fmt.Errorf("contact point '%s' is currently used by a notification policy", name) } - data, err := json.Marshal(revision.cfg) - if err != nil { - return err - } + return ecp.xact.InTransaction(ctx, func(ctx context.Context) error { + if err := ecp.configStore.Save(ctx, revision, orgID); err != nil { + return err + } target := &apimodels.EmbeddedContactPoint{ UID: uid, } - err := ecp.provenanceStore.DeleteProvenance(ctx, target, orgID) - if err != nil { - return err - } - return PersistConfig(ctx, ecp.amStore, &models.SaveAlertmanagerConfigurationCmd{ - AlertmanagerConfiguration: string(data), - FetchedConfigurationHash: revision.concurrencyToken, - ConfigurationVersion: revision.version, - Default: false, - OrgID: orgID, - }) + return ecp.provenanceStore.DeleteProvenance(ctx, target, orgID) }) } @@ -423,8 +385,8 @@ func (ecp *ContactPointService) encryptValue(value string) (string, error) { return base64.StdEncoding.EncodeToString(encryptedData), nil } -// stitchReceiver modifies a receiver, target, in an alertmanager config. It modifies the given config in-place. -// Returns true if the config was altered in any way, and false otherwise. +// stitchReceiver modifies a receiver, target, in an alertmanager configStore. It modifies the given configStore in-place. +// Returns true if the configStore was altered in any way, and false otherwise. func stitchReceiver(cfg *apimodels.PostableUserConfig, target *apimodels.PostableGrafanaReceiver) bool { // Algorithm to fix up receivers. Receivers are very complex and depend heavily on internal consistency. // All receivers in a given receiver group have the same name. We must maintain this across renames. diff --git a/pkg/services/ngalert/provisioning/contactpoints_test.go b/pkg/services/ngalert/provisioning/contactpoints_test.go index 961cdd7a0b4..8bb9606806a 100644 --- a/pkg/services/ngalert/provisioning/contactpoints_test.go +++ b/pkg/services/ngalert/provisioning/contactpoints_test.go @@ -239,14 +239,14 @@ func TestContactPointService(t *testing.T) { t.Run("service respects concurrency token when updating", func(t *testing.T) { sut := createContactPointServiceSut(t, secretsService) newCp := createTestContactPoint() - config, err := sut.amStore.GetLatestAlertmanagerConfiguration(context.Background(), 1) + config, err := sut.configStore.store.GetLatestAlertmanagerConfiguration(context.Background(), 1) require.NoError(t, err) expectedConcurrencyToken := config.ConfigurationHash _, err = sut.CreateContactPoint(context.Background(), 1, newCp, models.ProvenanceAPI) require.NoError(t, err) - fake := sut.amStore.(*fakeAMConfigStore) + fake := sut.configStore.store.(*fakeAMConfigStore) intercepted := fake.lastSaveCommand require.Equal(t, expectedConcurrencyToken, intercepted.FetchedConfigurationHash) }) @@ -349,7 +349,7 @@ func createContactPointServiceSut(t *testing.T, secretService secrets.Service) * require.NoError(t, err) return &ContactPointService{ - amStore: newFakeAMConfigStore(string(raw)), + configStore: &alertmanagerConfigStoreImpl{store: newFakeAMConfigStore(string(raw))}, provenanceStore: NewFakeProvisioningStore(), xact: newNopTransactionManager(), encryptionService: secretService, diff --git a/pkg/services/ngalert/provisioning/errors.go b/pkg/services/ngalert/provisioning/errors.go index 1ca9f708dd4..cc01a1b9641 100644 --- a/pkg/services/ngalert/provisioning/errors.go +++ b/pkg/services/ngalert/provisioning/errors.go @@ -3,8 +3,26 @@ package provisioning import ( "errors" "fmt" + + "github.com/grafana/grafana/pkg/util/errutil" ) var ErrValidation = fmt.Errorf("invalid object specification") var ErrNotFound = fmt.Errorf("object not found") var ErrPermissionDenied = errors.New("permission denied") + +var ( + ErrNoAlertmanagerConfiguration = errutil.Internal("alerting.notification.configMissing", errutil.WithPublicMessage("No alertmanager configuration present in this organization")) + ErrBadAlertmanagerConfiguration = errutil.Internal("alerting.notification.configCorrupted").MustTemplate("Failed to unmarshal the Alertmanager configuration", errutil.WithPublic("Current Alertmanager configuration in the storage is corrupted. Reset the configuration or rollback to a recent valid one.")) +) + +func makeErrBadAlertmanagerConfiguration(err error) error { + data := errutil.TemplateData{ + Public: map[string]interface{}{ + "Error": err.Error(), + }, + Error: err, + } + + return ErrBadAlertmanagerConfiguration.Build(data) +} diff --git a/pkg/services/ngalert/provisioning/mute_timings.go b/pkg/services/ngalert/provisioning/mute_timings.go index 54d4b7d4294..0ca7a57bfbc 100644 --- a/pkg/services/ngalert/provisioning/mute_timings.go +++ b/pkg/services/ngalert/provisioning/mute_timings.go @@ -12,24 +12,24 @@ import ( ) type MuteTimingService struct { - config AMConfigStore - prov ProvisioningStore - xact TransactionManager - log log.Logger + configStore *alertmanagerConfigStoreImpl + provenanceStore ProvisioningStore + xact TransactionManager + log log.Logger } func NewMuteTimingService(config AMConfigStore, prov ProvisioningStore, xact TransactionManager, log log.Logger) *MuteTimingService { return &MuteTimingService{ - config: config, - prov: prov, - xact: xact, - log: log, + configStore: &alertmanagerConfigStoreImpl{store: config}, + provenanceStore: prov, + xact: xact, + log: log, } } // GetMuteTimings returns a slice of all mute timings within the specified org. func (svc *MuteTimingService) GetMuteTimings(ctx context.Context, orgID int64) ([]definitions.MuteTimeInterval, error) { - rev, err := getLastConfiguration(ctx, orgID, svc.config) + rev, err := svc.configStore.Get(ctx, orgID) if err != nil { return nil, err } @@ -51,7 +51,7 @@ func (svc *MuteTimingService) CreateMuteTiming(ctx context.Context, mt definitio return nil, fmt.Errorf("%w: %s", ErrValidation, err.Error()) } - revision, err := getLastConfiguration(ctx, orgID, svc.config) + revision, err := svc.configStore.Get(ctx, orgID) if err != nil { return nil, err } @@ -66,32 +66,15 @@ func (svc *MuteTimingService) CreateMuteTiming(ctx context.Context, mt definitio } revision.cfg.AlertmanagerConfig.MuteTimeIntervals = append(revision.cfg.AlertmanagerConfig.MuteTimeIntervals, mt.MuteTimeInterval) - serialized, err := serializeAlertmanagerConfig(*revision.cfg) - if err != nil { - return nil, err - } - cmd := models.SaveAlertmanagerConfigurationCmd{ - AlertmanagerConfiguration: string(serialized), - ConfigurationVersion: revision.version, - FetchedConfigurationHash: revision.concurrencyToken, - Default: false, - OrgID: orgID, - } err = svc.xact.InTransaction(ctx, func(ctx context.Context) error { - err = PersistConfig(ctx, svc.config, &cmd) - if err != nil { + if err := svc.configStore.Save(ctx, revision, orgID); err != nil { return err } - err = svc.prov.SetProvenance(ctx, &mt, orgID, models.Provenance(mt.Provenance)) - if err != nil { - return err - } - return nil + return svc.provenanceStore.SetProvenance(ctx, &mt, orgID, models.Provenance(mt.Provenance)) }) if err != nil { return nil, err } - return &mt, nil } @@ -101,7 +84,7 @@ func (svc *MuteTimingService) UpdateMuteTiming(ctx context.Context, mt definitio return nil, fmt.Errorf("%w: %s", ErrValidation, err.Error()) } - revision, err := getLastConfiguration(ctx, orgID, svc.config) + revision, err := svc.configStore.Get(ctx, orgID) if err != nil { return nil, err } @@ -121,38 +104,21 @@ func (svc *MuteTimingService) UpdateMuteTiming(ctx context.Context, mt definitio return nil, nil } - serialized, err := serializeAlertmanagerConfig(*revision.cfg) - if err != nil { - return nil, err - } - cmd := models.SaveAlertmanagerConfigurationCmd{ - AlertmanagerConfiguration: string(serialized), - ConfigurationVersion: revision.version, - FetchedConfigurationHash: revision.concurrencyToken, - Default: false, - OrgID: orgID, - } err = svc.xact.InTransaction(ctx, func(ctx context.Context) error { - err = PersistConfig(ctx, svc.config, &cmd) - if err != nil { + if err := svc.configStore.Save(ctx, revision, orgID); err != nil { return err } - err = svc.prov.SetProvenance(ctx, &mt, orgID, models.Provenance(mt.Provenance)) - if err != nil { - return err - } - return nil + return svc.provenanceStore.SetProvenance(ctx, &mt, orgID, models.Provenance(mt.Provenance)) }) if err != nil { return nil, err } - return &mt, err } // DeleteMuteTiming deletes the mute timing with the given name in the given org. If the mute timing does not exist, no error is returned. func (svc *MuteTimingService) DeleteMuteTiming(ctx context.Context, name string, orgID int64) error { - revision, err := getLastConfiguration(ctx, orgID, svc.config) + revision, err := svc.configStore.Get(ctx, orgID) if err != nil { return err } @@ -170,28 +136,12 @@ func (svc *MuteTimingService) DeleteMuteTiming(ctx context.Context, name string, } } - serialized, err := serializeAlertmanagerConfig(*revision.cfg) - if err != nil { - return err - } - cmd := models.SaveAlertmanagerConfigurationCmd{ - AlertmanagerConfiguration: string(serialized), - ConfigurationVersion: revision.version, - FetchedConfigurationHash: revision.concurrencyToken, - Default: false, - OrgID: orgID, - } return svc.xact.InTransaction(ctx, func(ctx context.Context) error { - err = PersistConfig(ctx, svc.config, &cmd) - if err != nil { + if err := svc.configStore.Save(ctx, revision, orgID); err != nil { return err } target := definitions.MuteTimeInterval{MuteTimeInterval: config.MuteTimeInterval{Name: name}} - err := svc.prov.DeleteProvenance(ctx, &target, orgID) - if err != nil { - return err - } - return nil + return svc.provenanceStore.DeleteProvenance(ctx, &target, orgID) }) } diff --git a/pkg/services/ngalert/provisioning/mute_timings_test.go b/pkg/services/ngalert/provisioning/mute_timings_test.go index 242012c2497..12cdc0a4d96 100644 --- a/pkg/services/ngalert/provisioning/mute_timings_test.go +++ b/pkg/services/ngalert/provisioning/mute_timings_test.go @@ -17,7 +17,7 @@ import ( func TestMuteTimingService(t *testing.T) { t.Run("service returns timings from config file", func(t *testing.T) { sut := createMuteTimingSvcSut() - sut.config.(*MockAMConfigStore).EXPECT(). + sut.configStore.store.(*MockAMConfigStore).EXPECT(). GetsConfig(models.AlertConfiguration{ AlertmanagerConfiguration: configWithMuteTimings, }) @@ -31,7 +31,7 @@ func TestMuteTimingService(t *testing.T) { t.Run("service returns empty list when config file contains no mute timings", func(t *testing.T) { sut := createMuteTimingSvcSut() - sut.config.(*MockAMConfigStore).EXPECT(). + sut.configStore.store.(*MockAMConfigStore).EXPECT(). GetsConfig(models.AlertConfiguration{ AlertmanagerConfiguration: defaultConfig, }) @@ -45,7 +45,7 @@ func TestMuteTimingService(t *testing.T) { t.Run("service propagates errors", func(t *testing.T) { t.Run("when unable to read config", func(t *testing.T) { sut := createMuteTimingSvcSut() - sut.config.(*MockAMConfigStore).EXPECT(). + sut.configStore.store.(*MockAMConfigStore).EXPECT(). GetLatestAlertmanagerConfiguration(mock.Anything, mock.Anything). Return(nil, fmt.Errorf("failed")) @@ -56,25 +56,25 @@ func TestMuteTimingService(t *testing.T) { t.Run("when config is invalid", func(t *testing.T) { sut := createMuteTimingSvcSut() - sut.config.(*MockAMConfigStore).EXPECT(). + sut.configStore.store.(*MockAMConfigStore).EXPECT(). GetsConfig(models.AlertConfiguration{ AlertmanagerConfiguration: brokenConfig, }) _, err := sut.GetMuteTimings(context.Background(), 1) - require.ErrorContains(t, err, "failed to deserialize") + require.Truef(t, ErrBadAlertmanagerConfiguration.Base.Is(err), "expected ErrBadAlertmanagerConfiguration but got %s", err.Error()) }) t.Run("when no AM config in current org", func(t *testing.T) { sut := createMuteTimingSvcSut() - sut.config.(*MockAMConfigStore).EXPECT(). + sut.configStore.store.(*MockAMConfigStore).EXPECT(). GetLatestAlertmanagerConfiguration(mock.Anything, mock.Anything). Return(nil, nil) _, err := sut.GetMuteTimings(context.Background(), 1) - require.ErrorContains(t, err, "no alertmanager configuration") + require.Truef(t, ErrNoAlertmanagerConfiguration.Is(err), "expected ErrNoAlertmanagerConfiguration but got %s", err.Error()) }) }) @@ -96,7 +96,7 @@ func TestMuteTimingService(t *testing.T) { t.Run("when unable to read config", func(t *testing.T) { sut := createMuteTimingSvcSut() timing := createMuteTiming() - sut.config.(*MockAMConfigStore).EXPECT(). + sut.configStore.store.(*MockAMConfigStore).EXPECT(). GetLatestAlertmanagerConfiguration(mock.Anything, mock.Anything). Return(nil, fmt.Errorf("failed")) @@ -108,37 +108,37 @@ func TestMuteTimingService(t *testing.T) { t.Run("when config is invalid", func(t *testing.T) { sut := createMuteTimingSvcSut() timing := createMuteTiming() - sut.config.(*MockAMConfigStore).EXPECT(). + sut.configStore.store.(*MockAMConfigStore).EXPECT(). GetsConfig(models.AlertConfiguration{ AlertmanagerConfiguration: brokenConfig, }) _, err := sut.CreateMuteTiming(context.Background(), timing, 1) - require.ErrorContains(t, err, "failed to deserialize") + require.Truef(t, ErrBadAlertmanagerConfiguration.Base.Is(err), "expected ErrBadAlertmanagerConfiguration but got %s", err.Error()) }) t.Run("when no AM config in current org", func(t *testing.T) { sut := createMuteTimingSvcSut() timing := createMuteTiming() - sut.config.(*MockAMConfigStore).EXPECT(). + sut.configStore.store.(*MockAMConfigStore).EXPECT(). GetLatestAlertmanagerConfiguration(mock.Anything, mock.Anything). Return(nil, nil) _, err := sut.CreateMuteTiming(context.Background(), timing, 1) - require.ErrorContains(t, err, "no alertmanager configuration") + require.Truef(t, ErrNoAlertmanagerConfiguration.Is(err), "expected ErrNoAlertmanagerConfiguration but got %s", err.Error()) }) t.Run("when provenance fails to save", func(t *testing.T) { sut := createMuteTimingSvcSut() timing := createMuteTiming() - sut.config.(*MockAMConfigStore).EXPECT(). + sut.configStore.store.(*MockAMConfigStore).EXPECT(). GetsConfig(models.AlertConfiguration{ AlertmanagerConfiguration: configWithMuteTimings, }) - sut.config.(*MockAMConfigStore).EXPECT().SaveSucceeds() - sut.prov.(*MockProvisioningStore).EXPECT(). + sut.configStore.store.(*MockAMConfigStore).EXPECT().SaveSucceeds() + sut.provenanceStore.(*MockProvisioningStore).EXPECT(). SetProvenance(mock.Anything, mock.Anything, mock.Anything, mock.Anything). Return(fmt.Errorf("failed to save provenance")) @@ -150,14 +150,14 @@ func TestMuteTimingService(t *testing.T) { t.Run("when AM config fails to save", func(t *testing.T) { sut := createMuteTimingSvcSut() timing := createMuteTiming() - sut.config.(*MockAMConfigStore).EXPECT(). + sut.configStore.store.(*MockAMConfigStore).EXPECT(). GetsConfig(models.AlertConfiguration{ AlertmanagerConfiguration: configWithMuteTimings, }) - sut.config.(*MockAMConfigStore).EXPECT(). + sut.configStore.store.(*MockAMConfigStore).EXPECT(). UpdateAlertmanagerConfiguration(mock.Anything, mock.Anything). - Return(fmt.Errorf("failed to save config")) - sut.prov.(*MockProvisioningStore).EXPECT().SaveSucceeds() + Return(fmt.Errorf("failed to save configStore")) + sut.provenanceStore.(*MockProvisioningStore).EXPECT().SaveSucceeds() _, err := sut.CreateMuteTiming(context.Background(), timing, 1) @@ -184,12 +184,12 @@ func TestMuteTimingService(t *testing.T) { sut := createMuteTimingSvcSut() timing := createMuteTiming() timing.Name = "does not exist" - sut.config.(*MockAMConfigStore).EXPECT(). + sut.configStore.store.(*MockAMConfigStore).EXPECT(). GetsConfig(models.AlertConfiguration{ AlertmanagerConfiguration: configWithMuteTimings, }) - sut.config.(*MockAMConfigStore).EXPECT().SaveSucceeds() - sut.prov.(*MockProvisioningStore).EXPECT().SaveSucceeds() + sut.configStore.store.(*MockAMConfigStore).EXPECT().SaveSucceeds() + sut.provenanceStore.(*MockProvisioningStore).EXPECT().SaveSucceeds() updated, err := sut.UpdateMuteTiming(context.Background(), timing, 1) @@ -202,7 +202,7 @@ func TestMuteTimingService(t *testing.T) { sut := createMuteTimingSvcSut() timing := createMuteTiming() timing.Name = "asdf" - sut.config.(*MockAMConfigStore).EXPECT(). + sut.configStore.store.(*MockAMConfigStore).EXPECT(). GetLatestAlertmanagerConfiguration(mock.Anything, mock.Anything). Return(nil, fmt.Errorf("failed")) @@ -215,39 +215,39 @@ func TestMuteTimingService(t *testing.T) { sut := createMuteTimingSvcSut() timing := createMuteTiming() timing.Name = "asdf" - sut.config.(*MockAMConfigStore).EXPECT(). + sut.configStore.store.(*MockAMConfigStore).EXPECT(). GetsConfig(models.AlertConfiguration{ AlertmanagerConfiguration: brokenConfig, }) _, err := sut.UpdateMuteTiming(context.Background(), timing, 1) - require.ErrorContains(t, err, "failed to deserialize") + require.Truef(t, ErrBadAlertmanagerConfiguration.Base.Is(err), "expected ErrBadAlertmanagerConfiguration but got %s", err.Error()) }) t.Run("when no AM config in current org", func(t *testing.T) { sut := createMuteTimingSvcSut() timing := createMuteTiming() timing.Name = "asdf" - sut.config.(*MockAMConfigStore).EXPECT(). + sut.configStore.store.(*MockAMConfigStore).EXPECT(). GetLatestAlertmanagerConfiguration(mock.Anything, mock.Anything). Return(nil, nil) _, err := sut.UpdateMuteTiming(context.Background(), timing, 1) - require.ErrorContains(t, err, "no alertmanager configuration") + require.Truef(t, ErrNoAlertmanagerConfiguration.Is(err), "expected ErrNoAlertmanagerConfiguration but got %s", err.Error()) }) t.Run("when provenance fails to save", func(t *testing.T) { sut := createMuteTimingSvcSut() timing := createMuteTiming() timing.Name = "asdf" - sut.config.(*MockAMConfigStore).EXPECT(). + sut.configStore.store.(*MockAMConfigStore).EXPECT(). GetsConfig(models.AlertConfiguration{ AlertmanagerConfiguration: configWithMuteTimings, }) - sut.config.(*MockAMConfigStore).EXPECT().SaveSucceeds() - sut.prov.(*MockProvisioningStore).EXPECT(). + sut.configStore.store.(*MockAMConfigStore).EXPECT().SaveSucceeds() + sut.provenanceStore.(*MockProvisioningStore).EXPECT(). SetProvenance(mock.Anything, mock.Anything, mock.Anything, mock.Anything). Return(fmt.Errorf("failed to save provenance")) @@ -260,14 +260,14 @@ func TestMuteTimingService(t *testing.T) { sut := createMuteTimingSvcSut() timing := createMuteTiming() timing.Name = "asdf" - sut.config.(*MockAMConfigStore).EXPECT(). + sut.configStore.store.(*MockAMConfigStore).EXPECT(). GetsConfig(models.AlertConfiguration{ AlertmanagerConfiguration: configWithMuteTimings, }) - sut.config.(*MockAMConfigStore).EXPECT(). + sut.configStore.store.(*MockAMConfigStore).EXPECT(). UpdateAlertmanagerConfiguration(mock.Anything, mock.Anything). Return(fmt.Errorf("failed to save config")) - sut.prov.(*MockProvisioningStore).EXPECT().SaveSucceeds() + sut.provenanceStore.(*MockProvisioningStore).EXPECT().SaveSucceeds() _, err := sut.UpdateMuteTiming(context.Background(), timing, 1) @@ -279,12 +279,12 @@ func TestMuteTimingService(t *testing.T) { t.Run("deleting mute timings", func(t *testing.T) { t.Run("returns nil if timing does not exist", func(t *testing.T) { sut := createMuteTimingSvcSut() - sut.config.(*MockAMConfigStore).EXPECT(). + sut.configStore.store.(*MockAMConfigStore).EXPECT(). GetsConfig(models.AlertConfiguration{ AlertmanagerConfiguration: configWithMuteTimings, }) - sut.config.(*MockAMConfigStore).EXPECT().SaveSucceeds() - sut.prov.(*MockProvisioningStore).EXPECT().SaveSucceeds() + sut.configStore.store.(*MockAMConfigStore).EXPECT().SaveSucceeds() + sut.provenanceStore.(*MockProvisioningStore).EXPECT().SaveSucceeds() err := sut.DeleteMuteTiming(context.Background(), "does not exist", 1) @@ -294,7 +294,7 @@ func TestMuteTimingService(t *testing.T) { t.Run("propagates errors", func(t *testing.T) { t.Run("when unable to read config", func(t *testing.T) { sut := createMuteTimingSvcSut() - sut.config.(*MockAMConfigStore).EXPECT(). + sut.configStore.store.(*MockAMConfigStore).EXPECT(). GetLatestAlertmanagerConfiguration(mock.Anything, mock.Anything). Return(nil, fmt.Errorf("failed")) @@ -305,35 +305,35 @@ func TestMuteTimingService(t *testing.T) { t.Run("when config is invalid", func(t *testing.T) { sut := createMuteTimingSvcSut() - sut.config.(*MockAMConfigStore).EXPECT(). + sut.configStore.store.(*MockAMConfigStore).EXPECT(). GetsConfig(models.AlertConfiguration{ AlertmanagerConfiguration: brokenConfig, }) err := sut.DeleteMuteTiming(context.Background(), "asdf", 1) - require.ErrorContains(t, err, "failed to deserialize") + require.Truef(t, ErrBadAlertmanagerConfiguration.Base.Is(err), "expected ErrBadAlertmanagerConfiguration but got %s", err.Error()) }) t.Run("when no AM config in current org", func(t *testing.T) { sut := createMuteTimingSvcSut() - sut.config.(*MockAMConfigStore).EXPECT(). + sut.configStore.store.(*MockAMConfigStore).EXPECT(). GetLatestAlertmanagerConfiguration(mock.Anything, mock.Anything). Return(nil, nil) err := sut.DeleteMuteTiming(context.Background(), "asdf", 1) - require.ErrorContains(t, err, "no alertmanager configuration") + require.Truef(t, ErrNoAlertmanagerConfiguration.Is(err), "expected ErrNoAlertmanagerConfiguration but got %s", err.Error()) }) t.Run("when provenance fails to save", func(t *testing.T) { sut := createMuteTimingSvcSut() - sut.config.(*MockAMConfigStore).EXPECT(). + sut.configStore.store.(*MockAMConfigStore).EXPECT(). GetsConfig(models.AlertConfiguration{ AlertmanagerConfiguration: configWithMuteTimings, }) - sut.config.(*MockAMConfigStore).EXPECT().SaveSucceeds() - sut.prov.(*MockProvisioningStore).EXPECT(). + sut.configStore.store.(*MockAMConfigStore).EXPECT().SaveSucceeds() + sut.provenanceStore.(*MockProvisioningStore).EXPECT(). DeleteProvenance(mock.Anything, mock.Anything, mock.Anything). Return(fmt.Errorf("failed to save provenance")) @@ -344,14 +344,14 @@ func TestMuteTimingService(t *testing.T) { t.Run("when AM config fails to save", func(t *testing.T) { sut := createMuteTimingSvcSut() - sut.config.(*MockAMConfigStore).EXPECT(). + sut.configStore.store.(*MockAMConfigStore).EXPECT(). GetsConfig(models.AlertConfiguration{ AlertmanagerConfiguration: configWithMuteTimings, }) - sut.config.(*MockAMConfigStore).EXPECT(). + sut.configStore.store.(*MockAMConfigStore).EXPECT(). UpdateAlertmanagerConfiguration(mock.Anything, mock.Anything). Return(fmt.Errorf("failed to save config")) - sut.prov.(*MockProvisioningStore).EXPECT().SaveSucceeds() + sut.provenanceStore.(*MockProvisioningStore).EXPECT().SaveSucceeds() err := sut.DeleteMuteTiming(context.Background(), "asdf", 1) @@ -360,7 +360,7 @@ func TestMuteTimingService(t *testing.T) { t.Run("when mute timing is used in route", func(t *testing.T) { sut := createMuteTimingSvcSut() - sut.config.(*MockAMConfigStore).EXPECT(). + sut.configStore.store.(*MockAMConfigStore).EXPECT(). GetsConfig(models.AlertConfiguration{ AlertmanagerConfiguration: configWithMuteTimingsInRoute, }) @@ -375,10 +375,10 @@ func TestMuteTimingService(t *testing.T) { func createMuteTimingSvcSut() *MuteTimingService { return &MuteTimingService{ - config: &MockAMConfigStore{}, - prov: &MockProvisioningStore{}, - xact: newNopTransactionManager(), - log: log.NewNopLogger(), + configStore: &alertmanagerConfigStoreImpl{store: &MockAMConfigStore{}}, + provenanceStore: &MockProvisioningStore{}, + xact: newNopTransactionManager(), + log: log.NewNopLogger(), } } diff --git a/pkg/services/ngalert/provisioning/notification_policies.go b/pkg/services/ngalert/provisioning/notification_policies.go index 640f04333f8..629a5717b31 100644 --- a/pkg/services/ngalert/provisioning/notification_policies.go +++ b/pkg/services/ngalert/provisioning/notification_policies.go @@ -11,7 +11,7 @@ import ( ) type NotificationPolicyService struct { - amStore AMConfigStore + configStore *alertmanagerConfigStoreImpl provenanceStore ProvisioningStore xact TransactionManager log log.Logger @@ -21,7 +21,7 @@ type NotificationPolicyService struct { func NewNotificationPolicyService(am AMConfigStore, prov ProvisioningStore, xact TransactionManager, settings setting.UnifiedAlertingSettings, log log.Logger) *NotificationPolicyService { return &NotificationPolicyService{ - amStore: am, + configStore: &alertmanagerConfigStoreImpl{store: am}, provenanceStore: prov, xact: xact, log: log, @@ -30,30 +30,25 @@ func NewNotificationPolicyService(am AMConfigStore, prov ProvisioningStore, } func (nps *NotificationPolicyService) GetAMConfigStore() AMConfigStore { - return nps.amStore + return nps.configStore.store } func (nps *NotificationPolicyService) GetPolicyTree(ctx context.Context, orgID int64) (definitions.Route, error) { - alertManagerConfig, err := nps.amStore.GetLatestAlertmanagerConfiguration(ctx, orgID) + rev, err := nps.configStore.Get(ctx, orgID) if err != nil { return definitions.Route{}, err } - cfg, err := deserializeAlertmanagerConfig([]byte(alertManagerConfig.AlertmanagerConfiguration)) - if err != nil { - return definitions.Route{}, err - } - - if cfg.AlertmanagerConfig.Config.Route == nil { + if rev.cfg.AlertmanagerConfig.Config.Route == nil { return definitions.Route{}, fmt.Errorf("no route present in current alertmanager config") } - provenance, err := nps.provenanceStore.GetProvenance(ctx, cfg.AlertmanagerConfig.Route, orgID) + provenance, err := nps.provenanceStore.GetProvenance(ctx, rev.cfg.AlertmanagerConfig.Route, orgID) if err != nil { return definitions.Route{}, err } - result := *cfg.AlertmanagerConfig.Route + result := *rev.cfg.AlertmanagerConfig.Route result.Provenance = definitions.Provenance(provenance) return result, nil @@ -65,7 +60,7 @@ func (nps *NotificationPolicyService) UpdatePolicyTree(ctx context.Context, orgI return fmt.Errorf("%w: %s", ErrValidation, err.Error()) } - revision, err := getLastConfiguration(ctx, orgID, nps.amStore) + revision, err := nps.configStore.Get(ctx, orgID) if err != nil { return err } @@ -91,33 +86,12 @@ func (nps *NotificationPolicyService) UpdatePolicyTree(ctx context.Context, orgI revision.cfg.AlertmanagerConfig.Config.Route = &tree - serialized, err := serializeAlertmanagerConfig(*revision.cfg) - if err != nil { - return err - } - cmd := models.SaveAlertmanagerConfigurationCmd{ - AlertmanagerConfiguration: string(serialized), - ConfigurationVersion: revision.version, - FetchedConfigurationHash: revision.concurrencyToken, - Default: false, - OrgID: orgID, - } - err = nps.xact.InTransaction(ctx, func(ctx context.Context) error { - err = PersistConfig(ctx, nps.amStore, &cmd) - if err != nil { + return nps.xact.InTransaction(ctx, func(ctx context.Context) error { + if err := nps.configStore.Save(ctx, revision, orgID); err != nil { return err } - err = nps.provenanceStore.SetProvenance(ctx, &tree, orgID, p) - if err != nil { - return err - } - return nil + return nps.provenanceStore.SetProvenance(ctx, &tree, orgID, p) }) - if err != nil { - return err - } - - return nil } func (nps *NotificationPolicyService) ResetPolicyTree(ctx context.Context, orgID int64) (definitions.Route, error) { @@ -128,7 +102,7 @@ func (nps *NotificationPolicyService) ResetPolicyTree(ctx context.Context, orgID } route := defaultCfg.AlertmanagerConfig.Route - revision, err := getLastConfiguration(ctx, orgID, nps.amStore) + revision, err := nps.configStore.Get(ctx, orgID) if err != nil { return definitions.Route{}, err } @@ -138,31 +112,16 @@ func (nps *NotificationPolicyService) ResetPolicyTree(ctx context.Context, orgID return definitions.Route{}, err } - serialized, err := serializeAlertmanagerConfig(*revision.cfg) - if err != nil { - return definitions.Route{}, err - } - cmd := models.SaveAlertmanagerConfigurationCmd{ - AlertmanagerConfiguration: string(serialized), - ConfigurationVersion: revision.version, - FetchedConfigurationHash: revision.concurrencyToken, - Default: false, - OrgID: orgID, - } err = nps.xact.InTransaction(ctx, func(ctx context.Context) error { - err := PersistConfig(ctx, nps.amStore, &cmd) - if err != nil { + if err := nps.configStore.Save(ctx, revision, orgID); err != nil { return err } - err = nps.provenanceStore.DeleteProvenance(ctx, route, orgID) - if err != nil { - return err - } - return nil + return nps.provenanceStore.DeleteProvenance(ctx, route, orgID) }) + if err != nil { return definitions.Route{}, nil - } + } // TODO should be error? return *route, nil } diff --git a/pkg/services/ngalert/provisioning/notification_policies_test.go b/pkg/services/ngalert/provisioning/notification_policies_test.go index 8b54ad3fa50..dc51c7f7ecf 100644 --- a/pkg/services/ngalert/provisioning/notification_policies_test.go +++ b/pkg/services/ngalert/provisioning/notification_policies_test.go @@ -28,7 +28,7 @@ func TestNotificationPolicyService(t *testing.T) { t.Run("error if referenced mute time interval is not existing", func(t *testing.T) { sut := createNotificationPolicyServiceSut() - sut.amStore = &MockAMConfigStore{} + sut.configStore.store = &MockAMConfigStore{} cfg := createTestAlertingConfig() cfg.AlertmanagerConfig.MuteTimeIntervals = []config.MuteTimeInterval{ { @@ -37,9 +37,9 @@ func TestNotificationPolicyService(t *testing.T) { }, } data, _ := serializeAlertmanagerConfig(*cfg) - sut.amStore.(*MockAMConfigStore).On("GetLatestAlertmanagerConfiguration", mock.Anything, mock.Anything). + sut.configStore.store.(*MockAMConfigStore).On("GetLatestAlertmanagerConfiguration", mock.Anything, mock.Anything). Return(&models.AlertConfiguration{AlertmanagerConfiguration: string(data)}, nil) - sut.amStore.(*MockAMConfigStore).EXPECT(). + sut.configStore.store.(*MockAMConfigStore).EXPECT(). UpdateAlertmanagerConfiguration(mock.Anything, mock.Anything). Return(nil) newRoute := createTestRoutingTree() @@ -54,7 +54,7 @@ func TestNotificationPolicyService(t *testing.T) { t.Run("pass if referenced mute time interval is existing", func(t *testing.T) { sut := createNotificationPolicyServiceSut() - sut.amStore = &MockAMConfigStore{} + sut.configStore.store = &MockAMConfigStore{} cfg := createTestAlertingConfig() cfg.AlertmanagerConfig.MuteTimeIntervals = []config.MuteTimeInterval{ { @@ -63,9 +63,9 @@ func TestNotificationPolicyService(t *testing.T) { }, } data, _ := serializeAlertmanagerConfig(*cfg) - sut.amStore.(*MockAMConfigStore).On("GetLatestAlertmanagerConfiguration", mock.Anything, mock.Anything). + sut.configStore.store.(*MockAMConfigStore).On("GetLatestAlertmanagerConfiguration", mock.Anything, mock.Anything). Return(&models.AlertConfiguration{AlertmanagerConfiguration: string(data)}, nil) - sut.amStore.(*MockAMConfigStore).EXPECT(). + sut.configStore.store.(*MockAMConfigStore).EXPECT(). UpdateAlertmanagerConfiguration(mock.Anything, mock.Anything). Return(nil) newRoute := createTestRoutingTree() @@ -105,12 +105,12 @@ func TestNotificationPolicyService(t *testing.T) { t.Run("existing receiver reference will pass", func(t *testing.T) { sut := createNotificationPolicyServiceSut() - sut.amStore = &MockAMConfigStore{} + sut.configStore.store = &MockAMConfigStore{} cfg := createTestAlertingConfig() data, _ := serializeAlertmanagerConfig(*cfg) - sut.amStore.(*MockAMConfigStore).On("GetLatestAlertmanagerConfiguration", mock.Anything, mock.Anything). + sut.configStore.store.(*MockAMConfigStore).On("GetLatestAlertmanagerConfiguration", mock.Anything, mock.Anything). Return(&models.AlertConfiguration{AlertmanagerConfiguration: string(data)}, nil) - sut.amStore.(*MockAMConfigStore).EXPECT(). + sut.configStore.store.(*MockAMConfigStore).EXPECT(). UpdateAlertmanagerConfiguration(mock.Anything, mock.Anything). Return(nil) newRoute := createTestRoutingTree() @@ -183,7 +183,7 @@ func TestNotificationPolicyService(t *testing.T) { t.Run("deleting route with missing default receiver restores receiver", func(t *testing.T) { sut := createNotificationPolicyServiceSut() - sut.amStore = &MockAMConfigStore{} + sut.configStore.store = &MockAMConfigStore{} cfg := createTestAlertingConfig() cfg.AlertmanagerConfig.Route = &definitions.Route{ Receiver: "a new receiver", @@ -197,17 +197,17 @@ func TestNotificationPolicyService(t *testing.T) { // No default receiver! Only our custom one. } data, _ := serializeAlertmanagerConfig(*cfg) - sut.amStore.(*MockAMConfigStore).On("GetLatestAlertmanagerConfiguration", mock.Anything, mock.Anything). + sut.configStore.store.(*MockAMConfigStore).On("GetLatestAlertmanagerConfiguration", mock.Anything, mock.Anything). Return(&models.AlertConfiguration{AlertmanagerConfiguration: string(data)}, nil) var interceptedSave = models.SaveAlertmanagerConfigurationCmd{} - sut.amStore.(*MockAMConfigStore).EXPECT().SaveSucceedsIntercept(&interceptedSave) + sut.configStore.store.(*MockAMConfigStore).EXPECT().SaveSucceedsIntercept(&interceptedSave) tree, err := sut.ResetPolicyTree(context.Background(), 1) require.NoError(t, err) require.Equal(t, "grafana-default-email", tree.Receiver) require.NotEmpty(t, interceptedSave.AlertmanagerConfiguration) - // Deserializing with no error asserts that the saved config is semantically valid. + // Deserializing with no error asserts that the saved configStore is semantically valid. newCfg, err := deserializeAlertmanagerConfig([]byte(interceptedSave.AlertmanagerConfiguration)) require.NoError(t, err) require.Len(t, newCfg.AlertmanagerConfig.Receivers, 2) @@ -216,7 +216,7 @@ func TestNotificationPolicyService(t *testing.T) { func createNotificationPolicyServiceSut() *NotificationPolicyService { return &NotificationPolicyService{ - amStore: newFakeAMConfigStore(defaultAlertmanagerConfigJSON), + configStore: &alertmanagerConfigStoreImpl{store: newFakeAMConfigStore(defaultAlertmanagerConfigJSON)}, provenanceStore: NewFakeProvisioningStore(), xact: newNopTransactionManager(), log: log.NewNopLogger(), diff --git a/pkg/services/ngalert/provisioning/templates.go b/pkg/services/ngalert/provisioning/templates.go index 0e655224eb1..d2697eac030 100644 --- a/pkg/services/ngalert/provisioning/templates.go +++ b/pkg/services/ngalert/provisioning/templates.go @@ -10,23 +10,23 @@ import ( ) type TemplateService struct { - config AMConfigStore - prov ProvisioningStore - xact TransactionManager - log log.Logger + configStore *alertmanagerConfigStoreImpl + provenanceStore ProvisioningStore + xact TransactionManager + log log.Logger } func NewTemplateService(config AMConfigStore, prov ProvisioningStore, xact TransactionManager, log log.Logger) *TemplateService { return &TemplateService{ - config: config, - prov: prov, - xact: xact, - log: log, + configStore: &alertmanagerConfigStoreImpl{store: config}, + provenanceStore: prov, + xact: xact, + log: log, } } func (t *TemplateService) GetTemplates(ctx context.Context, orgID int64) (map[string]string, error) { - revision, err := getLastConfiguration(ctx, orgID, t.config) + revision, err := t.configStore.Get(ctx, orgID) if err != nil { return nil, err } @@ -44,7 +44,7 @@ func (t *TemplateService) SetTemplate(ctx context.Context, orgID int64, tmpl def return definitions.NotificationTemplate{}, fmt.Errorf("%w: %s", ErrValidation, err.Error()) } - revision, err := getLastConfiguration(ctx, orgID, t.config) + revision, err := t.configStore.Get(ctx, orgID) if err != nil { return definitions.NotificationTemplate{}, err } @@ -59,27 +59,11 @@ func (t *TemplateService) SetTemplate(ctx context.Context, orgID int64, tmpl def } revision.cfg.AlertmanagerConfig.Templates = tmpls - serialized, err := serializeAlertmanagerConfig(*revision.cfg) - if err != nil { - return definitions.NotificationTemplate{}, err - } - cmd := models.SaveAlertmanagerConfigurationCmd{ - AlertmanagerConfiguration: string(serialized), - ConfigurationVersion: revision.version, - FetchedConfigurationHash: revision.concurrencyToken, - Default: false, - OrgID: orgID, - } err = t.xact.InTransaction(ctx, func(ctx context.Context) error { - err = PersistConfig(ctx, t.config, &cmd) - if err != nil { + if err := t.configStore.Save(ctx, revision, orgID); err != nil { return err } - err = t.prov.SetProvenance(ctx, &tmpl, orgID, models.Provenance(tmpl.Provenance)) - if err != nil { - return err - } - return nil + return t.provenanceStore.SetProvenance(ctx, &tmpl, orgID, models.Provenance(tmpl.Provenance)) }) if err != nil { return definitions.NotificationTemplate{}, err @@ -89,42 +73,20 @@ func (t *TemplateService) SetTemplate(ctx context.Context, orgID int64, tmpl def } func (t *TemplateService) DeleteTemplate(ctx context.Context, orgID int64, name string) error { - revision, err := getLastConfiguration(ctx, orgID, t.config) + revision, err := t.configStore.Get(ctx, orgID) if err != nil { return err } delete(revision.cfg.TemplateFiles, name) - serialized, err := serializeAlertmanagerConfig(*revision.cfg) - if err != nil { - return err - } - - cmd := models.SaveAlertmanagerConfigurationCmd{ - AlertmanagerConfiguration: string(serialized), - ConfigurationVersion: revision.version, - FetchedConfigurationHash: revision.concurrencyToken, - Default: false, - OrgID: orgID, - } - err = t.xact.InTransaction(ctx, func(ctx context.Context) error { - err = PersistConfig(ctx, t.config, &cmd) - if err != nil { + return t.xact.InTransaction(ctx, func(ctx context.Context) error { + if err := t.configStore.Save(ctx, revision, orgID); err != nil { return err } tgt := definitions.NotificationTemplate{ Name: name, } - err = t.prov.DeleteProvenance(ctx, &tgt, orgID) - if err != nil { - return err - } - return nil + return t.provenanceStore.DeleteProvenance(ctx, &tgt, orgID) }) - if err != nil { - return err - } - - return nil } diff --git a/pkg/services/ngalert/provisioning/templates_test.go b/pkg/services/ngalert/provisioning/templates_test.go index 40bde70e0c6..810ca32c1a5 100644 --- a/pkg/services/ngalert/provisioning/templates_test.go +++ b/pkg/services/ngalert/provisioning/templates_test.go @@ -17,7 +17,7 @@ import ( func TestTemplateService(t *testing.T) { t.Run("service returns templates from config file", func(t *testing.T) { sut := createTemplateServiceSut() - sut.config.(*MockAMConfigStore).EXPECT(). + sut.configStore.store.(*MockAMConfigStore).EXPECT(). GetsConfig(models.AlertConfiguration{ AlertmanagerConfiguration: configWithTemplates, }) @@ -30,7 +30,7 @@ func TestTemplateService(t *testing.T) { t.Run("service returns empty map when config file contains no templates", func(t *testing.T) { sut := createTemplateServiceSut() - sut.config.(*MockAMConfigStore).EXPECT(). + sut.configStore.store.(*MockAMConfigStore).EXPECT(). GetsConfig(models.AlertConfiguration{ AlertmanagerConfiguration: defaultConfig, }) @@ -44,7 +44,7 @@ func TestTemplateService(t *testing.T) { t.Run("service propagates errors", func(t *testing.T) { t.Run("when unable to read config", func(t *testing.T) { sut := createTemplateServiceSut() - sut.config.(*MockAMConfigStore).EXPECT(). + sut.configStore.store.(*MockAMConfigStore).EXPECT(). GetLatestAlertmanagerConfiguration(mock.Anything, mock.Anything). Return(nil, fmt.Errorf("failed")) @@ -55,25 +55,25 @@ func TestTemplateService(t *testing.T) { t.Run("when config is invalid", func(t *testing.T) { sut := createTemplateServiceSut() - sut.config.(*MockAMConfigStore).EXPECT(). + sut.configStore.store.(*MockAMConfigStore).EXPECT(). GetsConfig(models.AlertConfiguration{ AlertmanagerConfiguration: brokenConfig, }) _, err := sut.GetTemplates(context.Background(), 1) - require.ErrorContains(t, err, "failed to deserialize") + require.Truef(t, ErrBadAlertmanagerConfiguration.Base.Is(err), "expected ErrBadAlertmanagerConfiguration but got %s", err.Error()) }) t.Run("when no AM config in current org", func(t *testing.T) { sut := createTemplateServiceSut() - sut.config.(*MockAMConfigStore).EXPECT(). + sut.configStore.store.(*MockAMConfigStore).EXPECT(). GetLatestAlertmanagerConfiguration(mock.Anything, mock.Anything). Return(nil, nil) _, err := sut.GetTemplates(context.Background(), 1) - require.ErrorContains(t, err, "no alertmanager configuration") + require.Truef(t, ErrNoAlertmanagerConfiguration.Is(err), "expected ErrNoAlertmanagerConfiguration but got %s", err.Error()) }) }) @@ -94,7 +94,7 @@ func TestTemplateService(t *testing.T) { t.Run("when unable to read config", func(t *testing.T) { sut := createTemplateServiceSut() tmpl := createNotificationTemplate() - sut.config.(*MockAMConfigStore).EXPECT(). + sut.configStore.store.(*MockAMConfigStore).EXPECT(). GetLatestAlertmanagerConfiguration(mock.Anything, mock.Anything). Return(nil, fmt.Errorf("failed")) @@ -106,37 +106,37 @@ func TestTemplateService(t *testing.T) { t.Run("when config is invalid", func(t *testing.T) { sut := createTemplateServiceSut() tmpl := createNotificationTemplate() - sut.config.(*MockAMConfigStore).EXPECT(). + sut.configStore.store.(*MockAMConfigStore).EXPECT(). GetsConfig(models.AlertConfiguration{ AlertmanagerConfiguration: brokenConfig, }) _, err := sut.SetTemplate(context.Background(), 1, tmpl) - require.ErrorContains(t, err, "failed to deserialize") + require.Truef(t, ErrBadAlertmanagerConfiguration.Base.Is(err), "expected ErrBadAlertmanagerConfiguration but got %s", err.Error()) }) t.Run("when no AM config in current org", func(t *testing.T) { sut := createTemplateServiceSut() tmpl := createNotificationTemplate() - sut.config.(*MockAMConfigStore).EXPECT(). + sut.configStore.store.(*MockAMConfigStore).EXPECT(). GetLatestAlertmanagerConfiguration(mock.Anything, mock.Anything). Return(nil, nil) _, err := sut.SetTemplate(context.Background(), 1, tmpl) - require.ErrorContains(t, err, "no alertmanager configuration") + require.Truef(t, ErrNoAlertmanagerConfiguration.Is(err), "expected ErrNoAlertmanagerConfiguration but got %s", err.Error()) }) t.Run("when provenance fails to save", func(t *testing.T) { sut := createTemplateServiceSut() tmpl := createNotificationTemplate() - sut.config.(*MockAMConfigStore).EXPECT(). + sut.configStore.store.(*MockAMConfigStore).EXPECT(). GetsConfig(models.AlertConfiguration{ AlertmanagerConfiguration: configWithTemplates, }) - sut.config.(*MockAMConfigStore).EXPECT().SaveSucceeds() - sut.prov.(*MockProvisioningStore).EXPECT(). + sut.configStore.store.(*MockAMConfigStore).EXPECT().SaveSucceeds() + sut.provenanceStore.(*MockProvisioningStore).EXPECT(). SetProvenance(mock.Anything, mock.Anything, mock.Anything, mock.Anything). Return(fmt.Errorf("failed to save provenance")) @@ -148,14 +148,14 @@ func TestTemplateService(t *testing.T) { t.Run("when AM config fails to save", func(t *testing.T) { sut := createTemplateServiceSut() tmpl := createNotificationTemplate() - sut.config.(*MockAMConfigStore).EXPECT(). + sut.configStore.store.(*MockAMConfigStore).EXPECT(). GetsConfig(models.AlertConfiguration{ AlertmanagerConfiguration: configWithTemplates, }) - sut.config.(*MockAMConfigStore).EXPECT(). + sut.configStore.store.(*MockAMConfigStore).EXPECT(). UpdateAlertmanagerConfiguration(mock.Anything, mock.Anything). Return(fmt.Errorf("failed to save config")) - sut.prov.(*MockProvisioningStore).EXPECT().SaveSucceeds() + sut.provenanceStore.(*MockProvisioningStore).EXPECT().SaveSucceeds() _, err := sut.SetTemplate(context.Background(), 1, tmpl) @@ -166,12 +166,12 @@ func TestTemplateService(t *testing.T) { t.Run("adds new template to config file on success", func(t *testing.T) { sut := createTemplateServiceSut() tmpl := createNotificationTemplate() - sut.config.(*MockAMConfigStore).EXPECT(). + sut.configStore.store.(*MockAMConfigStore).EXPECT(). GetsConfig(models.AlertConfiguration{ AlertmanagerConfiguration: configWithTemplates, }) - sut.config.(*MockAMConfigStore).EXPECT().SaveSucceeds() - sut.prov.(*MockProvisioningStore).EXPECT().SaveSucceeds() + sut.configStore.store.(*MockAMConfigStore).EXPECT().SaveSucceeds() + sut.provenanceStore.(*MockProvisioningStore).EXPECT().SaveSucceeds() _, err := sut.SetTemplate(context.Background(), 1, tmpl) @@ -181,12 +181,12 @@ func TestTemplateService(t *testing.T) { t.Run("succeeds when stitching config file with no templates", func(t *testing.T) { sut := createTemplateServiceSut() tmpl := createNotificationTemplate() - sut.config.(*MockAMConfigStore).EXPECT(). + sut.configStore.store.(*MockAMConfigStore).EXPECT(). GetsConfig(models.AlertConfiguration{ AlertmanagerConfiguration: defaultConfig, }) - sut.config.(*MockAMConfigStore).EXPECT().SaveSucceeds() - sut.prov.(*MockProvisioningStore).EXPECT().SaveSucceeds() + sut.configStore.store.(*MockAMConfigStore).EXPECT().SaveSucceeds() + sut.provenanceStore.(*MockProvisioningStore).EXPECT().SaveSucceeds() _, err := sut.SetTemplate(context.Background(), 1, tmpl) @@ -199,12 +199,12 @@ func TestTemplateService(t *testing.T) { Name: "name", Template: "content", } - sut.config.(*MockAMConfigStore).EXPECT(). + sut.configStore.store.(*MockAMConfigStore).EXPECT(). GetsConfig(models.AlertConfiguration{ AlertmanagerConfiguration: defaultConfig, }) - sut.config.(*MockAMConfigStore).EXPECT().SaveSucceeds() - sut.prov.(*MockProvisioningStore).EXPECT().SaveSucceeds() + sut.configStore.store.(*MockAMConfigStore).EXPECT().SaveSucceeds() + sut.provenanceStore.(*MockProvisioningStore).EXPECT().SaveSucceeds() result, _ := sut.SetTemplate(context.Background(), 1, tmpl) @@ -218,12 +218,12 @@ func TestTemplateService(t *testing.T) { Name: "name", Template: "{{define \"name\"}}content{{end}}", } - sut.config.(*MockAMConfigStore).EXPECT(). + sut.configStore.store.(*MockAMConfigStore).EXPECT(). GetsConfig(models.AlertConfiguration{ AlertmanagerConfiguration: defaultConfig, }) - sut.config.(*MockAMConfigStore).EXPECT().SaveSucceeds() - sut.prov.(*MockProvisioningStore).EXPECT().SaveSucceeds() + sut.configStore.store.(*MockAMConfigStore).EXPECT().SaveSucceeds() + sut.provenanceStore.(*MockProvisioningStore).EXPECT().SaveSucceeds() result, _ := sut.SetTemplate(context.Background(), 1, tmpl) @@ -236,12 +236,12 @@ func TestTemplateService(t *testing.T) { Name: "name", Template: "{{ .MyField }", } - sut.config.(*MockAMConfigStore).EXPECT(). + sut.configStore.store.(*MockAMConfigStore).EXPECT(). GetsConfig(models.AlertConfiguration{ AlertmanagerConfiguration: defaultConfig, }) - sut.config.(*MockAMConfigStore).EXPECT().SaveSucceeds() - sut.prov.(*MockProvisioningStore).EXPECT().SaveSucceeds() + sut.configStore.store.(*MockAMConfigStore).EXPECT().SaveSucceeds() + sut.provenanceStore.(*MockProvisioningStore).EXPECT().SaveSucceeds() _, err := sut.SetTemplate(context.Background(), 1, tmpl) @@ -254,12 +254,12 @@ func TestTemplateService(t *testing.T) { Name: "name", Template: "{{ .NotAField }}", } - sut.config.(*MockAMConfigStore).EXPECT(). + sut.configStore.store.(*MockAMConfigStore).EXPECT(). GetsConfig(models.AlertConfiguration{ AlertmanagerConfiguration: defaultConfig, }) - sut.config.(*MockAMConfigStore).EXPECT().SaveSucceeds() - sut.prov.(*MockProvisioningStore).EXPECT().SaveSucceeds() + sut.configStore.store.(*MockAMConfigStore).EXPECT().SaveSucceeds() + sut.provenanceStore.(*MockProvisioningStore).EXPECT().SaveSucceeds() _, err := sut.SetTemplate(context.Background(), 1, tmpl) @@ -271,7 +271,7 @@ func TestTemplateService(t *testing.T) { t.Run("propagates errors", func(t *testing.T) { t.Run("when unable to read config", func(t *testing.T) { sut := createTemplateServiceSut() - sut.config.(*MockAMConfigStore).EXPECT(). + sut.configStore.store.(*MockAMConfigStore).EXPECT(). GetLatestAlertmanagerConfiguration(mock.Anything, mock.Anything). Return(nil, fmt.Errorf("failed")) @@ -282,35 +282,35 @@ func TestTemplateService(t *testing.T) { t.Run("when config is invalid", func(t *testing.T) { sut := createTemplateServiceSut() - sut.config.(*MockAMConfigStore).EXPECT(). + sut.configStore.store.(*MockAMConfigStore).EXPECT(). GetsConfig(models.AlertConfiguration{ AlertmanagerConfiguration: brokenConfig, }) err := sut.DeleteTemplate(context.Background(), 1, "template") - require.ErrorContains(t, err, "failed to deserialize") + require.Truef(t, ErrBadAlertmanagerConfiguration.Base.Is(err), "expected ErrBadAlertmanagerConfiguration but got %s", err.Error()) }) t.Run("when no AM config in current org", func(t *testing.T) { sut := createTemplateServiceSut() - sut.config.(*MockAMConfigStore).EXPECT(). + sut.configStore.store.(*MockAMConfigStore).EXPECT(). GetLatestAlertmanagerConfiguration(mock.Anything, mock.Anything). Return(nil, nil) err := sut.DeleteTemplate(context.Background(), 1, "template") - require.ErrorContains(t, err, "no alertmanager configuration") + require.Truef(t, ErrNoAlertmanagerConfiguration.Is(err), "expected ErrNoAlertmanagerConfiguration but got %s", err.Error()) }) t.Run("when provenance fails to save", func(t *testing.T) { sut := createTemplateServiceSut() - sut.config.(*MockAMConfigStore).EXPECT(). + sut.configStore.store.(*MockAMConfigStore).EXPECT(). GetsConfig(models.AlertConfiguration{ AlertmanagerConfiguration: configWithTemplates, }) - sut.config.(*MockAMConfigStore).EXPECT().SaveSucceeds() - sut.prov.(*MockProvisioningStore).EXPECT(). + sut.configStore.store.(*MockAMConfigStore).EXPECT().SaveSucceeds() + sut.provenanceStore.(*MockProvisioningStore).EXPECT(). DeleteProvenance(mock.Anything, mock.Anything, mock.Anything). Return(fmt.Errorf("failed to save provenance")) @@ -321,14 +321,14 @@ func TestTemplateService(t *testing.T) { t.Run("when AM config fails to save", func(t *testing.T) { sut := createTemplateServiceSut() - sut.config.(*MockAMConfigStore).EXPECT(). + sut.configStore.store.(*MockAMConfigStore).EXPECT(). GetsConfig(models.AlertConfiguration{ AlertmanagerConfiguration: configWithTemplates, }) - sut.config.(*MockAMConfigStore).EXPECT(). + sut.configStore.store.(*MockAMConfigStore).EXPECT(). UpdateAlertmanagerConfiguration(mock.Anything, mock.Anything). Return(fmt.Errorf("failed to save config")) - sut.prov.(*MockProvisioningStore).EXPECT().SaveSucceeds() + sut.provenanceStore.(*MockProvisioningStore).EXPECT().SaveSucceeds() err := sut.DeleteTemplate(context.Background(), 1, "template") @@ -338,12 +338,12 @@ func TestTemplateService(t *testing.T) { t.Run("deletes template from config file on success", func(t *testing.T) { sut := createTemplateServiceSut() - sut.config.(*MockAMConfigStore).EXPECT(). + sut.configStore.store.(*MockAMConfigStore).EXPECT(). GetsConfig(models.AlertConfiguration{ AlertmanagerConfiguration: configWithTemplates, }) - sut.config.(*MockAMConfigStore).EXPECT().SaveSucceeds() - sut.prov.(*MockProvisioningStore).EXPECT().SaveSucceeds() + sut.configStore.store.(*MockAMConfigStore).EXPECT().SaveSucceeds() + sut.provenanceStore.(*MockProvisioningStore).EXPECT().SaveSucceeds() err := sut.DeleteTemplate(context.Background(), 1, "a") @@ -352,12 +352,12 @@ func TestTemplateService(t *testing.T) { t.Run("does not error when deleting templates that do not exist", func(t *testing.T) { sut := createTemplateServiceSut() - sut.config.(*MockAMConfigStore).EXPECT(). + sut.configStore.store.(*MockAMConfigStore).EXPECT(). GetsConfig(models.AlertConfiguration{ AlertmanagerConfiguration: configWithTemplates, }) - sut.config.(*MockAMConfigStore).EXPECT().SaveSucceeds() - sut.prov.(*MockProvisioningStore).EXPECT().SaveSucceeds() + sut.configStore.store.(*MockAMConfigStore).EXPECT().SaveSucceeds() + sut.provenanceStore.(*MockProvisioningStore).EXPECT().SaveSucceeds() err := sut.DeleteTemplate(context.Background(), 1, "does not exist") @@ -366,12 +366,12 @@ func TestTemplateService(t *testing.T) { t.Run("succeeds when deleting from config file with no template section", func(t *testing.T) { sut := createTemplateServiceSut() - sut.config.(*MockAMConfigStore).EXPECT(). + sut.configStore.store.(*MockAMConfigStore).EXPECT(). GetsConfig(models.AlertConfiguration{ AlertmanagerConfiguration: defaultConfig, }) - sut.config.(*MockAMConfigStore).EXPECT().SaveSucceeds() - sut.prov.(*MockProvisioningStore).EXPECT().SaveSucceeds() + sut.configStore.store.(*MockAMConfigStore).EXPECT().SaveSucceeds() + sut.provenanceStore.(*MockProvisioningStore).EXPECT().SaveSucceeds() err := sut.DeleteTemplate(context.Background(), 1, "a") @@ -382,10 +382,10 @@ func TestTemplateService(t *testing.T) { func createTemplateServiceSut() *TemplateService { return &TemplateService{ - config: &MockAMConfigStore{}, - prov: &MockProvisioningStore{}, - xact: newNopTransactionManager(), - log: log.NewNopLogger(), + configStore: &alertmanagerConfigStoreImpl{store: &MockAMConfigStore{}}, + provenanceStore: &MockProvisioningStore{}, + xact: newNopTransactionManager(), + log: log.NewNopLogger(), } }