Alerting: Update provisioning services that handle Alertmanager configuraiton to access config via storage (#79814)

* extract get and save operations to a alertmanagerConfigStore. this removes duplicated code in service (currently only mute timings) and improves testing
* replace generic errors with errutils one with better messages.
* update provisioning services to use new store

---------

Co-authored-by: Alexander Weaver <weaver.alex.d@gmail.com>
This commit is contained in:
Yuri Tseretyan 2024-01-05 16:15:18 -05:00 committed by GitHub
parent 8dc04ea63a
commit 494f36e0bd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 376 additions and 375 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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(),
}
}

View File

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

View File

@ -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(),

View File

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

View File

@ -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(),
}
}