diff --git a/db/notifications.go b/db/notifications.go index 75782dee..47c1c13d 100644 --- a/db/notifications.go +++ b/db/notifications.go @@ -125,14 +125,12 @@ func NotificationSendSummary(ctx context.Context, id string, window time.Duratio return earliest.Time, count, err } -func GetMatchingNotificationSilencesCount(ctx context.Context, resources models.NotificationSilenceResource) (int64, error) { +func GetMatchingNotificationSilences(ctx context.Context, resources models.NotificationSilenceResource) ([]models.NotificationSilence, error) { _ = ctx.DB().Use(extraClausePlugin.New()) query := ctx.DB().Model(&models.NotificationSilence{}) - // Initialize with a false condition, - // if no resources are provided, the query won't return all records - orClauses := ctx.DB().Where("1 = 0") + orClauses := ctx.DB().Where("filter != ''") if resources.ConfigID != nil { orClauses = orClauses.Or("config_id = ?", *resources.ConfigID) @@ -168,13 +166,13 @@ func GetMatchingNotificationSilencesCount(ctx context.Context, resources models. query = query.Where(orClauses) - var count int64 - err := query.Count(&count).Where(`"from" <= NOW()`).Where("until >= NOW()").Where("deleted_at IS NULL").Error + var silences []models.NotificationSilence + err := query.Where(`"from" <= NOW()`).Where("until >= NOW()").Where("deleted_at IS NULL").Find(&silences).Error if err != nil { - return 0, err + return nil, err } - return count, nil + return silences, nil } func SaveUnsentNotificationToHistory(ctx context.Context, sendHistory models.NotificationSendHistory, window time.Duration) error { diff --git a/db/notifications_test.go b/db/notifications_test.go index b2862884..d5aac062 100644 --- a/db/notifications_test.go +++ b/db/notifications_test.go @@ -52,55 +52,55 @@ var _ = ginkgo.Describe("Notification Silence", ginkgo.Ordered, func() { ginkgo.Context("non recursive match", func() { ginkgo.It("should match", func() { - matched, err := GetMatchingNotificationSilencesCount(DefaultContext, models.NotificationSilenceResource{ConfigID: lo.ToPtr(dummy.EKSCluster.ID.String())}) + matched, err := GetMatchingNotificationSilences(DefaultContext, models.NotificationSilenceResource{ConfigID: lo.ToPtr(dummy.EKSCluster.ID.String())}) Expect(err).To(BeNil()) - Expect(matched).To(Equal(int64(1))) + Expect(len(matched)).To(Equal(int64(1))) }) ginkgo.It("should not match", func() { - matched, err := GetMatchingNotificationSilencesCount(DefaultContext, models.NotificationSilenceResource{ConfigID: lo.ToPtr(dummy.KubernetesCluster.ID.String())}) + matched, err := GetMatchingNotificationSilences(DefaultContext, models.NotificationSilenceResource{ConfigID: lo.ToPtr(dummy.KubernetesCluster.ID.String())}) Expect(err).To(BeNil()) - Expect(matched).To(Equal(int64(0))) + Expect(len(matched)).To(Equal(int64(0))) }) }) ginkgo.Context("config recursive match", func() { ginkgo.It("should match a child", func() { - matched, err := GetMatchingNotificationSilencesCount(DefaultContext, models.NotificationSilenceResource{ConfigID: lo.ToPtr(dummy.LogisticsAPIReplicaSet.ID.String())}) + matched, err := GetMatchingNotificationSilences(DefaultContext, models.NotificationSilenceResource{ConfigID: lo.ToPtr(dummy.LogisticsAPIReplicaSet.ID.String())}) Expect(err).To(BeNil()) - Expect(matched).To(Equal(int64(1))) + Expect(len(matched)).To(Equal(int64(1))) }) ginkgo.It("should match a grand child", func() { - matched, err := GetMatchingNotificationSilencesCount(DefaultContext, models.NotificationSilenceResource{ConfigID: lo.ToPtr(dummy.LogisticsAPIPodConfig.ID.String())}) + matched, err := GetMatchingNotificationSilences(DefaultContext, models.NotificationSilenceResource{ConfigID: lo.ToPtr(dummy.LogisticsAPIPodConfig.ID.String())}) Expect(err).To(BeNil()) - Expect(matched).To(Equal(int64(1))) + Expect(len(matched)).To(Equal(int64(1))) }) ginkgo.It("should not match", func() { - matched, err := GetMatchingNotificationSilencesCount(DefaultContext, models.NotificationSilenceResource{ConfigID: lo.ToPtr(dummy.LogisticsUIDeployment.ID.String())}) + matched, err := GetMatchingNotificationSilences(DefaultContext, models.NotificationSilenceResource{ConfigID: lo.ToPtr(dummy.LogisticsUIDeployment.ID.String())}) Expect(err).To(BeNil()) - Expect(matched).To(Equal(int64(0))) + Expect(len(matched)).To(Equal(int64(0))) }) }) ginkgo.Context("component recursive match", func() { ginkgo.It("should match a child", func() { - matched, err := GetMatchingNotificationSilencesCount(DefaultContext, models.NotificationSilenceResource{ComponentID: lo.ToPtr(dummy.LogisticsAPI.ID.String())}) + matched, err := GetMatchingNotificationSilences(DefaultContext, models.NotificationSilenceResource{ComponentID: lo.ToPtr(dummy.LogisticsAPI.ID.String())}) Expect(err).To(BeNil()) - Expect(matched).To(Equal(int64(1))) + Expect(len(matched)).To(Equal(int64(1))) }) ginkgo.It("should match a grand child", func() { - matched, err := GetMatchingNotificationSilencesCount(DefaultContext, models.NotificationSilenceResource{ComponentID: lo.ToPtr(dummy.LogisticsWorker.ID.String())}) + matched, err := GetMatchingNotificationSilences(DefaultContext, models.NotificationSilenceResource{ComponentID: lo.ToPtr(dummy.LogisticsWorker.ID.String())}) Expect(err).To(BeNil()) - Expect(matched).To(Equal(int64(1))) + Expect(len(matched)).To(Equal(int64(1))) }) ginkgo.It("should not match", func() { - matched, err := GetMatchingNotificationSilencesCount(DefaultContext, models.NotificationSilenceResource{ComponentID: lo.ToPtr(dummy.ClusterComponent.ID.String())}) + matched, err := GetMatchingNotificationSilences(DefaultContext, models.NotificationSilenceResource{ComponentID: lo.ToPtr(dummy.ClusterComponent.ID.String())}) Expect(err).To(BeNil()) - Expect(matched).To(Equal(int64(0))) + Expect(len(matched)).To(Equal(int64(0))) }) }) }) diff --git a/notification/events.go b/notification/events.go index f03fe717..48486310 100644 --- a/notification/events.go +++ b/notification/events.go @@ -133,7 +133,7 @@ func (t *notificationHandler) addNotificationEvent(ctx context.Context, event mo t.Ring.Add(event, celEnv.AsMap()) silencedResource := getSilencedResourceFromCelEnv(celEnv) - matchingSilences, err := db.GetMatchingNotificationSilencesCount(ctx, silencedResource) + matchingSilences, err := db.GetMatchingNotificationSilences(ctx, silencedResource) if err != nil { return err } @@ -147,7 +147,7 @@ func (t *notificationHandler) addNotificationEvent(ctx context.Context, event mo return nil } -func addNotificationEvent(ctx context.Context, id string, celEnv map[string]any, event models.Event, matchingSilences int64) error { +func addNotificationEvent(ctx context.Context, id string, celEnv map[string]any, event models.Event, matchingSilences []models.NotificationSilence) error { n, err := GetNotification(ctx, id) if err != nil { return fmt.Errorf("failed to get notification %s: %w", id, err) @@ -191,7 +191,9 @@ func addNotificationEvent(ctx context.Context, id string, celEnv map[string]any, } for _, payload := range payloads { - if matchingSilences > 0 { + if ok, err := shouldSilence(celEnv, matchingSilences); err != nil { + return err + } else if ok { ctx.Logger.V(6).Infof("silencing notification for event %s due to %d matching silences", event.ID, matchingSilences) ctx.Counter("notification_silenced", "id", id, "resource", payload.ID.String()).Add(1) @@ -527,7 +529,12 @@ func GetEnvForEvent(ctx context.Context, event models.Event) (*celVariables, err } eventSuffix := strings.TrimPrefix(event.Name, "config.") - isStateUpdateEvent := slices.Contains([]string{api.EventConfigCreated, api.EventConfigChanged, api.EventConfigUpdated, api.EventConfigDeleted}, event.Name) + isStateUpdateEvent := slices.Contains([]string{ + api.EventConfigChanged, + api.EventConfigCreated, + api.EventConfigDeleted, + api.EventConfigUpdated, + }, event.Name) if isStateUpdateEvent { env.NewState = eventSuffix } else { @@ -544,3 +551,19 @@ func GetEnvForEvent(ctx context.Context, event models.Event) (*celVariables, err env.SetSilenceURL(api.FrontendURL) return &env, nil } + +func shouldSilence(celEnv map[string]any, matchingSilences []models.NotificationSilence) (bool, error) { + for _, silence := range matchingSilences { + if silence.Filter != "" { + if ok, err := gomplate.RunTemplateBool(celEnv, gomplate.Template{Expression: string(silence.Filter)}); err != nil { + return false, fmt.Errorf("failed to run filter expression(%s): %w", silence.Filter, err) + } else if !ok { + continue + } + } + + return true, nil + } + + return false, nil +} diff --git a/notification/silence.go b/notification/silence.go index 2ed41669..45486900 100644 --- a/notification/silence.go +++ b/notification/silence.go @@ -8,16 +8,18 @@ import ( "github.com/flanksource/duty/context" "github.com/flanksource/duty/db" "github.com/flanksource/duty/models" + "github.com/flanksource/duty/types" "github.com/samber/lo" "github.com/timberio/go-datemath" ) type SilenceSaveRequest struct { models.NotificationSilenceResource - From string `json:"from"` - Until string `json:"until"` - Description string `json:"description"` - Recursive bool `json:"recursive"` + From string `json:"from"` + Until string `json:"until"` + Description string `json:"description"` + Recursive bool `json:"recursive"` + Filter types.CelExpression `json:"filter"` from time.Time until time.Time @@ -63,6 +65,7 @@ func SaveNotificationSilence(ctx context.Context, req SilenceSaveRequest) error silence := models.NotificationSilence{ NotificationSilenceResource: req.NotificationSilenceResource, From: req.from, + Filter: req.Filter, Until: req.until, Description: req.Description, Recursive: req.Recursive,