mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: Time interval Delete API to check for usages in alert rules (#90500)
* Check if a time interval is used in alert rules before deleting it * Add time interval to parameters of ListAlertRulesQuery and ListNotificationSettings of DbStore == Refacorings == * refactor isMuteTimeInUse to accept a single route * update getMuteTiming to not return err * update delete to get the mute timing from config first
This commit is contained in:
parent
79df1b1e10
commit
970cafa20f
@ -1891,7 +1891,7 @@ func createProvisioningSrvSutFromEnv(t *testing.T, env *testEnvironment) Provisi
|
|||||||
policies: newFakeNotificationPolicyService(),
|
policies: newFakeNotificationPolicyService(),
|
||||||
contactPointService: provisioning.NewContactPointService(env.configs, env.secrets, env.prov, env.xact, receiverSvc, env.log, env.store),
|
contactPointService: provisioning.NewContactPointService(env.configs, env.secrets, env.prov, env.xact, receiverSvc, env.log, env.store),
|
||||||
templates: provisioning.NewTemplateService(env.configs, env.prov, env.xact, env.log),
|
templates: provisioning.NewTemplateService(env.configs, env.prov, env.xact, env.log),
|
||||||
muteTimings: provisioning.NewMuteTimingService(env.configs, env.prov, env.xact, env.log),
|
muteTimings: provisioning.NewMuteTimingService(env.configs, env.prov, env.xact, env.log, env.store),
|
||||||
alertRules: provisioning.NewAlertRuleService(env.store, env.prov, env.folderService, env.quotas, env.xact, 60, 10, 100, env.log, &provisioning.NotificationSettingsValidatorProviderFake{}, env.rulesAuthz),
|
alertRules: provisioning.NewAlertRuleService(env.store, env.prov, env.folderService, env.quotas, env.xact, 60, 10, 100, env.log, &provisioning.NotificationSettingsValidatorProviderFake{}, env.rulesAuthz),
|
||||||
folderSvc: env.folderService,
|
folderSvc: env.folderService,
|
||||||
featureManager: env.features,
|
featureManager: env.features,
|
||||||
|
@ -671,6 +671,7 @@ type ListAlertRulesQuery struct {
|
|||||||
PanelID int64
|
PanelID int64
|
||||||
|
|
||||||
ReceiverName string
|
ReceiverName string
|
||||||
|
TimeIntervalName string
|
||||||
}
|
}
|
||||||
|
|
||||||
// CountAlertRulesQuery is the query for counting alert rules
|
// CountAlertRulesQuery is the query for counting alert rules
|
||||||
|
@ -20,6 +20,7 @@ var DefaultNotificationSettingsGroupBy = []string{FolderTitleLabel, model.AlertN
|
|||||||
type ListNotificationSettingsQuery struct {
|
type ListNotificationSettingsQuery struct {
|
||||||
OrgID int64
|
OrgID int64
|
||||||
ReceiverName string
|
ReceiverName string
|
||||||
|
TimeIntervalName string
|
||||||
}
|
}
|
||||||
|
|
||||||
// NotificationSettings represents the settings for sending notifications for a single AlertRule. It is used to
|
// NotificationSettings represents the settings for sending notifications for a single AlertRule. It is used to
|
||||||
|
@ -412,7 +412,7 @@ func (ng *AlertNG) init() error {
|
|||||||
policyService := provisioning.NewNotificationPolicyService(ng.store, ng.store, ng.store, ng.Cfg.UnifiedAlerting, ng.Log)
|
policyService := provisioning.NewNotificationPolicyService(ng.store, ng.store, ng.store, ng.Cfg.UnifiedAlerting, ng.Log)
|
||||||
contactPointService := provisioning.NewContactPointService(ng.store, ng.SecretsService, ng.store, ng.store, receiverService, ng.Log, ng.store)
|
contactPointService := provisioning.NewContactPointService(ng.store, ng.SecretsService, ng.store, ng.store, receiverService, ng.Log, ng.store)
|
||||||
templateService := provisioning.NewTemplateService(ng.store, ng.store, ng.store, ng.Log)
|
templateService := provisioning.NewTemplateService(ng.store, ng.store, ng.store, ng.Log)
|
||||||
muteTimingService := provisioning.NewMuteTimingService(ng.store, ng.store, ng.store, ng.Log)
|
muteTimingService := provisioning.NewMuteTimingService(ng.store, ng.store, ng.store, ng.Log, ng.store)
|
||||||
alertRuleService := provisioning.NewAlertRuleService(ng.store, ng.store, ng.folderService, ng.QuotaService, ng.store,
|
alertRuleService := provisioning.NewAlertRuleService(ng.store, ng.store, ng.folderService, ng.QuotaService, ng.store,
|
||||||
int64(ng.Cfg.UnifiedAlerting.DefaultRuleEvaluationInterval.Seconds()),
|
int64(ng.Cfg.UnifiedAlerting.DefaultRuleEvaluationInterval.Seconds()),
|
||||||
int64(ng.Cfg.UnifiedAlerting.BaseInterval.Seconds()),
|
int64(ng.Cfg.UnifiedAlerting.BaseInterval.Seconds()),
|
||||||
|
@ -5,6 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/apimachinery/errutil"
|
"github.com/grafana/grafana/pkg/apimachinery/errutil"
|
||||||
|
"github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
var ErrValidation = fmt.Errorf("invalid object specification")
|
var ErrValidation = fmt.Errorf("invalid object specification")
|
||||||
@ -20,7 +21,7 @@ var (
|
|||||||
ErrTimeIntervalNotFound = errutil.NotFound("alerting.notifications.time-intervals.notFound")
|
ErrTimeIntervalNotFound = errutil.NotFound("alerting.notifications.time-intervals.notFound")
|
||||||
ErrTimeIntervalExists = errutil.BadRequest("alerting.notifications.time-intervals.nameExists", errutil.WithPublicMessage("Time interval with this name already exists. Use a different name or update existing one."))
|
ErrTimeIntervalExists = errutil.BadRequest("alerting.notifications.time-intervals.nameExists", errutil.WithPublicMessage("Time interval with this name already exists. Use a different name or update existing one."))
|
||||||
ErrTimeIntervalInvalid = errutil.BadRequest("alerting.notifications.time-intervals.invalidFormat").MustTemplate("Invalid format of the submitted time interval", errutil.WithPublic("Time interval is in invalid format. Correct the payload and try again."))
|
ErrTimeIntervalInvalid = errutil.BadRequest("alerting.notifications.time-intervals.invalidFormat").MustTemplate("Invalid format of the submitted time interval", errutil.WithPublic("Time interval is in invalid format. Correct the payload and try again."))
|
||||||
ErrTimeIntervalInUse = errutil.Conflict("alerting.notifications.time-intervals.used", errutil.WithPublicMessage("Time interval is used by one or many notification policies"))
|
ErrTimeIntervalInUse = errutil.Conflict("alerting.notifications.time-intervals.used").MustTemplate("Time interval is used")
|
||||||
|
|
||||||
ErrContactPointReferenced = errutil.Conflict("alerting.notifications.contact-points.referenced", errutil.WithPublicMessage("Contact point is currently referenced by a notification policy."))
|
ErrContactPointReferenced = errutil.Conflict("alerting.notifications.contact-points.referenced", errutil.WithPublicMessage("Contact point is currently referenced by a notification policy."))
|
||||||
ErrContactPointUsedInRule = errutil.Conflict("alerting.notifications.contact-points.used-by-rule", errutil.WithPublicMessage("Contact point is currently used in the notification settings of one or many alert rules."))
|
ErrContactPointUsedInRule = errutil.Conflict("alerting.notifications.contact-points.used-by-rule", errutil.WithPublicMessage("Contact point is currently used in the notification settings of one or many alert rules."))
|
||||||
@ -47,3 +48,22 @@ func MakeErrTimeIntervalInvalid(err error) error {
|
|||||||
|
|
||||||
return ErrTimeIntervalInvalid.Build(data)
|
return ErrTimeIntervalInvalid.Build(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func MakeErrTimeIntervalInUse(usedByRoutes bool, rules []models.AlertRuleKey) error {
|
||||||
|
uids := make([]string, 0, len(rules))
|
||||||
|
for _, key := range rules {
|
||||||
|
uids = append(uids, key.UID)
|
||||||
|
}
|
||||||
|
data := make(map[string]any, 2)
|
||||||
|
if len(uids) > 0 {
|
||||||
|
data["UsedByRules"] = uids
|
||||||
|
}
|
||||||
|
if usedByRoutes {
|
||||||
|
data["UsedByRoutes"] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return ErrTimeIntervalInUse.Build(errutil.TemplateData{
|
||||||
|
Public: data,
|
||||||
|
Error: nil,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@ -5,10 +5,12 @@ import (
|
|||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"fmt"
|
"fmt"
|
||||||
"hash/fnv"
|
"hash/fnv"
|
||||||
|
"slices"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
|
|
||||||
"github.com/prometheus/alertmanager/config"
|
"github.com/prometheus/alertmanager/config"
|
||||||
"github.com/prometheus/alertmanager/timeinterval"
|
"github.com/prometheus/alertmanager/timeinterval"
|
||||||
|
"golang.org/x/exp/maps"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/infra/log"
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
"github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
"github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
||||||
@ -22,15 +24,17 @@ type MuteTimingService struct {
|
|||||||
xact TransactionManager
|
xact TransactionManager
|
||||||
log log.Logger
|
log log.Logger
|
||||||
validator validation.ProvenanceStatusTransitionValidator
|
validator validation.ProvenanceStatusTransitionValidator
|
||||||
|
ruleNotificationsStore AlertRuleNotificationSettingsStore
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewMuteTimingService(config AMConfigStore, prov ProvisioningStore, xact TransactionManager, log log.Logger) *MuteTimingService {
|
func NewMuteTimingService(config AMConfigStore, prov ProvisioningStore, xact TransactionManager, log log.Logger, ns AlertRuleNotificationSettingsStore) *MuteTimingService {
|
||||||
return &MuteTimingService{
|
return &MuteTimingService{
|
||||||
configStore: &alertmanagerConfigStoreImpl{store: config},
|
configStore: &alertmanagerConfigStoreImpl{store: config},
|
||||||
provenanceStore: prov,
|
provenanceStore: prov,
|
||||||
xact: xact,
|
xact: xact,
|
||||||
log: log,
|
log: log,
|
||||||
validator: validation.ValidateProvenanceRelaxed,
|
validator: validation.ValidateProvenanceRelaxed,
|
||||||
|
ruleNotificationsStore: ns,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -69,9 +73,9 @@ func (svc *MuteTimingService) GetMuteTiming(ctx context.Context, name string, or
|
|||||||
return definitions.MuteTimeInterval{}, err
|
return definitions.MuteTimeInterval{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
mt, _, err := getMuteTiming(rev, name)
|
mt, idx := getMuteTiming(rev, name)
|
||||||
if err != nil {
|
if idx == -1 {
|
||||||
return definitions.MuteTimeInterval{}, err
|
return definitions.MuteTimeInterval{}, ErrTimeIntervalNotFound.Errorf("")
|
||||||
}
|
}
|
||||||
|
|
||||||
result := definitions.MuteTimeInterval{
|
result := definitions.MuteTimeInterval{
|
||||||
@ -98,14 +102,10 @@ func (svc *MuteTimingService) CreateMuteTiming(ctx context.Context, mt definitio
|
|||||||
return definitions.MuteTimeInterval{}, err
|
return definitions.MuteTimeInterval{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if revision.cfg.AlertmanagerConfig.MuteTimeIntervals == nil {
|
_, idx := getMuteTiming(revision, mt.Name)
|
||||||
revision.cfg.AlertmanagerConfig.MuteTimeIntervals = []config.MuteTimeInterval{}
|
if idx != -1 {
|
||||||
}
|
|
||||||
for _, existing := range revision.cfg.AlertmanagerConfig.MuteTimeIntervals {
|
|
||||||
if mt.Name == existing.Name {
|
|
||||||
return definitions.MuteTimeInterval{}, ErrTimeIntervalExists.Errorf("")
|
return definitions.MuteTimeInterval{}, ErrTimeIntervalExists.Errorf("")
|
||||||
}
|
}
|
||||||
}
|
|
||||||
revision.cfg.AlertmanagerConfig.MuteTimeIntervals = append(revision.cfg.AlertmanagerConfig.MuteTimeIntervals, mt.MuteTimeInterval)
|
revision.cfg.AlertmanagerConfig.MuteTimeIntervals = append(revision.cfg.AlertmanagerConfig.MuteTimeIntervals, mt.MuteTimeInterval)
|
||||||
|
|
||||||
err = svc.xact.InTransaction(ctx, func(ctx context.Context) error {
|
err = svc.xact.InTransaction(ctx, func(ctx context.Context) error {
|
||||||
@ -148,9 +148,9 @@ func (svc *MuteTimingService) UpdateMuteTiming(ctx context.Context, mt definitio
|
|||||||
return definitions.MuteTimeInterval{}, nil
|
return definitions.MuteTimeInterval{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
old, idx, err := getMuteTiming(revision, mt.Name)
|
old, idx := getMuteTiming(revision, mt.Name)
|
||||||
if err != nil {
|
if idx == -1 {
|
||||||
return definitions.MuteTimeInterval{}, err
|
return definitions.MuteTimeInterval{}, ErrTimeIntervalNotFound.Errorf("")
|
||||||
}
|
}
|
||||||
|
|
||||||
err = svc.checkOptimisticConcurrency(old, models.Provenance(mt.Provenance), mt.Version, "update")
|
err = svc.checkOptimisticConcurrency(old, models.Provenance(mt.Provenance), mt.Version, "update")
|
||||||
@ -179,7 +179,18 @@ func (svc *MuteTimingService) UpdateMuteTiming(ctx context.Context, mt definitio
|
|||||||
|
|
||||||
// DeleteMuteTiming deletes the mute timing with the given name in the given org. If the mute timing does not exist, no error is returned.
|
// 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, provenance definitions.Provenance, version string) error {
|
func (svc *MuteTimingService) DeleteMuteTiming(ctx context.Context, name string, orgID int64, provenance definitions.Provenance, version string) error {
|
||||||
target := definitions.MuteTimeInterval{MuteTimeInterval: config.MuteTimeInterval{Name: name}, Provenance: provenance}
|
revision, err := svc.configStore.Get(ctx, orgID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
existing, idx := getMuteTiming(revision, name)
|
||||||
|
if idx == -1 {
|
||||||
|
svc.log.FromContext(ctx).Debug("Time interval was not found. Skip deleting", "name", name)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
target := definitions.MuteTimeInterval{MuteTimeInterval: existing, Provenance: provenance}
|
||||||
// check that provenance is not changed in an invalid way
|
// check that provenance is not changed in an invalid way
|
||||||
storedProvenance, err := svc.provenanceStore.GetProvenance(ctx, &target, orgID)
|
storedProvenance, err := svc.provenanceStore.GetProvenance(ctx, &target, orgID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -189,30 +200,27 @@ func (svc *MuteTimingService) DeleteMuteTiming(ctx context.Context, name string,
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
revision, err := svc.configStore.Get(ctx, orgID)
|
if isMuteTimeInUseInRoutes(name, revision.cfg.AlertmanagerConfig.Route) {
|
||||||
if err != nil {
|
ns, _ := svc.ruleNotificationsStore.ListNotificationSettings(ctx, models.ListNotificationSettingsQuery{OrgID: orgID, TimeIntervalName: existing.Name})
|
||||||
return err
|
// ignore error here because it's not important
|
||||||
|
return MakeErrTimeIntervalInUse(true, maps.Keys(ns))
|
||||||
}
|
}
|
||||||
|
|
||||||
if revision.cfg.AlertmanagerConfig.MuteTimeIntervals == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if isMuteTimeInUse(name, []*definitions.Route{revision.cfg.AlertmanagerConfig.Route}) {
|
|
||||||
return ErrTimeIntervalInUse.Errorf("")
|
|
||||||
}
|
|
||||||
for i, existing := range revision.cfg.AlertmanagerConfig.MuteTimeIntervals {
|
|
||||||
if name != existing.Name {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
err = svc.checkOptimisticConcurrency(existing, models.Provenance(provenance), version, "delete")
|
err = svc.checkOptimisticConcurrency(existing, models.Provenance(provenance), version, "delete")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
intervals := revision.cfg.AlertmanagerConfig.MuteTimeIntervals
|
revision.cfg.AlertmanagerConfig.MuteTimeIntervals = slices.Delete(revision.cfg.AlertmanagerConfig.MuteTimeIntervals, idx, idx+1)
|
||||||
revision.cfg.AlertmanagerConfig.MuteTimeIntervals = append(intervals[:i], intervals[i+1:]...)
|
|
||||||
}
|
|
||||||
|
|
||||||
return svc.xact.InTransaction(ctx, func(ctx context.Context) error {
|
return svc.xact.InTransaction(ctx, func(ctx context.Context) error {
|
||||||
|
keys, err := svc.ruleNotificationsStore.ListNotificationSettings(ctx, models.ListNotificationSettingsQuery{OrgID: orgID, TimeIntervalName: existing.Name})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(keys) > 0 {
|
||||||
|
return MakeErrTimeIntervalInUse(false, maps.Keys(keys))
|
||||||
|
}
|
||||||
|
|
||||||
if err := svc.configStore.Save(ctx, revision, orgID); err != nil {
|
if err := svc.configStore.Save(ctx, revision, orgID); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -220,33 +228,29 @@ func (svc *MuteTimingService) DeleteMuteTiming(ctx context.Context, name string,
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func isMuteTimeInUse(name string, routes []*definitions.Route) bool {
|
func isMuteTimeInUseInRoutes(name string, route *definitions.Route) bool {
|
||||||
if len(routes) == 0 {
|
if route == nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
for _, route := range routes {
|
if slices.Contains(route.MuteTimeIntervals, name) {
|
||||||
for _, mtName := range route.MuteTimeIntervals {
|
|
||||||
if mtName == name {
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
for _, route := range route.Routes {
|
||||||
if isMuteTimeInUse(name, route.Routes) {
|
if isMuteTimeInUseInRoutes(name, route) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func getMuteTiming(rev *cfgRevision, name string) (config.MuteTimeInterval, int, error) {
|
func getMuteTiming(rev *cfgRevision, name string) (config.MuteTimeInterval, int) {
|
||||||
if rev.cfg.AlertmanagerConfig.MuteTimeIntervals == nil {
|
idx := slices.IndexFunc(rev.cfg.AlertmanagerConfig.MuteTimeIntervals, func(interval config.MuteTimeInterval) bool {
|
||||||
return config.MuteTimeInterval{}, -1, ErrTimeIntervalNotFound.Errorf("")
|
return interval.Name == name
|
||||||
|
})
|
||||||
|
if idx == -1 {
|
||||||
|
return config.MuteTimeInterval{}, idx
|
||||||
}
|
}
|
||||||
for idx, mt := range rev.cfg.AlertmanagerConfig.MuteTimeIntervals {
|
return rev.cfg.AlertmanagerConfig.MuteTimeIntervals[idx], idx
|
||||||
if mt.Name == name {
|
|
||||||
return mt, idx, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return config.MuteTimeInterval{}, -1, ErrTimeIntervalNotFound.Errorf("")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func calculateMuteTimeIntervalFingerprint(interval config.MuteTimeInterval) string {
|
func calculateMuteTimeIntervalFingerprint(interval config.MuteTimeInterval) string {
|
||||||
|
@ -9,6 +9,7 @@ import (
|
|||||||
|
|
||||||
"github.com/prometheus/alertmanager/config"
|
"github.com/prometheus/alertmanager/config"
|
||||||
"github.com/prometheus/alertmanager/timeinterval"
|
"github.com/prometheus/alertmanager/timeinterval"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/mock"
|
"github.com/stretchr/testify/mock"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
@ -635,31 +636,7 @@ func TestDeleteMuteTimings(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Run("re-saves config and deletes provenance if mute timing does not exist", func(t *testing.T) {
|
t.Run("fails if provenance check fails", func(t *testing.T) {
|
||||||
sut, store, prov := createMuteTimingSvcSut()
|
|
||||||
store.GetFn = func(ctx context.Context, orgID int64) (*cfgRevision, error) {
|
|
||||||
return &cfgRevision{cfg: initialConfig()}, nil
|
|
||||||
}
|
|
||||||
prov.EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceAPI, nil)
|
|
||||||
prov.EXPECT().DeleteProvenance(mock.Anything, mock.Anything, mock.Anything).Return(nil)
|
|
||||||
|
|
||||||
err := sut.DeleteMuteTiming(context.Background(), "no-timing", orgID, definitions.Provenance(models.ProvenanceAPI), "")
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
require.Len(t, store.Calls, 2)
|
|
||||||
require.Equal(t, "Get", store.Calls[0].Method)
|
|
||||||
require.Equal(t, orgID, store.Calls[0].Args[1])
|
|
||||||
|
|
||||||
require.Equal(t, "Save", store.Calls[1].Method)
|
|
||||||
require.Equal(t, orgID, store.Calls[1].Args[2])
|
|
||||||
revision := store.Calls[1].Args[1].(*cfgRevision)
|
|
||||||
|
|
||||||
require.EqualValues(t, initialConfig().AlertmanagerConfig.MuteTimeIntervals, revision.cfg.AlertmanagerConfig.MuteTimeIntervals)
|
|
||||||
|
|
||||||
prov.AssertCalled(t, "DeleteProvenance", mock.Anything, &definitions.MuteTimeInterval{MuteTimeInterval: config.MuteTimeInterval{Name: "no-timing"}, Provenance: definitions.Provenance(models.ProvenanceAPI)}, orgID)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("fails if it was created with different provenance", func(t *testing.T) {
|
|
||||||
sut, store, prov := createMuteTimingSvcSut()
|
sut, store, prov := createMuteTimingSvcSut()
|
||||||
expectedErr := errors.New("test")
|
expectedErr := errors.New("test")
|
||||||
sut.validator = func(from, to models.Provenance) error {
|
sut.validator = func(from, to models.Provenance) error {
|
||||||
@ -669,15 +646,12 @@ func TestDeleteMuteTimings(t *testing.T) {
|
|||||||
return &cfgRevision{cfg: initialConfig()}, nil
|
return &cfgRevision{cfg: initialConfig()}, nil
|
||||||
}
|
}
|
||||||
prov.EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceAPI, nil)
|
prov.EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceAPI, nil)
|
||||||
prov.EXPECT().DeleteProvenance(mock.Anything, mock.Anything, mock.Anything).Return(nil)
|
|
||||||
|
|
||||||
err := sut.DeleteMuteTiming(context.Background(), "no-timing", orgID, definitions.Provenance(models.ProvenanceNone), correctVersion)
|
err := sut.DeleteMuteTiming(context.Background(), timingToDelete.Name, orgID, definitions.Provenance(models.ProvenanceNone), correctVersion)
|
||||||
require.ErrorIs(t, err, expectedErr)
|
require.ErrorIs(t, err, expectedErr)
|
||||||
|
|
||||||
require.Len(t, store.Calls, 0)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("returns ErrTimeIntervalInUse if mute timing is used", func(t *testing.T) {
|
t.Run("returns ErrTimeIntervalInUse if mute timing is used by a route", func(t *testing.T) {
|
||||||
sut, store, prov := createMuteTimingSvcSut()
|
sut, store, prov := createMuteTimingSvcSut()
|
||||||
store.GetFn = func(ctx context.Context, orgID int64) (*cfgRevision, error) {
|
store.GetFn = func(ctx context.Context, orgID int64) (*cfgRevision, error) {
|
||||||
return &cfgRevision{cfg: initialConfig()}, nil
|
return &cfgRevision{cfg: initialConfig()}, nil
|
||||||
@ -689,7 +663,36 @@ func TestDeleteMuteTimings(t *testing.T) {
|
|||||||
require.Len(t, store.Calls, 1)
|
require.Len(t, store.Calls, 1)
|
||||||
require.Equal(t, "Get", store.Calls[0].Method)
|
require.Equal(t, "Get", store.Calls[0].Method)
|
||||||
require.Equal(t, orgID, store.Calls[0].Args[1])
|
require.Equal(t, orgID, store.Calls[0].Args[1])
|
||||||
require.Truef(t, ErrTimeIntervalInUse.Is(err), "expected ErrTimeIntervalInUse but got %s", err)
|
require.ErrorIs(t, err, ErrTimeIntervalInUse)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("returns ErrTimeIntervalInUse if mute timing is used by rules", func(t *testing.T) {
|
||||||
|
sut, store, prov := createMuteTimingSvcSut()
|
||||||
|
ruleNsStore := fakeAlertRuleNotificationStore{
|
||||||
|
ListNotificationSettingsFn: func(ctx context.Context, q models.ListNotificationSettingsQuery) (map[models.AlertRuleKey][]models.NotificationSettings, error) {
|
||||||
|
assertInTransaction(t, ctx)
|
||||||
|
assert.Equal(t, orgID, q.OrgID)
|
||||||
|
assert.Equal(t, timingToDelete.Name, q.TimeIntervalName)
|
||||||
|
assert.Empty(t, q.ReceiverName)
|
||||||
|
return map[models.AlertRuleKey][]models.NotificationSettings{
|
||||||
|
models.GenerateRuleKey(orgID): nil,
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
sut.ruleNotificationsStore = &ruleNsStore
|
||||||
|
store.GetFn = func(ctx context.Context, orgID int64) (*cfgRevision, error) {
|
||||||
|
return &cfgRevision{cfg: initialConfig()}, nil
|
||||||
|
}
|
||||||
|
prov.EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceAPI, nil)
|
||||||
|
|
||||||
|
err := sut.DeleteMuteTiming(context.Background(), timingToDelete.Name, orgID, definitions.Provenance(models.ProvenanceAPI), correctVersion)
|
||||||
|
|
||||||
|
require.Len(t, store.Calls, 1)
|
||||||
|
require.Equal(t, "Get", store.Calls[0].Method)
|
||||||
|
require.Equal(t, orgID, store.Calls[0].Args[1])
|
||||||
|
require.ErrorIs(t, err, ErrTimeIntervalInUse)
|
||||||
|
require.Len(t, ruleNsStore.Calls, 1)
|
||||||
|
require.Equal(t, "ListNotificationSettings", ruleNsStore.Calls[0].Method)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("returns ErrVersionConflict if provided version does not match", func(t *testing.T) {
|
t.Run("returns ErrVersionConflict if provided version does not match", func(t *testing.T) {
|
||||||
@ -828,5 +831,6 @@ func createMuteTimingSvcSut() (*MuteTimingService, *alertmanagerConfigStoreFake,
|
|||||||
validator: func(from, to models.Provenance) error {
|
validator: func(from, to models.Provenance) error {
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
|
ruleNotificationsStore: &fakeAlertRuleNotificationStore{},
|
||||||
}, store, prov
|
}, store, prov
|
||||||
}
|
}
|
||||||
|
@ -217,3 +217,40 @@ func (s *fakeRuleAccessControlService) CanWriteAllRules(ctx context.Context, use
|
|||||||
}
|
}
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type fakeAlertRuleNotificationStore struct {
|
||||||
|
Calls []call
|
||||||
|
|
||||||
|
RenameReceiverInNotificationSettingsFn func(ctx context.Context, orgID int64, oldReceiver, newReceiver string) (int, error)
|
||||||
|
ListNotificationSettingsFn func(ctx context.Context, q models.ListNotificationSettingsQuery) (map[models.AlertRuleKey][]models.NotificationSettings, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeAlertRuleNotificationStore) RenameReceiverInNotificationSettings(ctx context.Context, orgID int64, oldReceiver, newReceiver string) (int, error) {
|
||||||
|
call := call{
|
||||||
|
Method: "RenameReceiverInNotificationSettings",
|
||||||
|
Args: []interface{}{ctx, orgID, oldReceiver, newReceiver},
|
||||||
|
}
|
||||||
|
f.Calls = append(f.Calls, call)
|
||||||
|
|
||||||
|
if f.RenameReceiverInNotificationSettingsFn != nil {
|
||||||
|
return f.RenameReceiverInNotificationSettingsFn(ctx, orgID, oldReceiver, newReceiver)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default values when no function hook is provided
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeAlertRuleNotificationStore) ListNotificationSettings(ctx context.Context, q models.ListNotificationSettingsQuery) (map[models.AlertRuleKey][]models.NotificationSettings, error) {
|
||||||
|
call := call{
|
||||||
|
Method: "ListNotificationSettings",
|
||||||
|
Args: []interface{}{ctx, q},
|
||||||
|
}
|
||||||
|
f.Calls = append(f.Calls, call)
|
||||||
|
|
||||||
|
if f.ListNotificationSettingsFn != nil {
|
||||||
|
return f.ListNotificationSettingsFn(ctx, q)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default values when no function hook is provided
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
@ -399,7 +399,14 @@ func (st DBstore) ListAlertRules(ctx context.Context, query *ngmodels.ListAlertR
|
|||||||
}
|
}
|
||||||
|
|
||||||
if query.ReceiverName != "" {
|
if query.ReceiverName != "" {
|
||||||
q, err = st.filterByReceiverName(query.ReceiverName, q)
|
q, err = st.filterByContentInNotificationSettings(query.ReceiverName, q)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if query.TimeIntervalName != "" {
|
||||||
|
q, err = st.filterByContentInNotificationSettings(query.TimeIntervalName, q)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -432,6 +439,13 @@ func (st DBstore) ListAlertRules(ctx context.Context, query *ngmodels.ListAlertR
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if query.TimeIntervalName != "" {
|
||||||
|
if !slices.ContainsFunc(rule.NotificationSettings, func(settings ngmodels.NotificationSettings) bool {
|
||||||
|
return slices.Contains(settings.MuteTimeIntervals, query.TimeIntervalName)
|
||||||
|
}) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
// MySQL (and potentially other databases) can use case-insensitive comparison.
|
// MySQL (and potentially other databases) can use case-insensitive comparison.
|
||||||
// This code makes sure we return groups that only exactly match the filter.
|
// This code makes sure we return groups that only exactly match the filter.
|
||||||
if groupsMap != nil {
|
if groupsMap != nil {
|
||||||
@ -720,13 +734,24 @@ func (st DBstore) ListNotificationSettings(ctx context.Context, q ngmodels.ListN
|
|||||||
var rules []ngmodels.AlertRule
|
var rules []ngmodels.AlertRule
|
||||||
err := st.SQLStore.WithDbSession(ctx, func(sess *db.Session) error {
|
err := st.SQLStore.WithDbSession(ctx, func(sess *db.Session) error {
|
||||||
query := sess.Table(ngmodels.AlertRule{}).Select("uid, notification_settings").Where("org_id = ?", q.OrgID)
|
query := sess.Table(ngmodels.AlertRule{}).Select("uid, notification_settings").Where("org_id = ?", q.OrgID)
|
||||||
|
hasFilter := false
|
||||||
if q.ReceiverName != "" {
|
if q.ReceiverName != "" {
|
||||||
var err error
|
var err error
|
||||||
query, err = st.filterByReceiverName(q.ReceiverName, query)
|
query, err = st.filterByContentInNotificationSettings(q.ReceiverName, query)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else {
|
hasFilter = true
|
||||||
|
}
|
||||||
|
if q.TimeIntervalName != "" {
|
||||||
|
var err error
|
||||||
|
query, err = st.filterByContentInNotificationSettings(q.TimeIntervalName, query)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
hasFilter = true
|
||||||
|
}
|
||||||
|
if !hasFilter {
|
||||||
query = query.And("notification_settings IS NOT NULL AND notification_settings <> 'null'")
|
query = query.And("notification_settings IS NOT NULL AND notification_settings <> 'null'")
|
||||||
}
|
}
|
||||||
return query.Find(&rules)
|
return query.Find(&rules)
|
||||||
@ -736,16 +761,15 @@ func (st DBstore) ListNotificationSettings(ctx context.Context, q ngmodels.ListN
|
|||||||
}
|
}
|
||||||
result := make(map[ngmodels.AlertRuleKey][]ngmodels.NotificationSettings, len(rules))
|
result := make(map[ngmodels.AlertRuleKey][]ngmodels.NotificationSettings, len(rules))
|
||||||
for _, rule := range rules {
|
for _, rule := range rules {
|
||||||
var ns []ngmodels.NotificationSettings
|
ns := make([]ngmodels.NotificationSettings, 0, len(rule.NotificationSettings))
|
||||||
if q.ReceiverName != "" { // if filter by receiver name is specified, perform fine filtering on client to avoid false-positives
|
|
||||||
for _, setting := range rule.NotificationSettings {
|
for _, setting := range rule.NotificationSettings {
|
||||||
if q.ReceiverName == setting.Receiver { // currently, there can be only one setting. If in future there are more, we will return all settings of a rule that has a setting with receiver
|
if q.ReceiverName != "" && q.ReceiverName != setting.Receiver { // currently, there can be only one setting. If in future there are more, we will return all settings of a rule that has a setting with receiver
|
||||||
ns = rule.NotificationSettings
|
continue
|
||||||
break
|
|
||||||
}
|
}
|
||||||
|
if q.TimeIntervalName != "" && !slices.Contains(setting.MuteTimeIntervals, q.TimeIntervalName) {
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
} else {
|
ns = append(ns, setting)
|
||||||
ns = rule.NotificationSettings
|
|
||||||
}
|
}
|
||||||
if len(ns) > 0 {
|
if len(ns) > 0 {
|
||||||
key := ngmodels.AlertRuleKey{
|
key := ngmodels.AlertRuleKey{
|
||||||
@ -758,14 +782,14 @@ func (st DBstore) ListNotificationSettings(ctx context.Context, q ngmodels.ListN
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (st DBstore) filterByReceiverName(receiver string, sess *xorm.Session) (*xorm.Session, error) {
|
func (st DBstore) filterByContentInNotificationSettings(value string, sess *xorm.Session) (*xorm.Session, error) {
|
||||||
if receiver == "" {
|
if value == "" {
|
||||||
return sess, nil
|
return sess, nil
|
||||||
}
|
}
|
||||||
// marshall string according to JSON rules so we follow escaping rules.
|
// marshall string according to JSON rules so we follow escaping rules.
|
||||||
b, err := json.Marshal(receiver)
|
b, err := json.Marshal(value)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to marshall receiver name query: %w", err)
|
return nil, fmt.Errorf("failed to marshall string for notification settings content filter: %w", err)
|
||||||
}
|
}
|
||||||
var search = string(b)
|
var search = string(b)
|
||||||
if st.SQLStore.GetDialect().DriverName() != migrator.SQLite {
|
if st.SQLStore.GetDialect().DriverName() != migrator.SQLite {
|
||||||
|
@ -12,6 +12,7 @@ import (
|
|||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
"golang.org/x/exp/maps"
|
||||||
"golang.org/x/exp/rand"
|
"golang.org/x/exp/rand"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/bus"
|
"github.com/grafana/grafana/pkg/bus"
|
||||||
@ -776,6 +777,14 @@ func TestIntegrationAlertRulesNotificationSettings(t *testing.T) {
|
|||||||
t.Skip("skipping integration test")
|
t.Skip("skipping integration test")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getKeyMap := func(r []*models.AlertRule) map[models.AlertRuleKey]struct{} {
|
||||||
|
result := make(map[models.AlertRuleKey]struct{}, len(r))
|
||||||
|
for _, rule := range r {
|
||||||
|
result[rule.GetKey()] = struct{}{}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
sqlStore := db.InitTestReplDB(t)
|
sqlStore := db.InitTestReplDB(t)
|
||||||
cfg := setting.NewCfg()
|
cfg := setting.NewCfg()
|
||||||
cfg.UnifiedAlerting.BaseInterval = 1 * time.Second
|
cfg.UnifiedAlerting.BaseInterval = 1 * time.Second
|
||||||
@ -787,15 +796,17 @@ func TestIntegrationAlertRulesNotificationSettings(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
receiverName := "receiver\"-" + uuid.NewString()
|
receiverName := "receiver\"-" + uuid.NewString()
|
||||||
|
timeIntervalName := "time-" + util.GenerateShortUID()
|
||||||
|
|
||||||
gen := models.RuleGen
|
gen := models.RuleGen
|
||||||
gen = gen.With(gen.WithOrgID(1), gen.WithIntervalMatching(store.Cfg.BaseInterval))
|
gen = gen.With(gen.WithOrgID(1), gen.WithIntervalMatching(store.Cfg.BaseInterval))
|
||||||
rules := gen.GenerateManyRef(3)
|
rules := gen.GenerateManyRef(3)
|
||||||
receiveRules := gen.With(gen.WithNotificationSettingsGen(models.NotificationSettingsGen(models.NSMuts.WithReceiver(receiverName)))).GenerateManyRef(3)
|
receiveRules := gen.With(gen.WithNotificationSettingsGen(models.NotificationSettingsGen(models.NSMuts.WithReceiver(receiverName)))).GenerateManyRef(3)
|
||||||
noise := gen.With(gen.WithNotificationSettingsGen(models.NotificationSettingsGen(models.NSMuts.WithMuteTimeIntervals(receiverName)))).GenerateManyRef(3)
|
timeIntervalRules := gen.With(gen.WithNotificationSettingsGen(models.NotificationSettingsGen(models.NSMuts.WithMuteTimeIntervals(timeIntervalName)))).GenerateManyRef(3)
|
||||||
|
noise := gen.With(gen.WithNotificationSettingsGen(models.NotificationSettingsGen(models.NSMuts.WithReceiver(timeIntervalName), models.NSMuts.WithMuteTimeIntervals(receiverName)))).GenerateManyRef(3)
|
||||||
|
|
||||||
deref := make([]models.AlertRule, 0, len(rules)+len(receiveRules)+len(noise))
|
deref := make([]models.AlertRule, 0, len(rules)+len(receiveRules)+len(timeIntervalRules)+len(noise))
|
||||||
for _, rule := range append(append(rules, receiveRules...), noise...) {
|
for _, rule := range append(append(append(rules, receiveRules...), noise...), timeIntervalRules...) {
|
||||||
r := *rule
|
r := *rule
|
||||||
r.ID = 0
|
r.ID = 0
|
||||||
deref = append(deref, r)
|
deref = append(deref, r)
|
||||||
@ -805,18 +816,60 @@ func TestIntegrationAlertRulesNotificationSettings(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
t.Run("should find rules by receiver name", func(t *testing.T) {
|
t.Run("should find rules by receiver name", func(t *testing.T) {
|
||||||
expectedUIDs := map[string]struct{}{}
|
expected := getKeyMap(receiveRules)
|
||||||
for _, rule := range receiveRules {
|
|
||||||
expectedUIDs[rule.UID] = struct{}{}
|
|
||||||
}
|
|
||||||
actual, err := store.ListAlertRules(context.Background(), &models.ListAlertRulesQuery{
|
actual, err := store.ListAlertRules(context.Background(), &models.ListAlertRulesQuery{
|
||||||
OrgID: 1,
|
OrgID: 1,
|
||||||
ReceiverName: receiverName,
|
ReceiverName: receiverName,
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Len(t, actual, len(expectedUIDs))
|
assert.Len(t, actual, len(expected))
|
||||||
for _, rule := range actual {
|
for _, rule := range actual {
|
||||||
assert.Contains(t, expectedUIDs, rule.UID)
|
assert.Contains(t, expected, rule.GetKey())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should find rules by time interval name", func(t *testing.T) {
|
||||||
|
expected := getKeyMap(timeIntervalRules)
|
||||||
|
actual, err := store.ListAlertRules(context.Background(), &models.ListAlertRulesQuery{
|
||||||
|
OrgID: 1,
|
||||||
|
TimeIntervalName: timeIntervalName,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Len(t, actual, len(expected))
|
||||||
|
for _, rule := range actual {
|
||||||
|
assert.Contains(t, expected, rule.GetKey())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should find rules by receiver and time-interval name", func(t *testing.T) {
|
||||||
|
var receiver, intervalName string
|
||||||
|
var expected []models.AlertRuleKey
|
||||||
|
rand.Shuffle(len(deref), func(i, j int) {
|
||||||
|
deref[i], deref[j] = deref[j], deref[i]
|
||||||
|
})
|
||||||
|
for _, rule := range deref {
|
||||||
|
if len(rule.NotificationSettings) == 0 || rule.NotificationSettings[0].Receiver == "" || len(rule.NotificationSettings[0].MuteTimeIntervals) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if len(expected) > 0 {
|
||||||
|
if rule.NotificationSettings[0].Receiver == receiver && slices.Contains(rule.NotificationSettings[0].MuteTimeIntervals, intervalName) {
|
||||||
|
expected = append(expected, rule.GetKey())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
receiver = rule.NotificationSettings[0].Receiver
|
||||||
|
intervalName = rule.NotificationSettings[0].MuteTimeIntervals[0]
|
||||||
|
expected = append(expected, rule.GetKey())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
actual, err := store.ListAlertRules(context.Background(), &models.ListAlertRulesQuery{
|
||||||
|
OrgID: 1,
|
||||||
|
ReceiverName: receiver,
|
||||||
|
TimeIntervalName: intervalName,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Len(t, actual, len(expected))
|
||||||
|
for _, rule := range actual {
|
||||||
|
assert.Contains(t, expected, rule.GetKey())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -864,13 +917,17 @@ func TestIntegrationListNotificationSettings(t *testing.T) {
|
|||||||
Cfg: cfg.UnifiedAlerting,
|
Cfg: cfg.UnifiedAlerting,
|
||||||
}
|
}
|
||||||
|
|
||||||
receiverName := `receiver%"-👍'test`
|
searchName := `name-%"-👍'test`
|
||||||
gen := models.RuleGen
|
gen := models.RuleGen
|
||||||
gen = gen.With(gen.WithOrgID(1), gen.WithIntervalMatching(store.Cfg.BaseInterval))
|
gen = gen.With(gen.WithOrgID(1), gen.WithIntervalMatching(store.Cfg.BaseInterval))
|
||||||
|
|
||||||
rulesWithNotifications := gen.With(
|
rulesWithNotificationsAndReceiver := gen.With(
|
||||||
gen.WithNotificationSettingsGen(models.NotificationSettingsGen(models.NSMuts.WithReceiver(receiverName))),
|
gen.WithNotificationSettingsGen(models.NotificationSettingsGen(models.NSMuts.WithReceiver(searchName))),
|
||||||
).GenerateMany(5)
|
).GenerateMany(5)
|
||||||
|
rulesWithNotificationsAndTimeInterval := gen.With(
|
||||||
|
gen.WithNotificationSettingsGen(models.NotificationSettingsGen(models.NSMuts.WithMuteTimeIntervals(searchName))),
|
||||||
|
).GenerateMany(5)
|
||||||
|
|
||||||
rulesInOtherOrg := gen.With(
|
rulesInOtherOrg := gen.With(
|
||||||
gen.WithOrgID(2),
|
gen.WithOrgID(2),
|
||||||
gen.WithNotificationSettingsGen(models.NotificationSettingsGen()),
|
gen.WithNotificationSettingsGen(models.NotificationSettingsGen()),
|
||||||
@ -878,15 +935,18 @@ func TestIntegrationListNotificationSettings(t *testing.T) {
|
|||||||
|
|
||||||
rulesWithNoNotifications := gen.With(gen.WithNoNotificationSettings()).GenerateMany(5)
|
rulesWithNoNotifications := gen.With(gen.WithNoNotificationSettings()).GenerateMany(5)
|
||||||
|
|
||||||
deref := append(append(rulesWithNotifications, rulesWithNoNotifications...), rulesInOtherOrg...)
|
deref := append(append(rulesWithNotificationsAndReceiver, rulesWithNoNotifications...), rulesInOtherOrg...)
|
||||||
|
deref = append(deref, rulesWithNotificationsAndTimeInterval...)
|
||||||
|
|
||||||
|
orgRules := append(rulesWithNotificationsAndReceiver, rulesWithNotificationsAndTimeInterval...)
|
||||||
|
|
||||||
_, err := store.InsertAlertRules(context.Background(), deref)
|
_, err := store.InsertAlertRules(context.Background(), deref)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
result, err := store.ListNotificationSettings(context.Background(), models.ListNotificationSettingsQuery{OrgID: 1})
|
result, err := store.ListNotificationSettings(context.Background(), models.ListNotificationSettingsQuery{OrgID: 1})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Len(t, result, len(rulesWithNotifications))
|
require.Len(t, result, len(orgRules))
|
||||||
for _, rule := range rulesWithNotifications {
|
for _, rule := range rulesWithNotificationsAndReceiver {
|
||||||
if !assert.Contains(t, result, rule.GetKey()) {
|
if !assert.Contains(t, result, rule.GetKey()) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@ -895,13 +955,13 @@ func TestIntegrationListNotificationSettings(t *testing.T) {
|
|||||||
|
|
||||||
t.Run("should list notification settings by receiver name", func(t *testing.T) {
|
t.Run("should list notification settings by receiver name", func(t *testing.T) {
|
||||||
expectedUIDs := map[models.AlertRuleKey]struct{}{}
|
expectedUIDs := map[models.AlertRuleKey]struct{}{}
|
||||||
for _, rule := range rulesWithNotifications {
|
for _, rule := range rulesWithNotificationsAndReceiver {
|
||||||
expectedUIDs[rule.GetKey()] = struct{}{}
|
expectedUIDs[rule.GetKey()] = struct{}{}
|
||||||
}
|
}
|
||||||
|
|
||||||
actual, err := store.ListNotificationSettings(context.Background(), models.ListNotificationSettingsQuery{
|
actual, err := store.ListNotificationSettings(context.Background(), models.ListNotificationSettingsQuery{
|
||||||
OrgID: 1,
|
OrgID: 1,
|
||||||
ReceiverName: receiverName,
|
ReceiverName: searchName,
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Len(t, actual, len(expectedUIDs))
|
assert.Len(t, actual, len(expectedUIDs))
|
||||||
@ -909,6 +969,60 @@ func TestIntegrationListNotificationSettings(t *testing.T) {
|
|||||||
assert.Contains(t, expectedUIDs, ruleKey)
|
assert.Contains(t, expectedUIDs, ruleKey)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
t.Run("should filter notification settings by time interval name", func(t *testing.T) {
|
||||||
|
expectedUIDs := map[models.AlertRuleKey]struct{}{}
|
||||||
|
for _, rule := range rulesWithNotificationsAndTimeInterval {
|
||||||
|
expectedUIDs[rule.GetKey()] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
actual, err := store.ListNotificationSettings(context.Background(), models.ListNotificationSettingsQuery{
|
||||||
|
OrgID: 1,
|
||||||
|
TimeIntervalName: searchName,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Len(t, actual, len(expectedUIDs))
|
||||||
|
for ruleKey := range actual {
|
||||||
|
assert.Contains(t, expectedUIDs, ruleKey)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("should return nothing if filter does not match", func(t *testing.T) {
|
||||||
|
result, err := store.ListNotificationSettings(context.Background(), models.ListNotificationSettingsQuery{
|
||||||
|
OrgID: 1,
|
||||||
|
ReceiverName: "not-found-receiver",
|
||||||
|
TimeIntervalName: "not-found-time-interval",
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Empty(t, result)
|
||||||
|
})
|
||||||
|
t.Run("should filter by time interval and receiver", func(t *testing.T) {
|
||||||
|
var receiver, timeInterval string
|
||||||
|
var expected []models.AlertRuleKey
|
||||||
|
rand.Shuffle(len(orgRules), func(i, j int) {
|
||||||
|
orgRules[i], orgRules[j] = orgRules[j], orgRules[i]
|
||||||
|
})
|
||||||
|
for _, rule := range orgRules {
|
||||||
|
if len(rule.NotificationSettings) == 0 || rule.NotificationSettings[0].Receiver == "" || len(rule.NotificationSettings[0].MuteTimeIntervals) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if len(expected) > 0 {
|
||||||
|
if rule.NotificationSettings[0].Receiver == receiver && slices.Contains(rule.NotificationSettings[0].MuteTimeIntervals, timeInterval) {
|
||||||
|
expected = append(expected, rule.GetKey())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
receiver = rule.NotificationSettings[0].Receiver
|
||||||
|
timeInterval = rule.NotificationSettings[0].MuteTimeIntervals[0]
|
||||||
|
expected = append(expected, rule.GetKey())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
actual, err := store.ListNotificationSettings(context.Background(), models.ListNotificationSettingsQuery{
|
||||||
|
OrgID: 1,
|
||||||
|
ReceiverName: receiver,
|
||||||
|
TimeIntervalName: timeInterval,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.EqualValuesf(t, expected, maps.Keys(actual), "got more rules than expected: %#v", actual)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestIntegrationGetNamespacesByRuleUID(t *testing.T) {
|
func TestIntegrationGetNamespacesByRuleUID(t *testing.T) {
|
||||||
|
@ -275,7 +275,7 @@ func (ps *ProvisioningServiceImpl) ProvisionAlerting(ctx context.Context) error
|
|||||||
st, ps.SQLStore, receiverSvc, ps.log, &st)
|
st, ps.SQLStore, receiverSvc, ps.log, &st)
|
||||||
notificationPolicyService := provisioning.NewNotificationPolicyService(&st,
|
notificationPolicyService := provisioning.NewNotificationPolicyService(&st,
|
||||||
st, ps.SQLStore, ps.Cfg.UnifiedAlerting, ps.log)
|
st, ps.SQLStore, ps.Cfg.UnifiedAlerting, ps.log)
|
||||||
mutetimingsService := provisioning.NewMuteTimingService(&st, st, &st, ps.log)
|
mutetimingsService := provisioning.NewMuteTimingService(&st, st, &st, ps.log, &st)
|
||||||
templateService := provisioning.NewTemplateService(&st, st, &st, ps.log)
|
templateService := provisioning.NewTemplateService(&st, st, &st, ps.log)
|
||||||
cfg := prov_alerting.ProvisionerConfig{
|
cfg := prov_alerting.ProvisionerConfig{
|
||||||
Path: alertingPath,
|
Path: alertingPath,
|
||||||
|
@ -240,6 +240,17 @@ type apiClient struct {
|
|||||||
url string
|
url string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type LegacyApiClient struct {
|
||||||
|
apiClient
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAlertingLegacyAPIClient(host, user, pass string) LegacyApiClient {
|
||||||
|
cli := newAlertingApiClient(host, user, pass)
|
||||||
|
return LegacyApiClient{
|
||||||
|
apiClient: cli,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func newAlertingApiClient(host, user, pass string) apiClient {
|
func newAlertingApiClient(host, user, pass string) apiClient {
|
||||||
if len(user) == 0 && len(pass) == 0 {
|
if len(user) == 0 && len(pass) == 0 {
|
||||||
return apiClient{url: fmt.Sprintf("http://%s", host)}
|
return apiClient{url: fmt.Sprintf("http://%s", host)}
|
||||||
|
@ -0,0 +1,47 @@
|
|||||||
|
{
|
||||||
|
"template_files": {},
|
||||||
|
"template_file_provenances": {},
|
||||||
|
"alertmanager_config": {
|
||||||
|
"route": {
|
||||||
|
"receiver": "grafana-default-email",
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"object_matchers": [
|
||||||
|
[
|
||||||
|
"test",
|
||||||
|
"=",
|
||||||
|
"test"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"mute_time_intervals": [
|
||||||
|
"test-interval"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"mute_time_intervals": [
|
||||||
|
{
|
||||||
|
"name": "test-interval",
|
||||||
|
"time_intervals": [
|
||||||
|
{
|
||||||
|
"start_time": "06:00",
|
||||||
|
"end_time": "23:59"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"receivers": [
|
||||||
|
{
|
||||||
|
"name": "grafana-default-email",
|
||||||
|
"grafana_managed_receiver_configs": [
|
||||||
|
{
|
||||||
|
"type": "email",
|
||||||
|
"settings": {
|
||||||
|
"addresses": "<example@email.com>"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"name": "Group1",
|
||||||
|
"interval": "1m",
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"for": "0",
|
||||||
|
"labels": {
|
||||||
|
"label1": "test-label"
|
||||||
|
},
|
||||||
|
"annotations": {
|
||||||
|
"annotation": "test-annotation"
|
||||||
|
},
|
||||||
|
"grafana_alert": {
|
||||||
|
"title": "Rule1",
|
||||||
|
"condition": "A",
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"refId": "A",
|
||||||
|
"datasourceUid": "__expr__",
|
||||||
|
"model": {
|
||||||
|
"expression": "0 > 0",
|
||||||
|
"type": "math"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"no_data_state": "NoData",
|
||||||
|
"exec_err_state": "Alerting",
|
||||||
|
"notification_settings": {
|
||||||
|
"receiver": "grafana-default-email",
|
||||||
|
"mute_time_intervals": [
|
||||||
|
"test-interval"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -2,8 +2,11 @@ package timeinterval
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"embed"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"path"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/prometheus/alertmanager/config"
|
"github.com/prometheus/alertmanager/config"
|
||||||
@ -24,12 +27,16 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
"github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
||||||
"github.com/grafana/grafana/pkg/services/ngalert/store"
|
"github.com/grafana/grafana/pkg/services/ngalert/store"
|
||||||
"github.com/grafana/grafana/pkg/services/org"
|
"github.com/grafana/grafana/pkg/services/org"
|
||||||
|
"github.com/grafana/grafana/pkg/tests/api/alerting"
|
||||||
"github.com/grafana/grafana/pkg/tests/apis"
|
"github.com/grafana/grafana/pkg/tests/apis"
|
||||||
"github.com/grafana/grafana/pkg/tests/testinfra"
|
"github.com/grafana/grafana/pkg/tests/testinfra"
|
||||||
"github.com/grafana/grafana/pkg/tests/testsuite"
|
"github.com/grafana/grafana/pkg/tests/testsuite"
|
||||||
"github.com/grafana/grafana/pkg/util"
|
"github.com/grafana/grafana/pkg/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
//go:embed test-data/*.*
|
||||||
|
var testData embed.FS
|
||||||
|
|
||||||
func TestMain(m *testing.M) {
|
func TestMain(m *testing.M) {
|
||||||
testsuite.Run(m)
|
testsuite.Run(m)
|
||||||
}
|
}
|
||||||
@ -626,3 +633,58 @@ func TestIntegrationTimeIntervalListSelector(t *testing.T) {
|
|||||||
require.Empty(t, list.Items)
|
require.Empty(t, list.Items)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestIntegrationTimeIntervalReferentialIntegrity(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("skipping integration test")
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
helper := getTestHelper(t)
|
||||||
|
|
||||||
|
cliCfg := helper.Org1.Admin.NewRestConfig()
|
||||||
|
legacyCli := alerting.NewAlertingLegacyAPIClient(helper.GetEnv().Server.HTTPServer.Listener.Addr().String(), cliCfg.Username, cliCfg.Password)
|
||||||
|
|
||||||
|
// Prepare environment and create notification policy and rule that use time interval
|
||||||
|
alertmanagerRaw, err := testData.ReadFile(path.Join("test-data", "notification-settings.json"))
|
||||||
|
require.NoError(t, err)
|
||||||
|
var amConfig definitions.PostableUserConfig
|
||||||
|
require.NoError(t, json.Unmarshal(alertmanagerRaw, &amConfig))
|
||||||
|
|
||||||
|
success, err := legacyCli.PostConfiguration(t, amConfig)
|
||||||
|
require.Truef(t, success, "Failed to post Alertmanager configuration: %s", err)
|
||||||
|
|
||||||
|
postGroupRaw, err := testData.ReadFile(path.Join("test-data", "rulegroup-1.json"))
|
||||||
|
require.NoError(t, err)
|
||||||
|
var ruleGroup definitions.PostableRuleGroupConfig
|
||||||
|
require.NoError(t, json.Unmarshal(postGroupRaw, &ruleGroup))
|
||||||
|
|
||||||
|
folderUID := "test-folder"
|
||||||
|
legacyCli.CreateFolder(t, folderUID, "TEST")
|
||||||
|
_, status, data := legacyCli.PostRulesGroupWithStatus(t, folderUID, &ruleGroup)
|
||||||
|
require.Equalf(t, http.StatusAccepted, status, "Failed to post Rule: %s", data)
|
||||||
|
|
||||||
|
adminK8sClient, err := versioned.NewForConfig(cliCfg)
|
||||||
|
require.NoError(t, err)
|
||||||
|
adminClient := adminK8sClient.NotificationsV0alpha1().TimeIntervals("default")
|
||||||
|
|
||||||
|
intervals, err := adminClient.List(ctx, v1.ListOptions{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, intervals.Items, 1)
|
||||||
|
|
||||||
|
intervalToDelete := intervals.Items[0]
|
||||||
|
|
||||||
|
t.Run("should fail to delete if time interval is used in rule and routes", func(t *testing.T) {
|
||||||
|
err := adminClient.Delete(ctx, intervalToDelete.Name, v1.DeleteOptions{})
|
||||||
|
require.Truef(t, errors.IsConflict(err), "Expected Conflict, got: %s", err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should fail to delete if time interval is used in only rule", func(t *testing.T) {
|
||||||
|
amConfig.AlertmanagerConfig.Route.Routes[0].MuteTimeIntervals = nil
|
||||||
|
success, err := legacyCli.PostConfiguration(t, amConfig)
|
||||||
|
require.Truef(t, success, "Failed to post Alertmanager configuration: %s", err)
|
||||||
|
|
||||||
|
err = adminClient.Delete(ctx, intervalToDelete.Name, v1.DeleteOptions{})
|
||||||
|
require.Truef(t, errors.IsConflict(err), "Expected Conflict, got: %s", err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user