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-10 10:28:34 -05:00
committed by Moustafa Baiou
parent 2728e5cf14
commit fb0d6be79e
8 changed files with 463 additions and 42 deletions
+232 -25
View File
@@ -2970,6 +2970,7 @@ func TestIntegrationAlertRuleCRUD(t *testing.T) {
assert.Empty(t, resp.Deleted)
}
createdRuleUIDs := make(map[string]string)
// With the rules created, let's make sure that rule definition is stored correctly.
{
u := fmt.Sprintf("http://grafana:password@%s/api/ruler/grafana/api/v1/rules/default", grafanaListedAddr)
@@ -3096,9 +3097,11 @@ func TestIntegrationAlertRuleCRUD(t *testing.T) {
]
}`
assert.JSONEq(t, expectedGetNamespaceResponseBody, body)
createdRuleUIDs["AlwaysFiring"] = generatedUIDs[0]
createdRuleUIDs["AlwaysFiringButSilenced"] = generatedUIDs[1]
}
// try to update by pass an invalid UID
// validate that a rulegroup with a new rule with a user specified UID can be created while others updated
{
interval, err := model.ParseDuration("30s")
require.NoError(t, err)
@@ -3106,6 +3109,57 @@ func TestIntegrationAlertRuleCRUD(t *testing.T) {
rules := apimodels.PostableRuleGroupConfig{
Name: "arulegroup",
Rules: []apimodels.PostableExtendedRuleNode{
{
ApiRuleNode: &apimodels.ApiRuleNode{
For: &interval,
Labels: map[string]string{"label1": "val1"},
Annotations: map[string]string{"annotation1": "val1"},
},
// this rule does not explicitly set no data and error states
// therefore it should get the default values
GrafanaManagedAlert: &apimodels.PostableGrafanaRule{
Title: "AlwaysFiring",
UID: createdRuleUIDs["AlwaysFiring"],
Condition: "A",
Data: []apimodels.AlertQuery{
{
RefID: "A",
RelativeTimeRange: apimodels.RelativeTimeRange{
From: apimodels.Duration(time.Duration(5) * time.Hour),
To: apimodels.Duration(time.Duration(3) * time.Hour),
},
DatasourceUID: expr.DatasourceUID,
Model: json.RawMessage(`{
"type": "math",
"expression": "2 + 3 > 1"
}`),
},
},
},
},
{
GrafanaManagedAlert: &apimodels.PostableGrafanaRule{
Title: "AlwaysFiringButSilenced",
UID: createdRuleUIDs["AlwaysFiringButSilenced"],
Condition: "A",
Data: []apimodels.AlertQuery{
{
RefID: "A",
RelativeTimeRange: apimodels.RelativeTimeRange{
From: apimodels.Duration(time.Duration(5) * time.Hour),
To: apimodels.Duration(time.Duration(3) * time.Hour),
},
DatasourceUID: expr.DatasourceUID,
Model: json.RawMessage(`{
"type": "math",
"expression": "2 + 3 > 1"
}`),
},
},
NoDataState: apimodels.NoDataState(ngmodels.Alerting),
ExecErrState: apimodels.ExecutionErrorState(ngmodels.AlertingErrState),
},
},
{
ApiRuleNode: &apimodels.ApiRuleNode{
For: &interval,
@@ -3144,31 +3198,83 @@ func TestIntegrationAlertRuleCRUD(t *testing.T) {
Interval: interval,
}
_, status, body := apiClient.PostRulesGroupWithStatus(t, "default", &rules)
assert.Equal(t, http.StatusNotFound, status)
var res map[string]any
assert.NoError(t, json.Unmarshal([]byte(body), &res))
require.Equal(t, "failed to update rule group: failed to update rule with UID unknown because could not find alert rule", res["message"])
response, status, _ := apiClient.PostRulesGroupWithStatus(t, "default", &rules)
assert.Equal(t, http.StatusAccepted, status)
// let's make sure that rule definitions are not affected by the failed POST request.
u := fmt.Sprintf("http://grafana:password@%s/api/ruler/grafana/api/v1/rules/default", grafanaListedAddr)
// nolint:gosec
resp, err := http.Get(u)
require.NoError(t, err)
t.Cleanup(func() {
err := resp.Body.Close()
require.NoError(t, err)
})
b, err := io.ReadAll(resp.Body)
require.Len(t, response.Created, 1)
require.Len(t, response.Updated, 2)
require.Len(t, response.Deleted, 0)
}
// remove the added rule and set the interval back to 1m
{
interval, err := model.ParseDuration("1m")
require.NoError(t, err)
assert.Equal(t, resp.StatusCode, 202)
rules := apimodels.PostableRuleGroupConfig{
Name: "arulegroup",
Rules: []apimodels.PostableExtendedRuleNode{
{
ApiRuleNode: &apimodels.ApiRuleNode{
For: &interval,
Labels: map[string]string{"label1": "val1"},
Annotations: map[string]string{"annotation1": "val1"},
},
// this rule does not explicitly set no data and error states
// therefore it should get the default values
GrafanaManagedAlert: &apimodels.PostableGrafanaRule{
Title: "AlwaysFiring",
UID: createdRuleUIDs["AlwaysFiring"],
Condition: "A",
Data: []apimodels.AlertQuery{
{
RefID: "A",
RelativeTimeRange: apimodels.RelativeTimeRange{
From: apimodels.Duration(time.Duration(5) * time.Hour),
To: apimodels.Duration(time.Duration(3) * time.Hour),
},
DatasourceUID: expr.DatasourceUID,
Model: json.RawMessage(`{
"type": "math",
"expression": "2 + 3 > 1"
}`),
},
},
},
},
{
GrafanaManagedAlert: &apimodels.PostableGrafanaRule{
Title: "AlwaysFiringButSilenced",
UID: createdRuleUIDs["AlwaysFiringButSilenced"],
Condition: "A",
Data: []apimodels.AlertQuery{
{
RefID: "A",
RelativeTimeRange: apimodels.RelativeTimeRange{
From: apimodels.Duration(time.Duration(5) * time.Hour),
To: apimodels.Duration(time.Duration(3) * time.Hour),
},
DatasourceUID: expr.DatasourceUID,
Model: json.RawMessage(`{
"type": "math",
"expression": "2 + 3 > 1"
}`),
},
},
NoDataState: apimodels.NoDataState(ngmodels.Alerting),
ExecErrState: apimodels.ExecutionErrorState(ngmodels.AlertingErrState),
},
},
},
Interval: interval,
}
body, m := rulesNamespaceWithoutVariableValues(t, b)
returnedUIDs, ok := m["default,arulegroup"]
assert.True(t, ok)
assert.Equal(t, 2, len(returnedUIDs))
assert.JSONEq(t, expectedGetNamespaceResponseBody, body)
response, status, _ := apiClient.PostRulesGroupWithStatus(t, "default", &rules)
assert.Equal(t, http.StatusAccepted, status)
require.Len(t, response.Created, 0)
require.Len(t, response.Updated, 2)
require.Len(t, response.Deleted, 1)
}
// try to update by pass two rules with conflicting UIDs
@@ -3274,6 +3380,107 @@ func TestIntegrationAlertRuleCRUD(t *testing.T) {
returnedUIDs, ok := m["default,arulegroup"]
assert.True(t, ok)
assert.Equal(t, 2, len(returnedUIDs))
expectedGetNamespaceResponseBody = `
{
"default":[
{
"name":"arulegroup",
"interval":"1m",
"rules":[
{
"annotations": {
"annotation1": "val1"
},
"expr":"",
"for": "1m",
"labels": {
"label1": "val1"
},
"grafana_alert":{
"title":"AlwaysFiring",
"condition":"A",
"data":[
{
"refId":"A",
"queryType":"",
"relativeTimeRange":{
"from":18000,
"to":10800
},
"datasourceUid":"__expr__",
"model":{
"expression":"2 + 3 \u003e 1",
"intervalMs":1000,
"maxDataPoints":43200,
"type":"math"
}
}
],
"updated":"2021-02-21T01:10:30Z",
"id":1,
"orgId":1,
"intervalSeconds":60,
"is_paused": false,
"version":3,
"uid":"uid",
"namespace_uid":"nsuid",
"rule_group":"arulegroup",
"no_data_state":"NoData",
"exec_err_state":"Alerting",
"metadata": {
"editor_settings": {
"simplified_query_and_expressions_section": false,
"simplified_notifications_section": false
}
}
}
},
{
"expr":"",
"for": "0s",
"grafana_alert":{
"title":"AlwaysFiringButSilenced",
"condition":"A",
"data":[
{
"refId":"A",
"queryType":"",
"relativeTimeRange":{
"from":18000,
"to":10800
},
"datasourceUid":"__expr__",
"model":{
"expression":"2 + 3 \u003e 1",
"intervalMs":1000,
"maxDataPoints":43200,
"type":"math"
}
}
],
"updated":"2021-02-21T01:10:30Z",
"intervalSeconds":60,
"is_paused": false,
"id":2,
"orgId":1,
"version":3,
"uid":"uid",
"namespace_uid":"nsuid",
"rule_group":"arulegroup",
"no_data_state":"Alerting",
"exec_err_state":"Alerting",
"metadata": {
"editor_settings": {
"simplified_query_and_expressions_section": false,
"simplified_notifications_section": false
}
}
}
}
]
}
]
}`
assert.JSONEq(t, expectedGetNamespaceResponseBody, body)
}
@@ -3391,7 +3598,7 @@ func TestIntegrationAlertRuleCRUD(t *testing.T) {
"updated":"2021-02-21T01:10:30Z",
"intervalSeconds":60,
"is_paused": false,
"version":2,
"version":4,
"uid":"uid",
"namespace_uid":"nsuid",
"rule_group":"arulegroup",
@@ -3506,7 +3713,7 @@ func TestIntegrationAlertRuleCRUD(t *testing.T) {
"updated":"2021-02-21T01:10:30Z",
"intervalSeconds":60,
"is_paused":false,
"version":3,
"version":5,
"uid":"uid",
"namespace_uid":"nsuid",
"rule_group":"arulegroup",
@@ -3600,7 +3807,7 @@ func TestIntegrationAlertRuleCRUD(t *testing.T) {
"updated":"2021-02-21T01:10:30Z",
"intervalSeconds":60,
"is_paused":false,
"version":3,
"version":5,
"uid":"uid",
"namespace_uid":"nsuid",
"rule_group":"arulegroup",