Alerting: Template API to return errutil errors (#91821)

This commit is contained in:
Yuri Tseretyan
2024-08-13 10:59:19 -04:00
committed by GitHub
parent 9067797eb4
commit db33df5041
11 changed files with 104 additions and 65 deletions

View File

@@ -1060,7 +1060,7 @@ GET /api/v1/provisioning/templates/:name
###### <span id="route-post-contactpoints-400-schema"></span> Schema
[ValidationError](#validation-error)
### <span id="route-post-mute-timing"></span> Create a new mute timing. (_RoutePostMuteTiming_)
@@ -1074,7 +1074,7 @@ Status: OK
| Name | Source | Type | Go type | Separator | Required | Default | Description |
| -------------------------- | -------- | --------------------------------------- | ------------------------- | --------- | :------: | ------- | --------------------------------------------------------- |
| X-Disable-Provenance: true | `header` | string | `string` | | | | Allows editing of provisioned resources in the Grafana UI |
| Body | `body` | [MuteTimeInterval](#mute-time-interval) | `models.MuteTimeInterval` | | | | |
{{% /responsive-table %}}
@@ -1086,10 +1086,9 @@ GET /api/v1/provisioning/templates
| [201](#route-post-mute-timing-201) | Created | MuteTimeInterval | | [schema](#route-post-mute-timing-201-schema) |
| [400](#route-post-mute-timing-400) | Bad Request | ValidationError | | [schema](#route-post-mute-timing-400-schema) |
#### All responses
| Code | Status | Description | Has headers | Schema |
| ---------------------------------- | ----------- | ---------------- | :---------: | -------------------------------------------- |
#### Responses
##### <span id="route-post-mute-timing-201"></span> 201 - MuteTimeInterval
Status: Created
@@ -1101,12 +1100,6 @@ Status: OK
Status: Bad Request
[MuteTimeInterval](#mute-time-interval)
##### <span id="route-post-mute-timing-400"></span> 400 - ValidationError
Status: Bad Request
###### <span id="route-post-mute-timing-400-schema"></span> Schema
[ValidationError](#validation-error)
@@ -1480,7 +1473,7 @@ PUT /api/v1/provisioning/templates/:name
| Name | Type | Go type | Required | Default | Description | Example |
| --------- | ------------------------------------------------- | ------------------------- | :------: | ------- | ----------- | ------- |
| folderUid | string | `string` | | | | |
| interval | int64 (formatted integer) | `int64` | | | | |
| interval | int64 (formatted integer) | `int64` | | | | |
| rules | [][ProvisionedAlertRule](#provisioned-alert-rule) | `[]*ProvisionedAlertRule` | | | | |
| title | string | `string` | | | | |
@@ -1499,7 +1492,7 @@ Status: Bad Request
| name | string | `string` | | | | |
| orgId | int64 (formatted integer) | `int64` | | | | |
| rules | [][AlertRuleExport](#alert-rule-export) | `[]*AlertRuleExport` | | | | |
{{% /responsive-table %}}
### <span id="alerting-file-export"></span> AlertingFileExport

View File

@@ -201,7 +201,7 @@ func (srv *ProvisioningSrv) RouteDeleteContactPoint(c *contextmodel.ReqContext,
func (srv *ProvisioningSrv) RouteGetTemplates(c *contextmodel.ReqContext) response.Response {
templates, err := srv.templates.GetTemplates(c.Req.Context(), c.SignedInUser.GetOrgID())
if err != nil {
return ErrResp(http.StatusInternalServerError, err, "")
return response.ErrOrFallback(http.StatusInternalServerError, "", err)
}
return response.JSON(http.StatusOK, templates)
}
@@ -209,14 +209,14 @@ func (srv *ProvisioningSrv) RouteGetTemplates(c *contextmodel.ReqContext) respon
func (srv *ProvisioningSrv) RouteGetTemplate(c *contextmodel.ReqContext, name string) response.Response {
templates, err := srv.templates.GetTemplates(c.Req.Context(), c.SignedInUser.GetOrgID())
if err != nil {
return ErrResp(http.StatusInternalServerError, err, "")
return response.ErrOrFallback(http.StatusInternalServerError, "", err)
}
for _, tmpl := range templates {
if tmpl.Name == name {
return response.JSON(http.StatusOK, tmpl)
}
}
return response.Empty(http.StatusNotFound)
return response.Err(provisioning.ErrTemplateNotFound)
}
func (srv *ProvisioningSrv) RoutePutTemplate(c *contextmodel.ReqContext, body definitions.NotificationTemplateContent, name string) response.Response {
@@ -228,9 +228,6 @@ func (srv *ProvisioningSrv) RoutePutTemplate(c *contextmodel.ReqContext, body de
}
modified, err := srv.templates.SetTemplate(c.Req.Context(), c.SignedInUser.GetOrgID(), tmpl)
if err != nil {
if errors.Is(err, provisioning.ErrValidation) {
return ErrResp(http.StatusBadRequest, err, "")
}
return response.ErrOrFallback(http.StatusInternalServerError, "", err)
}
return response.JSON(http.StatusAccepted, modified)

View File

@@ -1906,7 +1906,7 @@
"type": "array"
}
},
"title": "Headers represents the configuration for HTTP headers.",
"title": "Header represents the configuration for a single HTTP header.",
"type": "object"
},
"Headers": {
@@ -3403,6 +3403,12 @@
"Route": {
"description": "A Route is a node that contains definitions of how to handle alerts. This is modified\nfrom the upstream alertmanager in that it adds the ObjectMatchers property.",
"properties": {
"active_time_intervals": {
"items": {
"type": "string"
},
"type": "array"
},
"continue": {
"type": "boolean"
},
@@ -4575,6 +4581,7 @@
"type": "object"
},
"alertGroups": {
"description": "AlertGroups alert groups",
"items": {
"$ref": "#/definitions/alertGroup",
"type": "object"
@@ -6173,9 +6180,6 @@
"schema": {
"$ref": "#/definitions/NotificationTemplates"
}
},
"404": {
"description": " Not found."
}
},
"summary": "Get all notification templates.",
@@ -6237,7 +6241,10 @@
}
},
"404": {
"description": " Not found."
"description": "GenericPublicError",
"schema": {
"$ref": "#/definitions/GenericPublicError"
}
}
},
"summary": "Get a notification template.",
@@ -6279,9 +6286,9 @@
}
},
"400": {
"description": "ValidationError",
"description": "GenericPublicError",
"schema": {
"$ref": "#/definitions/ValidationError"
"$ref": "#/definitions/GenericPublicError"
}
},
"409": {

View File

@@ -6,7 +6,6 @@ package definitions
//
// Responses:
// 200: NotificationTemplates
// 404: description: Not found.
// swagger:route GET /v1/provisioning/templates/{name} provisioning stable RouteGetTemplate
//
@@ -14,7 +13,7 @@ package definitions
//
// Responses:
// 200: NotificationTemplate
// 404: description: Not found.
// 404: GenericPublicError
// swagger:route PUT /v1/provisioning/templates/{name} provisioning stable RoutePutTemplate
//
@@ -25,7 +24,7 @@ package definitions
//
// Responses:
// 202: NotificationTemplate
// 400: ValidationError
// 400: GenericPublicError
// 409: GenericPublicError
// swagger:route DELETE /v1/provisioning/templates/{name} provisioning stable RouteDeleteTemplate

View File

@@ -1906,7 +1906,7 @@
"type": "array"
}
},
"title": "Headers represents the configuration for HTTP headers.",
"title": "Header represents the configuration for a single HTTP header.",
"type": "object"
},
"Headers": {
@@ -3403,6 +3403,12 @@
"Route": {
"description": "A Route is a node that contains definitions of how to handle alerts. This is modified\nfrom the upstream alertmanager in that it adds the ObjectMatchers property.",
"properties": {
"active_time_intervals": {
"items": {
"type": "string"
},
"type": "array"
},
"continue": {
"type": "boolean"
},
@@ -4310,6 +4316,7 @@
"type": "object"
},
"URL": {
"description": "The general form represented is:\n\n[scheme:][//[userinfo@]host][/]path[?query][#fragment]\n\nURLs that do not start with a slash after the scheme are interpreted as:\n\nscheme:opaque[?query][#fragment]\n\nThe Host field contains the host and port subcomponents of the URL.\nWhen the port is present, it is separated from the host with a colon.\nWhen the host is an IPv6 address, it must be enclosed in square brackets:\n\"[fe80::1]:80\". The [net.JoinHostPort] function combines a host and port\ninto a string suitable for the Host field, adding square brackets to\nthe host when necessary.\n\nNote that the Path field is stored in decoded form: /%47%6f%2f becomes /Go/.\nA consequence is that it is impossible to tell which slashes in the Path were\nslashes in the raw URL and which were %2f. This distinction is rarely important,\nbut when it is, the code should use the [URL.EscapedPath] method, which preserves\nthe original encoding of Path.\n\nThe RawPath field is an optional field which is only set when the default\nencoding of Path is different from the escaped path. See the EscapedPath method\nfor more details.\n\nURL's String method uses the EscapedPath method to obtain the path.",
"properties": {
"ForceQuery": {
"type": "boolean"
@@ -4345,7 +4352,7 @@
"$ref": "#/definitions/Userinfo"
}
},
"title": "URL is a custom URL type that allows validation at configuration load time.",
"title": "A URL represents a parsed URL (technically, a URI reference).",
"type": "object"
},
"UpdateRuleGroupResponse": {
@@ -4575,7 +4582,6 @@
"type": "object"
},
"alertGroups": {
"description": "AlertGroups alert groups",
"items": {
"$ref": "#/definitions/alertGroup",
"type": "object"
@@ -8385,9 +8391,6 @@
"schema": {
"$ref": "#/definitions/NotificationTemplates"
}
},
"404": {
"description": " Not found."
}
},
"summary": "Get all notification templates.",
@@ -8449,7 +8452,10 @@
}
},
"404": {
"description": " Not found."
"description": "GenericPublicError",
"schema": {
"$ref": "#/definitions/GenericPublicError"
}
}
},
"summary": "Get a notification template.",
@@ -8491,9 +8497,9 @@
}
},
"400": {
"description": "ValidationError",
"description": "GenericPublicError",
"schema": {
"$ref": "#/definitions/ValidationError"
"$ref": "#/definitions/GenericPublicError"
}
},
"409": {

View File

@@ -3314,9 +3314,6 @@
"schema": {
"$ref": "#/definitions/NotificationTemplates"
}
},
"404": {
"description": " Not found."
}
}
}
@@ -3346,7 +3343,10 @@
}
},
"404": {
"description": " Not found."
"description": "GenericPublicError",
"schema": {
"$ref": "#/definitions/GenericPublicError"
}
}
}
},
@@ -3389,9 +3389,9 @@
}
},
"400": {
"description": "ValidationError",
"description": "GenericPublicError",
"schema": {
"$ref": "#/definitions/ValidationError"
"$ref": "#/definitions/GenericPublicError"
}
},
"409": {
@@ -5519,7 +5519,7 @@
},
"Header": {
"type": "object",
"title": "Headers represents the configuration for HTTP headers.",
"title": "Header represents the configuration for a single HTTP header.",
"properties": {
"files": {
"type": "array",
@@ -7037,6 +7037,12 @@
"description": "A Route is a node that contains definitions of how to handle alerts. This is modified\nfrom the upstream alertmanager in that it adds the ObjectMatchers property.",
"type": "object",
"properties": {
"active_time_intervals": {
"type": "array",
"items": {
"type": "string"
}
},
"continue": {
"type": "boolean"
},
@@ -7943,8 +7949,9 @@
}
},
"URL": {
"description": "The general form represented is:\n\n[scheme:][//[userinfo@]host][/]path[?query][#fragment]\n\nURLs that do not start with a slash after the scheme are interpreted as:\n\nscheme:opaque[?query][#fragment]\n\nThe Host field contains the host and port subcomponents of the URL.\nWhen the port is present, it is separated from the host with a colon.\nWhen the host is an IPv6 address, it must be enclosed in square brackets:\n\"[fe80::1]:80\". The [net.JoinHostPort] function combines a host and port\ninto a string suitable for the Host field, adding square brackets to\nthe host when necessary.\n\nNote that the Path field is stored in decoded form: /%47%6f%2f becomes /Go/.\nA consequence is that it is impossible to tell which slashes in the Path were\nslashes in the raw URL and which were %2f. This distinction is rarely important,\nbut when it is, the code should use the [URL.EscapedPath] method, which preserves\nthe original encoding of Path.\n\nThe RawPath field is an optional field which is only set when the default\nencoding of Path is different from the escaped path. See the EscapedPath method\nfor more details.\n\nURL's String method uses the EscapedPath method to obtain the path.",
"type": "object",
"title": "URL is a custom URL type that allows validation at configuration load time.",
"title": "A URL represents a parsed URL (technically, a URI reference).",
"properties": {
"ForceQuery": {
"type": "boolean"
@@ -8208,7 +8215,6 @@
}
},
"alertGroups": {
"description": "AlertGroups alert groups",
"type": "array",
"items": {
"type": "object",

View File

@@ -19,6 +19,7 @@ var (
ErrTimeIntervalInUse = errutil.Conflict("alerting.notifications.time-intervals.used").MustTemplate("Time interval is used")
ErrTemplateNotFound = errutil.NotFound("alerting.notifications.templates.notFound")
ErrTemplateInvalid = errutil.BadRequest("alerting.notifications.templates.invalidFormat").MustTemplate("Invalid format of the submitted template", errutil.WithPublic("Template is in invalid format. Correct the payload and try again."))
ErrContactPointReferenced = errutil.Conflict("alerting.notifications.contact-points.referenced", errutil.WithPublicMessage("Contact point is currently referenced by a notification policy."))
ErrContactPointUsedInRule = errutil.Conflict("alerting.notifications.contact-points.used-by-rule", errutil.WithPublicMessage("Contact point is currently used in the notification settings of one or many alert rules."))
@@ -54,3 +55,15 @@ func MakeErrTimeIntervalInUse(usedByRoutes bool, rules []models.AlertRuleKey) er
Error: nil,
})
}
// MakeErrTimeIntervalInvalid creates an error with the ErrTimeIntervalInvalid template
func MakeErrTemplateInvalid(err error) error {
data := errutil.TemplateData{
Public: map[string]interface{}{
"Error": err.Error(),
},
Error: err,
}
return ErrTemplateInvalid.Build(data)
}

View File

@@ -59,7 +59,7 @@ func (t *TemplateService) GetTemplates(ctx context.Context, orgID int64) ([]defi
func (t *TemplateService) SetTemplate(ctx context.Context, orgID int64, tmpl definitions.NotificationTemplate) (definitions.NotificationTemplate, error) {
err := tmpl.Validate()
if err != nil {
return definitions.NotificationTemplate{}, fmt.Errorf("%w: %s", ErrValidation, err.Error())
return definitions.NotificationTemplate{}, MakeErrTemplateInvalid(err)
}
revision, err := t.configStore.Get(ctx, orgID)

View File

@@ -98,7 +98,7 @@ func TestTemplateService(t *testing.T) {
_, err := sut.SetTemplate(context.Background(), 1, tmpl)
require.ErrorIs(t, err, ErrValidation)
require.ErrorIs(t, err, ErrTemplateInvalid)
})
t.Run("rejects existing templates if provenance is not right", func(t *testing.T) {
@@ -341,7 +341,7 @@ func TestTemplateService(t *testing.T) {
_, err := sut.SetTemplate(context.Background(), 1, tmpl)
require.ErrorIs(t, err, ErrValidation)
require.ErrorIs(t, err, ErrTemplateInvalid)
})
t.Run("does not reject template with unknown field", func(t *testing.T) {

View File

@@ -11719,9 +11719,6 @@
"schema": {
"$ref": "#/definitions/NotificationTemplates"
}
},
"404": {
"description": " Not found."
}
}
}
@@ -11750,7 +11747,10 @@
}
},
"404": {
"description": " Not found."
"description": "GenericPublicError",
"schema": {
"$ref": "#/definitions/GenericPublicError"
}
}
}
},
@@ -11792,9 +11792,9 @@
}
},
"400": {
"description": "ValidationError",
"description": "GenericPublicError",
"schema": {
"$ref": "#/definitions/ValidationError"
"$ref": "#/definitions/GenericPublicError"
}
},
"409": {
@@ -16225,7 +16225,7 @@
},
"Header": {
"type": "object",
"title": "Headers represents the configuration for HTTP headers.",
"title": "Header represents the configuration for a single HTTP header.",
"properties": {
"files": {
"type": "array",
@@ -19500,6 +19500,12 @@
"description": "A Route is a node that contains definitions of how to handle alerts. This is modified\nfrom the upstream alertmanager in that it adds the ObjectMatchers property.",
"type": "object",
"properties": {
"active_time_intervals": {
"type": "array",
"items": {
"type": "string"
}
},
"continue": {
"type": "boolean"
},
@@ -22163,6 +22169,7 @@
}
},
"alertGroups": {
"description": "AlertGroups alert groups",
"type": "array",
"items": {
"type": "object",

View File

@@ -6304,7 +6304,7 @@
"type": "array"
}
},
"title": "Headers represents the configuration for HTTP headers.",
"title": "Header represents the configuration for a single HTTP header.",
"type": "object"
},
"Headers": {
@@ -9559,6 +9559,12 @@
"Route": {
"description": "A Route is a node that contains definitions of how to handle alerts. This is modified\nfrom the upstream alertmanager in that it adds the ObjectMatchers property.",
"properties": {
"active_time_intervals": {
"items": {
"type": "string"
},
"type": "array"
},
"continue": {
"type": "boolean"
},
@@ -12222,6 +12228,7 @@
"type": "object"
},
"alertGroups": {
"description": "AlertGroups alert groups",
"items": {
"$ref": "#/components/schemas/alertGroup"
},
@@ -25833,9 +25840,6 @@
}
},
"description": "NotificationTemplates"
},
"404": {
"description": " Not found."
}
},
"summary": "Get all notification templates.",
@@ -25911,7 +25915,14 @@
"description": "NotificationTemplate"
},
"404": {
"description": " Not found."
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/GenericPublicError"
}
}
},
"description": "GenericPublicError"
}
},
"summary": "Get a notification template.",
@@ -25964,11 +25975,11 @@
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationError"
"$ref": "#/components/schemas/GenericPublicError"
}
}
},
"description": "ValidationError"
"description": "GenericPublicError"
},
"409": {
"content": {