diff --git a/pkg/services/ngalert/api/api_alertmanager_guards_test.go b/pkg/services/ngalert/api/api_alertmanager_guards_test.go index a2330d6d533..6912baed2c5 100644 --- a/pkg/services/ngalert/api/api_alertmanager_guards_test.go +++ b/pkg/services/ngalert/api/api_alertmanager_guards_test.go @@ -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, diff --git a/pkg/services/ngalert/models/alert_rule.go b/pkg/services/ngalert/models/alert_rule.go index b7da813141f..26f7d84eee1 100644 --- a/pkg/services/ngalert/models/alert_rule.go +++ b/pkg/services/ngalert/models/alert_rule.go @@ -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" ) @@ -571,6 +572,9 @@ func (alertRule *AlertRule) PreSave(timeNow func() time.Time, userUID *UserUID) // 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) } diff --git a/pkg/services/ngalert/provisioning/alert_rules.go b/pkg/services/ngalert/provisioning/alert_rules.go index c31413b3bdc..e58e674eff2 100644 --- a/pkg/services/ngalert/provisioning/alert_rules.go +++ b/pkg/services/ngalert/provisioning/alert_rules.go @@ -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 } diff --git a/pkg/services/ngalert/store/deltas.go b/pkg/services/ngalert/store/deltas.go index 27469f5733a..e08bc3c5cfc 100644 --- a/pkg/services/ngalert/store/deltas.go +++ b/pkg/services/ngalert/store/deltas.go @@ -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)) diff --git a/pkg/services/ngalert/store/deltas_test.go b/pkg/services/ngalert/store/deltas_test.go index 2b5c9760575..904c0da5164 100644 --- a/pkg/services/ngalert/store/deltas_test.go +++ b/pkg/services/ngalert/store/deltas_test.go @@ -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) { diff --git a/pkg/tests/api/alerting/api_provisioning_test.go b/pkg/tests/api/alerting/api_provisioning_test.go index f4bc4dab890..f177c2b849f 100644 --- a/pkg/tests/api/alerting/api_provisioning_test.go +++ b/pkg/tests/api/alerting/api_provisioning_test.go @@ -508,7 +508,34 @@ func TestIntegrationProvisioning(t *testing.T) { t.Run("when provisioning alert rules", func(t *testing.T) { url := fmt.Sprintf("http://%s/api/v1/provisioning/alert-rules", grafanaListedAddr) - body := `{"orgID":1,"folderUID":"default","ruleGroup":"Test Group","title":"Provisioned","condition":"A","data":[{"refId":"A","queryType":"","relativeTimeRange":{"from":600,"to":0},"datasourceUid":"f558c85f-66ad-4fd1-b31d-7979e6c93db4","model":{"editorMode":"code","exemplar":false,"expr":"sum(rate(low_card[5m])) \u003e 0","format":"time_series","instant":true,"intervalMs":1000,"legendFormat":"__auto","maxDataPoints":43200,"range":false,"refId":"A"}}],"noDataState":"NoData","execErrState":"Error","for":"0s"}` + body := ` + { + "orgID":1, + "folderUID":"default", + "ruleGroup":"Test Group", + "title":"Provisioned", + "condition":"A", + "data":[{ + "refId":"A", + "queryType":"", + "relativeTimeRange":{"from":600,"to":0}, + "datasourceUid":"f558c85f-66ad-4fd1-b31d-7979e6c93db4", + "model":{ + "editorMode":"code", + "exemplar":false, + "expr":"sum(rate(low_card[5m])) \u003e 0", + "format":"time_series", + "instant":true, + "intervalMs":1000, + "legendFormat":"__auto", + "maxDataPoints":43200, + "range":false,"refId":"A" + } + }], + "noDataState":"NoData", + "execErrState":"Error", + "for":"0s" + }` req := createTestRequest("POST", url, "admin", body) resp, err := http.DefaultClient.Do(req) require.NoError(t, err) @@ -535,6 +562,149 @@ func TestIntegrationProvisioning(t *testing.T) { }) } +func TestIntegrationProvisioningRules(t *testing.T) { + testinfra.SQLiteIntegrationTest(t) + + dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ + DisableLegacyAlerting: true, + EnableUnifiedAlerting: true, + DisableAnonymous: true, + AppModeProduction: true, + }) + + grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, path) + + // Create a users to make authenticated requests + createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{ + DefaultOrgRole: string(org.RoleViewer), + Password: "viewer", + Login: "viewer", + }) + createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{ + DefaultOrgRole: string(org.RoleEditor), + Password: "editor", + Login: "editor", + }) + createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{ + DefaultOrgRole: string(org.RoleAdmin), + Password: "admin", + Login: "admin", + }) + + apiClient := newAlertingApiClient(grafanaListedAddr, "editor", "editor") + // Create the namespace we'll save our alerts to. + namespaceUID := "default" + apiClient.CreateFolder(t, namespaceUID, namespaceUID) + + t.Run("when provisioning alert rules", func(t *testing.T) { + originalRuleGroup := definitions.AlertRuleGroup{ + Title: "TestGroup", + Interval: 60, + FolderUID: "default", + Rules: []definitions.ProvisionedAlertRule{ + { + UID: "rule1", + Title: "Rule1", + OrgID: 1, + RuleGroup: "TestGroup", + Condition: "A", + NoDataState: definitions.Alerting, + ExecErrState: definitions.AlertingErrState, + For: model.Duration(time.Duration(60) * time.Second), + Data: []definitions.AlertQuery{ + { + RefID: "A", + RelativeTimeRange: definitions.RelativeTimeRange{ + From: definitions.Duration(time.Duration(5) * time.Hour), + To: definitions.Duration(time.Duration(3) * time.Hour), + }, + DatasourceUID: expr.DatasourceUID, + Model: json.RawMessage([]byte(`{"type":"math","expression":"2 + 3 \u003e 1"}`)), + }, + }, + }, + { + UID: "rule2", + Title: "Rule2", + OrgID: 1, + RuleGroup: "TestGroup", + Condition: "A", + NoDataState: definitions.Alerting, + ExecErrState: definitions.AlertingErrState, + For: model.Duration(time.Duration(60) * time.Second), + Data: []definitions.AlertQuery{ + { + RefID: "A", + RelativeTimeRange: definitions.RelativeTimeRange{ + From: definitions.Duration(time.Duration(5) * time.Hour), + To: definitions.Duration(time.Duration(3) * time.Hour), + }, + DatasourceUID: expr.DatasourceUID, + Model: json.RawMessage([]byte(`{"type":"math","expression":"2 + 3 \u003e 1"}`)), + }, + }, + }, + { + UID: "rule3", + Title: "Rule3", + OrgID: 1, + RuleGroup: "TestGroup", + Condition: "A", + NoDataState: definitions.Alerting, + ExecErrState: definitions.AlertingErrState, + For: model.Duration(time.Duration(60) * time.Second), + Data: []definitions.AlertQuery{ + { + RefID: "A", + RelativeTimeRange: definitions.RelativeTimeRange{ + From: definitions.Duration(time.Duration(5) * time.Hour), + To: definitions.Duration(time.Duration(3) * time.Hour), + }, + DatasourceUID: expr.DatasourceUID, + Model: json.RawMessage([]byte(`{"type":"math","expression":"2 + 3 \u003e 1"}`)), + }, + }, + }, + }, + } + + result, status, raw := apiClient.CreateOrUpdateRuleGroupProvisioning(t, originalRuleGroup) + t.Run("should create a new rule group with UIDs specified", func(t *testing.T) { + requireStatusCode(t, http.StatusOK, status, raw) + require.Equal(t, originalRuleGroup, result) + }) + + t.Run("should remove a rule when updating group with a rule removed", func(t *testing.T) { + existingRuleGroup, status, raw := apiClient.GetRuleGroupProvisioning(t, "default", "TestGroup") + requireStatusCode(t, http.StatusOK, status, raw) + require.Len(t, existingRuleGroup.Rules, 3) + + updatedRuleGroup := existingRuleGroup + updatedRuleGroup.Rules = updatedRuleGroup.Rules[:2] + result, status, raw := apiClient.CreateOrUpdateRuleGroupProvisioning(t, updatedRuleGroup) + requireStatusCode(t, http.StatusOK, status, raw) + require.Equal(t, updatedRuleGroup, result) + + // Check that the rule was removed + rules, status, raw := apiClient.GetRuleGroupProvisioning(t, existingRuleGroup.FolderUID, existingRuleGroup.Title) + requireStatusCode(t, http.StatusOK, status, raw) + require.Len(t, rules.Rules, 2) + }) + + t.Run("should recreate a rule when updating group with the rule added back", func(t *testing.T) { + result, status, raw := apiClient.CreateOrUpdateRuleGroupProvisioning(t, originalRuleGroup) + requireStatusCode(t, http.StatusOK, status, raw) + require.Equal(t, originalRuleGroup, result) + require.Len(t, result.Rules, 3) + + // Check that the rule was re-added + rules, status, raw := apiClient.GetRuleGroupProvisioning(t, originalRuleGroup.FolderUID, originalRuleGroup.Title) + requireStatusCode(t, http.StatusOK, status, raw) + require.Len(t, rules.Rules, 3) + }) + }) +} + func TestMuteTimings(t *testing.T) { dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ DisableLegacyAlerting: true, diff --git a/pkg/tests/api/alerting/api_ruler_test.go b/pkg/tests/api/alerting/api_ruler_test.go index c177c6c4412..9f03a070f53 100644 --- a/pkg/tests/api/alerting/api_ruler_test.go +++ b/pkg/tests/api/alerting/api_ruler_test.go @@ -3098,6 +3098,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) @@ -3228,9 +3229,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) @@ -3238,6 +3241,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, @@ -3276,31 +3330,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 @@ -3406,6 +3512,111 @@ 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", + "updated_by": { + "uid": "uid", + "name": "grafana" + }, + "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", + "updated_by": { + "uid": "uid", + "name": "grafana" + }, + "intervalSeconds":60, + "is_paused": false, + "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) } @@ -3525,7 +3736,7 @@ func TestIntegrationAlertRuleCRUD(t *testing.T) { }, "intervalSeconds":60, "is_paused": false, - "version":2, + "version":4, "uid":"uid", "namespace_uid":"nsuid", "rule_group":"arulegroup", @@ -3642,7 +3853,7 @@ func TestIntegrationAlertRuleCRUD(t *testing.T) { }, "intervalSeconds":60, "is_paused":false, - "version":3, + "version":5, "uid":"uid", "namespace_uid":"nsuid", "rule_group":"arulegroup", @@ -3738,7 +3949,7 @@ func TestIntegrationAlertRuleCRUD(t *testing.T) { }, "intervalSeconds":60, "is_paused":false, - "version":3, + "version":5, "uid":"uid", "namespace_uid":"nsuid", "rule_group":"arulegroup", diff --git a/pkg/tests/api/alerting/testing.go b/pkg/tests/api/alerting/testing.go index ed5fb7ace1a..5272883af9e 100644 --- a/pkg/tests/api/alerting/testing.go +++ b/pkg/tests/api/alerting/testing.go @@ -687,6 +687,30 @@ func (a apiClient) ExportRulesWithStatus(t *testing.T, params *apimodels.AlertRu return resp.StatusCode, string(b) } +func (a apiClient) GetRuleGroupProvisioning(t *testing.T, folderUID string, groupName string) (apimodels.AlertRuleGroup, int, string) { + t.Helper() + + req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/api/v1/provisioning/folder/%s/rule-groups/%s", a.url, folderUID, groupName), nil) + require.NoError(t, err) + + return sendRequest[apimodels.AlertRuleGroup](t, req, http.StatusOK) +} + +func (a apiClient) CreateOrUpdateRuleGroupProvisioning(t *testing.T, group apimodels.AlertRuleGroup) (apimodels.AlertRuleGroup, int, string) { + t.Helper() + + buf := bytes.Buffer{} + enc := json.NewEncoder(&buf) + err := enc.Encode(group) + require.NoError(t, err) + + req, err := http.NewRequest(http.MethodPut, fmt.Sprintf("%s/api/v1/provisioning/folder/%s/rule-groups/%s", a.url, group.FolderUID, group.Title), &buf) + require.NoError(t, err) + req.Header.Add("Content-Type", "application/json") + + return sendRequest[apimodels.AlertRuleGroup](t, req, http.StatusOK) +} + func (a apiClient) SubmitRuleForBacktesting(t *testing.T, config apimodels.BacktestConfig) (int, string) { t.Helper() buf := bytes.Buffer{}