Alerting: Extend PUT rule-group route to write the entire rule group rather than top-level fields only (#53078)

* Wire up to full alert rule struct

* Extract group change detection logic to dedicated file

* GroupDiff -> GroupDelta for consistency

* Calculate deltas and handle backwards compatible requests

* Separate changes and insert/update/delete as needed

* Regenerate files

* Don't touch the DB if there are no changes

* Quota checking, delete unused file

* Mark modified records as provisioned

* Validation + a couple API layer tests

* Address linter errors

* Fix issue with UID assignment and rule creation

* Propagate top level group fields to all rules

* Tests for repeated updates and versioning

* Tests for quota and provenance checks

* Fix linter errors

* Regenerate

* Factor out some shared logic

* Drop unnecessary multiple nilchecks

* Use alternative strategy for rolling UIDs on inserted rules

* Fix tests, add back nilcheck, refresh UIDs during test

* Address feedback

* Add missing nil-check
This commit is contained in:
Alexander Weaver
2022-08-10 12:33:41 -05:00
committed by GitHub
parent dc23643bee
commit b198559225
12 changed files with 374 additions and 41 deletions

View File

@@ -7,6 +7,7 @@ import (
"time"
"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/store"
"github.com/grafana/grafana/pkg/services/sqlstore"
@@ -35,6 +36,22 @@ func TestAlertRuleService(t *testing.T) {
require.Equal(t, models.ProvenanceAPI, provenance)
})
t.Run("group creation should set the right provenance", func(t *testing.T) {
var orgID int64 = 1
group := createDummyGroup("group-test-1", orgID)
err := ruleService.ReplaceRuleGroup(context.Background(), orgID, group, 0, models.ProvenanceAPI)
require.NoError(t, err)
readGroup, err := ruleService.GetRuleGroup(context.Background(), orgID, "my-namespace", "group-test-1")
require.NoError(t, err)
require.NotEmpty(t, readGroup.Rules)
for _, rule := range readGroup.Rules {
_, provenance, err := ruleService.GetAlertRule(context.Background(), orgID, rule.UID)
require.NoError(t, err)
require.Equal(t, models.ProvenanceAPI, provenance)
}
})
t.Run("alert rule group should be updated correctly", func(t *testing.T) {
var orgID int64 = 1
rule := dummyRule("test#3", orgID)
@@ -52,6 +69,22 @@ func TestAlertRuleService(t *testing.T) {
require.Equal(t, interval, rule.IntervalSeconds)
})
t.Run("group creation should propagate group title correctly", func(t *testing.T) {
var orgID int64 = 1
group := createDummyGroup("group-test-3", orgID)
group.Rules[0].RuleGroup = "something different"
err := ruleService.ReplaceRuleGroup(context.Background(), orgID, group, 0, models.ProvenanceAPI)
require.NoError(t, err)
readGroup, err := ruleService.GetRuleGroup(context.Background(), orgID, "my-namespace", "group-test-3")
require.NoError(t, err)
require.NotEmpty(t, readGroup.Rules)
for _, rule := range readGroup.Rules {
require.Equal(t, "group-test-3", rule.RuleGroup)
}
})
t.Run("alert rule should get interval from existing rule group", func(t *testing.T) {
var orgID int64 = 1
rule := dummyRule("test#4", orgID)
@@ -70,7 +103,7 @@ func TestAlertRuleService(t *testing.T) {
require.Equal(t, interval, rule.IntervalSeconds)
})
t.Run("updating a rule group should bump the version number", func(t *testing.T) {
t.Run("updating a rule group's top level fields should bump the version number", func(t *testing.T) {
const (
orgID = 123
namespaceUID = "abc"
@@ -99,6 +132,26 @@ func TestAlertRuleService(t *testing.T) {
require.Equal(t, newInterval, rule.IntervalSeconds)
})
t.Run("updating a group by updating a rule should bump that rule's data and version number", func(t *testing.T) {
var orgID int64 = 1
group := createDummyGroup("group-test-5", orgID)
err := ruleService.ReplaceRuleGroup(context.Background(), orgID, group, 0, models.ProvenanceAPI)
require.NoError(t, err)
updatedGroup, err := ruleService.GetRuleGroup(context.Background(), orgID, "my-namespace", "group-test-5")
require.NoError(t, err)
updatedGroup.Rules[0].Title = "some-other-title-asdf"
err = ruleService.ReplaceRuleGroup(context.Background(), orgID, updatedGroup, 0, models.ProvenanceAPI)
require.NoError(t, err)
readGroup, err := ruleService.GetRuleGroup(context.Background(), orgID, "my-namespace", "group-test-5")
require.NoError(t, err)
require.NotEmpty(t, readGroup.Rules)
require.Len(t, readGroup.Rules, 1)
require.Equal(t, "some-other-title-asdf", readGroup.Rules[0].Title)
require.Equal(t, int64(2), readGroup.Rules[0].Version)
})
t.Run("alert rule provenace should be correctly checked", func(t *testing.T) {
tests := []struct {
name string
@@ -160,6 +213,68 @@ func TestAlertRuleService(t *testing.T) {
}
})
t.Run("alert rule provenace should be correctly checked when writing groups", func(t *testing.T) {
tests := []struct {
name string
from models.Provenance
to models.Provenance
errNil bool
}{
{
name: "should be able to update from provenance none to api",
from: models.ProvenanceNone,
to: models.ProvenanceAPI,
errNil: true,
},
{
name: "should be able to update from provenance none to file",
from: models.ProvenanceNone,
to: models.ProvenanceFile,
errNil: true,
},
{
name: "should not be able to update from provenance api to file",
from: models.ProvenanceAPI,
to: models.ProvenanceFile,
errNil: false,
},
{
name: "should not be able to update from provenance api to none",
from: models.ProvenanceAPI,
to: models.ProvenanceNone,
errNil: false,
},
{
name: "should not be able to update from provenance file to api",
from: models.ProvenanceFile,
to: models.ProvenanceAPI,
errNil: false,
},
{
name: "should not be able to update from provenance file to none",
from: models.ProvenanceFile,
to: models.ProvenanceNone,
errNil: false,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
var orgID int64 = 1
group := createDummyGroup(t.Name(), orgID)
err := ruleService.ReplaceRuleGroup(context.Background(), 1, group, 0, test.from)
require.NoError(t, err)
group.Rules[0].Title = t.Name()
err = ruleService.ReplaceRuleGroup(context.Background(), 1, group, 0, test.to)
if test.errNil {
require.NoError(t, err)
} else {
require.Error(t, err)
}
})
}
})
t.Run("quota met causes create to be rejected", func(t *testing.T) {
ruleService := createAlertRuleService(t)
checker := &MockQuotaChecker{}
@@ -170,6 +285,18 @@ func TestAlertRuleService(t *testing.T) {
require.ErrorIs(t, err, models.ErrQuotaReached)
})
t.Run("quota met causes group write to be rejected", func(t *testing.T) {
ruleService := createAlertRuleService(t)
checker := &MockQuotaChecker{}
checker.EXPECT().LimitExceeded()
ruleService.quotas = checker
group := createDummyGroup("quota-reached", 1)
err := ruleService.ReplaceRuleGroup(context.Background(), 1, group, 0, models.ProvenanceAPI)
require.ErrorIs(t, err, models.ErrQuotaReached)
})
}
func createAlertRuleService(t *testing.T) AlertRuleService {
@@ -180,6 +307,7 @@ func createAlertRuleService(t *testing.T) AlertRuleService {
Cfg: setting.UnifiedAlertingSettings{
BaseInterval: time.Second * 10,
},
Logger: log.NewNopLogger(),
}
quotas := MockQuotaChecker{}
quotas.EXPECT().LimitOK()
@@ -195,6 +323,10 @@ func createAlertRuleService(t *testing.T) AlertRuleService {
}
func dummyRule(title string, orgID int64) models.AlertRule {
return createTestRule(title, "my-cool-group", orgID)
}
func createTestRule(title string, groupTitle string, orgID int64) models.AlertRule {
return models.AlertRule{
OrgID: orgID,
Title: title,
@@ -212,9 +344,21 @@ func dummyRule(title string, orgID int64) models.AlertRule {
},
},
},
RuleGroup: "my-cool-group",
NamespaceUID: "my-namespace",
RuleGroup: groupTitle,
For: time.Second * 60,
NoDataState: models.OK,
ExecErrState: models.OkErrState,
}
}
func createDummyGroup(title string, orgID int64) definitions.AlertRuleGroup {
return definitions.AlertRuleGroup{
Title: title,
Interval: 60,
FolderUID: "my-namespace",
Rules: []models.AlertRule{
dummyRule(title+"-"+"rule-1", orgID),
},
}
}