mirror of
https://github.com/grafana/grafana.git
synced 2024-11-25 18:30:41 -06:00
Alerting: Add provisioning GET routes for message templates (#48367)
* Template service * Add GET routes and implement them * Generate mock for persist layer * Unit tests for reading templates * Set up composition root and get integration tests working * Fix prealloc issue * Extract setup boilerplate * Update AuthorizationTest * Rebase and resolve * Fix linter error
This commit is contained in:
parent
d4616cfe26
commit
735822e48a
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -181,7 +181,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",
|
||||
|
@ -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}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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"),
|
||||
|
@ -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{}
|
@ -2868,6 +2868,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"
|
||||
@ -2900,9 +2901,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.",
|
||||
@ -3553,7 +3554,6 @@
|
||||
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models"
|
||||
},
|
||||
"receiver": {
|
||||
"description": "Receiver receiver",
|
||||
"properties": {
|
||||
"name": {
|
||||
"description": "name",
|
||||
@ -3564,7 +3564,9 @@
|
||||
"required": [
|
||||
"name"
|
||||
],
|
||||
"type": "object"
|
||||
"type": "object",
|
||||
"x-go-name": "Receiver",
|
||||
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models"
|
||||
},
|
||||
"silence": {
|
||||
"description": "Silence silence",
|
||||
@ -4956,6 +4958,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",
|
||||
|
@ -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",
|
||||
@ -4788,8 +4825,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"
|
||||
@ -4822,7 +4860,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.",
|
||||
@ -5480,7 +5518,6 @@
|
||||
"$ref": "#/definitions/postableSilence"
|
||||
},
|
||||
"receiver": {
|
||||
"description": "Receiver receiver",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"name"
|
||||
@ -5492,6 +5529,8 @@
|
||||
"x-go-name": "Name"
|
||||
}
|
||||
},
|
||||
"x-go-name": "Receiver",
|
||||
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models",
|
||||
"$ref": "#/definitions/receiver"
|
||||
},
|
||||
"silence": {
|
||||
|
@ -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())
|
||||
|
||||
|
@ -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
|
||||
|
111
pkg/services/ngalert/provisioning/persist_mock.go
Normal file
111
pkg/services/ngalert/provisioning/persist_mock.go
Normal file
@ -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
|
||||
}
|
48
pkg/services/ngalert/provisioning/templates.go
Normal file
48
pkg/services/ngalert/provisioning/templates.go
Normal file
@ -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
|
||||
}
|
140
pkg/services/ngalert/provisioning/templates_test.go
Normal file
140
pkg/services/ngalert/provisioning/templates_test.go
Normal file
@ -0,0 +1,140 @@
|
||||
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.ErrorContains(t, err, "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.ErrorContains(t, err, "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": "<example@email.com>"
|
||||
}
|
||||
}]
|
||||
}]
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
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": {}
|
||||
}]
|
||||
}]
|
||||
}
|
||||
}`
|
@ -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 {
|
||||
|
Loading…
Reference in New Issue
Block a user