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
19 changed files with 847 additions and 78 deletions

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",