diff --git a/pkg/services/ngalert/api/api.go b/pkg/services/ngalert/api/api.go index 0b6123b6603..4562f951d1a 100644 --- a/pkg/services/ngalert/api/api.go +++ b/pkg/services/ngalert/api/api.go @@ -78,6 +78,7 @@ type API struct { AccessControl accesscontrol.AccessControl Policies *provisioning.NotificationPolicyService ContactPointService *provisioning.ContactPointService + Templates *provisioning.TemplateService } // RegisterAPIEndpoints registers API handlers @@ -136,6 +137,7 @@ func (api *API) RegisterAPIEndpoints(m *metrics.API) { log: logger, policies: api.Policies, contactPointService: api.ContactPointService, + templates: api.Templates, }), m) } } diff --git a/pkg/services/ngalert/api/api_provisioning.go b/pkg/services/ngalert/api/api_provisioning.go index 5c4d0baeaa4..fadc94642b3 100644 --- a/pkg/services/ngalert/api/api_provisioning.go +++ b/pkg/services/ngalert/api/api_provisioning.go @@ -20,6 +20,7 @@ type ProvisioningSrv struct { log log.Logger policies NotificationPolicyService contactPointService ContactPointService + templates TemplateService } type ContactPointService interface { @@ -29,6 +30,10 @@ type ContactPointService interface { DeleteContactPoint(ctx context.Context, orgID int64, uid string) error } +type TemplateService interface { + GetTemplates(ctx context.Context, orgID int64) (map[string]string, error) +} + type NotificationPolicyService interface { GetPolicyTree(ctx context.Context, orgID int64) (apimodels.Route, error) UpdatePolicyTree(ctx context.Context, orgID int64, tree apimodels.Route, p alerting_models.Provenance) error @@ -95,3 +100,27 @@ func (srv *ProvisioningSrv) RouteDeleteContactPoint(c *models.ReqContext) respon } return response.JSON(http.StatusAccepted, util.DynMap{"message": "contactpoint deleted"}) } + +func (srv *ProvisioningSrv) RouteGetTemplates(c *models.ReqContext) response.Response { + templates, err := srv.templates.GetTemplates(c.Req.Context(), c.OrgId) + if err != nil { + return ErrResp(http.StatusInternalServerError, err, "") + } + result := make([]apimodels.MessageTemplate, 0, len(templates)) + for k, v := range templates { + result = append(result, apimodels.MessageTemplate{Name: k, Template: v}) + } + return response.JSON(http.StatusOK, result) +} + +func (srv *ProvisioningSrv) RouteGetTemplate(c *models.ReqContext) response.Response { + id := web.Params(c.Req)[":ID"] + templates, err := srv.templates.GetTemplates(c.Req.Context(), c.OrgId) + if err != nil { + return ErrResp(http.StatusInternalServerError, err, "") + } + if tmpl, ok := templates[id]; ok { + return response.JSON(http.StatusOK, apimodels.MessageTemplate{Name: id, Template: tmpl}) + } + return response.Empty(http.StatusNotFound) +} diff --git a/pkg/services/ngalert/api/authorization.go b/pkg/services/ngalert/api/authorization.go index 6394ec50ac3..921ed826ba6 100644 --- a/pkg/services/ngalert/api/authorization.go +++ b/pkg/services/ngalert/api/authorization.go @@ -182,7 +182,9 @@ func (api *API) authorize(method, path string) web.Handler { // Grafana-only Provisioning Read Paths case http.MethodGet + "/api/provisioning/policies", - http.MethodGet + "/api/provisioning/contact-points": + http.MethodGet + "/api/provisioning/contact-points", + http.MethodGet + "/api/provisioning/templates", + http.MethodGet + "/api/provisioning/templates/{ID}": return middleware.ReqSignedIn case http.MethodPost + "/api/provisioning/policies", diff --git a/pkg/services/ngalert/api/authorization_test.go b/pkg/services/ngalert/api/authorization_test.go index 0f6579760e6..bc6c14072a1 100644 --- a/pkg/services/ngalert/api/authorization_test.go +++ b/pkg/services/ngalert/api/authorization_test.go @@ -47,7 +47,7 @@ func TestAuthorize(t *testing.T) { } paths[p] = methods } - require.Len(t, paths, 32) + require.Len(t, paths, 34) 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 a808d9eca3e..a0623822248 100644 --- a/pkg/services/ngalert/api/forked_provisioning.go +++ b/pkg/services/ngalert/api/forked_provisioning.go @@ -42,3 +42,11 @@ func (f *ForkedProvisioningApi) forkRoutePutContactpoints(ctx *models.ReqContext func (f *ForkedProvisioningApi) forkRouteDeleteContactpoints(ctx *models.ReqContext) response.Response { return f.svc.RouteDeleteContactPoint(ctx) } + +func (f *ForkedProvisioningApi) forkRouteGetTemplates(ctx *models.ReqContext) response.Response { + return f.svc.RouteGetTemplates(ctx) +} + +func (f *ForkedProvisioningApi) forkRouteGetTemplate(ctx *models.ReqContext) response.Response { + return f.svc.RouteGetTemplate(ctx) +} diff --git a/pkg/services/ngalert/api/generated_base_api_provisioning.go b/pkg/services/ngalert/api/generated_base_api_provisioning.go index f310cdaddd5..346a60d2f55 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 RouteGetContactpoints(*models.ReqContext) response.Response RouteGetPolicyTree(*models.ReqContext) response.Response + RouteGetTemplate(*models.ReqContext) response.Response + RouteGetTemplates(*models.ReqContext) response.Response RoutePostContactpoints(*models.ReqContext) response.Response RoutePostPolicyTree(*models.ReqContext) response.Response RoutePutContactpoints(*models.ReqContext) response.Response @@ -40,6 +42,14 @@ func (f *ForkedProvisioningApi) RouteGetPolicyTree(ctx *models.ReqContext) respo return f.forkRouteGetPolicyTree(ctx) } +func (f *ForkedProvisioningApi) RouteGetTemplate(ctx *models.ReqContext) response.Response { + return f.forkRouteGetTemplate(ctx) +} + +func (f *ForkedProvisioningApi) RouteGetTemplates(ctx *models.ReqContext) response.Response { + return f.forkRouteGetTemplates(ctx) +} + func (f *ForkedProvisioningApi) RoutePostContactpoints(ctx *models.ReqContext) response.Response { conf := apimodels.EmbeddedContactPoint{} if err := web.Bind(ctx.Req, &conf); err != nil { @@ -96,6 +106,26 @@ func (api *API) RegisterProvisioningApiEndpoints(srv ProvisioningApiForkingServi m, ), ) + group.Get( + toMacaronPath("/api/provisioning/templates/{ID}"), + api.authorize(http.MethodGet, "/api/provisioning/templates/{ID}"), + metrics.Instrument( + http.MethodGet, + "/api/provisioning/templates/{ID}", + srv.RouteGetTemplate, + m, + ), + ) + group.Get( + toMacaronPath("/api/provisioning/templates"), + api.authorize(http.MethodGet, "/api/provisioning/templates"), + metrics.Instrument( + http.MethodGet, + "/api/provisioning/templates", + srv.RouteGetTemplates, + m, + ), + ) group.Post( toMacaronPath("/api/provisioning/contact-points"), api.authorize(http.MethodPost, "/api/provisioning/contact-points"), diff --git a/pkg/services/ngalert/api/tooling/definitions/provisioning_templates.go b/pkg/services/ngalert/api/tooling/definitions/provisioning_templates.go new file mode 100644 index 00000000000..95c3059ae14 --- /dev/null +++ b/pkg/services/ngalert/api/tooling/definitions/provisioning_templates.go @@ -0,0 +1,24 @@ +package definitions + +// swagger:route GET /api/provisioning/templates provisioning RouteGetTemplates +// +// Get all message templates. +// +// Responses: +// 200: []MessageTemplate +// 400: ValidationError + +// swagger:route GET /api/provisioning/templates/{ID} provisioning RouteGetTemplate +// +// Get a message template. +// +// Responses: +// 200: MessageTemplate +// 404: NotFound + +type MessageTemplate struct { + Name string + Template string +} + +type NotFound struct{} diff --git a/pkg/services/ngalert/api/tooling/post.json b/pkg/services/ngalert/api/tooling/post.json index f9373171bf5..85dada96ad6 100644 --- a/pkg/services/ngalert/api/tooling/post.json +++ b/pkg/services/ngalert/api/tooling/post.json @@ -2833,6 +2833,7 @@ "x-go-package": "github.com/prometheus/alertmanager/timeinterval" }, "URL": { + "description": "The general form represented is:\n\n[scheme:][//[userinfo@]host][/]path[?query][#fragment]\n\nURLs that do not start with a slash after the scheme are interpreted as:\n\nscheme:opaque[?query][#fragment]\n\nNote that the Path field is stored in decoded form: /%47%6f%2f becomes /Go/.\nA consequence is that it is impossible to tell which slashes in the Path were\nslashes in the raw URL and which were %2f. This distinction is rarely important,\nbut when it is, the code should use RawPath, an optional field which only gets\nset if the default encoding is different from Path.\n\nURL's String method uses the EscapedPath method to obtain the path. See the\nEscapedPath method for more details.", "properties": { "ForceQuery": { "type": "boolean" @@ -2865,9 +2866,9 @@ "$ref": "#/definitions/Userinfo" } }, - "title": "URL is a custom URL type that allows validation at configuration load time.", + "title": "A URL represents a parsed URL (technically, a URI reference).", "type": "object", - "x-go-package": "github.com/prometheus/common/config" + "x-go-package": "net/url" }, "Userinfo": { "description": "The Userinfo type is an immutable encapsulation of username and\npassword details for a URL. An existing Userinfo value is guaranteed\nto have a username set (potentially empty, as allowed by RFC 2396),\nand optionally a password.", @@ -4919,6 +4920,43 @@ ] } }, + "/api/provisioning/templates": { + "get": { + "operationId": "RouteGetTemplates", + "responses": { + "200": { + "$ref": "#/responses/MessageTemplate" + }, + "400": { + "description": "ValidationError", + "schema": { + "$ref": "#/definitions/ValidationError" + } + } + }, + "summary": "Get all message templates.", + "tags": [ + "provisioning" + ] + } + }, + "/api/provisioning/templates/{ID}": { + "get": { + "operationId": "RouteGetTemplate", + "responses": { + "200": { + "$ref": "#/responses/MessageTemplate" + }, + "404": { + "$ref": "#/responses/NotFound" + } + }, + "summary": "Get a message template.", + "tags": [ + "provisioning" + ] + } + }, "/api/ruler/grafana/api/v1/rules": { "get": { "description": "List rule groups", diff --git a/pkg/services/ngalert/api/tooling/spec.json b/pkg/services/ngalert/api/tooling/spec.json index 188a5c4b885..8f81b1bb71d 100644 --- a/pkg/services/ngalert/api/tooling/spec.json +++ b/pkg/services/ngalert/api/tooling/spec.json @@ -1295,6 +1295,43 @@ } } }, + "/api/provisioning/templates": { + "get": { + "tags": [ + "provisioning" + ], + "summary": "Get all message templates.", + "operationId": "RouteGetTemplates", + "responses": { + "200": { + "$ref": "#/responses/MessageTemplate" + }, + "400": { + "description": "ValidationError", + "schema": { + "$ref": "#/definitions/ValidationError" + } + } + } + } + }, + "/api/provisioning/templates/{ID}": { + "get": { + "tags": [ + "provisioning" + ], + "summary": "Get a message template.", + "operationId": "RouteGetTemplate", + "responses": { + "200": { + "$ref": "#/responses/MessageTemplate" + }, + "404": { + "$ref": "#/responses/NotFound" + } + } + } + }, "/api/ruler/grafana/api/v1/rules": { "get": { "description": "List rule groups", @@ -4753,8 +4790,9 @@ "x-go-package": "github.com/prometheus/alertmanager/timeinterval" }, "URL": { + "description": "The general form represented is:\n\n[scheme:][//[userinfo@]host][/]path[?query][#fragment]\n\nURLs that do not start with a slash after the scheme are interpreted as:\n\nscheme:opaque[?query][#fragment]\n\nNote that the Path field is stored in decoded form: /%47%6f%2f becomes /Go/.\nA consequence is that it is impossible to tell which slashes in the Path were\nslashes in the raw URL and which were %2f. This distinction is rarely important,\nbut when it is, the code should use RawPath, an optional field which only gets\nset if the default encoding is different from Path.\n\nURL's String method uses the EscapedPath method to obtain the path. See the\nEscapedPath method for more details.", "type": "object", - "title": "URL is a custom URL type that allows validation at configuration load time.", + "title": "A URL represents a parsed URL (technically, a URI reference).", "properties": { "ForceQuery": { "type": "boolean" @@ -4787,7 +4825,7 @@ "$ref": "#/definitions/Userinfo" } }, - "x-go-package": "github.com/prometheus/common/config" + "x-go-package": "net/url" }, "Userinfo": { "description": "The Userinfo type is an immutable encapsulation of username and\npassword details for a URL. An existing Userinfo value is guaranteed\nto have a username set (potentially empty, as allowed by RFC 2396),\nand optionally a password.", diff --git a/pkg/services/ngalert/ngalert.go b/pkg/services/ngalert/ngalert.go index b47cc404a33..477842c86c6 100644 --- a/pkg/services/ngalert/ngalert.go +++ b/pkg/services/ngalert/ngalert.go @@ -140,6 +140,7 @@ func (ng *AlertNG) init() error { // Provisioning 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) api := api.API{ Cfg: ng.Cfg, @@ -160,6 +161,7 @@ func (ng *AlertNG) init() error { AccessControl: ng.accesscontrol, Policies: policyService, ContactPointService: contactPointService, + Templates: templateService, } api.RegisterAPIEndpoints(ng.Metrics.GetAPIMetrics()) diff --git a/pkg/services/ngalert/provisioning/persist.go b/pkg/services/ngalert/provisioning/persist.go index 9368d79f0b7..3e52e481256 100644 --- a/pkg/services/ngalert/provisioning/persist.go +++ b/pkg/services/ngalert/provisioning/persist.go @@ -7,6 +7,7 @@ import ( ) // AMStore is a store of Alertmanager configurations. +//go:generate mockery --name AMConfigStore --structname MockAMConfigStore --inpackage --filename persist_mock.go --with-expecter type AMConfigStore interface { GetLatestAlertmanagerConfiguration(ctx context.Context, query *models.GetLatestAlertmanagerConfigurationQuery) error UpdateAlertmanagerConfiguration(ctx context.Context, cmd *models.SaveAlertmanagerConfigurationCmd) error diff --git a/pkg/services/ngalert/provisioning/persist_mock.go b/pkg/services/ngalert/provisioning/persist_mock.go new file mode 100644 index 00000000000..5f292cacd93 --- /dev/null +++ b/pkg/services/ngalert/provisioning/persist_mock.go @@ -0,0 +1,111 @@ +// Code generated by mockery v2.12.0. DO NOT EDIT. + +package provisioning + +import ( + context "context" + + models "github.com/grafana/grafana/pkg/services/ngalert/models" + mock "github.com/stretchr/testify/mock" + + testing "testing" +) + +// MockAMConfigStore is an autogenerated mock type for the AMConfigStore type +type MockAMConfigStore struct { + mock.Mock +} + +type MockAMConfigStore_Expecter struct { + mock *mock.Mock +} + +func (_m *MockAMConfigStore) EXPECT() *MockAMConfigStore_Expecter { + return &MockAMConfigStore_Expecter{mock: &_m.Mock} +} + +// GetLatestAlertmanagerConfiguration provides a mock function with given fields: ctx, query +func (_m *MockAMConfigStore) GetLatestAlertmanagerConfiguration(ctx context.Context, query *models.GetLatestAlertmanagerConfigurationQuery) error { + ret := _m.Called(ctx, query) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *models.GetLatestAlertmanagerConfigurationQuery) error); ok { + r0 = rf(ctx, query) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockAMConfigStore_GetLatestAlertmanagerConfiguration_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetLatestAlertmanagerConfiguration' +type MockAMConfigStore_GetLatestAlertmanagerConfiguration_Call struct { + *mock.Call +} + +// GetLatestAlertmanagerConfiguration is a helper method to define mock.On call +// - ctx context.Context +// - query *models.GetLatestAlertmanagerConfigurationQuery +func (_e *MockAMConfigStore_Expecter) GetLatestAlertmanagerConfiguration(ctx interface{}, query interface{}) *MockAMConfigStore_GetLatestAlertmanagerConfiguration_Call { + return &MockAMConfigStore_GetLatestAlertmanagerConfiguration_Call{Call: _e.mock.On("GetLatestAlertmanagerConfiguration", ctx, query)} +} + +func (_c *MockAMConfigStore_GetLatestAlertmanagerConfiguration_Call) Run(run func(ctx context.Context, query *models.GetLatestAlertmanagerConfigurationQuery)) *MockAMConfigStore_GetLatestAlertmanagerConfiguration_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(*models.GetLatestAlertmanagerConfigurationQuery)) + }) + return _c +} + +func (_c *MockAMConfigStore_GetLatestAlertmanagerConfiguration_Call) Return(_a0 error) *MockAMConfigStore_GetLatestAlertmanagerConfiguration_Call { + _c.Call.Return(_a0) + return _c +} + +// UpdateAlertmanagerConfiguration provides a mock function with given fields: ctx, cmd +func (_m *MockAMConfigStore) UpdateAlertmanagerConfiguration(ctx context.Context, cmd *models.SaveAlertmanagerConfigurationCmd) error { + ret := _m.Called(ctx, cmd) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *models.SaveAlertmanagerConfigurationCmd) error); ok { + r0 = rf(ctx, cmd) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockAMConfigStore_UpdateAlertmanagerConfiguration_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateAlertmanagerConfiguration' +type MockAMConfigStore_UpdateAlertmanagerConfiguration_Call struct { + *mock.Call +} + +// UpdateAlertmanagerConfiguration is a helper method to define mock.On call +// - ctx context.Context +// - cmd *models.SaveAlertmanagerConfigurationCmd +func (_e *MockAMConfigStore_Expecter) UpdateAlertmanagerConfiguration(ctx interface{}, cmd interface{}) *MockAMConfigStore_UpdateAlertmanagerConfiguration_Call { + return &MockAMConfigStore_UpdateAlertmanagerConfiguration_Call{Call: _e.mock.On("UpdateAlertmanagerConfiguration", ctx, cmd)} +} + +func (_c *MockAMConfigStore_UpdateAlertmanagerConfiguration_Call) Run(run func(ctx context.Context, cmd *models.SaveAlertmanagerConfigurationCmd)) *MockAMConfigStore_UpdateAlertmanagerConfiguration_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(*models.SaveAlertmanagerConfigurationCmd)) + }) + return _c +} + +func (_c *MockAMConfigStore_UpdateAlertmanagerConfiguration_Call) Return(_a0 error) *MockAMConfigStore_UpdateAlertmanagerConfiguration_Call { + _c.Call.Return(_a0) + return _c +} + +// NewMockAMConfigStore creates a new instance of MockAMConfigStore. It also registers the testing.TB interface on the mock and a cleanup function to assert the mocks expectations. +func NewMockAMConfigStore(t testing.TB) *MockAMConfigStore { + mock := &MockAMConfigStore{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/services/ngalert/provisioning/templates.go b/pkg/services/ngalert/provisioning/templates.go new file mode 100644 index 00000000000..c60d04c25b6 --- /dev/null +++ b/pkg/services/ngalert/provisioning/templates.go @@ -0,0 +1,48 @@ +package provisioning + +import ( + "context" + "fmt" + + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/services/ngalert/models" +) + +type TemplateService struct { + config AMConfigStore + prov ProvisioningStore + xact TransactionManager + log log.Logger +} + +func NewTemplateService(config AMConfigStore, prov ProvisioningStore, xact TransactionManager, log log.Logger) *TemplateService { + return &TemplateService{ + config: config, + prov: prov, + xact: xact, + log: log, + } +} +func (t *TemplateService) GetTemplates(ctx context.Context, orgID int64) (map[string]string, error) { + q := models.GetLatestAlertmanagerConfigurationQuery{ + OrgID: orgID, + } + err := t.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 + } + + if cfg.TemplateFiles == nil { + return map[string]string{}, nil + } + + return cfg.TemplateFiles, nil +} diff --git a/pkg/services/ngalert/provisioning/templates_test.go b/pkg/services/ngalert/provisioning/templates_test.go new file mode 100644 index 00000000000..cd76e0386d8 --- /dev/null +++ b/pkg/services/ngalert/provisioning/templates_test.go @@ -0,0 +1,142 @@ +package provisioning + +import ( + "context" + "fmt" + "testing" + + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/services/ngalert/models" + "github.com/grafana/grafana/pkg/setting" + mock "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +func TestTemplateService(t *testing.T) { + t.Run("service returns templates from config file", func(t *testing.T) { + sut := createTemplateServiceSut() + sut.config.(*MockAMConfigStore).EXPECT(). + setupGetConfig(models.AlertConfiguration{ + AlertmanagerConfiguration: configWithTemplates, + }) + + result, err := sut.GetTemplates(context.Background(), 1) + + require.NoError(t, err) + require.Len(t, result, 1) + }) + + t.Run("service returns empty map when config file contains no templates", func(t *testing.T) { + sut := createTemplateServiceSut() + sut.config.(*MockAMConfigStore).EXPECT(). + setupGetConfig(models.AlertConfiguration{ + AlertmanagerConfiguration: defaultConfig, + }) + + result, err := sut.GetTemplates(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 := createTemplateServiceSut() + sut.config.(*MockAMConfigStore).EXPECT(). + GetLatestAlertmanagerConfiguration(mock.Anything, mock.Anything). + Return(fmt.Errorf("failed")) + + _, err := sut.GetTemplates(context.Background(), 1) + + require.Error(t, err) + }) + + t.Run("when config is invalid", func(t *testing.T) { + sut := createTemplateServiceSut() + sut.config.(*MockAMConfigStore).EXPECT(). + setupGetConfig(models.AlertConfiguration{ + AlertmanagerConfiguration: brokenConfig, + }) + + _, err := sut.GetTemplates(context.Background(), 1) + + require.Error(t, err) + require.Contains(t, err.Error(), "failed to deserialize") + }) + + t.Run("when no AM config in current org", func(t *testing.T) { + sut := createTemplateServiceSut() + sut.config.(*MockAMConfigStore).EXPECT(). + GetLatestAlertmanagerConfiguration(mock.Anything, mock.Anything). + Return(nil) + + _, err := sut.GetTemplates(context.Background(), 1) + + require.Error(t, err) + require.Contains(t, err.Error(), "no alertmanager configuration") + }) + }) +} + +func createTemplateServiceSut() *TemplateService { + return &TemplateService{ + config: &MockAMConfigStore{}, + prov: NewFakeProvisioningStore(), + xact: newNopTransactionManager(), + log: log.NewNopLogger(), + } +} + +func (m *MockAMConfigStore_Expecter) setupGetConfig(ac models.AlertConfiguration) *MockAMConfigStore_Expecter { + m.GetLatestAlertmanagerConfiguration(mock.Anything, mock.Anything). + Run(func(ctx context.Context, q *models.GetLatestAlertmanagerConfigurationQuery) { + q.Result = &ac + }). + Return(nil) + return m +} + +var defaultConfig = setting.GetAlertmanagerDefaultConfiguration() + +var configWithTemplates = ` +{ + "template_files": { + "a": "template" + }, + "alertmanager_config": { + "route": { + "receiver": "grafana-default-email" + }, + "receivers": [{ + "name": "grafana-default-email", + "grafana_managed_receiver_configs": [{ + "uid": "", + "name": "email receiver", + "type": "email", + "isDefault": true, + "settings": { + "addresses": "" + } + }] + }] + } +} +` + +var brokenConfig = ` + "alertmanager_config": { + "route": { + "receiver": "grafana-default-email" + }, + "receivers": [{ + "name": "grafana-default-email", + "grafana_managed_receiver_configs": [{ + "uid": "abc", + "name": "default-email", + "type": "email", + "isDefault": true, + "settings": {} + }] + }] + } +}` diff --git a/pkg/tests/api/alerting/api_provisioning_test.go b/pkg/tests/api/alerting/api_provisioning_test.go index 38428d00ca5..eb3095ab8cb 100644 --- a/pkg/tests/api/alerting/api_provisioning_test.go +++ b/pkg/tests/api/alerting/api_provisioning_test.go @@ -136,6 +136,7 @@ func TestProvisioning(t *testing.T) { require.Equal(t, 202, resp.StatusCode) }) }) + t.Run("when provisioning contactpoints", func(t *testing.T) { url := fmt.Sprintf("http://%s/api/provisioning/contact-points", grafanaListedAddr) body := ` @@ -228,6 +229,50 @@ func TestProvisioning(t *testing.T) { require.Equal(t, 202, resp.StatusCode) }) }) + + t.Run("when provisioning templates", func(t *testing.T) { + url := fmt.Sprintf("http://%s/api/provisioning/templates", 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 {