mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: Provisioning message templates (#48665)
* Generate API for writing templates * Persist templates app logic layer * Validate templates * Extract logic, make set and delete methods * Drop post route for templates * Fix response details, wire up remainder of API * Authorize routes * Mirror some existing tests on new APIs * Generate mock for prov store * Wire up prov store mock, add tests using it * Cover cases for both storage paths * Add happy path tests and fix bugs if file contains no template section * Normalize template content with define statement * Tests for deletion * Fix linter error * Move provenance field to DTO * empty commit * ID to name * Fix in auth too
This commit is contained in:
parent
6de77283c6
commit
0f56462fbe
@ -32,6 +32,8 @@ type ContactPointService interface {
|
||||
|
||||
type TemplateService interface {
|
||||
GetTemplates(ctx context.Context, orgID int64) (map[string]string, error)
|
||||
SetTemplate(ctx context.Context, orgID int64, tmpl apimodels.MessageTemplate) (apimodels.MessageTemplate, error)
|
||||
DeleteTemplate(ctx context.Context, orgID int64, name string) error
|
||||
}
|
||||
|
||||
type NotificationPolicyService interface {
|
||||
@ -52,7 +54,6 @@ func (srv *ProvisioningSrv) RouteGetPolicyTree(c *models.ReqContext) response.Re
|
||||
}
|
||||
|
||||
func (srv *ProvisioningSrv) RoutePostPolicyTree(c *models.ReqContext, tree apimodels.Route) response.Response {
|
||||
// TODO: lift validation out of definitions.Rotue.UnmarshalJSON and friends into a dedicated validator.
|
||||
err := srv.policies.UpdatePolicyTree(c.Req.Context(), c.OrgId, tree, alerting_models.ProvenanceAPI)
|
||||
if errors.Is(err, store.ErrNoAlertmanagerConfiguration) {
|
||||
return ErrResp(http.StatusNotFound, err, "")
|
||||
@ -114,7 +115,7 @@ func (srv *ProvisioningSrv) RouteGetTemplates(c *models.ReqContext) response.Res
|
||||
}
|
||||
|
||||
func (srv *ProvisioningSrv) RouteGetTemplate(c *models.ReqContext) response.Response {
|
||||
id := web.Params(c.Req)[":ID"]
|
||||
id := web.Params(c.Req)[":name"]
|
||||
templates, err := srv.templates.GetTemplates(c.Req.Context(), c.OrgId)
|
||||
if err != nil {
|
||||
return ErrResp(http.StatusInternalServerError, err, "")
|
||||
@ -124,3 +125,29 @@ func (srv *ProvisioningSrv) RouteGetTemplate(c *models.ReqContext) response.Resp
|
||||
}
|
||||
return response.Empty(http.StatusNotFound)
|
||||
}
|
||||
|
||||
func (srv *ProvisioningSrv) RoutePutTemplate(c *models.ReqContext, body apimodels.MessageTemplateContent) response.Response {
|
||||
name := web.Params(c.Req)[":name"]
|
||||
tmpl := apimodels.MessageTemplate{
|
||||
Name: name,
|
||||
Template: body.Template,
|
||||
Provenance: alerting_models.ProvenanceAPI,
|
||||
}
|
||||
modified, err := srv.templates.SetTemplate(c.Req.Context(), c.OrgId, tmpl)
|
||||
if err != nil {
|
||||
if errors.Is(err, provisioning.ErrValidation) {
|
||||
return ErrResp(http.StatusBadRequest, err, "")
|
||||
}
|
||||
return ErrResp(http.StatusInternalServerError, err, "")
|
||||
}
|
||||
return response.JSON(http.StatusAccepted, modified)
|
||||
}
|
||||
|
||||
func (srv *ProvisioningSrv) RouteDeleteTemplate(c *models.ReqContext) response.Response {
|
||||
name := web.Params(c.Req)[":name"]
|
||||
err := srv.templates.DeleteTemplate(c.Req.Context(), c.OrgId, name)
|
||||
if err != nil {
|
||||
return ErrResp(http.StatusInternalServerError, err, "")
|
||||
}
|
||||
return response.JSON(http.StatusNoContent, nil)
|
||||
}
|
||||
|
@ -183,13 +183,15 @@ 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/{ID}":
|
||||
http.MethodGet + "/api/provisioning/templates/{name}":
|
||||
return middleware.ReqSignedIn
|
||||
|
||||
case http.MethodPost + "/api/provisioning/policies",
|
||||
http.MethodPost + "/api/provisioning/contact-points",
|
||||
http.MethodPut + "/api/provisioning/contact-points",
|
||||
http.MethodDelete + "/api/provisioning/contact-points/{ID}":
|
||||
http.MethodDelete + "/api/provisioning/contact-points/{ID}",
|
||||
http.MethodPut + "/api/provisioning/templates/{name}",
|
||||
http.MethodDelete + "/api/provisioning/templates/{name}":
|
||||
return middleware.ReqEditorRole
|
||||
}
|
||||
|
||||
|
@ -50,3 +50,11 @@ func (f *ForkedProvisioningApi) forkRouteGetTemplates(ctx *models.ReqContext) re
|
||||
func (f *ForkedProvisioningApi) forkRouteGetTemplate(ctx *models.ReqContext) response.Response {
|
||||
return f.svc.RouteGetTemplate(ctx)
|
||||
}
|
||||
|
||||
func (f *ForkedProvisioningApi) forkRoutePutTemplate(ctx *models.ReqContext, body apimodels.MessageTemplateContent) response.Response {
|
||||
return f.svc.RoutePutTemplate(ctx, body)
|
||||
}
|
||||
|
||||
func (f *ForkedProvisioningApi) forkRouteDeleteTemplate(ctx *models.ReqContext) response.Response {
|
||||
return f.svc.RouteDeleteTemplate(ctx)
|
||||
}
|
||||
|
@ -21,6 +21,7 @@ import (
|
||||
|
||||
type ProvisioningApiForkingService interface {
|
||||
RouteDeleteContactpoints(*models.ReqContext) response.Response
|
||||
RouteDeleteTemplate(*models.ReqContext) response.Response
|
||||
RouteGetContactpoints(*models.ReqContext) response.Response
|
||||
RouteGetPolicyTree(*models.ReqContext) response.Response
|
||||
RouteGetTemplate(*models.ReqContext) response.Response
|
||||
@ -28,12 +29,17 @@ type ProvisioningApiForkingService interface {
|
||||
RoutePostContactpoints(*models.ReqContext) response.Response
|
||||
RoutePostPolicyTree(*models.ReqContext) response.Response
|
||||
RoutePutContactpoints(*models.ReqContext) response.Response
|
||||
RoutePutTemplate(*models.ReqContext) response.Response
|
||||
}
|
||||
|
||||
func (f *ForkedProvisioningApi) RouteDeleteContactpoints(ctx *models.ReqContext) response.Response {
|
||||
return f.forkRouteDeleteContactpoints(ctx)
|
||||
}
|
||||
|
||||
func (f *ForkedProvisioningApi) RouteDeleteTemplate(ctx *models.ReqContext) response.Response {
|
||||
return f.forkRouteDeleteTemplate(ctx)
|
||||
}
|
||||
|
||||
func (f *ForkedProvisioningApi) RouteGetContactpoints(ctx *models.ReqContext) response.Response {
|
||||
return f.forkRouteGetContactpoints(ctx)
|
||||
}
|
||||
@ -74,6 +80,14 @@ func (f *ForkedProvisioningApi) RoutePutContactpoints(ctx *models.ReqContext) re
|
||||
return f.forkRoutePutContactpoints(ctx, conf)
|
||||
}
|
||||
|
||||
func (f *ForkedProvisioningApi) RoutePutTemplate(ctx *models.ReqContext) response.Response {
|
||||
conf := apimodels.MessageTemplateContent{}
|
||||
if err := web.Bind(ctx.Req, &conf); err != nil {
|
||||
return response.Error(http.StatusBadRequest, "bad request data", err)
|
||||
}
|
||||
return f.forkRoutePutTemplate(ctx, conf)
|
||||
}
|
||||
|
||||
func (api *API) RegisterProvisioningApiEndpoints(srv ProvisioningApiForkingService, m *metrics.API) {
|
||||
api.RouteRegister.Group("", func(group routing.RouteRegister) {
|
||||
group.Delete(
|
||||
@ -86,6 +100,16 @@ func (api *API) RegisterProvisioningApiEndpoints(srv ProvisioningApiForkingServi
|
||||
m,
|
||||
),
|
||||
)
|
||||
group.Delete(
|
||||
toMacaronPath("/api/provisioning/templates/{name}"),
|
||||
api.authorize(http.MethodDelete, "/api/provisioning/templates/{name}"),
|
||||
metrics.Instrument(
|
||||
http.MethodDelete,
|
||||
"/api/provisioning/templates/{name}",
|
||||
srv.RouteDeleteTemplate,
|
||||
m,
|
||||
),
|
||||
)
|
||||
group.Get(
|
||||
toMacaronPath("/api/provisioning/contact-points"),
|
||||
api.authorize(http.MethodGet, "/api/provisioning/contact-points"),
|
||||
@ -107,11 +131,11 @@ func (api *API) RegisterProvisioningApiEndpoints(srv ProvisioningApiForkingServi
|
||||
),
|
||||
)
|
||||
group.Get(
|
||||
toMacaronPath("/api/provisioning/templates/{ID}"),
|
||||
api.authorize(http.MethodGet, "/api/provisioning/templates/{ID}"),
|
||||
toMacaronPath("/api/provisioning/templates/{name}"),
|
||||
api.authorize(http.MethodGet, "/api/provisioning/templates/{name}"),
|
||||
metrics.Instrument(
|
||||
http.MethodGet,
|
||||
"/api/provisioning/templates/{ID}",
|
||||
"/api/provisioning/templates/{name}",
|
||||
srv.RouteGetTemplate,
|
||||
m,
|
||||
),
|
||||
@ -156,5 +180,15 @@ func (api *API) RegisterProvisioningApiEndpoints(srv ProvisioningApiForkingServi
|
||||
m,
|
||||
),
|
||||
)
|
||||
group.Put(
|
||||
toMacaronPath("/api/provisioning/templates/{name}"),
|
||||
api.authorize(http.MethodPut, "/api/provisioning/templates/{name}"),
|
||||
metrics.Instrument(
|
||||
http.MethodPut,
|
||||
"/api/provisioning/templates/{name}",
|
||||
srv.RoutePutTemplate,
|
||||
m,
|
||||
),
|
||||
)
|
||||
}, middleware.ReqSignedIn)
|
||||
}
|
||||
|
@ -1,5 +1,13 @@
|
||||
package definitions
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||
)
|
||||
|
||||
// swagger:route GET /api/provisioning/templates provisioning RouteGetTemplates
|
||||
//
|
||||
// Get all message templates.
|
||||
@ -8,7 +16,7 @@ package definitions
|
||||
// 200: []MessageTemplate
|
||||
// 400: ValidationError
|
||||
|
||||
// swagger:route GET /api/provisioning/templates/{ID} provisioning RouteGetTemplate
|
||||
// swagger:route GET /api/provisioning/templates/{name} provisioning RouteGetTemplate
|
||||
//
|
||||
// Get a message template.
|
||||
//
|
||||
@ -16,9 +24,70 @@ package definitions
|
||||
// 200: MessageTemplate
|
||||
// 404: NotFound
|
||||
|
||||
// swagger:route PUT /api/provisioning/templates/{name} provisioning RoutePutTemplate
|
||||
//
|
||||
// Updates an existing template.
|
||||
//
|
||||
// Consumes:
|
||||
// - application/json
|
||||
//
|
||||
// Responses:
|
||||
// 202: Accepted
|
||||
// 400: ValidationError
|
||||
|
||||
// swagger:route DELETE /api/provisioning/templates/{name} provisioning RouteDeleteTemplate
|
||||
//
|
||||
// Delete a template.
|
||||
//
|
||||
// Responses:
|
||||
// 204: Accepted
|
||||
|
||||
type MessageTemplate struct {
|
||||
Name string
|
||||
Name string
|
||||
Template string
|
||||
Provenance models.Provenance `json:"provenance,omitempty"`
|
||||
}
|
||||
|
||||
type MessageTemplateContent struct {
|
||||
Template string
|
||||
}
|
||||
|
||||
type NotFound struct{}
|
||||
// swagger:parameters RoutePutTemplate
|
||||
type MessageTemplatePayload struct {
|
||||
// in:body
|
||||
Body MessageTemplateContent
|
||||
}
|
||||
|
||||
func (t *MessageTemplate) ResourceType() string {
|
||||
return "template"
|
||||
}
|
||||
|
||||
func (t *MessageTemplate) ResourceID() string {
|
||||
return t.Name
|
||||
}
|
||||
|
||||
func (t *MessageTemplate) Validate() error {
|
||||
if t.Name == "" {
|
||||
return fmt.Errorf("template must have a name")
|
||||
}
|
||||
if t.Template == "" {
|
||||
return fmt.Errorf("template must have content")
|
||||
}
|
||||
|
||||
content := strings.TrimSpace(t.Template)
|
||||
found, err := regexp.MatchString(`\{\{\s*define`, content)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to match regex: %w", err)
|
||||
}
|
||||
if !found {
|
||||
lines := strings.Split(content, "\n")
|
||||
for i, s := range lines {
|
||||
lines[i] = " " + s
|
||||
}
|
||||
content = strings.Join(lines, "\n")
|
||||
content = fmt.Sprintf("{{ define \"%s\" }}\n%s\n{{ end }}", t.Name, content)
|
||||
}
|
||||
t.Template = content
|
||||
|
||||
return nil
|
||||
}
|
||||
|
3
pkg/services/ngalert/api/tooling/definitions/shared.go
Normal file
3
pkg/services/ngalert/api/tooling/definitions/shared.go
Normal file
@ -0,0 +1,3 @@
|
||||
package definitions
|
||||
|
||||
type NotFound struct{}
|
@ -1364,6 +1364,15 @@
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"MessageTemplateContent": {
|
||||
"properties": {
|
||||
"Template": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
||||
},
|
||||
"MonthRange": {
|
||||
"properties": {
|
||||
"Begin": {
|
||||
@ -3106,6 +3115,7 @@
|
||||
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models"
|
||||
},
|
||||
"alertGroup": {
|
||||
"description": "AlertGroup alert group",
|
||||
"properties": {
|
||||
"alerts": {
|
||||
"description": "alerts",
|
||||
@ -3127,9 +3137,7 @@
|
||||
"labels",
|
||||
"receiver"
|
||||
],
|
||||
"type": "object",
|
||||
"x-go-name": "AlertGroup",
|
||||
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models"
|
||||
"type": "object"
|
||||
},
|
||||
"alertGroups": {
|
||||
"items": {
|
||||
@ -3559,6 +3567,7 @@
|
||||
"type": "object"
|
||||
},
|
||||
"receiver": {
|
||||
"description": "Receiver receiver",
|
||||
"properties": {
|
||||
"name": {
|
||||
"description": "name",
|
||||
@ -3569,9 +3578,7 @@
|
||||
"required": [
|
||||
"name"
|
||||
],
|
||||
"type": "object",
|
||||
"x-go-name": "Receiver",
|
||||
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models"
|
||||
"type": "object"
|
||||
},
|
||||
"silence": {
|
||||
"description": "Silence silence",
|
||||
@ -4971,7 +4978,19 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/provisioning/templates/{ID}": {
|
||||
"/api/provisioning/templates/{name}": {
|
||||
"delete": {
|
||||
"operationId": "RouteDeleteTemplate",
|
||||
"responses": {
|
||||
"204": {
|
||||
"$ref": "#/responses/Accepted"
|
||||
}
|
||||
},
|
||||
"summary": "Delete a template.",
|
||||
"tags": [
|
||||
"provisioning"
|
||||
]
|
||||
},
|
||||
"get": {
|
||||
"operationId": "RouteGetTemplate",
|
||||
"responses": {
|
||||
@ -4986,6 +5005,36 @@
|
||||
"tags": [
|
||||
"provisioning"
|
||||
]
|
||||
},
|
||||
"put": {
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"operationId": "RoutePutTemplate",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "body",
|
||||
"name": "Body",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/MessageTemplateContent"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"202": {
|
||||
"$ref": "#/responses/Accepted"
|
||||
},
|
||||
"400": {
|
||||
"description": "ValidationError",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/ValidationError"
|
||||
}
|
||||
}
|
||||
},
|
||||
"summary": "Updates an existing template.",
|
||||
"tags": [
|
||||
"provisioning"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/ruler/grafana/api/v1/rules": {
|
||||
|
@ -1303,7 +1303,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/provisioning/templates/{ID}": {
|
||||
"/api/provisioning/templates/{name}": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"provisioning"
|
||||
@ -1318,6 +1318,48 @@
|
||||
"$ref": "#/responses/NotFound"
|
||||
}
|
||||
}
|
||||
},
|
||||
"put": {
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"provisioning"
|
||||
],
|
||||
"summary": "Updates an existing template.",
|
||||
"operationId": "RoutePutTemplate",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "Body",
|
||||
"in": "body",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/MessageTemplateContent"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"202": {
|
||||
"$ref": "#/responses/Accepted"
|
||||
},
|
||||
"400": {
|
||||
"description": "ValidationError",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/ValidationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"tags": [
|
||||
"provisioning"
|
||||
],
|
||||
"summary": "Delete a template.",
|
||||
"operationId": "RouteDeleteTemplate",
|
||||
"responses": {
|
||||
"204": {
|
||||
"$ref": "#/responses/Accepted"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/ruler/grafana/api/v1/rules": {
|
||||
@ -3303,6 +3345,15 @@
|
||||
},
|
||||
"$ref": "#/definitions/Matchers"
|
||||
},
|
||||
"MessageTemplateContent": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Template": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
||||
},
|
||||
"MonthRange": {
|
||||
"type": "object",
|
||||
"title": "A MonthRange is an inclusive range between [1, 12] where 1 = January.",
|
||||
@ -5045,6 +5096,7 @@
|
||||
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models"
|
||||
},
|
||||
"alertGroup": {
|
||||
"description": "AlertGroup alert group",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"alerts",
|
||||
@ -5067,8 +5119,6 @@
|
||||
"$ref": "#/definitions/receiver"
|
||||
}
|
||||
},
|
||||
"x-go-name": "AlertGroup",
|
||||
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models",
|
||||
"$ref": "#/definitions/alertGroup"
|
||||
},
|
||||
"alertGroups": {
|
||||
@ -5505,6 +5555,7 @@
|
||||
"$ref": "#/definitions/postableSilence"
|
||||
},
|
||||
"receiver": {
|
||||
"description": "Receiver receiver",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"name"
|
||||
@ -5516,8 +5567,6 @@
|
||||
"x-go-name": "Name"
|
||||
}
|
||||
},
|
||||
"x-go-name": "Receiver",
|
||||
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models",
|
||||
"$ref": "#/definitions/receiver"
|
||||
},
|
||||
"silence": {
|
||||
|
@ -14,6 +14,7 @@ type AMConfigStore interface {
|
||||
}
|
||||
|
||||
// ProvisioningStore is a store of provisioning data for arbitrary objects.
|
||||
//go:generate mockery --name ProvisioningStore --structname MockProvisioningStore --inpackage --filename provisioning_store_mock.go --with-expecter
|
||||
type ProvisioningStore interface {
|
||||
GetProvenance(ctx context.Context, o models.Provisionable, org int64) (models.Provenance, error)
|
||||
GetProvenances(ctx context.Context, org int64, resourceType string) (map[string]models.Provenance, error)
|
||||
|
208
pkg/services/ngalert/provisioning/provisioning_store_mock.go
Normal file
208
pkg/services/ngalert/provisioning/provisioning_store_mock.go
Normal file
@ -0,0 +1,208 @@
|
||||
// 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"
|
||||
)
|
||||
|
||||
// MockProvisioningStore is an autogenerated mock type for the ProvisioningStore type
|
||||
type MockProvisioningStore struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
type MockProvisioningStore_Expecter struct {
|
||||
mock *mock.Mock
|
||||
}
|
||||
|
||||
func (_m *MockProvisioningStore) EXPECT() *MockProvisioningStore_Expecter {
|
||||
return &MockProvisioningStore_Expecter{mock: &_m.Mock}
|
||||
}
|
||||
|
||||
// DeleteProvenance provides a mock function with given fields: ctx, o, org
|
||||
func (_m *MockProvisioningStore) DeleteProvenance(ctx context.Context, o models.Provisionable, org int64) error {
|
||||
ret := _m.Called(ctx, o, org)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, models.Provisionable, int64) error); ok {
|
||||
r0 = rf(ctx, o, org)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// MockProvisioningStore_DeleteProvenance_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteProvenance'
|
||||
type MockProvisioningStore_DeleteProvenance_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// DeleteProvenance is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - o models.Provisionable
|
||||
// - org int64
|
||||
func (_e *MockProvisioningStore_Expecter) DeleteProvenance(ctx interface{}, o interface{}, org interface{}) *MockProvisioningStore_DeleteProvenance_Call {
|
||||
return &MockProvisioningStore_DeleteProvenance_Call{Call: _e.mock.On("DeleteProvenance", ctx, o, org)}
|
||||
}
|
||||
|
||||
func (_c *MockProvisioningStore_DeleteProvenance_Call) Run(run func(ctx context.Context, o models.Provisionable, org int64)) *MockProvisioningStore_DeleteProvenance_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(context.Context), args[1].(models.Provisionable), args[2].(int64))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockProvisioningStore_DeleteProvenance_Call) Return(_a0 error) *MockProvisioningStore_DeleteProvenance_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
// GetProvenance provides a mock function with given fields: ctx, o, org
|
||||
func (_m *MockProvisioningStore) GetProvenance(ctx context.Context, o models.Provisionable, org int64) (models.Provenance, error) {
|
||||
ret := _m.Called(ctx, o, org)
|
||||
|
||||
var r0 models.Provenance
|
||||
if rf, ok := ret.Get(0).(func(context.Context, models.Provisionable, int64) models.Provenance); ok {
|
||||
r0 = rf(ctx, o, org)
|
||||
} else {
|
||||
r0 = ret.Get(0).(models.Provenance)
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, models.Provisionable, int64) error); ok {
|
||||
r1 = rf(ctx, o, org)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MockProvisioningStore_GetProvenance_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetProvenance'
|
||||
type MockProvisioningStore_GetProvenance_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// GetProvenance is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - o models.Provisionable
|
||||
// - org int64
|
||||
func (_e *MockProvisioningStore_Expecter) GetProvenance(ctx interface{}, o interface{}, org interface{}) *MockProvisioningStore_GetProvenance_Call {
|
||||
return &MockProvisioningStore_GetProvenance_Call{Call: _e.mock.On("GetProvenance", ctx, o, org)}
|
||||
}
|
||||
|
||||
func (_c *MockProvisioningStore_GetProvenance_Call) Run(run func(ctx context.Context, o models.Provisionable, org int64)) *MockProvisioningStore_GetProvenance_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(context.Context), args[1].(models.Provisionable), args[2].(int64))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockProvisioningStore_GetProvenance_Call) Return(_a0 models.Provenance, _a1 error) *MockProvisioningStore_GetProvenance_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
// GetProvenances provides a mock function with given fields: ctx, org, resourceType
|
||||
func (_m *MockProvisioningStore) GetProvenances(ctx context.Context, org int64, resourceType string) (map[string]models.Provenance, error) {
|
||||
ret := _m.Called(ctx, org, resourceType)
|
||||
|
||||
var r0 map[string]models.Provenance
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int64, string) map[string]models.Provenance); ok {
|
||||
r0 = rf(ctx, org, resourceType)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(map[string]models.Provenance)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, int64, string) error); ok {
|
||||
r1 = rf(ctx, org, resourceType)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MockProvisioningStore_GetProvenances_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetProvenances'
|
||||
type MockProvisioningStore_GetProvenances_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// GetProvenances is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - org int64
|
||||
// - resourceType string
|
||||
func (_e *MockProvisioningStore_Expecter) GetProvenances(ctx interface{}, org interface{}, resourceType interface{}) *MockProvisioningStore_GetProvenances_Call {
|
||||
return &MockProvisioningStore_GetProvenances_Call{Call: _e.mock.On("GetProvenances", ctx, org, resourceType)}
|
||||
}
|
||||
|
||||
func (_c *MockProvisioningStore_GetProvenances_Call) Run(run func(ctx context.Context, org int64, resourceType string)) *MockProvisioningStore_GetProvenances_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(context.Context), args[1].(int64), args[2].(string))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockProvisioningStore_GetProvenances_Call) Return(_a0 map[string]models.Provenance, _a1 error) *MockProvisioningStore_GetProvenances_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetProvenance provides a mock function with given fields: ctx, o, org, p
|
||||
func (_m *MockProvisioningStore) SetProvenance(ctx context.Context, o models.Provisionable, org int64, p models.Provenance) error {
|
||||
ret := _m.Called(ctx, o, org, p)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, models.Provisionable, int64, models.Provenance) error); ok {
|
||||
r0 = rf(ctx, o, org, p)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// MockProvisioningStore_SetProvenance_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetProvenance'
|
||||
type MockProvisioningStore_SetProvenance_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// SetProvenance is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - o models.Provisionable
|
||||
// - org int64
|
||||
// - p models.Provenance
|
||||
func (_e *MockProvisioningStore_Expecter) SetProvenance(ctx interface{}, o interface{}, org interface{}, p interface{}) *MockProvisioningStore_SetProvenance_Call {
|
||||
return &MockProvisioningStore_SetProvenance_Call{Call: _e.mock.On("SetProvenance", ctx, o, org, p)}
|
||||
}
|
||||
|
||||
func (_c *MockProvisioningStore_SetProvenance_Call) Run(run func(ctx context.Context, o models.Provisionable, org int64, p models.Provenance)) *MockProvisioningStore_SetProvenance_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(context.Context), args[1].(models.Provisionable), args[2].(int64), args[3].(models.Provenance))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockProvisioningStore_SetProvenance_Call) Return(_a0 error) *MockProvisioningStore_SetProvenance_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
// NewMockProvisioningStore creates a new instance of MockProvisioningStore. It also registers the testing.TB interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
func NewMockProvisioningStore(t testing.TB) *MockProvisioningStore {
|
||||
mock := &MockProvisioningStore{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
@ -5,6 +5,7 @@ import (
|
||||
"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"
|
||||
)
|
||||
|
||||
@ -23,7 +24,107 @@ func NewTemplateService(config AMConfigStore, prov ProvisioningStore, xact Trans
|
||||
log: log,
|
||||
}
|
||||
}
|
||||
|
||||
func (t *TemplateService) GetTemplates(ctx context.Context, orgID int64) (map[string]string, error) {
|
||||
revision, err := t.getLastConfiguration(ctx, orgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if revision.cfg.TemplateFiles == nil {
|
||||
return map[string]string{}, nil
|
||||
}
|
||||
|
||||
return revision.cfg.TemplateFiles, nil
|
||||
}
|
||||
|
||||
func (t *TemplateService) SetTemplate(ctx context.Context, orgID int64, tmpl definitions.MessageTemplate) (definitions.MessageTemplate, error) {
|
||||
err := tmpl.Validate()
|
||||
if err != nil {
|
||||
return definitions.MessageTemplate{}, fmt.Errorf("%w: %s", ErrValidation, err.Error())
|
||||
}
|
||||
|
||||
revision, err := t.getLastConfiguration(ctx, orgID)
|
||||
if err != nil {
|
||||
return definitions.MessageTemplate{}, err
|
||||
}
|
||||
|
||||
if revision.cfg.TemplateFiles == nil {
|
||||
revision.cfg.TemplateFiles = map[string]string{}
|
||||
}
|
||||
revision.cfg.TemplateFiles[tmpl.Name] = tmpl.Template
|
||||
|
||||
serialized, err := SerializeAlertmanagerConfig(*revision.cfg)
|
||||
if err != nil {
|
||||
return definitions.MessageTemplate{}, err
|
||||
}
|
||||
cmd := models.SaveAlertmanagerConfigurationCmd{
|
||||
AlertmanagerConfiguration: string(serialized),
|
||||
ConfigurationVersion: revision.version,
|
||||
FetchedConfigurationHash: revision.concurrencyToken,
|
||||
Default: false,
|
||||
OrgID: orgID,
|
||||
}
|
||||
err = t.xact.InTransaction(ctx, func(ctx context.Context) error {
|
||||
err = t.config.UpdateAlertmanagerConfiguration(ctx, &cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = t.prov.SetProvenance(ctx, &tmpl, orgID, tmpl.Provenance)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return definitions.MessageTemplate{}, err
|
||||
}
|
||||
|
||||
return tmpl, nil
|
||||
}
|
||||
|
||||
func (t *TemplateService) DeleteTemplate(ctx context.Context, orgID int64, name string) error {
|
||||
revision, err := t.getLastConfiguration(ctx, orgID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
delete(revision.cfg.TemplateFiles, name)
|
||||
|
||||
serialized, err := SerializeAlertmanagerConfig(*revision.cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd := models.SaveAlertmanagerConfigurationCmd{
|
||||
AlertmanagerConfiguration: string(serialized),
|
||||
ConfigurationVersion: revision.version,
|
||||
FetchedConfigurationHash: revision.concurrencyToken,
|
||||
Default: false,
|
||||
OrgID: orgID,
|
||||
}
|
||||
err = t.xact.InTransaction(ctx, func(ctx context.Context) error {
|
||||
err = t.config.UpdateAlertmanagerConfiguration(ctx, &cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tgt := definitions.MessageTemplate{
|
||||
Name: name,
|
||||
}
|
||||
err = t.prov.DeleteProvenance(ctx, &tgt, orgID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *TemplateService) getLastConfiguration(ctx context.Context, orgID int64) (*cfgRevision, error) {
|
||||
q := models.GetLatestAlertmanagerConfigurationQuery{
|
||||
OrgID: orgID,
|
||||
}
|
||||
@ -31,18 +132,26 @@ func (t *TemplateService) GetTemplates(ctx context.Context, orgID int64) (map[st
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if q.Result == nil {
|
||||
return nil, fmt.Errorf("no alertmanager configuration present in this org")
|
||||
}
|
||||
|
||||
concurrencyToken := q.Result.ConfigurationHash
|
||||
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
|
||||
return &cfgRevision{
|
||||
cfg: cfg,
|
||||
concurrencyToken: concurrencyToken,
|
||||
version: q.Result.ConfigurationVersion,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type cfgRevision struct {
|
||||
cfg *definitions.PostableUserConfig
|
||||
concurrencyToken string
|
||||
version string
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"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"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
@ -16,7 +17,7 @@ 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{
|
||||
getsConfig(models.AlertConfiguration{
|
||||
AlertmanagerConfiguration: configWithTemplates,
|
||||
})
|
||||
|
||||
@ -29,7 +30,7 @@ func TestTemplateService(t *testing.T) {
|
||||
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{
|
||||
getsConfig(models.AlertConfiguration{
|
||||
AlertmanagerConfiguration: defaultConfig,
|
||||
})
|
||||
|
||||
@ -54,7 +55,7 @@ func TestTemplateService(t *testing.T) {
|
||||
t.Run("when config is invalid", func(t *testing.T) {
|
||||
sut := createTemplateServiceSut()
|
||||
sut.config.(*MockAMConfigStore).EXPECT().
|
||||
setupGetConfig(models.AlertConfiguration{
|
||||
getsConfig(models.AlertConfiguration{
|
||||
AlertmanagerConfiguration: brokenConfig,
|
||||
})
|
||||
|
||||
@ -74,18 +75,291 @@ func TestTemplateService(t *testing.T) {
|
||||
require.ErrorContains(t, err, "no alertmanager configuration")
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("setting templates", func(t *testing.T) {
|
||||
t.Run("rejects templates that fail validation", func(t *testing.T) {
|
||||
sut := createTemplateServiceSut()
|
||||
tmpl := definitions.MessageTemplate{
|
||||
Name: "",
|
||||
Template: "",
|
||||
}
|
||||
|
||||
_, err := sut.SetTemplate(context.Background(), 1, tmpl)
|
||||
|
||||
require.ErrorIs(t, err, ErrValidation)
|
||||
})
|
||||
|
||||
t.Run("propagates errors", func(t *testing.T) {
|
||||
t.Run("when unable to read config", func(t *testing.T) {
|
||||
sut := createTemplateServiceSut()
|
||||
tmpl := createMessageTemplate()
|
||||
sut.config.(*MockAMConfigStore).EXPECT().
|
||||
GetLatestAlertmanagerConfiguration(mock.Anything, mock.Anything).
|
||||
Return(fmt.Errorf("failed"))
|
||||
|
||||
_, err := sut.SetTemplate(context.Background(), 1, tmpl)
|
||||
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("when config is invalid", func(t *testing.T) {
|
||||
sut := createTemplateServiceSut()
|
||||
tmpl := createMessageTemplate()
|
||||
sut.config.(*MockAMConfigStore).EXPECT().
|
||||
getsConfig(models.AlertConfiguration{
|
||||
AlertmanagerConfiguration: brokenConfig,
|
||||
})
|
||||
|
||||
_, err := sut.SetTemplate(context.Background(), 1, tmpl)
|
||||
|
||||
require.ErrorContains(t, err, "failed to deserialize")
|
||||
})
|
||||
|
||||
t.Run("when no AM config in current org", func(t *testing.T) {
|
||||
sut := createTemplateServiceSut()
|
||||
tmpl := createMessageTemplate()
|
||||
sut.config.(*MockAMConfigStore).EXPECT().
|
||||
GetLatestAlertmanagerConfiguration(mock.Anything, mock.Anything).
|
||||
Return(nil)
|
||||
|
||||
_, err := sut.SetTemplate(context.Background(), 1, tmpl)
|
||||
|
||||
require.ErrorContains(t, err, "no alertmanager configuration")
|
||||
})
|
||||
|
||||
t.Run("when provenance fails to save", func(t *testing.T) {
|
||||
sut := createTemplateServiceSut()
|
||||
tmpl := createMessageTemplate()
|
||||
sut.config.(*MockAMConfigStore).EXPECT().
|
||||
getsConfig(models.AlertConfiguration{
|
||||
AlertmanagerConfiguration: configWithTemplates,
|
||||
})
|
||||
sut.config.(*MockAMConfigStore).EXPECT().saveSucceeds()
|
||||
sut.prov.(*MockProvisioningStore).EXPECT().
|
||||
SetProvenance(mock.Anything, mock.Anything, mock.Anything, mock.Anything).
|
||||
Return(fmt.Errorf("failed to save provenance"))
|
||||
|
||||
_, err := sut.SetTemplate(context.Background(), 1, tmpl)
|
||||
|
||||
require.ErrorContains(t, err, "failed to save provenance")
|
||||
})
|
||||
|
||||
t.Run("when AM config fails to save", func(t *testing.T) {
|
||||
sut := createTemplateServiceSut()
|
||||
tmpl := createMessageTemplate()
|
||||
sut.config.(*MockAMConfigStore).EXPECT().
|
||||
getsConfig(models.AlertConfiguration{
|
||||
AlertmanagerConfiguration: configWithTemplates,
|
||||
})
|
||||
sut.config.(*MockAMConfigStore).EXPECT().
|
||||
UpdateAlertmanagerConfiguration(mock.Anything, mock.Anything).
|
||||
Return(fmt.Errorf("failed to save config"))
|
||||
sut.prov.(*MockProvisioningStore).EXPECT().saveSucceeds()
|
||||
|
||||
_, err := sut.SetTemplate(context.Background(), 1, tmpl)
|
||||
|
||||
require.ErrorContains(t, err, "failed to save config")
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("adds new template to config file on success", func(t *testing.T) {
|
||||
sut := createTemplateServiceSut()
|
||||
tmpl := createMessageTemplate()
|
||||
sut.config.(*MockAMConfigStore).EXPECT().
|
||||
getsConfig(models.AlertConfiguration{
|
||||
AlertmanagerConfiguration: configWithTemplates,
|
||||
})
|
||||
sut.config.(*MockAMConfigStore).EXPECT().saveSucceeds()
|
||||
sut.prov.(*MockProvisioningStore).EXPECT().saveSucceeds()
|
||||
|
||||
_, err := sut.SetTemplate(context.Background(), 1, tmpl)
|
||||
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("succeeds when stitching config file with no templates", func(t *testing.T) {
|
||||
sut := createTemplateServiceSut()
|
||||
tmpl := createMessageTemplate()
|
||||
sut.config.(*MockAMConfigStore).EXPECT().
|
||||
getsConfig(models.AlertConfiguration{
|
||||
AlertmanagerConfiguration: defaultConfig,
|
||||
})
|
||||
sut.config.(*MockAMConfigStore).EXPECT().saveSucceeds()
|
||||
sut.prov.(*MockProvisioningStore).EXPECT().saveSucceeds()
|
||||
|
||||
_, err := sut.SetTemplate(context.Background(), 1, tmpl)
|
||||
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("normalizes template content with no define", func(t *testing.T) {
|
||||
sut := createTemplateServiceSut()
|
||||
tmpl := definitions.MessageTemplate{
|
||||
Name: "name",
|
||||
Template: "content",
|
||||
}
|
||||
sut.config.(*MockAMConfigStore).EXPECT().
|
||||
getsConfig(models.AlertConfiguration{
|
||||
AlertmanagerConfiguration: defaultConfig,
|
||||
})
|
||||
sut.config.(*MockAMConfigStore).EXPECT().saveSucceeds()
|
||||
sut.prov.(*MockProvisioningStore).EXPECT().saveSucceeds()
|
||||
|
||||
result, _ := sut.SetTemplate(context.Background(), 1, tmpl)
|
||||
|
||||
exp := "{{ define \"name\" }}\n content\n{{ end }}"
|
||||
require.Equal(t, exp, result.Template)
|
||||
})
|
||||
|
||||
t.Run("avoids normalizing template content with define", func(t *testing.T) {
|
||||
sut := createTemplateServiceSut()
|
||||
tmpl := definitions.MessageTemplate{
|
||||
Name: "name",
|
||||
Template: "{{define \"name\"}}content{{end}}",
|
||||
}
|
||||
sut.config.(*MockAMConfigStore).EXPECT().
|
||||
getsConfig(models.AlertConfiguration{
|
||||
AlertmanagerConfiguration: defaultConfig,
|
||||
})
|
||||
sut.config.(*MockAMConfigStore).EXPECT().saveSucceeds()
|
||||
sut.prov.(*MockProvisioningStore).EXPECT().saveSucceeds()
|
||||
|
||||
result, _ := sut.SetTemplate(context.Background(), 1, tmpl)
|
||||
|
||||
require.Equal(t, tmpl.Template, result.Template)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("deleting templates", func(t *testing.T) {
|
||||
t.Run("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.DeleteTemplate(context.Background(), 1, "template")
|
||||
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("when config is invalid", func(t *testing.T) {
|
||||
sut := createTemplateServiceSut()
|
||||
sut.config.(*MockAMConfigStore).EXPECT().
|
||||
getsConfig(models.AlertConfiguration{
|
||||
AlertmanagerConfiguration: brokenConfig,
|
||||
})
|
||||
|
||||
err := sut.DeleteTemplate(context.Background(), 1, "template")
|
||||
|
||||
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.DeleteTemplate(context.Background(), 1, "template")
|
||||
|
||||
require.ErrorContains(t, err, "no alertmanager configuration")
|
||||
})
|
||||
|
||||
t.Run("when provenance fails to save", func(t *testing.T) {
|
||||
sut := createTemplateServiceSut()
|
||||
sut.config.(*MockAMConfigStore).EXPECT().
|
||||
getsConfig(models.AlertConfiguration{
|
||||
AlertmanagerConfiguration: configWithTemplates,
|
||||
})
|
||||
sut.config.(*MockAMConfigStore).EXPECT().saveSucceeds()
|
||||
sut.prov.(*MockProvisioningStore).EXPECT().
|
||||
DeleteProvenance(mock.Anything, mock.Anything, mock.Anything).
|
||||
Return(fmt.Errorf("failed to save provenance"))
|
||||
|
||||
err := sut.DeleteTemplate(context.Background(), 1, "template")
|
||||
|
||||
require.ErrorContains(t, err, "failed to save provenance")
|
||||
})
|
||||
|
||||
t.Run("when AM config fails to save", func(t *testing.T) {
|
||||
sut := createTemplateServiceSut()
|
||||
sut.config.(*MockAMConfigStore).EXPECT().
|
||||
getsConfig(models.AlertConfiguration{
|
||||
AlertmanagerConfiguration: configWithTemplates,
|
||||
})
|
||||
sut.config.(*MockAMConfigStore).EXPECT().
|
||||
UpdateAlertmanagerConfiguration(mock.Anything, mock.Anything).
|
||||
Return(fmt.Errorf("failed to save config"))
|
||||
sut.prov.(*MockProvisioningStore).EXPECT().saveSucceeds()
|
||||
|
||||
err := sut.DeleteTemplate(context.Background(), 1, "template")
|
||||
|
||||
require.ErrorContains(t, err, "failed to save config")
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("deletes template from config file on success", func(t *testing.T) {
|
||||
sut := createTemplateServiceSut()
|
||||
sut.config.(*MockAMConfigStore).EXPECT().
|
||||
getsConfig(models.AlertConfiguration{
|
||||
AlertmanagerConfiguration: configWithTemplates,
|
||||
})
|
||||
sut.config.(*MockAMConfigStore).EXPECT().saveSucceeds()
|
||||
sut.prov.(*MockProvisioningStore).EXPECT().saveSucceeds()
|
||||
|
||||
err := sut.DeleteTemplate(context.Background(), 1, "a")
|
||||
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("does not error when deleting templates that do not exist", func(t *testing.T) {
|
||||
sut := createTemplateServiceSut()
|
||||
sut.config.(*MockAMConfigStore).EXPECT().
|
||||
getsConfig(models.AlertConfiguration{
|
||||
AlertmanagerConfiguration: configWithTemplates,
|
||||
})
|
||||
sut.config.(*MockAMConfigStore).EXPECT().saveSucceeds()
|
||||
sut.prov.(*MockProvisioningStore).EXPECT().saveSucceeds()
|
||||
|
||||
err := sut.DeleteTemplate(context.Background(), 1, "does not exist")
|
||||
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("succeeds when deleting from config file with no template section", func(t *testing.T) {
|
||||
sut := createTemplateServiceSut()
|
||||
sut.config.(*MockAMConfigStore).EXPECT().
|
||||
getsConfig(models.AlertConfiguration{
|
||||
AlertmanagerConfiguration: defaultConfig,
|
||||
})
|
||||
sut.config.(*MockAMConfigStore).EXPECT().saveSucceeds()
|
||||
sut.prov.(*MockProvisioningStore).EXPECT().saveSucceeds()
|
||||
|
||||
err := sut.DeleteTemplate(context.Background(), 1, "a")
|
||||
|
||||
require.NoError(t, err)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func createTemplateServiceSut() *TemplateService {
|
||||
return &TemplateService{
|
||||
config: &MockAMConfigStore{},
|
||||
prov: NewFakeProvisioningStore(),
|
||||
prov: &MockProvisioningStore{},
|
||||
xact: newNopTransactionManager(),
|
||||
log: log.NewNopLogger(),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MockAMConfigStore_Expecter) setupGetConfig(ac models.AlertConfiguration) *MockAMConfigStore_Expecter {
|
||||
func createMessageTemplate() definitions.MessageTemplate {
|
||||
return definitions.MessageTemplate{
|
||||
Name: "test",
|
||||
Template: "asdf",
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MockAMConfigStore_Expecter) getsConfig(ac models.AlertConfiguration) *MockAMConfigStore_Expecter {
|
||||
m.GetLatestAlertmanagerConfiguration(mock.Anything, mock.Anything).
|
||||
Run(func(ctx context.Context, q *models.GetLatestAlertmanagerConfigurationQuery) {
|
||||
q.Result = &ac
|
||||
@ -94,6 +368,17 @@ func (m *MockAMConfigStore_Expecter) setupGetConfig(ac models.AlertConfiguration
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *MockAMConfigStore_Expecter) saveSucceeds() *MockAMConfigStore_Expecter {
|
||||
m.UpdateAlertmanagerConfiguration(mock.Anything, mock.Anything).Return(nil)
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *MockProvisioningStore_Expecter) saveSucceeds() *MockProvisioningStore_Expecter {
|
||||
m.SetProvenance(mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil)
|
||||
m.DeleteProvenance(mock.Anything, mock.Anything, mock.Anything).Return(nil)
|
||||
return m
|
||||
}
|
||||
|
||||
var defaultConfig = setting.GetAlertmanagerDefaultConfiguration()
|
||||
|
||||
var configWithTemplates = `
|
||||
|
Loading…
Reference in New Issue
Block a user