Alerting: Template Testing API (#67450)

This commit is contained in:
Matthew Jacobson 2023-04-28 10:56:59 -04:00 committed by GitHub
parent 9eb10bee1f
commit 91471ac7ae
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 847 additions and 78 deletions

2
go.mod
View File

@ -57,7 +57,7 @@ require (
github.com/google/uuid v1.3.0
github.com/google/wire v0.5.0
github.com/gorilla/websocket v1.5.0
github.com/grafana/alerting v0.0.0-20230426193323-4f09f516596b
github.com/grafana/alerting v0.0.0-20230428095912-33c5aa68a5ba
github.com/grafana/grafana-aws-sdk v0.12.0
github.com/grafana/grafana-azure-sdk-go v1.6.0
github.com/grafana/grafana-plugin-sdk-go v0.160.0

8
go.sum
View File

@ -1275,8 +1275,12 @@ github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grafana/alerting v0.0.0-20230426193323-4f09f516596b h1:dAHlNtW62HcSh+XmhV7kmcWqnOSnYsw/pGHiEyix544=
github.com/grafana/alerting v0.0.0-20230426193323-4f09f516596b/go.mod h1:5edgy6tQY4+W2wuJdi8g2GjbVmpJufguy7QGIRl2q4o=
github.com/grafana/alerting v0.0.0-20230427200804-f8565cd8b74b h1:SyatotlHGK+05AxICctry09ZEawhE0AasWpOifljH4M=
github.com/grafana/alerting v0.0.0-20230427200804-f8565cd8b74b/go.mod h1:nHfrSTdV7/l74N5/ezqlQ+JwSvIChhN3G5+PjCfwG/E=
github.com/grafana/alerting v0.0.0-20230428094731-e067f119be06 h1:g+J4UGslzpi9Kt+846NDAjGtVyt8K9Lgjj/vUh61yK0=
github.com/grafana/alerting v0.0.0-20230428094731-e067f119be06/go.mod h1:5edgy6tQY4+W2wuJdi8g2GjbVmpJufguy7QGIRl2q4o=
github.com/grafana/alerting v0.0.0-20230428095912-33c5aa68a5ba h1:aNTu22ojw4XY24DYNAuvw8v/5iFUNk2bkdTeeWQ5+0o=
github.com/grafana/alerting v0.0.0-20230428095912-33c5aa68a5ba/go.mod h1:5edgy6tQY4+W2wuJdi8g2GjbVmpJufguy7QGIRl2q4o=
github.com/grafana/codejen v0.0.3 h1:tAWxoTUuhgmEqxJPOLtJoxlPBbMULFwKFOcRsPRPXDw=
github.com/grafana/codejen v0.0.3/go.mod h1:zmwwM/DRyQB7pfuBjTWII3CWtxcXh8LTwAYGfDfpR6s=
github.com/grafana/cuetsy v0.1.8 h1:l0AKXfHr0clu6qPirirDzNC/W5mqq5gG7iruOVolG34=

View File

@ -52,6 +52,7 @@ type Alertmanager interface {
// Receivers
GetReceivers(ctx context.Context) []apimodels.Receiver
TestReceivers(ctx context.Context, c apimodels.TestReceiversConfigBodyParams) (*notifier.TestReceiversResult, error)
TestTemplate(ctx context.Context, c apimodels.TestTemplatesConfigBodyParams) (*notifier.TestTemplatesResults, error)
}
type AlertingStore interface {

View File

@ -342,6 +342,20 @@ func (srv AlertmanagerSrv) RoutePostTestReceivers(c *contextmodel.ReqContext, bo
return response.JSON(statusForTestReceivers(result.Receivers), newTestReceiversResult(result))
}
func (srv AlertmanagerSrv) RoutePostTestTemplates(c *contextmodel.ReqContext, body apimodels.TestTemplatesConfigBodyParams) response.Response {
am, errResp := srv.AlertmanagerFor(c.OrgID)
if errResp != nil {
return errResp
}
res, err := am.TestTemplate(c.Req.Context(), body)
if err != nil {
return response.Error(http.StatusInternalServerError, "", err)
}
return response.JSON(http.StatusOK, newTestTemplateResult(res))
}
// contextWithTimeoutFromRequest returns a context with a deadline set from the
// Request-Timeout header in the HTTP request. If the header is absent then the
// context will use the default timeout. The timeout in the Request-Timeout
@ -433,6 +447,24 @@ func statusForTestReceivers(v []notifier.TestReceiverResult) int {
}
}
func newTestTemplateResult(res *notifier.TestTemplatesResults) apimodels.TestTemplatesResults {
apiRes := apimodels.TestTemplatesResults{}
for _, r := range res.Results {
apiRes.Results = append(apiRes.Results, apimodels.TestTemplatesResult{
Name: r.Name,
Text: r.Text,
})
}
for _, e := range res.Errors {
apiRes.Errors = append(apiRes.Errors, apimodels.TestTemplatesErrorResult{
Name: e.Name,
Kind: apimodels.TemplateErrorKind(e.Kind),
Message: e.Error.Error(),
})
}
return apiRes
}
func (srv AlertmanagerSrv) AlertmanagerFor(orgID int64) (Alertmanager, *response.NormalResponse) {
am, err := srv.mam.AlertmanagerFor(orgID)
if err == nil {

View File

@ -421,6 +421,46 @@ func TestRoutePostGrafanaAlertingConfigHistoryActivate(t *testing.T) {
})
}
func TestRoutePostTestTemplates(t *testing.T) {
sut := createSut(t, nil)
t.Run("assert 404 when no alertmanager found", func(tt *testing.T) {
req, err := http.NewRequest(http.MethodGet, "https://grafana.net", nil)
require.NoError(tt, err)
q := req.URL.Query()
req.URL.RawQuery = q.Encode()
rc := createRequestCtxInOrg(10)
response := sut.RoutePostTestTemplates(rc, apimodels.TestTemplatesConfigBodyParams{})
require.Equal(tt, 404, response.Status())
})
t.Run("assert 409 when alertmanager not ready", func(tt *testing.T) {
req, err := http.NewRequest(http.MethodGet, "https://grafana.net", nil)
require.NoError(tt, err)
q := req.URL.Query()
req.URL.RawQuery = q.Encode()
rc := createRequestCtxInOrg(3)
response := sut.RoutePostTestTemplates(rc, apimodels.TestTemplatesConfigBodyParams{})
require.Equal(tt, 409, response.Status())
})
t.Run("assert 200 for a valid alertmanager", func(tt *testing.T) {
req, err := http.NewRequest(http.MethodGet, "https://grafana.net", nil)
require.NoError(tt, err)
q := req.URL.Query()
req.URL.RawQuery = q.Encode()
rc := createRequestCtxInOrg(1)
response := sut.RoutePostTestTemplates(rc, apimodels.TestTemplatesConfigBodyParams{})
require.Equal(tt, 200, response.Status())
})
}
func TestSilenceCreate(t *testing.T) {
makeSilence := func(comment string, createdBy string,
startsAt, endsAt strfmt.DateTime, matchers amv2.Matchers) amv2.Silence {

View File

@ -173,6 +173,9 @@ func (api *API) authorize(method, path string) web.Handler {
case http.MethodPost + "/api/alertmanager/grafana/config/api/v1/receivers/test":
fallback = middleware.ReqEditorRole
eval = ac.EvalPermission(ac.ActionAlertingNotificationsRead)
case http.MethodPost + "/api/alertmanager/grafana/config/api/v1/templates/test":
fallback = middleware.ReqSignedIn
eval = ac.EvalPermission(ac.ActionAlertingNotificationsRead)
// External Alertmanager Paths
case http.MethodDelete + "/api/alertmanager/{DatasourceUID}/config/api/v1/alerts":

View File

@ -49,7 +49,7 @@ func TestAuthorize(t *testing.T) {
}
paths[p] = methods
}
require.Len(t, paths, 47)
require.Len(t, paths, 48)
ac := acmock.New()
api := &API{AccessControl: ac}

View File

@ -189,3 +189,7 @@ func (f *AlertmanagerApiHandler) handleRouteGetGrafanaReceivers(ctx *contextmode
func (f *AlertmanagerApiHandler) handleRoutePostTestGrafanaReceivers(ctx *contextmodel.ReqContext, conf apimodels.TestReceiversConfigBodyParams) response.Response {
return f.GrafanaSvc.RoutePostTestReceivers(ctx, conf)
}
func (f *AlertmanagerApiHandler) handleRoutePostTestGrafanaTemplates(ctx *contextmodel.ReqContext, conf apimodels.TestTemplatesConfigBodyParams) response.Response {
return f.GrafanaSvc.RoutePostTestTemplates(ctx, conf)
}

View File

@ -44,6 +44,7 @@ type AlertmanagerApi interface {
RoutePostGrafanaAlertingConfig(*contextmodel.ReqContext) response.Response
RoutePostGrafanaAlertingConfigHistoryActivate(*contextmodel.ReqContext) response.Response
RoutePostTestGrafanaReceivers(*contextmodel.ReqContext) response.Response
RoutePostTestGrafanaTemplates(*contextmodel.ReqContext) response.Response
}
func (f *AlertmanagerApiHandler) RouteCreateGrafanaSilence(ctx *contextmodel.ReqContext) response.Response {
@ -181,6 +182,14 @@ func (f *AlertmanagerApiHandler) RoutePostTestGrafanaReceivers(ctx *contextmodel
}
return f.handleRoutePostTestGrafanaReceivers(ctx, conf)
}
func (f *AlertmanagerApiHandler) RoutePostTestGrafanaTemplates(ctx *contextmodel.ReqContext) response.Response {
// Parse Request Body
conf := apimodels.TestTemplatesConfigBodyParams{}
if err := web.Bind(ctx.Req, &conf); err != nil {
return response.Error(http.StatusBadRequest, "bad request data", err)
}
return f.handleRoutePostTestGrafanaTemplates(ctx, conf)
}
func (api *API) RegisterAlertmanagerApiEndpoints(srv AlertmanagerApi, m *metrics.API) {
api.RouteRegister.Group("", func(group routing.RouteRegister) {
@ -434,5 +443,15 @@ func (api *API) RegisterAlertmanagerApiEndpoints(srv AlertmanagerApi, m *metrics
m,
),
)
group.Post(
toMacaronPath("/api/alertmanager/grafana/config/api/v1/templates/test"),
api.authorize(http.MethodPost, "/api/alertmanager/grafana/config/api/v1/templates/test"),
metrics.Instrument(
http.MethodPost,
"/api/alertmanager/grafana/config/api/v1/templates/test",
api.Hooks.Wrap(srv.RoutePostTestGrafanaTemplates),
m,
),
)
}, middleware.ReqSignedIn)
}

View File

@ -299,6 +299,10 @@
"AlertingRule": {
"description": "adapted from cortex",
"properties": {
"activeAt": {
"format": "date-time",
"type": "string"
},
"alerts": {
"items": {
"$ref": "#/definitions/Alert"
@ -339,6 +343,20 @@
"description": "State can be \"pending\", \"firing\", \"inactive\".",
"type": "string"
},
"totals": {
"additionalProperties": {
"format": "int64",
"type": "integer"
},
"type": "object"
},
"totalsFiltered": {
"additionalProperties": {
"format": "int64",
"type": "integer"
},
"type": "object"
},
"type": {
"$ref": "#/definitions/RuleType"
}
@ -350,7 +368,7 @@
"type",
"state",
"annotations",
"alerts"
"activeAt"
],
"type": "object"
},
@ -2845,6 +2863,13 @@
"$ref": "#/definitions/RuleGroup"
},
"type": "array"
},
"totals": {
"additionalProperties": {
"format": "int64",
"type": "integer"
},
"type": "object"
}
},
"required": [
@ -2878,6 +2903,13 @@
"$ref": "#/definitions/AlertingRule"
},
"type": "array"
},
"totals": {
"additionalProperties": {
"format": "int64",
"type": "integer"
},
"type": "object"
}
},
"required": [
@ -3343,6 +3375,77 @@
},
"type": "object"
},
"TestTemplatesConfigBodyParams": {
"properties": {
"alerts": {
"description": "Alerts to use as data when testing the template.",
"items": {
"$ref": "#/definitions/postableAlert"
},
"type": "array"
},
"name": {
"description": "Name of the template file.",
"type": "string"
},
"template": {
"description": "Template string to test.",
"type": "string"
}
},
"type": "object"
},
"TestTemplatesErrorResult": {
"properties": {
"kind": {
"description": "Kind of template error that occurred.",
"enum": [
"invalid_template",
"execution_error"
],
"type": "string"
},
"message": {
"description": "Error message.",
"type": "string"
},
"name": {
"description": "Name of the associated template for this error. Will be empty if the Kind is \"invalid_template\".",
"type": "string"
}
},
"type": "object"
},
"TestTemplatesResult": {
"properties": {
"name": {
"description": "Name of the associated template definition for this result.",
"type": "string"
},
"text": {
"description": "Interpolated value of the template.",
"type": "string"
}
},
"type": "object"
},
"TestTemplatesResults": {
"properties": {
"errors": {
"items": {
"$ref": "#/definitions/TestTemplatesErrorResult"
},
"type": "array"
},
"results": {
"items": {
"$ref": "#/definitions/TestTemplatesResult"
},
"type": "array"
}
},
"type": "object"
},
"Threshold": {
"description": "Threshold a single step on the threshold list",
"properties": {
@ -3644,7 +3747,6 @@
"type": "object"
},
"alertGroup": {
"description": "AlertGroup alert group",
"properties": {
"alerts": {
"description": "alerts",
@ -3829,13 +3931,13 @@
"type": "object"
},
"gettableAlerts": {
"description": "GettableAlerts gettable alerts",
"items": {
"$ref": "#/definitions/gettableAlert"
},
"type": "array"
},
"gettableSilence": {
"description": "GettableSilence gettable silence",
"properties": {
"comment": {
"description": "comment",
@ -3891,7 +3993,6 @@
"type": "array"
},
"integration": {
"description": "Integration integration",
"properties": {
"lastNotifyAttempt": {
"description": "A timestamp indicating the last attempt to deliver a notification regardless of the outcome.\nFormat: date-time",
@ -4035,7 +4136,6 @@
"type": "array"
},
"postableSilence": {
"description": "PostableSilence postable silence",
"properties": {
"comment": {
"description": "comment",
@ -4073,6 +4173,7 @@
"type": "object"
},
"receiver": {
"description": "Receiver receiver",
"properties": {
"active": {
"description": "active",

View File

@ -168,6 +168,19 @@ import (
// 408: Failure
// 409: AlertManagerNotReady
// swagger:route POST /api/alertmanager/grafana/config/api/v1/templates/test alertmanager RoutePostTestGrafanaTemplates
//
// Test Grafana managed templates without saving them.
// Produces:
// - application/json
//
// Responses:
//
// 200: TestTemplatesResults
// 400: ValidationError
// 403: PermissionDenied
// 409: AlertManagerNotReady
// swagger:route GET /api/alertmanager/grafana/api/v2/silences alertmanager RouteGetGrafanaSilences
//
// get silences
@ -293,6 +306,56 @@ type TestReceiverConfigResult struct {
Error string `json:"error,omitempty"`
}
// swagger:parameters RoutePostTestGrafanaTemplates
type TestTemplatesConfigParams struct {
// in:body
Body TestTemplatesConfigBodyParams
}
type TestTemplatesConfigBodyParams struct {
// Alerts to use as data when testing the template.
Alerts []*amv2.PostableAlert `json:"alerts"`
// Template string to test.
Template string `json:"template"`
// Name of the template file.
Name string `json:"name"`
}
// swagger:model
type TestTemplatesResults struct {
Results []TestTemplatesResult `json:"results,omitempty"`
Errors []TestTemplatesErrorResult `json:"errors,omitempty"`
}
type TestTemplatesResult struct {
// Name of the associated template definition for this result.
Name string `json:"name"`
// Interpolated value of the template.
Text string `json:"text"`
}
type TestTemplatesErrorResult struct {
// Name of the associated template for this error. Will be empty if the Kind is "invalid_template".
Name string `json:"name,omitempty"`
// Kind of template error that occurred.
Kind TemplateErrorKind `json:"kind"`
// Error message.
Message string `json:"message"`
}
// swagger:enum TemplateErrorKind
type TemplateErrorKind string
const (
InvalidTemplate TemplateErrorKind = "invalid_template"
ExecutionError TemplateErrorKind = "execution_error"
)
// swagger:parameters RouteCreateSilence RouteCreateGrafanaSilence
type CreateSilenceParams struct {
// in:body

View File

@ -299,6 +299,10 @@
"AlertingRule": {
"description": "adapted from cortex",
"properties": {
"activeAt": {
"format": "date-time",
"type": "string"
},
"alerts": {
"items": {
"$ref": "#/definitions/Alert"
@ -339,6 +343,20 @@
"description": "State can be \"pending\", \"firing\", \"inactive\".",
"type": "string"
},
"totals": {
"additionalProperties": {
"format": "int64",
"type": "integer"
},
"type": "object"
},
"totalsFiltered": {
"additionalProperties": {
"format": "int64",
"type": "integer"
},
"type": "object"
},
"type": {
"$ref": "#/definitions/RuleType"
}
@ -350,7 +368,7 @@
"type",
"state",
"annotations",
"alerts"
"activeAt"
],
"type": "object"
},
@ -2845,6 +2863,13 @@
"$ref": "#/definitions/RuleGroup"
},
"type": "array"
},
"totals": {
"additionalProperties": {
"format": "int64",
"type": "integer"
},
"type": "object"
}
},
"required": [
@ -2878,6 +2903,13 @@
"$ref": "#/definitions/AlertingRule"
},
"type": "array"
},
"totals": {
"additionalProperties": {
"format": "int64",
"type": "integer"
},
"type": "object"
}
},
"required": [
@ -3343,6 +3375,77 @@
},
"type": "object"
},
"TestTemplatesConfigBodyParams": {
"properties": {
"alerts": {
"description": "Alerts to use as data when testing the template.",
"items": {
"$ref": "#/definitions/postableAlert"
},
"type": "array"
},
"name": {
"description": "Name of the template file.",
"type": "string"
},
"template": {
"description": "Template string to test.",
"type": "string"
}
},
"type": "object"
},
"TestTemplatesErrorResult": {
"properties": {
"kind": {
"description": "Kind of template error that occurred.",
"enum": [
"invalid_template",
"execution_error"
],
"type": "string"
},
"message": {
"description": "Error message.",
"type": "string"
},
"name": {
"description": "Name of the associated template for this error. Will be empty if the Kind is \"invalid_template\".",
"type": "string"
}
},
"type": "object"
},
"TestTemplatesResult": {
"properties": {
"name": {
"description": "Name of the associated template definition for this result.",
"type": "string"
},
"text": {
"description": "Interpolated value of the template.",
"type": "string"
}
},
"type": "object"
},
"TestTemplatesResults": {
"properties": {
"errors": {
"items": {
"$ref": "#/definitions/TestTemplatesErrorResult"
},
"type": "array"
},
"results": {
"items": {
"$ref": "#/definitions/TestTemplatesResult"
},
"type": "array"
}
},
"type": "object"
},
"Threshold": {
"description": "Threshold a single step on the threshold list",
"properties": {
@ -3668,6 +3771,7 @@
"type": "object"
},
"alertGroups": {
"description": "AlertGroups alert groups",
"items": {
"$ref": "#/definitions/alertGroup"
},
@ -3888,6 +3992,7 @@
"type": "array"
},
"integration": {
"description": "Integration integration",
"properties": {
"lastNotifyAttempt": {
"description": "A timestamp indicating the last attempt to deliver a notification regardless of the outcome.\nFormat: date-time",
@ -4031,7 +4136,6 @@
"type": "array"
},
"postableSilence": {
"description": "PostableSilence postable silence",
"properties": {
"comment": {
"description": "comment",
@ -4606,6 +4710,53 @@
]
}
},
"/api/alertmanager/grafana/config/api/v1/templates/test": {
"post": {
"operationId": "RoutePostTestGrafanaTemplates",
"parameters": [
{
"in": "body",
"name": "Body",
"schema": {
"$ref": "#/definitions/TestTemplatesConfigBodyParams"
}
}
],
"produces": [
"application/json"
],
"responses": {
"200": {
"description": "TestTemplatesResults",
"schema": {
"$ref": "#/definitions/TestTemplatesResults"
}
},
"400": {
"description": "ValidationError",
"schema": {
"$ref": "#/definitions/ValidationError"
}
},
"403": {
"description": "PermissionDenied",
"schema": {
"$ref": "#/definitions/PermissionDenied"
}
},
"409": {
"description": "AlertManagerNotReady",
"schema": {
"$ref": "#/definitions/AlertManagerNotReady"
}
}
},
"summary": "Test Grafana managed templates without saving them.",
"tags": [
"alertmanager"
]
}
},
"/api/alertmanager/grafana/config/history": {
"get": {
"description": "gets Alerting configurations that were successfully applied in the past",

View File

@ -435,6 +435,53 @@
}
}
},
"/api/alertmanager/grafana/config/api/v1/templates/test": {
"post": {
"produces": [
"application/json"
],
"tags": [
"alertmanager"
],
"summary": "Test Grafana managed templates without saving them.",
"operationId": "RoutePostTestGrafanaTemplates",
"parameters": [
{
"name": "Body",
"in": "body",
"schema": {
"$ref": "#/definitions/TestTemplatesConfigBodyParams"
}
}
],
"responses": {
"200": {
"description": "TestTemplatesResults",
"schema": {
"$ref": "#/definitions/TestTemplatesResults"
}
},
"400": {
"description": "ValidationError",
"schema": {
"$ref": "#/definitions/ValidationError"
}
},
"403": {
"description": "PermissionDenied",
"schema": {
"$ref": "#/definitions/PermissionDenied"
}
},
"409": {
"description": "AlertManagerNotReady",
"schema": {
"$ref": "#/definitions/AlertManagerNotReady"
}
}
}
}
},
"/api/alertmanager/grafana/config/history": {
"get": {
"description": "gets Alerting configurations that were successfully applied in the past",
@ -3016,9 +3063,13 @@
"type",
"state",
"annotations",
"alerts"
"activeAt"
],
"properties": {
"activeAt": {
"type": "string",
"format": "date-time"
},
"alerts": {
"type": "array",
"items": {
@ -3059,6 +3110,20 @@
"description": "State can be \"pending\", \"firing\", \"inactive\".",
"type": "string"
},
"totals": {
"type": "object",
"additionalProperties": {
"type": "integer",
"format": "int64"
}
},
"totalsFiltered": {
"type": "object",
"additionalProperties": {
"type": "integer",
"format": "int64"
}
},
"type": {
"$ref": "#/definitions/RuleType"
}
@ -5563,6 +5628,13 @@
"items": {
"$ref": "#/definitions/RuleGroup"
}
},
"totals": {
"type": "object",
"additionalProperties": {
"type": "integer",
"format": "int64"
}
}
}
},
@ -5599,6 +5671,13 @@
"items": {
"$ref": "#/definitions/AlertingRule"
}
},
"totals": {
"type": "object",
"additionalProperties": {
"type": "integer",
"format": "int64"
}
}
}
},
@ -6057,6 +6136,77 @@
}
}
},
"TestTemplatesConfigBodyParams": {
"type": "object",
"properties": {
"alerts": {
"description": "Alerts to use as data when testing the template.",
"type": "array",
"items": {
"$ref": "#/definitions/postableAlert"
}
},
"name": {
"description": "Name of the template file.",
"type": "string"
},
"template": {
"description": "Template string to test.",
"type": "string"
}
}
},
"TestTemplatesErrorResult": {
"type": "object",
"properties": {
"kind": {
"description": "Kind of template error that occurred.",
"type": "string",
"enum": [
"invalid_template",
"execution_error"
]
},
"message": {
"description": "Error message.",
"type": "string"
},
"name": {
"description": "Name of the associated template for this error. Will be empty if the Kind is \"invalid_template\".",
"type": "string"
}
}
},
"TestTemplatesResult": {
"type": "object",
"properties": {
"name": {
"description": "Name of the associated template definition for this result.",
"type": "string"
},
"text": {
"description": "Interpolated value of the template.",
"type": "string"
}
}
},
"TestTemplatesResults": {
"type": "object",
"properties": {
"errors": {
"type": "array",
"items": {
"$ref": "#/definitions/TestTemplatesErrorResult"
}
},
"results": {
"type": "array",
"items": {
"$ref": "#/definitions/TestTemplatesResult"
}
}
}
},
"Threshold": {
"description": "Threshold a single step on the threshold list",
"type": "object",
@ -6383,6 +6533,7 @@
"$ref": "#/definitions/alertGroup"
},
"alertGroups": {
"description": "AlertGroups alert groups",
"type": "array",
"items": {
"$ref": "#/definitions/alertGroup"
@ -6608,6 +6759,7 @@
"$ref": "#/definitions/gettableSilences"
},
"integration": {
"description": "Integration integration",
"type": "object",
"required": [
"name",
@ -6752,7 +6904,6 @@
}
},
"postableSilence": {
"description": "PostableSilence postable silence",
"type": "object",
"required": [
"comment",

View File

@ -117,7 +117,8 @@ func newAlertmanager(ctx context.Context, orgID int64, cfg *setting.Cfg, store A
}
amcfg := &alertingNotify.GrafanaAlertmanagerConfig{
WorkingDirectory: workingDir,
WorkingDirectory: filepath.Join(cfg.DataPath, workingDir, strconv.Itoa(int(orgID))),
ExternalURL: cfg.AppURL,
AlertStoreCallback: nil,
PeerTimeout: cfg.UnifiedAlerting.HAPeerTimeout,
Silences: silencesOptions,
@ -259,9 +260,10 @@ func (am *Alertmanager) applyConfig(cfg *apimodels.PostableUserConfig, rawConfig
cfg.TemplateFiles = map[string]string{}
}
cfg.TemplateFiles["__default__.tmpl"] = alertingTemplates.DefaultTemplateString
cfg.AlertmanagerConfig.Templates = append(cfg.AlertmanagerConfig.Templates, "__default__.tmpl")
// next, we need to make sure we persist the templates to disk.
paths, templatesChanged, err := PersistTemplates(cfg, am.WorkingDirPath())
_, templatesChanged, err := PersistTemplates(cfg, am.Base.WorkingDirectory())
if err != nil {
return false, err
}
@ -272,18 +274,11 @@ func (am *Alertmanager) applyConfig(cfg *apimodels.PostableUserConfig, rawConfig
return false, nil
}
// With the templates persisted, create the template list using the paths.
tmpl, err := am.Base.TemplateFromPaths(am.Settings.AppURL, paths...)
if err != nil {
return false, err
}
err = am.Base.ApplyConfig(AlertingConfiguration{
RawAlertmanagerConfig: rawConfig,
AlertmanagerConfig: cfg.AlertmanagerConfig,
AlertmanagerTemplates: tmpl,
IntegrationsFunc: am.buildIntegrationsMap,
ReceiverIntegrationsFunc: am.buildReceiverIntegration,
rawAlertmanagerConfig: rawConfig,
alertmanagerConfig: cfg.AlertmanagerConfig,
receivers: PostableApiAlertingConfigToApiReceivers(cfg.AlertmanagerConfig),
receiverIntegrationsFunc: am.buildReceiverIntegrations,
})
if err != nil {
return false, err
@ -310,22 +305,8 @@ func (am *Alertmanager) applyAndMarkConfig(ctx context.Context, hash string, cfg
return nil
}
func (am *Alertmanager) WorkingDirPath() string {
return filepath.Join(am.Settings.DataPath, workingDir, strconv.Itoa(int(am.orgID)))
}
// buildIntegrationsMap builds a map of name to the list of Grafana integration notifiers off of a list of receiver config.
func (am *Alertmanager) buildIntegrationsMap(receivers []*alertingNotify.APIReceiver, templates *alertingTemplates.Template) (map[string][]*alertingNotify.Integration, error) {
integrationsMap := make(map[string][]*alertingNotify.Integration, len(receivers))
for _, receiver := range receivers {
integrations, err := am.buildReceiverIntegrations(receiver, templates)
if err != nil {
return nil, err
}
integrationsMap[receiver.Name] = integrations
}
return integrationsMap, nil
func (am *Alertmanager) AppURL() string {
return am.Settings.AppURL
}
// buildReceiverIntegrations builds a list of integration notifiers off of a receiver config.
@ -356,23 +337,6 @@ func (am *Alertmanager) buildReceiverIntegrations(receiver *alertingNotify.APIRe
return integrations, nil
}
func (am *Alertmanager) buildReceiverIntegration(r *alertingNotify.GrafanaIntegrationConfig, tmpl *alertingTemplates.Template) (*alertingNotify.Integration, error) {
apiReceiver := &alertingNotify.APIReceiver{
GrafanaIntegrations: alertingNotify.GrafanaIntegrations{
Integrations: []*alertingNotify.GrafanaIntegrationConfig{r},
},
}
integrations, err := am.buildReceiverIntegrations(apiReceiver, tmpl)
if err != nil {
return nil, err
}
if len(integrations) == 0 {
// This should not happen, but it is better to return some error rather than having a panic.
return nil, fmt.Errorf("failed to build integration")
}
return integrations[0], nil
}
// PutAlerts receives the alerts and then sends them through the corresponding route based on whenever the alert has a receiver embedded or not
func (am *Alertmanager) PutAlerts(postableAlerts apimodels.PostableAlerts) error {
alerts := make(alertingNotify.PostableAlerts, 0, len(postableAlerts.PostableAlerts))

View File

@ -22,6 +22,7 @@ func setupAMTest(t *testing.T) *Alertmanager {
dir := t.TempDir()
cfg := &setting.Cfg{
DataPath: dir,
AppURL: "http://localhost:9093",
}
m := metrics.NewAlertmanagerMetrics(prometheus.NewRegistry())

View File

@ -92,18 +92,16 @@ func Load(rawConfig []byte) (*api.PostableUserConfig, error) {
// AlertingConfiguration provides configuration for an Alertmanager.
// It implements the notify.Configuration interface.
type AlertingConfiguration struct {
AlertmanagerConfig api.PostableApiAlertingConfig
RawAlertmanagerConfig []byte
alertmanagerConfig api.PostableApiAlertingConfig
rawAlertmanagerConfig []byte
AlertmanagerTemplates *alertingTemplates.Template
IntegrationsFunc func(receivers []*alertingNotify.APIReceiver, templates *alertingTemplates.Template) (map[string][]*alertingNotify.Integration, error)
ReceiverIntegrationsFunc func(r *alertingNotify.GrafanaIntegrationConfig, tmpl *alertingTemplates.Template) (*alertingNotify.Integration, error)
receivers []*alertingNotify.APIReceiver
receiverIntegrationsFunc func(r *alertingNotify.APIReceiver, tmpl *alertingTemplates.Template) ([]*alertingNotify.Integration, error)
}
func (a AlertingConfiguration) BuildReceiverIntegrationsFunc() func(next *alertingNotify.GrafanaIntegrationConfig, tmpl *alertingTemplates.Template) (alertingNotify.Notifier, error) {
return func(next *alertingNotify.GrafanaIntegrationConfig, tmpl *alertingTemplates.Template) (alertingNotify.Notifier, error) {
return a.ReceiverIntegrationsFunc(next, tmpl)
func (a AlertingConfiguration) BuildReceiverIntegrationsFunc() func(next *alertingNotify.APIReceiver, tmpl *alertingTemplates.Template) ([]*alertingNotify.Integration, error) {
return func(next *alertingNotify.APIReceiver, tmpl *alertingTemplates.Template) ([]*alertingNotify.Integration, error) {
return a.receiverIntegrationsFunc(next, tmpl)
}
}
@ -112,29 +110,29 @@ func (a AlertingConfiguration) DispatcherLimits() alertingNotify.DispatcherLimit
}
func (a AlertingConfiguration) InhibitRules() []alertingNotify.InhibitRule {
return a.AlertmanagerConfig.InhibitRules
return a.alertmanagerConfig.InhibitRules
}
func (a AlertingConfiguration) MuteTimeIntervals() []alertingNotify.MuteTimeInterval {
return a.AlertmanagerConfig.MuteTimeIntervals
return a.alertmanagerConfig.MuteTimeIntervals
}
func (a AlertingConfiguration) ReceiverIntegrations() (map[string][]*alertingNotify.Integration, error) {
return a.IntegrationsFunc(PostableApiAlertingConfigToApiReceivers(a.AlertmanagerConfig), a.AlertmanagerTemplates)
func (a AlertingConfiguration) Receivers() []*alertingNotify.APIReceiver {
return a.receivers
}
func (a AlertingConfiguration) RoutingTree() *alertingNotify.Route {
return a.AlertmanagerConfig.Route.AsAMRoute()
return a.alertmanagerConfig.Route.AsAMRoute()
}
func (a AlertingConfiguration) Templates() *alertingTemplates.Template {
return a.AlertmanagerTemplates
func (a AlertingConfiguration) Templates() []string {
return a.alertmanagerConfig.Templates
}
func (a AlertingConfiguration) Hash() [16]byte {
return md5.Sum(a.RawAlertmanagerConfig)
return md5.Sum(a.rawAlertmanagerConfig)
}
func (a AlertingConfiguration) Raw() []byte {
return a.RawAlertmanagerConfig
return a.rawAlertmanagerConfig
}

View File

@ -0,0 +1,63 @@
package notifier
import (
"context"
alertingModels "github.com/grafana/alerting/models"
alertingNotify "github.com/grafana/alerting/notify"
amv2 "github.com/prometheus/alertmanager/api/v2/models"
prometheusModel "github.com/prometheus/common/model"
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
)
type TestTemplatesResults = alertingNotify.TestTemplatesResults
var (
DefaultLabels = map[string]string{
prometheusModel.AlertNameLabel: `alert title`,
alertingModels.FolderTitleLabel: `folder title`,
}
DefaultAnnotations = map[string]string{
alertingModels.ValuesAnnotation: `{"B":22,"C":1}`,
alertingModels.ValueStringAnnotation: `[ var='B' labels={__name__=go_threads, instance=host.docker.internal:3000, job=grafana} value=22 ], [ var='C' labels={__name__=go_threads, instance=host.docker.internal:3000, job=grafana} value=1 ]`,
alertingModels.OrgIDAnnotation: `1`,
alertingModels.DashboardUIDAnnotation: `dashboard_uid`,
alertingModels.PanelIDAnnotation: `1`,
}
)
// TestTemplate tests the given template string against the given alerts. Existing templates are used to provide context for the test.
// If an existing template of the same filename as the one being tested is found, it will not be used as context.
func (am *Alertmanager) TestTemplate(ctx context.Context, c apimodels.TestTemplatesConfigBodyParams) (*TestTemplatesResults, error) {
for _, alert := range c.Alerts {
addDefaultLabelsAndAnnotations(alert)
}
return am.Base.TestTemplate(ctx, alertingNotify.TestTemplatesConfigBodyParams{
Alerts: c.Alerts,
Template: c.Template,
Name: c.Name,
})
}
// addDefaultLabelsAndAnnotations is a slimmed down version of schedule.stateToPostableAlert and schedule.getRuleExtraLabels using default values.
func addDefaultLabelsAndAnnotations(alert *amv2.PostableAlert) {
if alert.Labels == nil {
alert.Labels = make(map[string]string)
}
for k, v := range DefaultLabels {
if _, ok := alert.Labels[k]; !ok {
alert.Labels[k] = v
}
}
if alert.Annotations == nil {
alert.Annotations = make(map[string]string)
}
for k, v := range DefaultAnnotations {
if _, ok := alert.Annotations[k]; !ok {
alert.Annotations[k] = v
}
}
}

View File

@ -0,0 +1,174 @@
package notifier
import (
"context"
"fmt"
"testing"
"github.com/go-openapi/strfmt"
alertingModels "github.com/grafana/alerting/models"
alertingNotify "github.com/grafana/alerting/notify"
amv2 "github.com/prometheus/alertmanager/api/v2/models"
prometheusModel "github.com/prometheus/common/model"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
)
var (
simpleAlert = amv2.PostableAlert{
Alert: amv2.Alert{
Labels: amv2.LabelSet{"__alert_rule_uid__": "rule uid", "alertname": "alert1", "lbl1": "val1"},
},
Annotations: amv2.LabelSet{"ann1": "annv1", "__dashboardUid__": "abcd", "__panelId__": "efgh", "__alertImageToken__": "test-image-1"},
StartsAt: strfmt.DateTime{},
EndsAt: strfmt.DateTime{},
}
)
func TestTemplateDefaultData(t *testing.T) {
am := setupAMTest(t)
tests := []struct {
name string
input apimodels.TestTemplatesConfigBodyParams
expected TestTemplatesResults
}{{
name: "check various extended data",
input: apimodels.TestTemplatesConfigBodyParams{
Alerts: []*amv2.PostableAlert{&simpleAlert},
Name: "slack.title",
Template: `{{ define "slack.title" }}
Receiver: {{ .Receiver }}
Status: {{ .Status }}
ExternalURL: {{ .ExternalURL }}
Alerts: {{ len .Alerts }}
Firing Alerts: {{ len .Alerts.Firing }}
Resolved Alerts: {{ len .Alerts.Resolved }}
GroupLabels: {{ range .GroupLabels.SortedPairs }}{{ .Name }}={{ .Value }} {{ end }}
CommonLabels: {{ range .CommonLabels.SortedPairs }}{{ .Name }}={{ .Value }} {{ end }}
CommonAnnotations: {{ range .CommonAnnotations.SortedPairs }}{{ .Name }}={{ .Value }} {{ end }}
{{ end }}`,
},
expected: TestTemplatesResults{
Results: []alertingNotify.TestTemplatesResult{{
Name: "slack.title",
Text: "\nReceiver: TestReceiver\nStatus: firing\nExternalURL: http://localhost:9093\nAlerts: 1\nFiring Alerts: 1\nResolved Alerts: 0\nGroupLabels: group_label=group_label_value \nCommonLabels: alertname=alert1 grafana_folder=folder title lbl1=val1 \nCommonAnnotations: ann1=annv1 \n",
}},
Errors: nil,
},
}, {
name: "AlertNameLabel",
input: apimodels.TestTemplatesConfigBodyParams{
Alerts: []*amv2.PostableAlert{{}},
Name: "slack.title",
Template: fmt.Sprintf(`{{ define "slack.title" }}{{ index (index .Alerts 0 ).Labels "%s" }}{{ end }}`, prometheusModel.AlertNameLabel),
},
expected: TestTemplatesResults{
Results: []alertingNotify.TestTemplatesResult{{
Name: "slack.title",
Text: DefaultLabels[prometheusModel.AlertNameLabel],
}},
Errors: nil,
},
}, {
name: "FolderTitleLabel",
input: apimodels.TestTemplatesConfigBodyParams{
Alerts: []*amv2.PostableAlert{{}},
Name: "slack.title",
Template: fmt.Sprintf(`{{ define "slack.title" }}{{ index (index .Alerts 0 ).Labels "%s" }}{{ end }}`, alertingModels.FolderTitleLabel),
},
expected: TestTemplatesResults{
Results: []alertingNotify.TestTemplatesResult{{
Name: "slack.title",
Text: DefaultLabels[alertingModels.FolderTitleLabel],
}},
Errors: nil,
},
}, {
name: "ValuesAnnotation",
input: apimodels.TestTemplatesConfigBodyParams{
Alerts: []*amv2.PostableAlert{{}},
Name: "slack.title",
Template: `{{ define "slack.title" }}{{ range $key, $value := (index .Alerts 0 ).Values }}{{ $key }}={{ $value }} {{ end }}{{ end }}`,
},
expected: TestTemplatesResults{
Results: []alertingNotify.TestTemplatesResult{{
Name: "slack.title",
Text: "B=22 C=1 ",
}},
Errors: nil,
},
}, {
name: "ValueStringAnnotation",
input: apimodels.TestTemplatesConfigBodyParams{
Alerts: []*amv2.PostableAlert{{}},
Name: "slack.title",
Template: `{{ define "slack.title" }}{{ (index .Alerts 0 ).ValueString }}{{ end }}`,
},
expected: TestTemplatesResults{
Results: []alertingNotify.TestTemplatesResult{{
Name: "slack.title",
Text: DefaultAnnotations[alertingModels.ValueStringAnnotation],
}},
Errors: nil,
},
}, {
name: "DashboardURL generation contains DashboardUIDAnnotation and OrgIDAnnotation ",
input: apimodels.TestTemplatesConfigBodyParams{
Alerts: []*amv2.PostableAlert{{}},
Name: "slack.title",
Template: `{{ define "slack.title" }}{{ (index .Alerts 0 ).DashboardURL }}{{ end }}`,
},
expected: TestTemplatesResults{
Results: []alertingNotify.TestTemplatesResult{{
Name: "slack.title",
Text: fmt.Sprintf("http://localhost:9093/d/%s?orgId=%s",
DefaultAnnotations[alertingModels.DashboardUIDAnnotation],
DefaultAnnotations[alertingModels.OrgIDAnnotation]),
}},
Errors: nil,
},
}, {
name: "PanelURL generation contains DashboardUIDAnnotation, PanelIDAnnotation, and OrgIDAnnotation ",
input: apimodels.TestTemplatesConfigBodyParams{
Alerts: []*amv2.PostableAlert{{}},
Name: "slack.title",
Template: `{{ define "slack.title" }}{{ (index .Alerts 0 ).PanelURL }}{{ end }}`,
},
expected: TestTemplatesResults{
Results: []alertingNotify.TestTemplatesResult{{
Name: "slack.title",
Text: fmt.Sprintf("http://localhost:9093/d/%s?orgId=%s&viewPanel=%s",
DefaultAnnotations[alertingModels.DashboardUIDAnnotation],
DefaultAnnotations[alertingModels.OrgIDAnnotation],
DefaultAnnotations[alertingModels.PanelIDAnnotation]),
}},
Errors: nil,
},
}, {
name: "GeneratorURL generation ",
input: apimodels.TestTemplatesConfigBodyParams{
Alerts: []*amv2.PostableAlert{{Alert: amv2.Alert{GeneratorURL: "http://localhost:3000"}}},
Name: "slack.title",
Template: `{{ define "slack.title" }}{{ (index .Alerts 0 ).GeneratorURL }}{{ end }}`,
},
expected: TestTemplatesResults{
Results: []alertingNotify.TestTemplatesResult{{
Name: "slack.title",
Text: fmt.Sprintf("http://localhost:3000?orgId=%s", DefaultAnnotations[alertingModels.OrgIDAnnotation]),
}},
Errors: nil,
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
res, err := am.TestTemplate(context.Background(), test.input)
require.NoError(t, err)
assert.Equal(t, test.expected, *res)
})
}
}

View File

@ -102,7 +102,7 @@ func TestIntegrationAlertmanagerConfigurationIsTransactional(t *testing.T) {
require.NoError(t, err)
var res map[string]interface{}
require.NoError(t, json.Unmarshal(b, &res))
require.Regexp(t, `^failed to save and apply Alertmanager configuration: failed to build integration map: failed to validate integration "slack.receiver" \(UID [^\)]+\) of type "slack": token must be specified when using the Slack chat API`, res["message"])
require.Regexp(t, `^failed to save and apply Alertmanager configuration: failed to validate integration "slack.receiver" \(UID [^\)]+\) of type "slack": token must be specified when using the Slack chat API`, res["message"])
resp = getRequest(t, alertConfigURL, http.StatusOK) // nolint
require.JSONEq(t, defaultAlertmanagerConfigJSON, getBody(t, resp.Body))