diff --git a/pkg/services/ngalert/api/api_provisioning.go b/pkg/services/ngalert/api/api_provisioning.go index 2bba2976ff9..a1d893ecc1e 100644 --- a/pkg/services/ngalert/api/api_provisioning.go +++ b/pkg/services/ngalert/api/api_provisioning.go @@ -65,6 +65,7 @@ type AlertRuleService interface { DeleteAlertRule(ctx context.Context, orgID int64, ruleUID string, provenance alerting_models.Provenance) error GetRuleGroup(ctx context.Context, orgID int64, folder, group string) (alerting_models.AlertRuleGroup, error) ReplaceRuleGroup(ctx context.Context, orgID int64, group alerting_models.AlertRuleGroup, userID int64, provenance alerting_models.Provenance) error + DeleteRuleGroup(ctx context.Context, orgID int64, folder, group string, provenance alerting_models.Provenance) error GetAlertRuleWithFolderTitle(ctx context.Context, orgID int64, ruleUID string) (provisioning.AlertRuleWithFolderTitle, error) GetAlertRuleGroupWithFolderTitle(ctx context.Context, orgID int64, folder, group string) (alerting_models.AlertRuleGroupWithFolderTitle, error) GetAlertGroupsWithFolderTitle(ctx context.Context, orgID int64, folderUIDs []string) ([]alerting_models.AlertRuleGroupWithFolderTitle, error) @@ -505,6 +506,18 @@ func (srv *ProvisioningSrv) RoutePutAlertRuleGroup(c *contextmodel.ReqContext, a return response.JSON(http.StatusOK, ag) } +func (srv *ProvisioningSrv) RouteDeleteAlertRuleGroup(c *contextmodel.ReqContext, folderUID string, group string) response.Response { + provenance := determineProvenance(c) + err := srv.alertRules.DeleteRuleGroup(c.Req.Context(), c.SignedInUser.GetOrgID(), folderUID, group, alerting_models.Provenance(provenance)) + if err != nil { + if errors.Is(err, store.ErrAlertRuleGroupNotFound) { + return ErrResp(http.StatusNotFound, err, "") + } + return ErrResp(http.StatusInternalServerError, err, "") + } + return response.JSON(http.StatusNoContent, "") +} + func determineProvenance(ctx *contextmodel.ReqContext) definitions.Provenance { if _, disabled := ctx.Req.Header[disableProvenanceHeaderName]; disabled { return definitions.Provenance(alerting_models.ProvenanceNone) diff --git a/pkg/services/ngalert/api/api_provisioning_test.go b/pkg/services/ngalert/api/api_provisioning_test.go index df1d266747e..c8d60ed330f 100644 --- a/pkg/services/ngalert/api/api_provisioning_test.go +++ b/pkg/services/ngalert/api/api_provisioning_test.go @@ -343,24 +343,40 @@ func TestProvisioningApi(t *testing.T) { }) t.Run("alert rule groups", func(t *testing.T) { - t.Run("are present, GET returns 200", func(t *testing.T) { + t.Run("are present", func(t *testing.T) { sut := createProvisioningSrvSut(t) rc := createTestRequestCtx() insertRule(t, sut, createTestAlertRule("rule", 1)) - response := sut.RouteGetAlertRuleGroup(&rc, "folder-uid", "my-cool-group") + t.Run("GET returns 200", func(t *testing.T) { + response := sut.RouteGetAlertRuleGroup(&rc, "folder-uid", "my-cool-group") - require.Equal(t, 200, response.Status()) + require.Equal(t, 200, response.Status()) + }) + + t.Run("DELETE returns 204", func(t *testing.T) { + response := sut.RouteDeleteAlertRuleGroup(&rc, "folder-uid", "my-cool-group") + + require.Equal(t, 204, response.Status()) + }) }) - t.Run("are missing, GET returns 404", func(t *testing.T) { + t.Run("are missing", func(t *testing.T) { sut := createProvisioningSrvSut(t) rc := createTestRequestCtx() insertRule(t, sut, createTestAlertRule("rule", 1)) - response := sut.RouteGetAlertRuleGroup(&rc, "folder-uid", "does not exist") + t.Run("GET returns 404", func(t *testing.T) { + response := sut.RouteGetAlertRuleGroup(&rc, "folder-uid", "does not exist") - require.Equal(t, 404, response.Status()) + require.Equal(t, 404, response.Status()) + }) + + t.Run("DELETE returns 404", func(t *testing.T) { + response := sut.RouteDeleteAlertRuleGroup(&rc, "folder-uid", "does not exist") + + require.Equal(t, 404, response.Status()) + }) }) t.Run("are invalid at group level", func(t *testing.T) { @@ -1587,6 +1603,7 @@ func createTestEnv(t *testing.T, testConfig string) testEnvironment { }) sqlStore := db.InitTestDB(t) store := store.DBstore{ + Logger: log, SQLStore: sqlStore, Cfg: setting.UnifiedAlertingSettings{ BaseInterval: time.Second * 10, diff --git a/pkg/services/ngalert/api/authorization.go b/pkg/services/ngalert/api/authorization.go index 44cf3babbdd..0ee2e58f7d2 100644 --- a/pkg/services/ngalert/api/authorization.go +++ b/pkg/services/ngalert/api/authorization.go @@ -253,7 +253,8 @@ func (api *API) authorize(method, path string) web.Handler { http.MethodPost + "/api/v1/provisioning/alert-rules", http.MethodPut + "/api/v1/provisioning/alert-rules/{UID}", http.MethodDelete + "/api/v1/provisioning/alert-rules/{UID}", - http.MethodPut + "/api/v1/provisioning/folder/{FolderUID}/rule-groups/{Group}": + http.MethodPut + "/api/v1/provisioning/folder/{FolderUID}/rule-groups/{Group}", + http.MethodDelete + "/api/v1/provisioning/folder/{FolderUID}/rule-groups/{Group}": eval = ac.EvalPermission(ac.ActionAlertingProvisioningWrite) // organization scope case http.MethodGet + "/api/v1/notifications/time-intervals/{name}", http.MethodGet + "/api/v1/notifications/time-intervals": diff --git a/pkg/services/ngalert/api/generated_base_api_provisioning.go b/pkg/services/ngalert/api/generated_base_api_provisioning.go index 453d151101e..202d643a11d 100644 --- a/pkg/services/ngalert/api/generated_base_api_provisioning.go +++ b/pkg/services/ngalert/api/generated_base_api_provisioning.go @@ -21,6 +21,7 @@ import ( type ProvisioningApi interface { RouteDeleteAlertRule(*contextmodel.ReqContext) response.Response + RouteDeleteAlertRuleGroup(*contextmodel.ReqContext) response.Response RouteDeleteContactpoints(*contextmodel.ReqContext) response.Response RouteDeleteMuteTiming(*contextmodel.ReqContext) response.Response RouteDeleteTemplate(*contextmodel.ReqContext) response.Response @@ -57,6 +58,12 @@ func (f *ProvisioningApiHandler) RouteDeleteAlertRule(ctx *contextmodel.ReqConte uIDParam := web.Params(ctx.Req)[":UID"] return f.handleRouteDeleteAlertRule(ctx, uIDParam) } +func (f *ProvisioningApiHandler) RouteDeleteAlertRuleGroup(ctx *contextmodel.ReqContext) response.Response { + // Parse Path Parameters + folderUIDParam := web.Params(ctx.Req)[":FolderUID"] + groupParam := web.Params(ctx.Req)[":Group"] + return f.handleRouteDeleteAlertRuleGroup(ctx, folderUIDParam, groupParam) +} func (f *ProvisioningApiHandler) RouteDeleteContactpoints(ctx *contextmodel.ReqContext) response.Response { // Parse Path Parameters uIDParam := web.Params(ctx.Req)[":UID"] @@ -237,6 +244,18 @@ func (api *API) RegisterProvisioningApiEndpoints(srv ProvisioningApi, m *metrics m, ), ) + group.Delete( + toMacaronPath("/api/v1/provisioning/folder/{FolderUID}/rule-groups/{Group}"), + requestmeta.SetOwner(requestmeta.TeamAlerting), + requestmeta.SetSLOGroup(requestmeta.SLOGroupHighSlow), + api.authorize(http.MethodDelete, "/api/v1/provisioning/folder/{FolderUID}/rule-groups/{Group}"), + metrics.Instrument( + http.MethodDelete, + "/api/v1/provisioning/folder/{FolderUID}/rule-groups/{Group}", + api.Hooks.Wrap(srv.RouteDeleteAlertRuleGroup), + m, + ), + ) group.Delete( toMacaronPath("/api/v1/provisioning/contact-points/{UID}"), requestmeta.SetOwner(requestmeta.TeamAlerting), diff --git a/pkg/services/ngalert/api/provisioning.go b/pkg/services/ngalert/api/provisioning.go index f43e9bb8f7c..91d1a6da639 100644 --- a/pkg/services/ngalert/api/provisioning.go +++ b/pkg/services/ngalert/api/provisioning.go @@ -135,3 +135,7 @@ func (f *ProvisioningApiHandler) handleRouteExportMuteTiming(ctx *contextmodel.R func (f *ProvisioningApiHandler) handleRouteExportMuteTimings(ctx *contextmodel.ReqContext) response.Response { return f.svc.RouteGetMuteTimingsExport(ctx) } + +func (f *ProvisioningApiHandler) handleRouteDeleteAlertRuleGroup(ctx *contextmodel.ReqContext, folderUID, group string) response.Response { + return f.svc.RouteDeleteAlertRuleGroup(ctx, folderUID, group) +} diff --git a/pkg/services/ngalert/api/tooling/api.json b/pkg/services/ngalert/api/tooling/api.json index 4024e914140..be0f9033b1e 100644 --- a/pkg/services/ngalert/api/tooling/api.json +++ b/pkg/services/ngalert/api/tooling/api.json @@ -5962,6 +5962,44 @@ } }, "/v1/provisioning/folder/{FolderUID}/rule-groups/{Group}": { + "delete": { + "description": "Delete rule group", + "operationId": "RouteDeleteAlertRuleGroup", + "parameters": [ + { + "in": "path", + "name": "FolderUID", + "required": true, + "type": "string" + }, + { + "in": "path", + "name": "Group", + "required": true, + "type": "string" + } + ], + "responses": { + "204": { + "description": " The alert rule group was deleted successfully." + }, + "403": { + "description": "ForbiddenError", + "schema": { + "$ref": "#/definitions/ForbiddenError" + } + }, + "404": { + "description": "NotFound", + "schema": { + "$ref": "#/definitions/NotFound" + } + } + }, + "tags": [ + "provisioning" + ] + }, "get": { "operationId": "RouteGetAlertRuleGroup", "parameters": [ diff --git a/pkg/services/ngalert/api/tooling/definitions/provisioning_alert_rules.go b/pkg/services/ngalert/api/tooling/definitions/provisioning_alert_rules.go index bc1a1d40e16..3fd9f8159c5 100644 --- a/pkg/services/ngalert/api/tooling/definitions/provisioning_alert_rules.go +++ b/pkg/services/ngalert/api/tooling/definitions/provisioning_alert_rules.go @@ -168,6 +168,15 @@ type ProvisionedAlertRule struct { // 200: AlertRuleGroup // 404: description: Not found. +// swagger:route DELETE /v1/provisioning/folder/{FolderUID}/rule-groups/{Group} provisioning stable RouteDeleteAlertRuleGroup +// +// Delete rule group +// +// Responses: +// 204: description: The alert rule group was deleted successfully. +// 403: ForbiddenError +// 404: NotFound + // swagger:route GET /v1/provisioning/folder/{FolderUID}/rule-groups/{Group}/export provisioning stable RouteGetAlertRuleGroupExport // // Export an alert rule group in provisioning file format. @@ -192,13 +201,13 @@ type ProvisionedAlertRule struct { // 200: AlertRuleGroup // 400: ValidationError -// swagger:parameters RouteGetAlertRuleGroup RoutePutAlertRuleGroup RouteGetAlertRuleGroupExport +// swagger:parameters RouteGetAlertRuleGroup RoutePutAlertRuleGroup RouteGetAlertRuleGroupExport RouteDeleteAlertRuleGroup type FolderUIDPathParam struct { // in:path FolderUID string `json:"FolderUID"` } -// swagger:parameters RouteGetAlertRuleGroup RoutePutAlertRuleGroup RouteGetAlertRuleGroupExport +// swagger:parameters RouteGetAlertRuleGroup RoutePutAlertRuleGroup RouteGetAlertRuleGroupExport RouteDeleteAlertRuleGroup type RuleGroupPathParam struct { // in:path Group string `json:"Group"` diff --git a/pkg/services/ngalert/api/tooling/post.json b/pkg/services/ngalert/api/tooling/post.json index 115965f0d92..44b31d7c652 100644 --- a/pkg/services/ngalert/api/tooling/post.json +++ b/pkg/services/ngalert/api/tooling/post.json @@ -7721,6 +7721,44 @@ } }, "/v1/provisioning/folder/{FolderUID}/rule-groups/{Group}": { + "delete": { + "description": "Delete rule group", + "operationId": "RouteDeleteAlertRuleGroup", + "parameters": [ + { + "in": "path", + "name": "FolderUID", + "required": true, + "type": "string" + }, + { + "in": "path", + "name": "Group", + "required": true, + "type": "string" + } + ], + "responses": { + "204": { + "description": " The alert rule group was deleted successfully." + }, + "403": { + "description": "ForbiddenError", + "schema": { + "$ref": "#/definitions/ForbiddenError" + } + }, + "404": { + "description": "NotFound", + "schema": { + "$ref": "#/definitions/NotFound" + } + } + }, + "tags": [ + "provisioning" + ] + }, "get": { "operationId": "RouteGetAlertRuleGroup", "parameters": [ diff --git a/pkg/services/ngalert/api/tooling/spec.json b/pkg/services/ngalert/api/tooling/spec.json index 3177242b6b5..90aae29c3e5 100644 --- a/pkg/services/ngalert/api/tooling/spec.json +++ b/pkg/services/ngalert/api/tooling/spec.json @@ -2690,6 +2690,45 @@ } } } + }, + "delete": { + "description": "Delete rule group", + "tags": [ + "provisioning", + "stable" + ], + "operationId": "RouteDeleteAlertRuleGroup", + "parameters": [ + { + "type": "string", + "name": "FolderUID", + "in": "path", + "required": true + }, + { + "type": "string", + "name": "Group", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": " The alert rule group was deleted successfully." + }, + "403": { + "description": "ForbiddenError", + "schema": { + "$ref": "#/definitions/ForbiddenError" + } + }, + "404": { + "description": "NotFound", + "schema": { + "$ref": "#/definitions/NotFound" + } + } + } } }, "/v1/provisioning/folder/{FolderUID}/rule-groups/{Group}/export": { diff --git a/pkg/services/ngalert/provisioning/alert_rules.go b/pkg/services/ngalert/provisioning/alert_rules.go index bf7e93b3a1a..5e2a95277f4 100644 --- a/pkg/services/ngalert/provisioning/alert_rules.go +++ b/pkg/services/ngalert/provisioning/alert_rules.go @@ -276,6 +276,38 @@ func (service *AlertRuleService) ReplaceRuleGroup(ctx context.Context, orgID int return service.persistDelta(ctx, orgID, delta, userID, provenance) } +func (service *AlertRuleService) DeleteRuleGroup(ctx context.Context, orgID int64, namespaceUID, group string, provenance models.Provenance) error { + // List all rules in the group. + q := models.ListAlertRulesQuery{ + OrgID: orgID, + NamespaceUIDs: []string{namespaceUID}, + RuleGroup: group, + } + ruleList, err := service.ruleStore.ListAlertRules(ctx, &q) + if err != nil { + return err + } + if len(ruleList) == 0 { + return store.ErrAlertRuleGroupNotFound + } + + // Check provenance for all rules in the group. Fail to delete if any deletions aren't allowed. + for _, rule := range ruleList { + storedProvenance, err := service.provenanceStore.GetProvenance(ctx, rule, rule.OrgID) + if err != nil { + return err + } + if storedProvenance != provenance && storedProvenance != models.ProvenanceNone { + return fmt.Errorf("cannot delete with provided provenance '%s', needs '%s'", provenance, storedProvenance) + } + } + + // Delete all rules. + return service.xact.InTransaction(ctx, func(ctx context.Context) error { + return service.deleteRules(ctx, orgID, ruleList...) + }) +} + func (service *AlertRuleService) calcDelta(ctx context.Context, orgID int64, group models.AlertRuleGroup) (*store.GroupDelta, error) { // If the provided request did not provide the rules list at all, treat it as though it does not wish to change rules. // This is done for backwards compatibility. Requests which specify only the interval must update only the interval. diff --git a/public/api-merged.json b/public/api-merged.json index c5288644073..cc15b13c83c 100644 --- a/public/api-merged.json +++ b/public/api-merged.json @@ -10904,6 +10904,44 @@ } } } + }, + "delete": { + "description": "Delete rule group", + "tags": [ + "provisioning" + ], + "operationId": "RouteDeleteAlertRuleGroup", + "parameters": [ + { + "type": "string", + "name": "FolderUID", + "in": "path", + "required": true + }, + { + "type": "string", + "name": "Group", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": " The alert rule group was deleted successfully." + }, + "403": { + "description": "ForbiddenError", + "schema": { + "$ref": "#/definitions/ForbiddenError" + } + }, + "404": { + "description": "NotFound", + "schema": { + "$ref": "#/definitions/NotFound" + } + } + } } }, "/v1/provisioning/folder/{FolderUID}/rule-groups/{Group}/export": { diff --git a/public/openapi3.json b/public/openapi3.json index 718c294c00e..0f05866592f 100644 --- a/public/openapi3.json +++ b/public/openapi3.json @@ -24656,6 +24656,56 @@ } }, "/v1/provisioning/folder/{FolderUID}/rule-groups/{Group}": { + "delete": { + "description": "Delete rule group", + "operationId": "RouteDeleteAlertRuleGroup", + "parameters": [ + { + "in": "path", + "name": "FolderUID", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "Group", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": " The alert rule group was deleted successfully." + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenError" + } + } + }, + "description": "ForbiddenError" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFound" + } + } + }, + "description": "NotFound" + } + }, + "tags": [ + "provisioning" + ] + }, "get": { "operationId": "RouteGetAlertRuleGroup", "parameters": [