From fce283d73e079984d03e502fd123dd9ae6cf16dd Mon Sep 17 00:00:00 2001 From: Alexander Weaver Date: Fri, 8 Jul 2022 16:23:18 -0500 Subject: [PATCH] Alerting: Add method to reset notification policy tree back to the default (#51934) * Define route and run codegen * Wire up HTTP layer * Update API layer and test fakes * Implement reset of policy tree * Implement service layer test and authorization bindings * API layer testing * Be more specific when injecting settings --- pkg/services/ngalert/api/api_provisioning.go | 9 ++++ .../ngalert/api/api_provisioning_test.go | 34 +++++++++++++ pkg/services/ngalert/api/authorization.go | 1 + .../ngalert/api/forked_provisioning.go | 4 ++ .../api/generated_base_api_provisioning.go | 14 ++++++ pkg/services/ngalert/api/tooling/api.json | 21 +++++++- .../definitions/provisioning_policies.go | 10 ++++ pkg/services/ngalert/api/tooling/post.json | 19 ++++++++ pkg/services/ngalert/api/tooling/spec.json | 21 ++++++++ pkg/services/ngalert/ngalert.go | 2 +- .../provisioning/notification_policies.go | 48 ++++++++++++++++++- .../notification_policies_test.go | 15 ++++++ 12 files changed, 195 insertions(+), 3 deletions(-) diff --git a/pkg/services/ngalert/api/api_provisioning.go b/pkg/services/ngalert/api/api_provisioning.go index 2569d256b9f..5cc6c97f0d4 100644 --- a/pkg/services/ngalert/api/api_provisioning.go +++ b/pkg/services/ngalert/api/api_provisioning.go @@ -40,6 +40,7 @@ type TemplateService interface { type NotificationPolicyService interface { GetPolicyTree(ctx context.Context, orgID int64) (definitions.Route, error) UpdatePolicyTree(ctx context.Context, orgID int64, tree definitions.Route, p alerting_models.Provenance) error + ResetPolicyTree(ctx context.Context, orgID int64) (definitions.Route, error) } type MuteTimingService interface { @@ -85,6 +86,14 @@ func (srv *ProvisioningSrv) RoutePutPolicyTree(c *models.ReqContext, tree defini return response.JSON(http.StatusAccepted, util.DynMap{"message": "policies updated"}) } +func (srv *ProvisioningSrv) RouteResetPolicyTree(c *models.ReqContext) response.Response { + tree, err := srv.policies.ResetPolicyTree(c.Req.Context(), c.OrgId) + if err != nil { + return ErrResp(http.StatusInternalServerError, err, "") + } + return response.JSON(http.StatusAccepted, tree) +} + func (srv *ProvisioningSrv) RouteGetContactPoints(c *models.ReqContext) response.Response { cps, err := srv.contactPointService.GetContactPoints(c.Req.Context(), c.OrgId) if err != nil { diff --git a/pkg/services/ngalert/api/api_provisioning_test.go b/pkg/services/ngalert/api/api_provisioning_test.go index 88574902f5f..e176402fcea 100644 --- a/pkg/services/ngalert/api/api_provisioning_test.go +++ b/pkg/services/ngalert/api/api_provisioning_test.go @@ -44,6 +44,15 @@ func TestProvisioningApi(t *testing.T) { require.Equal(t, 202, response.Status()) }) + t.Run("successful DELETE returns 202", func(t *testing.T) { + sut := createProvisioningSrvSut(t) + rc := createTestRequestCtx() + + response := sut.RouteResetPolicyTree(&rc) + + require.Equal(t, 202, response.Status()) + }) + t.Run("when new policy tree is invalid", func(t *testing.T) { t.Run("PUT returns 400", func(t *testing.T) { sut := createProvisioningSrvSut(t) @@ -106,6 +115,18 @@ func TestProvisioningApi(t *testing.T) { require.NotEmpty(t, response.Body()) require.Contains(t, string(response.Body()), "something went wrong") }) + + t.Run("DELETE returns 500", func(t *testing.T) { + sut := createProvisioningSrvSut(t) + sut.policies = &fakeFailingNotificationPolicyService{} + rc := createTestRequestCtx() + + response := sut.RouteResetPolicyTree(&rc) + + require.Equal(t, 500, response.Status()) + require.NotEmpty(t, response.Body()) + require.Contains(t, string(response.Body()), "something went wrong") + }) }) }) @@ -335,6 +356,11 @@ func (f *fakeNotificationPolicyService) UpdatePolicyTree(ctx context.Context, or return nil } +func (f *fakeNotificationPolicyService) ResetPolicyTree(ctx context.Context, orgID int64) (definitions.Route, error) { + f.tree = definitions.Route{} // TODO + return f.tree, nil +} + type fakeFailingNotificationPolicyService struct{} func (f *fakeFailingNotificationPolicyService) GetPolicyTree(ctx context.Context, orgID int64) (definitions.Route, error) { @@ -345,6 +371,10 @@ func (f *fakeFailingNotificationPolicyService) UpdatePolicyTree(ctx context.Cont return fmt.Errorf("something went wrong") } +func (f *fakeFailingNotificationPolicyService) ResetPolicyTree(ctx context.Context, orgID int64) (definitions.Route, error) { + return definitions.Route{}, fmt.Errorf("something went wrong") +} + type fakeRejectingNotificationPolicyService struct{} func (f *fakeRejectingNotificationPolicyService) GetPolicyTree(ctx context.Context, orgID int64) (definitions.Route, error) { @@ -355,6 +385,10 @@ func (f *fakeRejectingNotificationPolicyService) UpdatePolicyTree(ctx context.Co return fmt.Errorf("%w: invalid policy tree", provisioning.ErrValidation) } +func (f *fakeRejectingNotificationPolicyService) ResetPolicyTree(ctx context.Context, orgID int64) (definitions.Route, error) { + return definitions.Route{}, nil +} + func createInvalidContactPoint() definitions.EmbeddedContactPoint { settings, _ := simplejson.NewJson([]byte(`{}`)) return definitions.EmbeddedContactPoint{ diff --git a/pkg/services/ngalert/api/authorization.go b/pkg/services/ngalert/api/authorization.go index 49f2ad16cd2..f5cd78b3140 100644 --- a/pkg/services/ngalert/api/authorization.go +++ b/pkg/services/ngalert/api/authorization.go @@ -191,6 +191,7 @@ func (api *API) authorize(method, path string) web.Handler { eval = ac.EvalPermission(ac.ActionAlertingProvisioningRead) // organization scope case http.MethodPut + "/api/v1/provisioning/policies", + http.MethodDelete + "/api/v1/provisioning/policies", http.MethodPost + "/api/v1/provisioning/contact-points", http.MethodPut + "/api/v1/provisioning/contact-points/{UID}", http.MethodDelete + "/api/v1/provisioning/contact-points/{UID}", diff --git a/pkg/services/ngalert/api/forked_provisioning.go b/pkg/services/ngalert/api/forked_provisioning.go index 3339ab10d1f..2f61d026795 100644 --- a/pkg/services/ngalert/api/forked_provisioning.go +++ b/pkg/services/ngalert/api/forked_provisioning.go @@ -27,6 +27,10 @@ func (f *ForkedProvisioningApi) forkRoutePutPolicyTree(ctx *models.ReqContext, r return f.svc.RoutePutPolicyTree(ctx, route) } +func (f *ForkedProvisioningApi) forkRouteResetPolicyTree(ctx *models.ReqContext) response.Response { + return f.svc.RouteResetPolicyTree(ctx) +} + func (f *ForkedProvisioningApi) forkRouteGetContactpoints(ctx *models.ReqContext) response.Response { return f.svc.RouteGetContactPoints(ctx) } diff --git a/pkg/services/ngalert/api/generated_base_api_provisioning.go b/pkg/services/ngalert/api/generated_base_api_provisioning.go index 8fb66c89cc1..a0e9e6aedf7 100644 --- a/pkg/services/ngalert/api/generated_base_api_provisioning.go +++ b/pkg/services/ngalert/api/generated_base_api_provisioning.go @@ -40,6 +40,7 @@ type ProvisioningApiForkingService interface { RoutePutMuteTiming(*models.ReqContext) response.Response RoutePutPolicyTree(*models.ReqContext) response.Response RoutePutTemplate(*models.ReqContext) response.Response + RouteResetPolicyTree(*models.ReqContext) response.Response } func (f *ForkedProvisioningApi) RouteDeleteAlertRule(ctx *models.ReqContext) response.Response { @@ -156,6 +157,9 @@ func (f *ForkedProvisioningApi) RoutePutTemplate(ctx *models.ReqContext) respons } return f.forkRoutePutTemplate(ctx, conf, nameParam) } +func (f *ForkedProvisioningApi) RouteResetPolicyTree(ctx *models.ReqContext) response.Response { + return f.forkRouteResetPolicyTree(ctx) +} func (api *API) RegisterProvisioningApiEndpoints(srv ProvisioningApiForkingService, m *metrics.API) { api.RouteRegister.Group("", func(group routing.RouteRegister) { @@ -369,5 +373,15 @@ func (api *API) RegisterProvisioningApiEndpoints(srv ProvisioningApiForkingServi m, ), ) + group.Delete( + toMacaronPath("/api/v1/provisioning/policies"), + api.authorize(http.MethodDelete, "/api/v1/provisioning/policies"), + metrics.Instrument( + http.MethodDelete, + "/api/v1/provisioning/policies", + srv.RouteResetPolicyTree, + m, + ), + ) }, middleware.ReqSignedIn) } diff --git a/pkg/services/ngalert/api/tooling/api.json b/pkg/services/ngalert/api/tooling/api.json index ac160369476..3078e893cf3 100644 --- a/pkg/services/ngalert/api/tooling/api.json +++ b/pkg/services/ngalert/api/tooling/api.json @@ -2785,6 +2785,7 @@ "type": "object" }, "alertGroups": { + "description": "AlertGroups alert groups", "items": { "$ref": "#/definitions/alertGroup" }, @@ -3113,6 +3114,7 @@ "type": "array" }, "postableSilence": { + "description": "PostableSilence postable silence", "properties": { "comment": { "description": "comment", @@ -3150,7 +3152,6 @@ "type": "object" }, "receiver": { - "description": "Receiver receiver", "properties": { "name": { "description": "name", @@ -3718,6 +3719,24 @@ } }, "/api/v1/provisioning/policies": { + "delete": { + "consumes": [ + "application/json" + ], + "operationId": "RouteResetPolicyTree", + "responses": { + "202": { + "description": "Ack", + "schema": { + "$ref": "#/definitions/Ack" + } + } + }, + "summary": "Clears the notification policy tree.", + "tags": [ + "provisioning" + ] + }, "get": { "operationId": "RouteGetPolicyTree", "responses": { diff --git a/pkg/services/ngalert/api/tooling/definitions/provisioning_policies.go b/pkg/services/ngalert/api/tooling/definitions/provisioning_policies.go index e1f0f48859a..e68e73f06df 100644 --- a/pkg/services/ngalert/api/tooling/definitions/provisioning_policies.go +++ b/pkg/services/ngalert/api/tooling/definitions/provisioning_policies.go @@ -19,6 +19,16 @@ package definitions // 202: Ack // 400: ValidationError +// swagger:route DELETE /api/v1/provisioning/policies provisioning stable RouteResetPolicyTree +// +// Clears the notification policy tree. +// +// Consumes: +// - application/json +// +// Responses: +// 202: Ack + // swagger:parameters RoutePutPolicyTree type Policytree struct { // The new notification routing tree to use diff --git a/pkg/services/ngalert/api/tooling/post.json b/pkg/services/ngalert/api/tooling/post.json index fd193aeeecc..d5181930c97 100644 --- a/pkg/services/ngalert/api/tooling/post.json +++ b/pkg/services/ngalert/api/tooling/post.json @@ -3003,6 +3003,7 @@ "type": "object" }, "gettableSilences": { + "description": "GettableSilences gettable silences", "items": { "$ref": "#/definitions/gettableSilence" }, @@ -5344,6 +5345,24 @@ } }, "/api/v1/provisioning/policies": { + "delete": { + "consumes": [ + "application/json" + ], + "operationId": "RouteResetPolicyTree", + "responses": { + "202": { + "description": "Ack", + "schema": { + "$ref": "#/definitions/Ack" + } + } + }, + "summary": "Clears the notification policy tree.", + "tags": [ + "provisioning" + ] + }, "get": { "operationId": "RouteGetPolicyTree", "responses": { diff --git a/pkg/services/ngalert/api/tooling/spec.json b/pkg/services/ngalert/api/tooling/spec.json index a9b374c9c8c..be46b5746e5 100644 --- a/pkg/services/ngalert/api/tooling/spec.json +++ b/pkg/services/ngalert/api/tooling/spec.json @@ -2169,6 +2169,25 @@ } } } + }, + "delete": { + "consumes": [ + "application/json" + ], + "tags": [ + "provisioning", + "stable" + ], + "summary": "Clears the notification policy tree.", + "operationId": "RouteResetPolicyTree", + "responses": { + "202": { + "description": "Ack", + "schema": { + "$ref": "#/definitions/Ack" + } + } + } } }, "/api/v1/provisioning/templates": { @@ -5366,6 +5385,7 @@ "$ref": "#/definitions/gettableSilence" }, "gettableSilences": { + "description": "GettableSilences gettable silences", "type": "array", "items": { "$ref": "#/definitions/gettableSilence" @@ -5477,6 +5497,7 @@ } }, "postableSilence": { + "description": "PostableSilence postable silence", "type": "object", "required": [ "comment", diff --git a/pkg/services/ngalert/ngalert.go b/pkg/services/ngalert/ngalert.go index 1ad04d91cbb..fa95a519bbb 100644 --- a/pkg/services/ngalert/ngalert.go +++ b/pkg/services/ngalert/ngalert.go @@ -158,7 +158,7 @@ func (ng *AlertNG) init() error { ng.schedule = scheduler // Provisioning - policyService := provisioning.NewNotificationPolicyService(store, store, store, ng.Log) + policyService := provisioning.NewNotificationPolicyService(store, store, store, ng.Cfg.UnifiedAlerting, ng.Log) contactPointService := provisioning.NewContactPointService(store, ng.SecretsService, store, store, ng.Log) templateService := provisioning.NewTemplateService(store, store, store, ng.Log) muteTimingService := provisioning.NewMuteTimingService(store, store, store, ng.Log) diff --git a/pkg/services/ngalert/provisioning/notification_policies.go b/pkg/services/ngalert/provisioning/notification_policies.go index 8597360fa03..cbd8d3d23c6 100644 --- a/pkg/services/ngalert/provisioning/notification_policies.go +++ b/pkg/services/ngalert/provisioning/notification_policies.go @@ -7,6 +7,7 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/models" + "github.com/grafana/grafana/pkg/setting" ) type NotificationPolicyService struct { @@ -14,15 +15,17 @@ type NotificationPolicyService struct { provenanceStore ProvisioningStore xact TransactionManager log log.Logger + settings setting.UnifiedAlertingSettings } func NewNotificationPolicyService(am AMConfigStore, prov ProvisioningStore, - xact TransactionManager, log log.Logger) *NotificationPolicyService { + xact TransactionManager, settings setting.UnifiedAlertingSettings, log log.Logger) *NotificationPolicyService { return &NotificationPolicyService{ amStore: am, provenanceStore: prov, xact: xact, log: log, + settings: settings, } } @@ -116,6 +119,49 @@ func (nps *NotificationPolicyService) UpdatePolicyTree(ctx context.Context, orgI return nil } +func (nps *NotificationPolicyService) ResetPolicyTree(ctx context.Context, orgID int64) (definitions.Route, error) { + defaultCfg, err := deserializeAlertmanagerConfig([]byte(nps.settings.DefaultConfiguration)) + if err != nil { + nps.log.Error("failed to parse default alertmanager config: %w", err) + return definitions.Route{}, fmt.Errorf("failed to parse default alertmanager config: %w", err) + } + route := defaultCfg.AlertmanagerConfig.Route + + revision, err := getLastConfiguration(ctx, orgID, nps.amStore) + if err != nil { + return definitions.Route{}, err + } + revision.cfg.AlertmanagerConfig.Config.Route = route + + serialized, err := serializeAlertmanagerConfig(*revision.cfg) + if err != nil { + return definitions.Route{}, err + } + cmd := models.SaveAlertmanagerConfigurationCmd{ + AlertmanagerConfiguration: string(serialized), + ConfigurationVersion: revision.version, + FetchedConfigurationHash: revision.concurrencyToken, + Default: false, + OrgID: orgID, + } + err = nps.xact.InTransaction(ctx, func(ctx context.Context) error { + err := nps.amStore.UpdateAlertmanagerConfiguration(ctx, &cmd) + if err != nil { + return err + } + err = nps.provenanceStore.DeleteProvenance(ctx, route, orgID) + if err != nil { + return err + } + return nil + }) + if err != nil { + return definitions.Route{}, nil + } + + return *route, nil +} + func (nps *NotificationPolicyService) receiversToMap(records []*definitions.PostableApiReceiver) (map[string]struct{}, error) { receivers := map[string]struct{}{} for _, receiver := range records { diff --git a/pkg/services/ngalert/provisioning/notification_policies_test.go b/pkg/services/ngalert/provisioning/notification_policies_test.go index cff09e8899a..da7211791e4 100644 --- a/pkg/services/ngalert/provisioning/notification_policies_test.go +++ b/pkg/services/ngalert/provisioning/notification_policies_test.go @@ -7,6 +7,7 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/models" + "github.com/grafana/grafana/pkg/setting" "github.com/prometheus/alertmanager/config" "github.com/prometheus/alertmanager/timeinterval" "github.com/prometheus/common/model" @@ -213,6 +214,17 @@ func TestNotificationPolicyService(t *testing.T) { require.Error(t, err) require.ErrorIs(t, err, ErrValidation) }) + + t.Run("deleting route replaces with default", func(t *testing.T) { + sut := createNotificationPolicyServiceSut() + + tree, err := sut.ResetPolicyTree(context.Background(), 1) + + require.NoError(t, err) + require.Equal(t, "grafana-default-email", tree.Receiver) + require.Nil(t, tree.Routes) + require.Nil(t, tree.GroupBy) + }) } func createNotificationPolicyServiceSut() *NotificationPolicyService { @@ -221,6 +233,9 @@ func createNotificationPolicyServiceSut() *NotificationPolicyService { provenanceStore: NewFakeProvisioningStore(), xact: newNopTransactionManager(), log: log.NewNopLogger(), + settings: setting.UnifiedAlertingSettings{ + DefaultConfiguration: setting.GetAlertmanagerDefaultConfiguration(), + }, } }