Alerting: Allow specifying uid for new rules added to groups (#99858)

When modifying rule groups the `uid` can be specified but only if the rule already existed in the DB. If the rule is new the update would be rejected.

This updates the RuleGroup provisioning apis to allow specifying the `uid` when creating/updating rule groups.

Additionally, the RuleGroupIdx was not being updated when rules were reordered in the group.

Context: https://github.com/grafana/terraform-provider-grafana/pull/1971#issuecomment-2599223897
Relates to: https://github.com/grafana/terraform-provider-grafana/issues/1928

Fixes: #98283
(cherry picked from commit 7dee4d1808)
This commit is contained in:
Moustafa Baiou
2025-02-12 09:10:39 -05:00
committed by Moustafa Baiou
parent 2728e5cf14
commit fb0d6be79e
8 changed files with 463 additions and 42 deletions
@@ -693,6 +693,7 @@ func TestCheckMuteTimes(t *testing.T) {
}
func gettableMuteIntervals(t *testing.T, muteTimeIntervals []amConfig.MuteTimeInterval, provenances map[string]definitions.Provenance) definitions.GettableUserConfig {
t.Helper()
return definitions.GettableUserConfig{
AlertmanagerConfig: definitions.GettableApiAlertingConfig{
MuteTimeProvenances: provenances,
@@ -24,6 +24,7 @@ import (
"github.com/grafana/grafana/pkg/services/folder"
"github.com/grafana/grafana/pkg/services/quota"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
"github.com/grafana/grafana/pkg/util/cmputil"
)
@@ -549,6 +550,9 @@ func (alertRule *AlertRule) PreSave(timeNow func() time.Time) error {
// ValidateAlertRule validates various alert rule fields.
func (alertRule *AlertRule) ValidateAlertRule(cfg setting.UnifiedAlertingSettings) error {
if err := util.ValidateUID(alertRule.UID); err != nil {
return errors.Join(ErrAlertRuleFailedValidation, fmt.Errorf("cannot create rule with UID '%s': %w", alertRule.UID, err))
}
if len(alertRule.Data) == 0 {
return fmt.Errorf("%w: no queries or expressions are found", ErrAlertRuleFailedValidation)
}
@@ -368,6 +368,16 @@ func (service *AlertRuleService) ReplaceRuleGroup(ctx context.Context, user iden
return err
}
for _, rule := range group.Rules {
if rule.UID == "" {
// if empty the UID will be generated before save
continue
}
if err := util.ValidateUID(rule.UID); err != nil {
return fmt.Errorf("%w: cannot create rule with UID %q: %w", models.ErrAlertRuleFailedValidation, rule.UID, err)
}
}
delta, err := service.calcDelta(ctx, user, group)
if err != nil {
return err
@@ -575,6 +585,10 @@ func (service *AlertRuleService) UpdateAlertRule(ctx context.Context, user ident
// No changes to the rule.
return rule, nil
}
// new rules not allowed in update for a single rule
if len(delta.New) > 0 {
return models.AlertRule{}, fmt.Errorf("failed to update rule with UID %s because %w", rule.UID, models.ErrAlertRuleNotFound)
}
for _, d := range delta.Update {
if d.Existing.GetKey() == rule.GetKey() {
storedRule = d.Existing
@@ -817,6 +831,7 @@ func syncGroupRuleFields(group *models.AlertRuleGroup, orgID int64) *models.Aler
group.Rules[i].RuleGroup = group.Title
group.Rules[i].NamespaceUID = group.FolderUID
group.Rules[i].OrgID = orgID
group.Rules[i].RuleGroupIndex = i
}
return group
}
+8 -13
View File
@@ -113,10 +113,9 @@ func calculateChanges(ctx context.Context, ruleReader RuleReader, groupKey model
}
loadedRulesByUID[rule.UID] = rule
}
if existing == nil {
return nil, fmt.Errorf("failed to update rule with UID %s because %w", r.UID, models.ErrAlertRuleNotFound)
if existing != nil {
affectedGroups[existing.GetGroupKey()] = ruleList
}
affectedGroups[existing.GetGroupKey()] = ruleList
}
}
@@ -126,18 +125,14 @@ func calculateChanges(ctx context.Context, ruleReader RuleReader, groupKey model
}
models.PatchPartialAlertRule(existing, r)
diff := existing.Diff(&r.AlertRule, AlertRuleFieldsToIgnoreInDiff[:]...)
if len(diff) == 0 {
continue
if len(diff) > 0 {
toUpdate = append(toUpdate, RuleDelta{
Existing: existing,
New: &r.AlertRule,
Diff: diff,
})
}
toUpdate = append(toUpdate, RuleDelta{
Existing: existing,
New: &r.AlertRule,
Diff: diff,
})
continue
}
toDelete := make([]*models.AlertRule, 0, len(existingGroupRulesUIDs))
+8 -3
View File
@@ -240,14 +240,19 @@ func TestCalculateChanges(t *testing.T) {
require.Len(t, changes.AffectedGroups[sourceGroupKey], len(inDatabase))
})
t.Run("should fail when submitted rule has UID that does not exist in db", func(t *testing.T) {
t.Run("should add rule when submitted rule has UID that does not exist in db", func(t *testing.T) {
fakeStore := fakes.NewRuleStore(t)
groupKey := models.GenerateGroupKey(orgId)
submitted := gen.With(gen.WithOrgID(orgId), simulateSubmitted).Generate()
require.NotEqual(t, "", submitted.UID)
_, err := CalculateChanges(context.Background(), fakeStore, groupKey, []*models.AlertRuleWithOptionals{{AlertRule: submitted}})
require.Error(t, err)
diff, err := CalculateChanges(context.Background(), fakeStore, groupKey, []*models.AlertRuleWithOptionals{{AlertRule: submitted}})
require.NoError(t, err)
require.Len(t, diff.New, 1)
require.Empty(t, diff.Delete)
require.Empty(t, diff.Update)
require.Equal(t, submitted, *diff.New[0])
})
t.Run("should fail if cannot fetch current rules in the group", func(t *testing.T) {