From 9af30f6570fef5a865f1b77eb65a57c6a63eab57 Mon Sep 17 00:00:00 2001 From: Alexander Weaver Date: Tue, 17 May 2022 13:42:48 -0500 Subject: [PATCH] Alerting: Provisioning GET routes for mute timings (#49044) * Define GET routes and run codegen * Wire up forked and non-generated API * Implement and wire * Tests, authorization * Fix linter error --- pkg/services/ngalert/api/api.go | 2 + pkg/services/ngalert/api/api_provisioning.go | 27 ++++ pkg/services/ngalert/api/authorization.go | 4 +- .../ngalert/api/authorization_test.go | 2 +- .../ngalert/api/forked_provisioning.go | 8 ++ .../api/generated_base_api_provisioning.go | 30 +++++ .../definitions/provisioning_mute_timings.go | 23 ++++ pkg/services/ngalert/api/tooling/post.json | 55 ++++++-- pkg/services/ngalert/api/tooling/spec.json | 49 +++++++- pkg/services/ngalert/ngalert.go | 2 + .../ngalert/provisioning/mute_timings.go | 51 ++++++++ .../ngalert/provisioning/mute_timings_test.go | 118 ++++++++++++++++++ .../api/alerting/api_provisioning_test.go | 44 +++++++ 13 files changed, 400 insertions(+), 15 deletions(-) create mode 100644 pkg/services/ngalert/api/tooling/definitions/provisioning_mute_timings.go create mode 100644 pkg/services/ngalert/provisioning/mute_timings.go create mode 100644 pkg/services/ngalert/provisioning/mute_timings_test.go diff --git a/pkg/services/ngalert/api/api.go b/pkg/services/ngalert/api/api.go index 7da24a5d636..c163b0f2836 100644 --- a/pkg/services/ngalert/api/api.go +++ b/pkg/services/ngalert/api/api.go @@ -80,6 +80,7 @@ type API struct { Policies *provisioning.NotificationPolicyService ContactPointService *provisioning.ContactPointService Templates *provisioning.TemplateService + MuteTimings *provisioning.MuteTimingService } // RegisterAPIEndpoints registers API handlers @@ -140,6 +141,7 @@ func (api *API) RegisterAPIEndpoints(m *metrics.API) { policies: api.Policies, contactPointService: api.ContactPointService, templates: api.Templates, + muteTimings: api.MuteTimings, }), m) } } diff --git a/pkg/services/ngalert/api/api_provisioning.go b/pkg/services/ngalert/api/api_provisioning.go index 89226730f0d..3bcd608b906 100644 --- a/pkg/services/ngalert/api/api_provisioning.go +++ b/pkg/services/ngalert/api/api_provisioning.go @@ -21,6 +21,7 @@ type ProvisioningSrv struct { policies NotificationPolicyService contactPointService ContactPointService templates TemplateService + muteTimings MuteTimingService } type ContactPointService interface { @@ -41,6 +42,10 @@ type NotificationPolicyService interface { UpdatePolicyTree(ctx context.Context, orgID int64, tree apimodels.Route, p alerting_models.Provenance) error } +type MuteTimingService interface { + GetMuteTimings(ctx context.Context, orgID int64) ([]apimodels.MuteTiming, error) +} + func (srv *ProvisioningSrv) RouteGetPolicyTree(c *models.ReqContext) response.Response { policies, err := srv.policies.GetPolicyTree(c.Req.Context(), c.OrgId) if errors.Is(err, store.ErrNoAlertmanagerConfiguration) { @@ -153,3 +158,25 @@ func (srv *ProvisioningSrv) RouteDeleteTemplate(c *models.ReqContext) response.R } return response.JSON(http.StatusNoContent, nil) } + +func (srv *ProvisioningSrv) RouteGetMuteTiming(c *models.ReqContext) response.Response { + name := web.Params(c.Req)[":name"] + timings, err := srv.muteTimings.GetMuteTimings(c.Req.Context(), c.OrgId) + if err != nil { + return ErrResp(http.StatusInternalServerError, err, "") + } + for _, timing := range timings { + if name == timing.Name { + return response.JSON(http.StatusOK, timing) + } + } + return response.Empty(http.StatusNotFound) +} + +func (srv *ProvisioningSrv) RouteGetMuteTimings(c *models.ReqContext) response.Response { + timings, err := srv.muteTimings.GetMuteTimings(c.Req.Context(), c.OrgId) + if err != nil { + return ErrResp(http.StatusInternalServerError, err, "") + } + return response.JSON(http.StatusOK, timings) +} diff --git a/pkg/services/ngalert/api/authorization.go b/pkg/services/ngalert/api/authorization.go index 10a248c8d7d..d7b439e65ad 100644 --- a/pkg/services/ngalert/api/authorization.go +++ b/pkg/services/ngalert/api/authorization.go @@ -181,7 +181,9 @@ func (api *API) authorize(method, path string) web.Handler { case http.MethodGet + "/api/provisioning/policies", http.MethodGet + "/api/provisioning/contact-points", http.MethodGet + "/api/provisioning/templates", - http.MethodGet + "/api/provisioning/templates/{name}": + http.MethodGet + "/api/provisioning/templates/{name}", + http.MethodGet + "/api/provisioning/mute-timings", + http.MethodGet + "/api/provisioning/mute-timings/{name}": return middleware.ReqSignedIn case http.MethodPut + "/api/provisioning/policies", diff --git a/pkg/services/ngalert/api/authorization_test.go b/pkg/services/ngalert/api/authorization_test.go index feedc0070cc..b7d0b869206 100644 --- a/pkg/services/ngalert/api/authorization_test.go +++ b/pkg/services/ngalert/api/authorization_test.go @@ -46,7 +46,7 @@ func TestAuthorize(t *testing.T) { } paths[p] = methods } - require.Len(t, paths, 34) + require.Len(t, paths, 36) ac := acmock.New() api := &API{AccessControl: ac} diff --git a/pkg/services/ngalert/api/forked_provisioning.go b/pkg/services/ngalert/api/forked_provisioning.go index 9d42a3c9779..6705c5ba176 100644 --- a/pkg/services/ngalert/api/forked_provisioning.go +++ b/pkg/services/ngalert/api/forked_provisioning.go @@ -58,3 +58,11 @@ func (f *ForkedProvisioningApi) forkRoutePutTemplate(ctx *models.ReqContext, bod func (f *ForkedProvisioningApi) forkRouteDeleteTemplate(ctx *models.ReqContext) response.Response { return f.svc.RouteDeleteTemplate(ctx) } + +func (f *ForkedProvisioningApi) forkRouteGetMuteTiming(ctx *models.ReqContext) response.Response { + return f.svc.RouteGetMuteTiming(ctx) +} + +func (f *ForkedProvisioningApi) forkRouteGetMuteTimings(ctx *models.ReqContext) response.Response { + return f.svc.RouteGetMuteTimings(ctx) +} diff --git a/pkg/services/ngalert/api/generated_base_api_provisioning.go b/pkg/services/ngalert/api/generated_base_api_provisioning.go index 6732a7d4047..66881649af6 100644 --- a/pkg/services/ngalert/api/generated_base_api_provisioning.go +++ b/pkg/services/ngalert/api/generated_base_api_provisioning.go @@ -23,6 +23,8 @@ type ProvisioningApiForkingService interface { RouteDeleteContactpoints(*models.ReqContext) response.Response RouteDeleteTemplate(*models.ReqContext) response.Response RouteGetContactpoints(*models.ReqContext) response.Response + RouteGetMuteTiming(*models.ReqContext) response.Response + RouteGetMuteTimings(*models.ReqContext) response.Response RouteGetPolicyTree(*models.ReqContext) response.Response RouteGetTemplate(*models.ReqContext) response.Response RouteGetTemplates(*models.ReqContext) response.Response @@ -44,6 +46,14 @@ func (f *ForkedProvisioningApi) RouteGetContactpoints(ctx *models.ReqContext) re return f.forkRouteGetContactpoints(ctx) } +func (f *ForkedProvisioningApi) RouteGetMuteTiming(ctx *models.ReqContext) response.Response { + return f.forkRouteGetMuteTiming(ctx) +} + +func (f *ForkedProvisioningApi) RouteGetMuteTimings(ctx *models.ReqContext) response.Response { + return f.forkRouteGetMuteTimings(ctx) +} + func (f *ForkedProvisioningApi) RouteGetPolicyTree(ctx *models.ReqContext) response.Response { return f.forkRouteGetPolicyTree(ctx) } @@ -120,6 +130,26 @@ func (api *API) RegisterProvisioningApiEndpoints(srv ProvisioningApiForkingServi m, ), ) + group.Get( + toMacaronPath("/api/provisioning/mute-timings/{name}"), + api.authorize(http.MethodGet, "/api/provisioning/mute-timings/{name}"), + metrics.Instrument( + http.MethodGet, + "/api/provisioning/mute-timings/{name}", + srv.RouteGetMuteTiming, + m, + ), + ) + group.Get( + toMacaronPath("/api/provisioning/mute-timings"), + api.authorize(http.MethodGet, "/api/provisioning/mute-timings"), + metrics.Instrument( + http.MethodGet, + "/api/provisioning/mute-timings", + srv.RouteGetMuteTimings, + m, + ), + ) group.Get( toMacaronPath("/api/provisioning/policies"), api.authorize(http.MethodGet, "/api/provisioning/policies"), diff --git a/pkg/services/ngalert/api/tooling/definitions/provisioning_mute_timings.go b/pkg/services/ngalert/api/tooling/definitions/provisioning_mute_timings.go new file mode 100644 index 00000000000..fd937890ced --- /dev/null +++ b/pkg/services/ngalert/api/tooling/definitions/provisioning_mute_timings.go @@ -0,0 +1,23 @@ +package definitions + +import prometheus "github.com/prometheus/alertmanager/config" + +// swagger:route GET /api/provisioning/mute-timings provisioning RouteGetMuteTimings +// +// Get all the mute timings. +// +// Responses: +// 200: []MuteTiming +// 400: ValidationError + +// swagger:route GET /api/provisioning/mute-timings/{name} provisioning RouteGetMuteTiming +// +// Get a mute timing. +// +// Responses: +// 200: MuteTiming +// 400: ValidationError + +type MuteTiming struct { + prometheus.MuteTimeInterval +} diff --git a/pkg/services/ngalert/api/tooling/post.json b/pkg/services/ngalert/api/tooling/post.json index 75168096e5f..b84a9e3fdd8 100644 --- a/pkg/services/ngalert/api/tooling/post.json +++ b/pkg/services/ngalert/api/tooling/post.json @@ -3265,6 +3265,7 @@ "$ref": "#/definitions/Duration" }, "gettableAlert": { + "description": "GettableAlert gettable alert", "properties": { "annotations": { "$ref": "#/definitions/labelSet" @@ -3323,9 +3324,7 @@ "status", "updatedAt" ], - "type": "object", - "x-go-name": "GettableAlert", - "x-go-package": "github.com/prometheus/alertmanager/api/v2/models" + "type": "object" }, "gettableAlerts": { "items": { @@ -3336,7 +3335,6 @@ "x-go-package": "github.com/prometheus/alertmanager/api/v2/models" }, "gettableSilence": { - "description": "GettableSilence gettable silence", "properties": { "comment": { "description": "comment", @@ -3388,7 +3386,9 @@ "status", "updatedAt" ], - "type": "object" + "type": "object", + "x-go-name": "GettableSilence", + "x-go-package": "github.com/prometheus/alertmanager/api/v2/models" }, "gettableSilences": { "items": { @@ -3525,6 +3525,7 @@ "x-go-package": "github.com/prometheus/alertmanager/api/v2/models" }, "postableSilence": { + "description": "PostableSilence postable silence", "properties": { "comment": { "description": "comment", @@ -3564,9 +3565,7 @@ "matchers", "startsAt" ], - "type": "object", - "x-go-name": "PostableSilence", - "x-go-package": "github.com/prometheus/alertmanager/api/v2/models" + "type": "object" }, "receiver": { "description": "Receiver receiver", @@ -4905,6 +4904,46 @@ ] } }, + "/api/provisioning/mute-timings": { + "get": { + "operationId": "RouteGetMuteTimings", + "responses": { + "200": { + "$ref": "#/responses/MuteTiming" + }, + "400": { + "description": "ValidationError", + "schema": { + "$ref": "#/definitions/ValidationError" + } + } + }, + "summary": "Get all the mute timings.", + "tags": [ + "provisioning" + ] + } + }, + "/api/provisioning/mute-timings/{name}": { + "get": { + "operationId": "RouteGetMuteTiming", + "responses": { + "200": { + "$ref": "#/responses/MuteTiming" + }, + "400": { + "description": "ValidationError", + "schema": { + "$ref": "#/definitions/ValidationError" + } + } + }, + "summary": "Get a mute timing.", + "tags": [ + "provisioning" + ] + } + }, "/api/provisioning/policies": { "get": { "operationId": "RouteGetPolicyTree", diff --git a/pkg/services/ngalert/api/tooling/spec.json b/pkg/services/ngalert/api/tooling/spec.json index c98e2381a0f..e49dc7855d8 100644 --- a/pkg/services/ngalert/api/tooling/spec.json +++ b/pkg/services/ngalert/api/tooling/spec.json @@ -1228,6 +1228,46 @@ } } }, + "/api/provisioning/mute-timings": { + "get": { + "tags": [ + "provisioning" + ], + "summary": "Get all the mute timings.", + "operationId": "RouteGetMuteTimings", + "responses": { + "200": { + "$ref": "#/responses/MuteTiming" + }, + "400": { + "description": "ValidationError", + "schema": { + "$ref": "#/definitions/ValidationError" + } + } + } + } + }, + "/api/provisioning/mute-timings/{name}": { + "get": { + "tags": [ + "provisioning" + ], + "summary": "Get a mute timing.", + "operationId": "RouteGetMuteTiming", + "responses": { + "200": { + "$ref": "#/responses/MuteTiming" + }, + "400": { + "description": "ValidationError", + "schema": { + "$ref": "#/definitions/ValidationError" + } + } + } + } + }, "/api/provisioning/policies": { "get": { "tags": [ @@ -5245,6 +5285,7 @@ "$ref": "#/definitions/Duration" }, "gettableAlert": { + "description": "GettableAlert gettable alert", "type": "object", "required": [ "labels", @@ -5304,8 +5345,6 @@ "x-go-name": "UpdatedAt" } }, - "x-go-name": "GettableAlert", - "x-go-package": "github.com/prometheus/alertmanager/api/v2/models", "$ref": "#/definitions/gettableAlert" }, "gettableAlerts": { @@ -5318,7 +5357,6 @@ "$ref": "#/definitions/gettableAlerts" }, "gettableSilence": { - "description": "GettableSilence gettable silence", "type": "object", "required": [ "comment", @@ -5371,6 +5409,8 @@ "x-go-name": "UpdatedAt" } }, + "x-go-name": "GettableSilence", + "x-go-package": "github.com/prometheus/alertmanager/api/v2/models", "$ref": "#/definitions/gettableSilence" }, "gettableSilences": { @@ -5509,6 +5549,7 @@ "x-go-package": "github.com/prometheus/alertmanager/api/v2/models" }, "postableSilence": { + "description": "PostableSilence postable silence", "type": "object", "required": [ "comment", @@ -5549,8 +5590,6 @@ "x-go-name": "StartsAt" } }, - "x-go-name": "PostableSilence", - "x-go-package": "github.com/prometheus/alertmanager/api/v2/models", "$ref": "#/definitions/postableSilence" }, "receiver": { diff --git a/pkg/services/ngalert/ngalert.go b/pkg/services/ngalert/ngalert.go index a348491d562..dcb38a17ced 100644 --- a/pkg/services/ngalert/ngalert.go +++ b/pkg/services/ngalert/ngalert.go @@ -141,6 +141,7 @@ func (ng *AlertNG) init() error { policyService := provisioning.NewNotificationPolicyService(store, store, store, 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) api := api.API{ Cfg: ng.Cfg, @@ -163,6 +164,7 @@ func (ng *AlertNG) init() error { Policies: policyService, ContactPointService: contactPointService, Templates: templateService, + MuteTimings: muteTimingService, } api.RegisterAPIEndpoints(ng.Metrics.GetAPIMetrics()) diff --git a/pkg/services/ngalert/provisioning/mute_timings.go b/pkg/services/ngalert/provisioning/mute_timings.go new file mode 100644 index 00000000000..97fa455d6af --- /dev/null +++ b/pkg/services/ngalert/provisioning/mute_timings.go @@ -0,0 +1,51 @@ +package provisioning + +import ( + "context" + "fmt" + + "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" +) + +type MuteTimingService struct { + config AMConfigStore + prov ProvisioningStore + xact TransactionManager + log log.Logger +} + +func NewMuteTimingService(config AMConfigStore, prov ProvisioningStore, xact TransactionManager, log log.Logger) *MuteTimingService { + return &MuteTimingService{ + config: config, + prov: prov, + xact: xact, + log: log, + } +} + +func (m *MuteTimingService) GetMuteTimings(ctx context.Context, orgID int64) ([]definitions.MuteTiming, error) { + q := models.GetLatestAlertmanagerConfigurationQuery{ + OrgID: orgID, + } + err := m.config.GetLatestAlertmanagerConfiguration(ctx, &q) + if err != nil { + return nil, err + } + + if q.Result == nil { + return nil, fmt.Errorf("no alertmanager configuration present in this org") + } + + cfg, err := DeserializeAlertmanagerConfig([]byte(q.Result.AlertmanagerConfiguration)) + if err != nil { + return nil, err + } + + result := make([]definitions.MuteTiming, 0, len(cfg.AlertmanagerConfig.MuteTimeIntervals)) + for _, interval := range cfg.AlertmanagerConfig.MuteTimeIntervals { + result = append(result, definitions.MuteTiming{MuteTimeInterval: interval}) + } + return result, nil +} diff --git a/pkg/services/ngalert/provisioning/mute_timings_test.go b/pkg/services/ngalert/provisioning/mute_timings_test.go new file mode 100644 index 00000000000..266edc5dd53 --- /dev/null +++ b/pkg/services/ngalert/provisioning/mute_timings_test.go @@ -0,0 +1,118 @@ +package provisioning + +import ( + "context" + "fmt" + "testing" + + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/services/ngalert/models" + mock "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +func TestMuteTimingService(t *testing.T) { + t.Run("service returns timings from config file", func(t *testing.T) { + sut := createMuteTimingSvcSut() + sut.config.(*MockAMConfigStore).EXPECT(). + getsConfig(models.AlertConfiguration{ + AlertmanagerConfiguration: configWithMuteTimings, + }) + + result, err := sut.GetMuteTimings(context.Background(), 1) + + require.NoError(t, err) + require.Len(t, result, 1) + require.Equal(t, "asdf", result[0].Name) + }) + + t.Run("service returns empty map when config file contains no templates", func(t *testing.T) { + sut := createMuteTimingSvcSut() + sut.config.(*MockAMConfigStore).EXPECT(). + getsConfig(models.AlertConfiguration{ + AlertmanagerConfiguration: defaultConfig, + }) + + result, err := sut.GetMuteTimings(context.Background(), 1) + + require.NoError(t, err) + require.Empty(t, result) + }) + + t.Run("service propagates errors", func(t *testing.T) { + t.Run("when unable to read config", func(t *testing.T) { + sut := createMuteTimingSvcSut() + sut.config.(*MockAMConfigStore).EXPECT(). + GetLatestAlertmanagerConfiguration(mock.Anything, mock.Anything). + Return(fmt.Errorf("failed")) + + _, err := sut.GetMuteTimings(context.Background(), 1) + + require.Error(t, err) + }) + + t.Run("when config is invalid", func(t *testing.T) { + sut := createMuteTimingSvcSut() + sut.config.(*MockAMConfigStore).EXPECT(). + getsConfig(models.AlertConfiguration{ + AlertmanagerConfiguration: brokenConfig, + }) + + _, err := sut.GetMuteTimings(context.Background(), 1) + + require.ErrorContains(t, err, "failed to deserialize") + }) + + t.Run("when no AM config in current org", func(t *testing.T) { + sut := createMuteTimingSvcSut() + sut.config.(*MockAMConfigStore).EXPECT(). + GetLatestAlertmanagerConfiguration(mock.Anything, mock.Anything). + Return(nil) + + _, err := sut.GetMuteTimings(context.Background(), 1) + + require.ErrorContains(t, err, "no alertmanager configuration") + }) + }) +} + +func createMuteTimingSvcSut() *MuteTimingService { + return &MuteTimingService{ + config: &MockAMConfigStore{}, + prov: &MockProvisioningStore{}, + xact: newNopTransactionManager(), + log: log.NewNopLogger(), + } +} + +var configWithMuteTimings = ` +{ + "template_files": { + "a": "template" + }, + "alertmanager_config": { + "route": { + "receiver": "grafana-default-email" + }, + "mute_time_intervals": [{ + "name": "asdf", + "time_intervals": [{ + "times": [], + "weekdays": ["monday"] + }] + }], + "receivers": [{ + "name": "grafana-default-email", + "grafana_managed_receiver_configs": [{ + "uid": "", + "name": "email receiver", + "type": "email", + "isDefault": true, + "settings": { + "addresses": "" + } + }] + }] + } +} +` diff --git a/pkg/tests/api/alerting/api_provisioning_test.go b/pkg/tests/api/alerting/api_provisioning_test.go index 27080e7f117..18e962eb529 100644 --- a/pkg/tests/api/alerting/api_provisioning_test.go +++ b/pkg/tests/api/alerting/api_provisioning_test.go @@ -273,6 +273,50 @@ func TestProvisioning(t *testing.T) { require.Equal(t, 200, resp.StatusCode) }) }) + + t.Run("when provisioning mute timings", func(t *testing.T) { + url := fmt.Sprintf("http://%s/api/provisioning/mute-timings", grafanaListedAddr) + + t.Run("un-authenticated GET should 401", func(t *testing.T) { + req := createTestRequest("GET", url, "", "") + + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + require.NoError(t, resp.Body.Close()) + + require.Equal(t, 401, resp.StatusCode) + }) + + t.Run("viewer GET should succeed", func(t *testing.T) { + req := createTestRequest("GET", url, "viewer", "") + + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + require.NoError(t, resp.Body.Close()) + + require.Equal(t, 200, resp.StatusCode) + }) + + t.Run("editor GET should succeed", func(t *testing.T) { + req := createTestRequest("GET", url, "editor", "") + + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + require.NoError(t, resp.Body.Close()) + + require.Equal(t, 200, resp.StatusCode) + }) + + t.Run("admin GET should succeed", func(t *testing.T) { + req := createTestRequest("GET", url, "admin", "") + + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + require.NoError(t, resp.Body.Close()) + + require.Equal(t, 200, resp.StatusCode) + }) + }) } func createTestRequest(method string, url string, user string, body string) *http.Request {