package provisioning import ( "context" "encoding/binary" "fmt" "hash/fnv" "slices" "strings" "unsafe" "github.com/prometheus/alertmanager/config" "github.com/prometheus/alertmanager/timeinterval" "golang.org/x/exp/maps" "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/notifier/legacy_storage" "github.com/grafana/grafana/pkg/services/ngalert/provisioning/validation" ) type MuteTimingService struct { configStore alertmanagerConfigStore provenanceStore ProvisioningStore xact TransactionManager log log.Logger validator validation.ProvenanceStatusTransitionValidator ruleNotificationsStore AlertRuleNotificationSettingsStore } func NewMuteTimingService(config alertmanagerConfigStore, prov ProvisioningStore, xact TransactionManager, log log.Logger, ns AlertRuleNotificationSettingsStore) *MuteTimingService { return &MuteTimingService{ configStore: config, provenanceStore: prov, xact: xact, log: log, validator: validation.ValidateProvenanceRelaxed, ruleNotificationsStore: ns, } } // 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 := svc.configStore.Get(ctx, orgID) if err != nil { return nil, err } intervals := getTimeIntervals(rev) if len(intervals) == 0 { return []definitions.MuteTimeInterval{}, nil } provenances, err := svc.provenanceStore.GetProvenances(ctx, orgID, (&definitions.MuteTimeInterval{}).ResourceType()) if err != nil { return nil, err } slices.SortFunc(intervals, func(a, b config.MuteTimeInterval) int { return strings.Compare(a.Name, b.Name) }) result := make([]definitions.MuteTimeInterval, 0, len(intervals)) for _, interval := range intervals { version := calculateMuteTimeIntervalFingerprint(interval) def := definitions.MuteTimeInterval{ UID: legacy_storage.NameToUid(interval.Name), MuteTimeInterval: interval, Version: version, } if prov, ok := provenances[def.ResourceID()]; ok { def.Provenance = definitions.Provenance(prov) } result = append(result, def) } return result, nil } // GetMuteTiming returns a mute timing by name func (svc *MuteTimingService) GetMuteTiming(ctx context.Context, nameOrUID string, orgID int64) (definitions.MuteTimeInterval, error) { rev, err := svc.configStore.Get(ctx, orgID) if err != nil { return definitions.MuteTimeInterval{}, err } mt, found := getMuteTimingByName(rev, nameOrUID) if !found { name, err := legacy_storage.UidToName(nameOrUID) if err == nil { mt, found = getMuteTimingByName(rev, name) } } if !found { return definitions.MuteTimeInterval{}, ErrTimeIntervalNotFound.Errorf("") } result := definitions.MuteTimeInterval{ UID: legacy_storage.NameToUid(mt.Name), MuteTimeInterval: mt, Version: calculateMuteTimeIntervalFingerprint(mt), } prov, err := svc.provenanceStore.GetProvenance(ctx, &result, orgID) if err != nil { return definitions.MuteTimeInterval{}, err } result.Provenance = definitions.Provenance(prov) return result, nil } // CreateMuteTiming adds a new mute timing within the specified org. The created mute timing is returned. func (svc *MuteTimingService) CreateMuteTiming(ctx context.Context, mt definitions.MuteTimeInterval, orgID int64) (definitions.MuteTimeInterval, error) { if err := mt.Validate(); err != nil { return definitions.MuteTimeInterval{}, MakeErrTimeIntervalInvalid(err) } revision, err := svc.configStore.Get(ctx, orgID) if err != nil { return definitions.MuteTimeInterval{}, err } _, found := getMuteTimingByName(revision, mt.Name) if found { return definitions.MuteTimeInterval{}, ErrTimeIntervalExists.Errorf("") } revision.Config.AlertmanagerConfig.TimeIntervals = append(revision.Config.AlertmanagerConfig.TimeIntervals, config.TimeInterval(mt.MuteTimeInterval)) err = svc.xact.InTransaction(ctx, func(ctx context.Context) error { if err := svc.configStore.Save(ctx, revision, orgID); err != nil { return err } return svc.provenanceStore.SetProvenance(ctx, &mt, orgID, models.Provenance(mt.Provenance)) }) if err != nil { return definitions.MuteTimeInterval{}, err } return definitions.MuteTimeInterval{ UID: legacy_storage.NameToUid(mt.Name), MuteTimeInterval: mt.MuteTimeInterval, Version: calculateMuteTimeIntervalFingerprint(mt.MuteTimeInterval), Provenance: mt.Provenance, }, nil } // UpdateMuteTiming replaces an existing mute timing within the specified org. The replaced mute timing is returned. If the mute timing does not exist, ErrMuteTimingsNotFound is returned. func (svc *MuteTimingService) UpdateMuteTiming(ctx context.Context, mt definitions.MuteTimeInterval, orgID int64) (definitions.MuteTimeInterval, error) { if err := mt.Validate(); err != nil { return definitions.MuteTimeInterval{}, MakeErrTimeIntervalInvalid(err) } revision, err := svc.configStore.Get(ctx, orgID) if err != nil { return definitions.MuteTimeInterval{}, err } var old config.MuteTimeInterval found := false if mt.UID != "" { name, err := legacy_storage.UidToName(mt.UID) if err == nil { old, found = getMuteTimingByName(revision, name) } } else { old, found = getMuteTimingByName(revision, mt.Name) } if !found { return definitions.MuteTimeInterval{}, ErrTimeIntervalNotFound.Errorf("") } // check optimistic concurrency err = svc.checkOptimisticConcurrency(old, models.Provenance(mt.Provenance), mt.Version, "update") if err != nil { return definitions.MuteTimeInterval{}, err } // check that provenance is not changed in an invalid way storedProvenance, err := svc.provenanceStore.GetProvenance(ctx, &definitions.MuteTimeInterval{MuteTimeInterval: old}, orgID) if err != nil { return definitions.MuteTimeInterval{}, err } if err := svc.validator(storedProvenance, models.Provenance(mt.Provenance)); err != nil { return definitions.MuteTimeInterval{}, err } // TODO add diff and noop detection err = svc.xact.InTransaction(ctx, func(ctx context.Context) error { // if the name of the time interval changed if old.Name != mt.Name { deleteTimeInterval(revision, old) revision.Config.AlertmanagerConfig.TimeIntervals = append(revision.Config.AlertmanagerConfig.TimeIntervals, config.TimeInterval(mt.MuteTimeInterval)) err = svc.renameTimeIntervalInDependentResources(ctx, orgID, revision.Config.AlertmanagerConfig.Route, old.Name, mt.Name, models.Provenance(mt.Provenance)) if err != nil { return err } err = svc.provenanceStore.DeleteProvenance(ctx, &definitions.MuteTimeInterval{MuteTimeInterval: old}, orgID) if err != nil { return err } } else { updateTimeInterval(revision, mt.MuteTimeInterval) } if err := svc.configStore.Save(ctx, revision, orgID); err != nil { return err } return svc.provenanceStore.SetProvenance(ctx, &mt, orgID, models.Provenance(mt.Provenance)) }) if err != nil { return definitions.MuteTimeInterval{}, err } return definitions.MuteTimeInterval{ UID: legacy_storage.NameToUid(mt.Name), MuteTimeInterval: mt.MuteTimeInterval, Version: calculateMuteTimeIntervalFingerprint(mt.MuteTimeInterval), Provenance: mt.Provenance, }, 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, nameOrUID string, orgID int64, provenance definitions.Provenance, version string) error { revision, err := svc.configStore.Get(ctx, orgID) if err != nil { return err } existing, found := getMuteTimingByName(revision, nameOrUID) if !found { name, err := legacy_storage.UidToName(nameOrUID) if err == nil { existing, found = getMuteTimingByName(revision, name) } } if !found { svc.log.FromContext(ctx).Debug("Time interval was not found. Skip deleting", "name", nameOrUID) return nil } target := definitions.MuteTimeInterval{MuteTimeInterval: existing, Provenance: provenance} // check that provenance is not changed in an invalid way storedProvenance, err := svc.provenanceStore.GetProvenance(ctx, &target, orgID) if err != nil { return err } if err := svc.validator(storedProvenance, models.Provenance(provenance)); err != nil { return err } if isMuteTimeInUseInRoutes(existing.Name, revision.Config.AlertmanagerConfig.Route) { ns, _ := svc.ruleNotificationsStore.ListNotificationSettings(ctx, models.ListNotificationSettingsQuery{OrgID: orgID, TimeIntervalName: existing.Name}) // ignore error here because it's not important return MakeErrTimeIntervalInUse(true, maps.Keys(ns)) } err = svc.checkOptimisticConcurrency(existing, models.Provenance(provenance), version, "delete") if err != nil { return err } deleteTimeInterval(revision, existing) 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 { return err } return svc.provenanceStore.DeleteProvenance(ctx, &target, orgID) }) } func isMuteTimeInUseInRoutes(name string, route *definitions.Route) bool { if route == nil { return false } if slices.Contains(route.MuteTimeIntervals, name) { return true } for _, route := range route.Routes { if isMuteTimeInUseInRoutes(name, route) { return true } } return false } func getMuteTimingByName(rev *legacy_storage.ConfigRevision, name string) (config.MuteTimeInterval, bool) { intervals := getTimeIntervals(rev) idx := slices.IndexFunc(intervals, func(interval config.MuteTimeInterval) bool { return interval.Name == name }) if idx == -1 { return config.MuteTimeInterval{}, false } return intervals[idx], true } func getTimeIntervals(rev *legacy_storage.ConfigRevision) []config.MuteTimeInterval { result := make([]config.MuteTimeInterval, 0, len(rev.Config.AlertmanagerConfig.TimeIntervals)+len(rev.Config.AlertmanagerConfig.MuteTimeIntervals)) for _, interval := range rev.Config.AlertmanagerConfig.TimeIntervals { result = append(result, config.MuteTimeInterval(interval)) } return append(result, rev.Config.AlertmanagerConfig.MuteTimeIntervals...) } func updateTimeInterval(rev *legacy_storage.ConfigRevision, interval config.MuteTimeInterval) { for idx := range rev.Config.AlertmanagerConfig.MuteTimeIntervals { if rev.Config.AlertmanagerConfig.MuteTimeIntervals[idx].Name == interval.Name { rev.Config.AlertmanagerConfig.MuteTimeIntervals[idx] = interval return } } for idx := range rev.Config.AlertmanagerConfig.TimeIntervals { if rev.Config.AlertmanagerConfig.TimeIntervals[idx].Name == interval.Name { rev.Config.AlertmanagerConfig.TimeIntervals[idx] = config.TimeInterval(interval) return } } } func deleteTimeInterval(rev *legacy_storage.ConfigRevision, interval config.MuteTimeInterval) { rev.Config.AlertmanagerConfig.MuteTimeIntervals = slices.DeleteFunc(rev.Config.AlertmanagerConfig.MuteTimeIntervals, func(i config.MuteTimeInterval) bool { return i.Name == interval.Name }) rev.Config.AlertmanagerConfig.TimeIntervals = slices.DeleteFunc(rev.Config.AlertmanagerConfig.TimeIntervals, func(i config.TimeInterval) bool { return i.Name == interval.Name }) } func calculateMuteTimeIntervalFingerprint(interval config.MuteTimeInterval) string { sum := fnv.New64() writeBytes := func(b []byte) { _, _ = sum.Write(b) // add a byte sequence that cannot happen in UTF-8 strings. _, _ = sum.Write([]byte{255}) } writeString := func(s string) { if len(s) == 0 { writeBytes(nil) return } // #nosec G103 // avoid allocation when converting string to byte slice writeBytes(unsafe.Slice(unsafe.StringData(s), len(s))) } // this temp slice is used to convert ints to bytes. tmp := make([]byte, 8) writeInt := func(u int) { binary.LittleEndian.PutUint64(tmp, uint64(u)) writeBytes(tmp) } writeRange := func(r timeinterval.InclusiveRange) { writeInt(r.Begin) writeInt(r.End) } // fields that determine the rule state writeString(interval.Name) for _, ti := range interval.TimeIntervals { for _, time := range ti.Times { writeInt(time.StartMinute) writeInt(time.EndMinute) } for _, itm := range ti.Months { writeRange(itm.InclusiveRange) } for _, itm := range ti.DaysOfMonth { writeRange(itm.InclusiveRange) } for _, itm := range ti.Weekdays { writeRange(itm.InclusiveRange) } for _, itm := range ti.Years { writeRange(itm.InclusiveRange) } if ti.Location != nil { writeString(ti.Location.String()) } } return fmt.Sprintf("%016x", sum.Sum64()) } func (svc *MuteTimingService) checkOptimisticConcurrency(current config.MuteTimeInterval, provenance models.Provenance, desiredVersion string, action string) error { if desiredVersion == "" { if provenance != models.ProvenanceFile { // if version is not specified and it's not a file provisioning, emit a log message to reflect that optimistic concurrency is disabled for this request svc.log.Debug("ignoring optimistic concurrency check because version was not provided", "timeInterval", current.Name, "operation", action) } return nil } currentVersion := calculateMuteTimeIntervalFingerprint(current) if currentVersion != desiredVersion { return ErrVersionConflict.Errorf("provided version %s of time interval %s does not match current version %s", desiredVersion, current.Name, currentVersion) } return nil } func (svc *MuteTimingService) renameTimeIntervalInDependentResources(ctx context.Context, orgID int64, route *definitions.Route, oldName, newName string, timeIntervalProvenance models.Provenance) error { validate := validation.ValidateProvenanceOfDependentResources(timeIntervalProvenance) // if there are no references to the old time interval, exit updatedRoutes := replaceMuteTiming(route, oldName, newName) canUpdate := true if updatedRoutes > 0 { routeProvenance, err := svc.provenanceStore.GetProvenance(ctx, route, orgID) if err != nil { return err } canUpdate = validate(routeProvenance) } dryRun := !canUpdate affected, invalidProvenance, err := svc.ruleNotificationsStore.RenameTimeIntervalInNotificationSettings(ctx, orgID, oldName, newName, validate, dryRun) if err != nil { return err } if !canUpdate || len(invalidProvenance) > 0 { return MakeErrTimeIntervalDependentResourcesProvenance(updatedRoutes > 0, invalidProvenance) } if len(affected) > 0 || updatedRoutes > 0 { svc.log.FromContext(ctx).Info("Updated rules and routes that use renamed time interval", "oldName", oldName, "newName", newName, "rules", len(affected), "routes", updatedRoutes) } return nil } func replaceMuteTiming(route *definitions.Route, oldName, newName string) int { if route == nil { return 0 } updated := 0 for idx := range route.MuteTimeIntervals { if route.MuteTimeIntervals[idx] == oldName { route.MuteTimeIntervals[idx] = newName updated++ } } for _, route := range route.Routes { updated += replaceMuteTiming(route, oldName, newName) } return updated }