Skip to content

Commit

Permalink
Alerting: Provisioning message templates (grafana#48665)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
alexweav authored May 5, 2022
1 parent 6de7728 commit 0f56462
Show file tree
Hide file tree
Showing 12 changed files with 875 additions and 31 deletions.
31 changes: 29 additions & 2 deletions pkg/services/ngalert/api/api_provisioning.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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, "")
Expand Down Expand Up @@ -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, "")
Expand All @@ -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)
}
6 changes: 4 additions & 2 deletions pkg/services/ngalert/api/authorization.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
8 changes: 8 additions & 0 deletions pkg/services/ngalert/api/forked_provisioning.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
40 changes: 37 additions & 3 deletions pkg/services/ngalert/api/generated_base_api_provisioning.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,25 @@ 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
RouteGetTemplates(*models.ReqContext) response.Response
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)
}
Expand Down Expand Up @@ -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(
Expand All @@ -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"),
Expand All @@ -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,
),
Expand Down Expand Up @@ -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)
}
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -8,17 +16,78 @@ 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.
//
// Responses:
// 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 changes: 3 additions & 0 deletions pkg/services/ngalert/api/tooling/definitions/shared.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package definitions

type NotFound struct{}
63 changes: 56 additions & 7 deletions pkg/services/ngalert/api/tooling/post.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down Expand Up @@ -3106,6 +3115,7 @@
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models"
},
"alertGroup": {
"description": "AlertGroup alert group",
"properties": {
"alerts": {
"description": "alerts",
Expand All @@ -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": {
Expand Down Expand Up @@ -3559,6 +3567,7 @@
"type": "object"
},
"receiver": {
"description": "Receiver receiver",
"properties": {
"name": {
"description": "name",
Expand All @@ -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",
Expand Down Expand Up @@ -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": {
Expand All @@ -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": {
Expand Down
Loading

0 comments on commit 0f56462

Please sign in to comment.