grafana/pkg/services/ngalert/provisioning/notification_policies_test.go
Julien Duchesne 2188516a21
Alerting: Fix receiver inheritance when provisioning a notification policy (#82007)
Terraform Issue: grafana/terraform-provider-grafana#1007
Nested routes should be allowed to inherit the contact point from the root (or direct parent) route but this fails in the provisioning API (it works in the UI)
2024-03-26 12:31:59 -04:00

278 lines
10 KiB
Go

package provisioning
import (
"context"
"testing"
"github.com/prometheus/alertmanager/config"
"github.com/prometheus/alertmanager/timeinterval"
"github.com/prometheus/common/model"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
"github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/ngalert/tests/fakes"
"github.com/grafana/grafana/pkg/setting"
)
func TestNotificationPolicyService(t *testing.T) {
t.Run("service gets policy tree from org's AM config", func(t *testing.T) {
sut := createNotificationPolicyServiceSut()
tree, err := sut.GetPolicyTree(context.Background(), 1)
require.NoError(t, err)
require.Equal(t, "grafana-default-email", tree.Receiver)
})
t.Run("error if referenced mute time interval is not existing", func(t *testing.T) {
sut := createNotificationPolicyServiceSut()
sut.configStore.store = &MockAMConfigStore{}
cfg := createTestAlertingConfig()
cfg.AlertmanagerConfig.MuteTimeIntervals = []config.MuteTimeInterval{
{
Name: "not-the-one-we-need",
TimeIntervals: []timeinterval.TimeInterval{},
},
}
data, _ := serializeAlertmanagerConfig(*cfg)
sut.configStore.store.(*MockAMConfigStore).On("GetLatestAlertmanagerConfiguration", mock.Anything, mock.Anything).
Return(&models.AlertConfiguration{AlertmanagerConfiguration: string(data)}, nil)
sut.configStore.store.(*MockAMConfigStore).EXPECT().
UpdateAlertmanagerConfiguration(mock.Anything, mock.Anything).
Return(nil)
newRoute := createTestRoutingTree()
newRoute.Routes = append(newRoute.Routes, &definitions.Route{
Receiver: "slack receiver",
MuteTimeIntervals: []string{"not-existing"},
})
err := sut.UpdatePolicyTree(context.Background(), 1, newRoute, models.ProvenanceNone)
require.Error(t, err)
})
t.Run("pass if referenced mute time interval is existing", func(t *testing.T) {
sut := createNotificationPolicyServiceSut()
sut.configStore.store = &MockAMConfigStore{}
cfg := createTestAlertingConfig()
cfg.AlertmanagerConfig.MuteTimeIntervals = []config.MuteTimeInterval{
{
Name: "existing",
TimeIntervals: []timeinterval.TimeInterval{},
},
}
data, _ := serializeAlertmanagerConfig(*cfg)
sut.configStore.store.(*MockAMConfigStore).On("GetLatestAlertmanagerConfiguration", mock.Anything, mock.Anything).
Return(&models.AlertConfiguration{AlertmanagerConfiguration: string(data)}, nil)
sut.configStore.store.(*MockAMConfigStore).EXPECT().
UpdateAlertmanagerConfiguration(mock.Anything, mock.Anything).
Return(nil)
newRoute := createTestRoutingTree()
newRoute.Routes = append(newRoute.Routes, &definitions.Route{
Receiver: "slack receiver",
MuteTimeIntervals: []string{"existing"},
})
err := sut.UpdatePolicyTree(context.Background(), 1, newRoute, models.ProvenanceNone)
require.NoError(t, err)
})
t.Run("service stitches policy tree into org's AM config", func(t *testing.T) {
sut := createNotificationPolicyServiceSut()
newRoute := createTestRoutingTree()
err := sut.UpdatePolicyTree(context.Background(), 1, newRoute, models.ProvenanceNone)
require.NoError(t, err)
updated, err := sut.GetPolicyTree(context.Background(), 1)
require.NoError(t, err)
require.Equal(t, "slack receiver", updated.Receiver)
})
t.Run("no root receiver will error", func(t *testing.T) {
sut := createNotificationPolicyServiceSut()
newRoute := createTestRoutingTree()
newRoute.Receiver = ""
newRoute.Routes = append(newRoute.Routes, &definitions.Route{
Receiver: "",
})
err := sut.UpdatePolicyTree(context.Background(), 1, newRoute, models.ProvenanceNone)
require.EqualError(t, err, "invalid object specification: root route must specify a default receiver")
})
t.Run("allow receiver inheritance", func(t *testing.T) {
sut := createNotificationPolicyServiceSut()
newRoute := createTestRoutingTree()
newRoute.Routes = append(newRoute.Routes, &definitions.Route{
Receiver: "",
})
err := sut.UpdatePolicyTree(context.Background(), 1, newRoute, models.ProvenanceNone)
require.NoError(t, err)
})
t.Run("not existing receiver reference will error", func(t *testing.T) {
sut := createNotificationPolicyServiceSut()
newRoute := createTestRoutingTree()
newRoute.Routes = append(newRoute.Routes, &definitions.Route{
Receiver: "not-existing",
})
err := sut.UpdatePolicyTree(context.Background(), 1, newRoute, models.ProvenanceNone)
require.Error(t, err)
})
t.Run("existing receiver reference will pass", func(t *testing.T) {
sut := createNotificationPolicyServiceSut()
sut.configStore.store = &MockAMConfigStore{}
cfg := createTestAlertingConfig()
data, _ := serializeAlertmanagerConfig(*cfg)
sut.configStore.store.(*MockAMConfigStore).On("GetLatestAlertmanagerConfiguration", mock.Anything, mock.Anything).
Return(&models.AlertConfiguration{AlertmanagerConfiguration: string(data)}, nil)
sut.configStore.store.(*MockAMConfigStore).EXPECT().
UpdateAlertmanagerConfiguration(mock.Anything, mock.Anything).
Return(nil)
newRoute := createTestRoutingTree()
newRoute.Routes = append(newRoute.Routes, &definitions.Route{
Receiver: "existing",
})
err := sut.UpdatePolicyTree(context.Background(), 1, newRoute, models.ProvenanceNone)
require.NoError(t, err)
})
t.Run("default provenance of records is none", func(t *testing.T) {
sut := createNotificationPolicyServiceSut()
tree, err := sut.GetPolicyTree(context.Background(), 1)
require.NoError(t, err)
require.Equal(t, models.ProvenanceNone, models.Provenance(tree.Provenance))
})
t.Run("service returns upgraded provenance value", func(t *testing.T) {
sut := createNotificationPolicyServiceSut()
newRoute := createTestRoutingTree()
err := sut.UpdatePolicyTree(context.Background(), 1, newRoute, models.ProvenanceAPI)
require.NoError(t, err)
updated, err := sut.GetPolicyTree(context.Background(), 1)
require.NoError(t, err)
require.Equal(t, models.ProvenanceAPI, models.Provenance(updated.Provenance))
})
t.Run("service respects concurrency token when updating", func(t *testing.T) {
sut := createNotificationPolicyServiceSut()
newRoute := createTestRoutingTree()
config, err := sut.GetAMConfigStore().GetLatestAlertmanagerConfiguration(context.Background(), 1)
require.NoError(t, err)
expectedConcurrencyToken := config.ConfigurationHash
err = sut.UpdatePolicyTree(context.Background(), 1, newRoute, models.ProvenanceAPI)
require.NoError(t, err)
fake := sut.GetAMConfigStore().(*fakes.FakeAlertmanagerConfigStore)
intercepted := fake.LastSaveCommand
require.Equal(t, expectedConcurrencyToken, intercepted.FetchedConfigurationHash)
})
t.Run("updating invalid route returns ValidationError", func(t *testing.T) {
sut := createNotificationPolicyServiceSut()
invalid := createTestRoutingTree()
repeat := model.Duration(0)
invalid.RepeatInterval = &repeat
err := sut.UpdatePolicyTree(context.Background(), 1, invalid, models.ProvenanceNone)
require.Error(t, err)
require.ErrorIs(t, err, ErrValidation)
})
t.Run("deleting route replaces with default", func(t *testing.T) {
sut := createNotificationPolicyServiceSut()
tree, err := sut.ResetPolicyTree(context.Background(), 1)
require.NoError(t, err)
require.Equal(t, "grafana-default-email", tree.Receiver)
require.Nil(t, tree.Routes)
require.Equal(t, []model.LabelName{models.FolderTitleLabel, model.AlertNameLabel}, tree.GroupBy)
})
t.Run("deleting route with missing default receiver restores receiver", func(t *testing.T) {
sut := createNotificationPolicyServiceSut()
sut.configStore.store = &MockAMConfigStore{}
cfg := createTestAlertingConfig()
cfg.AlertmanagerConfig.Route = &definitions.Route{
Receiver: "slack receiver",
}
cfg.AlertmanagerConfig.Receivers = []*definitions.PostableApiReceiver{
{
Receiver: config.Receiver{
Name: "slack receiver",
},
},
// No default receiver! Only our custom one.
}
data, _ := serializeAlertmanagerConfig(*cfg)
sut.configStore.store.(*MockAMConfigStore).On("GetLatestAlertmanagerConfiguration", mock.Anything, mock.Anything).
Return(&models.AlertConfiguration{AlertmanagerConfiguration: string(data)}, nil)
var interceptedSave = models.SaveAlertmanagerConfigurationCmd{}
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 configStore is semantically valid.
newCfg, err := deserializeAlertmanagerConfig([]byte(interceptedSave.AlertmanagerConfiguration))
require.NoError(t, err)
require.Len(t, newCfg.AlertmanagerConfig.Receivers, 2)
})
}
func createNotificationPolicyServiceSut() *NotificationPolicyService {
return &NotificationPolicyService{
configStore: &alertmanagerConfigStoreImpl{store: fakes.NewFakeAlertmanagerConfigStore(defaultAlertmanagerConfigJSON)},
provenanceStore: fakes.NewFakeProvisioningStore(),
xact: newNopTransactionManager(),
log: log.NewNopLogger(),
settings: setting.UnifiedAlertingSettings{
DefaultConfiguration: setting.GetAlertmanagerDefaultConfiguration(),
},
}
}
func createTestRoutingTree() definitions.Route {
return definitions.Route{
Receiver: "slack receiver",
}
}
func createTestAlertingConfig() *definitions.PostableUserConfig {
cfg, _ := deserializeAlertmanagerConfig([]byte(defaultConfig))
cfg.AlertmanagerConfig.Receivers = append(cfg.AlertmanagerConfig.Receivers,
&definitions.PostableApiReceiver{
Receiver: config.Receiver{
// default one from createTestRoutingTree()
Name: "slack receiver",
},
})
cfg.AlertmanagerConfig.Receivers = append(cfg.AlertmanagerConfig.Receivers,
&definitions.PostableApiReceiver{
Receiver: config.Receiver{
Name: "existing",
},
})
return cfg
}