Alerting: Support deleting rule groups in the provisioning API (#83514)

* Alerting: feat: support deleting rule groups in the provisioning API

Adds support for DELETE to the provisioning API's alert rule groups route, which allows deleting the rule group with a
single API call. Previously, groups were deleted by deleting rules one-by-one.

Fixes #81860

This change doesn't add any new paths to the API, only new methods.

---------

Co-authored-by: Yuri Tseretyan <yuriy.tseretyan@grafana.com>
This commit is contained in:
Joe Blubaugh 2024-02-28 23:19:02 +08:00 committed by GitHub
parent 467302480f
commit b905777ba9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 307 additions and 9 deletions

View File

@ -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)

View File

@ -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,

View File

@ -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":

View File

@ -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),

View File

@ -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)
}

View File

@ -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": [

View File

@ -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"`

View File

@ -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": [

View File

@ -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": {

View File

@ -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.

View File

@ -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": {

View File

@ -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": [