Alerting: Rule version history API (#99041)

* implement store method to read rule versions

* implement request handler

* declare a new endpoint

* fix fake to return correct response

* add tests

* add integration tests

* rename history to versions

* apply diff from swagger CI step

Signed-off-by: Yuri Tseretyan <yuriy.tseretyan@grafana.com>

---------

Signed-off-by: Yuri Tseretyan <yuriy.tseretyan@grafana.com>
This commit is contained in:
Yuri Tseretyan
2025-02-03 13:26:18 -05:00
committed by GitHub
parent 8a259ecafa
commit ac41c19350
20 changed files with 772 additions and 16 deletions

View File

@@ -241,6 +241,18 @@ func TestIntegrationAlertRulePermissions(t *testing.T) {
require.Len(t, export.Groups, 1)
require.Equal(t, expected, export.Groups[0])
})
t.Run("Get versions of any rule", func(t *testing.T) {
for _, groups := range allRules { // random rule from each folder
group := groups[rand.Intn(len(groups))]
rule := group.Rules[rand.Intn(len(group.Rules))]
versions, status, raw := apiClient.GetRuleVersionsWithStatus(t, rule.GrafanaManagedAlert.UID)
if assert.Equalf(t, http.StatusOK, status, "Expected status 200, got %d: %s", status, raw) {
assert.NotEmpty(t, versions)
assert.Equal(t, rule, versions[0]) // the first version in the collection should always be the current
}
}
})
})
t.Run("when permissions for folder2 removed", func(t *testing.T) {
@@ -310,6 +322,12 @@ func TestIntegrationAlertRulePermissions(t *testing.T) {
require.Equal(t, http.StatusForbidden, status)
})
t.Run("Versions of rule", func(t *testing.T) {
uid := allRules["folder2"][0].Rules[0].GrafanaManagedAlert.UID
_, status, raw := apiClient.GetRuleVersionsWithStatus(t, uid)
require.Equalf(t, http.StatusForbidden, status, "Expected status 403, got %d: %s", status, raw)
})
t.Run("when all permissions are revoked", func(t *testing.T) {
removeFolderPermission(t, permissionsStore, 1, userID, org.RoleEditor, "folder1")
apiClient.ReloadCachedPermissions(t)
@@ -4315,6 +4333,95 @@ func TestIntegrationRuleUpdateAllDatabases(t *testing.T) {
})
}
func TestIntegrationRuleVersions(t *testing.T) {
testinfra.SQLiteIntegrationTest(t)
// Setup Grafana and its Database
dir, p := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
DisableLegacyAlerting: true,
EnableUnifiedAlerting: true,
EnableQuota: true,
DisableAnonymous: true,
AppModeProduction: true,
})
grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, p)
createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{
DefaultOrgRole: string(org.RoleEditor),
Password: "password",
Login: "grafana",
})
apiClient := newAlertingApiClient(grafanaListedAddr, "grafana", "password")
// Create the namespace we'll save our alerts to.
apiClient.CreateFolder(t, "folder1", "folder1")
postGroupRaw, err := testData.ReadFile(path.Join("test-data", "rulegroup-1-post.json"))
require.NoError(t, err)
var group1 apimodels.PostableRuleGroupConfig
require.NoError(t, json.Unmarshal(postGroupRaw, &group1))
// Create rule under folder1
response := apiClient.PostRulesGroup(t, "folder1", &group1)
require.NotEmptyf(t, response.Created, "Expected created to be set")
uid := response.Created[0]
ruleV1 := apiClient.GetRuleByUID(t, uid)
t.Run("should return 1 version right after creation", func(t *testing.T) {
versions, status, raw := apiClient.GetRuleVersionsWithStatus(t, uid)
require.Equalf(t, http.StatusOK, status, "Expected status 200, got %d: %s", status, raw)
require.Lenf(t, versions, 1, "Expected 1 version, got %d", len(versions))
assert.Equal(t, ruleV1, versions[0])
})
group1Gettable := apiClient.GetRulesGroup(t, "folder1", group1.Name)
group1 = convertGettableRuleGroupToPostable(group1Gettable.GettableRuleGroupConfig)
group1.Rules[0].Annotations[util.GenerateShortUID()] = util.GenerateShortUID()
_ = apiClient.PostRulesGroup(t, "folder1", &group1)
ruleV2 := apiClient.GetRuleByUID(t, uid)
t.Run("should return previous versions after update", func(t *testing.T) {
versions, status, raw := apiClient.GetRuleVersionsWithStatus(t, uid)
require.Equalf(t, http.StatusOK, status, "Expected status 200, got %d: %s", status, raw)
require.Lenf(t, versions, 2, "Expected 2 versions, got %d", len(versions))
pathsToIgnore := []string{
"GrafanaManagedAlert.ID", // In versions ID has different value
}
// compare expected and actual and ignore the dynamic fields
diff := cmp.Diff(apimodels.GettableRuleVersions{ruleV2, ruleV1}, versions, cmp.FilterPath(func(path cmp.Path) bool {
for _, s := range pathsToIgnore {
if strings.Contains(path.String(), s) {
return true
}
}
return false
}, cmp.Ignore()))
assert.Empty(t, diff)
})
_ = apiClient.PostRulesGroup(t, "folder1", &group1) // Noop update
t.Run("should not add new version if rule was not changed", func(t *testing.T) {
versions, status, raw := apiClient.GetRuleVersionsWithStatus(t, uid)
require.Equalf(t, http.StatusOK, status, "Expected status 200, got %d: %s", status, raw)
require.Lenf(t, versions, 2, "Expected 2 versions, got %d", len(versions))
})
apiClient.DeleteRulesGroup(t, "folder1", group1.Name)
t.Run("should NotFound after rule was deleted", func(t *testing.T) {
_, status, raw := apiClient.GetRuleVersionsWithStatus(t, uid)
require.Equalf(t, http.StatusNotFound, status, "Expected status 404, got %d: %s", status, raw)
})
}
func newTestingRuleConfig(t *testing.T) apimodels.PostableRuleGroupConfig {
interval, err := model.ParseDuration("1m")
require.NoError(t, err)

View File

@@ -475,6 +475,13 @@ func (a apiClient) PostRulesGroupWithStatus(t *testing.T, folder string, group *
return m, resp.StatusCode, string(b)
}
func (a apiClient) PostRulesGroup(t *testing.T, folder string, group *apimodels.PostableRuleGroupConfig) apimodels.UpdateRuleGroupResponse {
t.Helper()
m, status, raw := a.PostRulesGroupWithStatus(t, folder, group)
requireStatusCode(t, http.StatusAccepted, status, raw)
return m
}
func (a apiClient) PostRulesExportWithStatus(t *testing.T, folder string, group *apimodels.PostableRuleGroupConfig, params *apimodels.ExportQueryParams) (int, string) {
t.Helper()
buf := bytes.Buffer{}
@@ -1048,6 +1055,22 @@ func (a apiClient) GetActiveAlertsWithStatus(t *testing.T) (apimodels.AlertGroup
return sendRequest[apimodels.AlertGroups](t, req, http.StatusOK)
}
func (a apiClient) GetRuleVersionsWithStatus(t *testing.T, ruleUID string) (apimodels.GettableRuleVersions, int, string) {
t.Helper()
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/api/ruler/grafana/api/v1/rule/%s/versions", a.url, ruleUID), nil)
require.NoError(t, err)
return sendRequest[apimodels.GettableRuleVersions](t, req, http.StatusOK)
}
func (a apiClient) GetRuleByUID(t *testing.T, ruleUID string) apimodels.GettableExtendedRuleNode {
t.Helper()
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/api/ruler/grafana/api/v1/rule/%s", a.url, ruleUID), nil)
require.NoError(t, err)
rule, status, raw := sendRequest[apimodels.GettableExtendedRuleNode](t, req, http.StatusOK)
requireStatusCode(t, http.StatusOK, status, raw)
return rule
}
func sendRequest[T any](t *testing.T, req *http.Request, successStatusCode int) (T, int, string) {
t.Helper()
client := &http.Client{}