mirror of
https://github.com/grafana/grafana.git
synced 2025-07-28 21:12:30 +08:00
Alerting: Allow provenance disable in alerting provisioning API (#63650)
* Allow provenance None in alert rule update and rule group replace * Allow provenance None in contact point update * Allow updating policies to none by sending x-disable-provenance header * Allow mute timings to disable provenance with x-disable-provenance header * Allow disabling provenance by using x-disable-provenance header * Add provenance helper to lower the cyclomatic complexity * Do not downgrade provenance except un ReplaceRuleGroup * Add function explanation and change error handling * Add docs for x-disable-provenance changes (#66300) * Add docs for x-disable-provenance changes * Apply suggestions from code review Co-authored-by: brendamuir <100768211+brendamuir@users.noreply.github.com> * Update _index.md --------- Co-authored-by: brendamuir <100768211+brendamuir@users.noreply.github.com> * Update docs/sources/alerting/set-up/provision-alerting-resources/_index.md Co-authored-by: George Robinson <george.robinson@grafana.com> * Add error message check in tests * Change docs --------- Co-authored-by: brendamuir <100768211+brendamuir@users.noreply.github.com> Co-authored-by: George Robinson <george.robinson@grafana.com>
This commit is contained in:
@ -24,22 +24,21 @@ There are three options to choose from:
|
|||||||
|
|
||||||
For more information on the Alerting Provisioning HTTP API, refer to [Alerting provisioning API](https://grafana.com/docs/grafana/latest/developers/http_api/alerting_provisioning/).
|
For more information on the Alerting Provisioning HTTP API, refer to [Alerting provisioning API](https://grafana.com/docs/grafana/latest/developers/http_api/alerting_provisioning/).
|
||||||
|
|
||||||
**Note:**
|
|
||||||
|
|
||||||
Typically, you cannot edit API-provisioned alert rules from the Grafana UI.
|
|
||||||
|
|
||||||
In order to enable editing, add the x-disable-provenance header to the following requests when creating or editing your alert rules in the API:
|
|
||||||
|
|
||||||
POST /api/v1/provisioning/alert-rules
|
|
||||||
|
|
||||||
PUT /api/v1/provisioning/alert-rules/{UID}
|
|
||||||
|
|
||||||
1. Provision your alerting resources using Terraform.
|
1. Provision your alerting resources using Terraform.
|
||||||
|
|
||||||
**Note:**
|
**Note:**
|
||||||
|
|
||||||
Currently, provisioning for Grafana Alerting supports alert rules, contact points, mute timings, and templates. Provisioned alerting resources using file provisioning or Terraform can only be edited in the source that created them and not from within Grafana or any other source. For example, if you provision your alerting resources using files from disk, you cannot edit the data in Terraform or from within Grafana.
|
Currently, provisioning for Grafana Alerting supports alert rules, contact points, mute timings, and templates. Provisioned alerting resources using file provisioning or Terraform can only be edited in the source that created them and not from within Grafana or any other source. For example, if you provision your alerting resources using files from disk, you cannot edit the data in Terraform or from within Grafana.
|
||||||
|
|
||||||
|
To allow editing of provisioned resources in the Grafana UI, add the `X-Disable-Provenance` header to the following requests in the API:
|
||||||
|
|
||||||
|
- `POST /api/v1/provisioning/alert-rules`
|
||||||
|
- `PUT /api/v1/provisioning/folder/{FolderUID}/rule-groups/{Group}` (calling this endpoint will change provenance for all alert rules within the alert group)
|
||||||
|
- `POST /api/v1/provisioning/contact-points`
|
||||||
|
- `POST /api/v1/provisioning/mute-timings`
|
||||||
|
- `PUT /api/v1/provisioning/policies`
|
||||||
|
- `PUT /api/v1/provisioning/templates/{name}`
|
||||||
|
|
||||||
**Useful Links:**
|
**Useful Links:**
|
||||||
|
|
||||||
[Grafana provisioning](/docs/grafana/latest/administration/provisioning/)
|
[Grafana provisioning](/docs/grafana/latest/administration/provisioning/)
|
||||||
|
@ -96,9 +96,10 @@ DELETE /api/v1/provisioning/alert-rules/{UID}
|
|||||||
|
|
||||||
#### Parameters
|
#### Parameters
|
||||||
|
|
||||||
| Name | Source | Type | Go type | Separator | Required | Default | Description |
|
| Name | Source | Type | Go type | Separator | Required | Default | Description |
|
||||||
| ---- | ------ | ------ | -------- | --------- | :------: | ------- | -------------- |
|
| -------------------- | -------- | ------ | -------- | --------- | :------: | ------- | -------------- |
|
||||||
| UID | `path` | string | `string` | | ✓ | | Alert rule UID |
|
| UID | `path` | string | `string` | | ✓ | | Alert rule UID |
|
||||||
|
| X-Disable-Provenance | `header` | string | `string` | | | | |
|
||||||
|
|
||||||
#### All responses
|
#### All responses
|
||||||
|
|
||||||
@ -637,9 +638,10 @@ POST /api/v1/provisioning/contact-points
|
|||||||
|
|
||||||
#### Parameters
|
#### Parameters
|
||||||
|
|
||||||
| Name | Source | Type | Go type | Separator | Required | Default | Description |
|
| Name | Source | Type | Go type | Separator | Required | Default | Description |
|
||||||
| ---- | ------ | ----------------------------------------------- | ----------------------------- | --------- | :------: | ------- | ----------- |
|
| -------------------- | -------- | ----------------------------------------------- | ----------------------------- | --------- | :------: | ------- | ----------- |
|
||||||
| Body | `body` | [EmbeddedContactPoint](#embedded-contact-point) | `models.EmbeddedContactPoint` | | | | |
|
| X-Disable-Provenance | `header` | string | `string` | | | | |
|
||||||
|
| Body | `body` | [EmbeddedContactPoint](#embedded-contact-point) | `models.EmbeddedContactPoint` | | | | |
|
||||||
|
|
||||||
#### All responses
|
#### All responses
|
||||||
|
|
||||||
@ -678,9 +680,10 @@ POST /api/v1/provisioning/mute-timings
|
|||||||
|
|
||||||
#### Parameters
|
#### Parameters
|
||||||
|
|
||||||
| Name | Source | Type | Go type | Separator | Required | Default | Description |
|
| Name | Source | Type | Go type | Separator | Required | Default | Description |
|
||||||
| ---- | ------ | --------------------------------------- | ------------------------- | --------- | :------: | ------- | ----------- |
|
| -------------------- | -------- | --------------------------------------- | ------------------------- | --------- | :------: | ------- | ----------- |
|
||||||
| Body | `body` | [MuteTimeInterval](#mute-time-interval) | `models.MuteTimeInterval` | | | | |
|
| X-Disable-Provenance | `header` | string | `string` | | | | |
|
||||||
|
| Body | `body` | [MuteTimeInterval](#mute-time-interval) | `models.MuteTimeInterval` | | | | |
|
||||||
|
|
||||||
#### All responses
|
#### All responses
|
||||||
|
|
||||||
@ -762,11 +765,12 @@ PUT /api/v1/provisioning/folder/{FolderUID}/rule-groups/{Group}
|
|||||||
|
|
||||||
#### Parameters
|
#### Parameters
|
||||||
|
|
||||||
| Name | Source | Type | Go type | Separator | Required | Default | Description |
|
| Name | Source | Type | Go type | Separator | Required | Default | Description |
|
||||||
| --------- | ------ | ----------------------------------- | ----------------------- | --------- | :------: | ------- | ----------- |
|
| -------------------- | -------- | ----------------------------------- | ----------------------- | --------- | :------: | ------- | ----------- |
|
||||||
| FolderUID | `path` | string | `string` | | ✓ | | |
|
| FolderUID | `path` | string | `string` | | ✓ | | |
|
||||||
| Group | `path` | string | `string` | | ✓ | | |
|
| Group | `path` | string | `string` | | ✓ | | |
|
||||||
| Body | `body` | [AlertRuleGroup](#alert-rule-group) | `models.AlertRuleGroup` | | | | |
|
| X-Disable-Provenance | `header` | string | `string` | | | | |
|
||||||
|
| Body | `body` | [AlertRuleGroup](#alert-rule-group) | `models.AlertRuleGroup` | | | | |
|
||||||
|
|
||||||
#### All responses
|
#### All responses
|
||||||
|
|
||||||
@ -805,10 +809,11 @@ PUT /api/v1/provisioning/contact-points/{UID}
|
|||||||
|
|
||||||
#### Parameters
|
#### Parameters
|
||||||
|
|
||||||
| Name | Source | Type | Go type | Separator | Required | Default | Description |
|
| Name | Source | Type | Go type | Separator | Required | Default | Description |
|
||||||
| ---- | ------ | ----------------------------------------------- | ----------------------------- | --------- | :------: | ------- | ------------------------------------------ |
|
| -------------------- | -------- | ----------------------------------------------- | ----------------------------- | --------- | :------: | ------- | ------------------------------------------ |
|
||||||
| UID | `path` | string | `string` | | ✓ | | UID is the contact point unique identifier |
|
| UID | `path` | string | `string` | | ✓ | | UID is the contact point unique identifier |
|
||||||
| Body | `body` | [EmbeddedContactPoint](#embedded-contact-point) | `models.EmbeddedContactPoint` | | | | |
|
| X-Disable-Provenance | `header` | string | `string` | | | | |
|
||||||
|
| Body | `body` | [EmbeddedContactPoint](#embedded-contact-point) | `models.EmbeddedContactPoint` | | | | |
|
||||||
|
|
||||||
#### All responses
|
#### All responses
|
||||||
|
|
||||||
@ -847,10 +852,11 @@ PUT /api/v1/provisioning/mute-timings/{name}
|
|||||||
|
|
||||||
#### Parameters
|
#### Parameters
|
||||||
|
|
||||||
| Name | Source | Type | Go type | Separator | Required | Default | Description |
|
| Name | Source | Type | Go type | Separator | Required | Default | Description |
|
||||||
| ---- | ------ | --------------------------------------- | ------------------------- | --------- | :------: | ------- | ---------------- |
|
| -------------------- | -------- | --------------------------------------- | ------------------------- | --------- | :------: | ------- | ---------------- |
|
||||||
| name | `path` | string | `string` | | ✓ | | Mute timing name |
|
| name | `path` | string | `string` | | ✓ | | Mute timing name |
|
||||||
| Body | `body` | [MuteTimeInterval](#mute-time-interval) | `models.MuteTimeInterval` | | | | |
|
| X-Disable-Provenance | `header` | string | `string` | | | | |
|
||||||
|
| Body | `body` | [MuteTimeInterval](#mute-time-interval) | `models.MuteTimeInterval` | | | | |
|
||||||
|
|
||||||
#### All responses
|
#### All responses
|
||||||
|
|
||||||
@ -889,9 +895,10 @@ PUT /api/v1/provisioning/policies
|
|||||||
|
|
||||||
#### Parameters
|
#### Parameters
|
||||||
|
|
||||||
| Name | Source | Type | Go type | Separator | Required | Default | Description |
|
| Name | Source | Type | Go type | Separator | Required | Default | Description |
|
||||||
| ---- | ------ | --------------- | -------------- | --------- | :------: | ------- | ---------------------------------------- |
|
| -------------------- | -------- | --------------- | -------------- | --------- | :------: | ------- | ---------------------------------------- |
|
||||||
| Body | `body` | [Route](#route) | `models.Route` | | | | The new notification routing tree to use |
|
| X-Disable-Provenance | `header` | string | `string` | | | | |
|
||||||
|
| Body | `body` | [Route](#route) | `models.Route` | | | | The new notification routing tree to use |
|
||||||
|
|
||||||
#### All responses
|
#### All responses
|
||||||
|
|
||||||
@ -930,10 +937,11 @@ PUT /api/v1/provisioning/templates/{name}
|
|||||||
|
|
||||||
#### Parameters
|
#### Parameters
|
||||||
|
|
||||||
| Name | Source | Type | Go type | Separator | Required | Default | Description |
|
| Name | Source | Type | Go type | Separator | Required | Default | Description |
|
||||||
| ---- | ------ | ------------------------------------------------------------- | ------------------------------------ | --------- | :------: | ------- | ------------- |
|
| -------------------- | -------- | ------------------------------------------------------------- | ------------------------------------ | --------- | :------: | ------- | ------------- |
|
||||||
| name | `path` | string | `string` | | ✓ | | Template Name |
|
| name | `path` | string | `string` | | ✓ | | Template Name |
|
||||||
| Body | `body` | [NotificationTemplateContent](#notification-template-content) | `models.NotificationTemplateContent` | | | | |
|
| X-Disable-Provenance | `header` | string | `string` | | | | |
|
||||||
|
| Body | `body` | [NotificationTemplateContent](#notification-template-content) | `models.NotificationTemplateContent` | | | | |
|
||||||
|
|
||||||
#### All responses
|
#### All responses
|
||||||
|
|
||||||
|
@ -80,7 +80,8 @@ func (srv *ProvisioningSrv) RouteGetPolicyTree(c *contextmodel.ReqContext) respo
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (srv *ProvisioningSrv) RoutePutPolicyTree(c *contextmodel.ReqContext, tree definitions.Route) response.Response {
|
func (srv *ProvisioningSrv) RoutePutPolicyTree(c *contextmodel.ReqContext, tree definitions.Route) response.Response {
|
||||||
err := srv.policies.UpdatePolicyTree(c.Req.Context(), c.OrgID, tree, alerting_models.ProvenanceAPI)
|
provenance := determineProvenance(c)
|
||||||
|
err := srv.policies.UpdatePolicyTree(c.Req.Context(), c.OrgID, tree, alerting_models.Provenance(provenance))
|
||||||
if errors.Is(err, store.ErrNoAlertmanagerConfiguration) {
|
if errors.Is(err, store.ErrNoAlertmanagerConfiguration) {
|
||||||
return ErrResp(http.StatusNotFound, err, "")
|
return ErrResp(http.StatusNotFound, err, "")
|
||||||
}
|
}
|
||||||
@ -115,8 +116,8 @@ func (srv *ProvisioningSrv) RouteGetContactPoints(c *contextmodel.ReqContext) re
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (srv *ProvisioningSrv) RoutePostContactPoint(c *contextmodel.ReqContext, cp definitions.EmbeddedContactPoint) response.Response {
|
func (srv *ProvisioningSrv) RoutePostContactPoint(c *contextmodel.ReqContext, cp definitions.EmbeddedContactPoint) response.Response {
|
||||||
// TODO: provenance is hardcoded for now, change it later to make it more flexible
|
provenance := determineProvenance(c)
|
||||||
contactPoint, err := srv.contactPointService.CreateContactPoint(c.Req.Context(), c.OrgID, cp, alerting_models.ProvenanceAPI)
|
contactPoint, err := srv.contactPointService.CreateContactPoint(c.Req.Context(), c.OrgID, cp, alerting_models.Provenance(provenance))
|
||||||
if errors.Is(err, provisioning.ErrValidation) {
|
if errors.Is(err, provisioning.ErrValidation) {
|
||||||
return ErrResp(http.StatusBadRequest, err, "")
|
return ErrResp(http.StatusBadRequest, err, "")
|
||||||
}
|
}
|
||||||
@ -128,7 +129,8 @@ func (srv *ProvisioningSrv) RoutePostContactPoint(c *contextmodel.ReqContext, cp
|
|||||||
|
|
||||||
func (srv *ProvisioningSrv) RoutePutContactPoint(c *contextmodel.ReqContext, cp definitions.EmbeddedContactPoint, UID string) response.Response {
|
func (srv *ProvisioningSrv) RoutePutContactPoint(c *contextmodel.ReqContext, cp definitions.EmbeddedContactPoint, UID string) response.Response {
|
||||||
cp.UID = UID
|
cp.UID = UID
|
||||||
err := srv.contactPointService.UpdateContactPoint(c.Req.Context(), c.OrgID, cp, alerting_models.ProvenanceAPI)
|
provenance := determineProvenance(c)
|
||||||
|
err := srv.contactPointService.UpdateContactPoint(c.Req.Context(), c.OrgID, cp, alerting_models.Provenance(provenance))
|
||||||
if errors.Is(err, provisioning.ErrValidation) {
|
if errors.Is(err, provisioning.ErrValidation) {
|
||||||
return ErrResp(http.StatusBadRequest, err, "")
|
return ErrResp(http.StatusBadRequest, err, "")
|
||||||
}
|
}
|
||||||
@ -176,7 +178,7 @@ func (srv *ProvisioningSrv) RoutePutTemplate(c *contextmodel.ReqContext, body de
|
|||||||
tmpl := definitions.NotificationTemplate{
|
tmpl := definitions.NotificationTemplate{
|
||||||
Name: name,
|
Name: name,
|
||||||
Template: body.Template,
|
Template: body.Template,
|
||||||
Provenance: definitions.Provenance(alerting_models.ProvenanceAPI),
|
Provenance: determineProvenance(c),
|
||||||
}
|
}
|
||||||
modified, err := srv.templates.SetTemplate(c.Req.Context(), c.OrgID, tmpl)
|
modified, err := srv.templates.SetTemplate(c.Req.Context(), c.OrgID, tmpl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -218,7 +220,7 @@ func (srv *ProvisioningSrv) RouteGetMuteTimings(c *contextmodel.ReqContext) resp
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (srv *ProvisioningSrv) RoutePostMuteTiming(c *contextmodel.ReqContext, mt definitions.MuteTimeInterval) response.Response {
|
func (srv *ProvisioningSrv) RoutePostMuteTiming(c *contextmodel.ReqContext, mt definitions.MuteTimeInterval) response.Response {
|
||||||
mt.Provenance = definitions.Provenance(alerting_models.ProvenanceAPI)
|
mt.Provenance = determineProvenance(c)
|
||||||
created, err := srv.muteTimings.CreateMuteTiming(c.Req.Context(), mt, c.OrgID)
|
created, err := srv.muteTimings.CreateMuteTiming(c.Req.Context(), mt, c.OrgID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, provisioning.ErrValidation) {
|
if errors.Is(err, provisioning.ErrValidation) {
|
||||||
@ -231,7 +233,7 @@ func (srv *ProvisioningSrv) RoutePostMuteTiming(c *contextmodel.ReqContext, mt d
|
|||||||
|
|
||||||
func (srv *ProvisioningSrv) RoutePutMuteTiming(c *contextmodel.ReqContext, mt definitions.MuteTimeInterval, name string) response.Response {
|
func (srv *ProvisioningSrv) RoutePutMuteTiming(c *contextmodel.ReqContext, mt definitions.MuteTimeInterval, name string) response.Response {
|
||||||
mt.Name = name
|
mt.Name = name
|
||||||
mt.Provenance = definitions.Provenance(alerting_models.ProvenanceAPI)
|
mt.Provenance = determineProvenance(c)
|
||||||
updated, err := srv.muteTimings.UpdateMuteTiming(c.Req.Context(), mt, c.OrgID)
|
updated, err := srv.muteTimings.UpdateMuteTiming(c.Req.Context(), mt, c.OrgID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, provisioning.ErrValidation) {
|
if errors.Is(err, provisioning.ErrValidation) {
|
||||||
@ -276,7 +278,7 @@ func (srv *ProvisioningSrv) RoutePostAlertRule(c *contextmodel.ReqContext, ar de
|
|||||||
return ErrResp(http.StatusBadRequest, err, "")
|
return ErrResp(http.StatusBadRequest, err, "")
|
||||||
}
|
}
|
||||||
provenance := determineProvenance(c)
|
provenance := determineProvenance(c)
|
||||||
createdAlertRule, err := srv.alertRules.CreateAlertRule(c.Req.Context(), upstreamModel, provenance, c.UserID)
|
createdAlertRule, err := srv.alertRules.CreateAlertRule(c.Req.Context(), upstreamModel, alerting_models.Provenance(provenance), c.UserID)
|
||||||
if errors.Is(err, alerting_models.ErrAlertRuleFailedValidation) {
|
if errors.Is(err, alerting_models.ErrAlertRuleFailedValidation) {
|
||||||
return ErrResp(http.StatusBadRequest, err, "")
|
return ErrResp(http.StatusBadRequest, err, "")
|
||||||
}
|
}
|
||||||
@ -290,7 +292,7 @@ func (srv *ProvisioningSrv) RoutePostAlertRule(c *contextmodel.ReqContext, ar de
|
|||||||
return ErrResp(http.StatusInternalServerError, err, "")
|
return ErrResp(http.StatusInternalServerError, err, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
resp := ProvisionedAlertRuleFromAlertRule(createdAlertRule, provenance)
|
resp := ProvisionedAlertRuleFromAlertRule(createdAlertRule, alerting_models.Provenance(provenance))
|
||||||
return response.JSON(http.StatusCreated, resp)
|
return response.JSON(http.StatusCreated, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -302,7 +304,7 @@ func (srv *ProvisioningSrv) RoutePutAlertRule(c *contextmodel.ReqContext, ar def
|
|||||||
updated.OrgID = c.OrgID
|
updated.OrgID = c.OrgID
|
||||||
updated.UID = UID
|
updated.UID = UID
|
||||||
provenance := determineProvenance(c)
|
provenance := determineProvenance(c)
|
||||||
updatedAlertRule, err := srv.alertRules.UpdateAlertRule(c.Req.Context(), updated, provenance)
|
updatedAlertRule, err := srv.alertRules.UpdateAlertRule(c.Req.Context(), updated, alerting_models.Provenance(provenance))
|
||||||
if errors.Is(err, alerting_models.ErrAlertRuleNotFound) {
|
if errors.Is(err, alerting_models.ErrAlertRuleNotFound) {
|
||||||
return response.Empty(http.StatusNotFound)
|
return response.Empty(http.StatusNotFound)
|
||||||
}
|
}
|
||||||
@ -316,12 +318,13 @@ func (srv *ProvisioningSrv) RoutePutAlertRule(c *contextmodel.ReqContext, ar def
|
|||||||
return ErrResp(http.StatusInternalServerError, err, "")
|
return ErrResp(http.StatusInternalServerError, err, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
resp := ProvisionedAlertRuleFromAlertRule(updatedAlertRule, provenance)
|
resp := ProvisionedAlertRuleFromAlertRule(updatedAlertRule, alerting_models.Provenance(provenance))
|
||||||
return response.JSON(http.StatusOK, resp)
|
return response.JSON(http.StatusOK, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (srv *ProvisioningSrv) RouteDeleteAlertRule(c *contextmodel.ReqContext, UID string) response.Response {
|
func (srv *ProvisioningSrv) RouteDeleteAlertRule(c *contextmodel.ReqContext, UID string) response.Response {
|
||||||
err := srv.alertRules.DeleteAlertRule(c.Req.Context(), c.OrgID, UID, alerting_models.ProvenanceAPI)
|
provenance := determineProvenance(c)
|
||||||
|
err := srv.alertRules.DeleteAlertRule(c.Req.Context(), c.OrgID, UID, alerting_models.Provenance(provenance))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ErrResp(http.StatusInternalServerError, err, "")
|
return ErrResp(http.StatusInternalServerError, err, "")
|
||||||
}
|
}
|
||||||
@ -406,7 +409,8 @@ func (srv *ProvisioningSrv) RoutePutAlertRuleGroup(c *contextmodel.ReqContext, a
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
ErrResp(http.StatusBadRequest, err, "")
|
ErrResp(http.StatusBadRequest, err, "")
|
||||||
}
|
}
|
||||||
err = srv.alertRules.ReplaceRuleGroup(c.Req.Context(), c.OrgID, groupModel, c.UserID, alerting_models.ProvenanceAPI)
|
provenance := determineProvenance(c)
|
||||||
|
err = srv.alertRules.ReplaceRuleGroup(c.Req.Context(), c.OrgID, groupModel, c.UserID, alerting_models.Provenance(provenance))
|
||||||
if errors.Is(err, alerting_models.ErrAlertRuleFailedValidation) {
|
if errors.Is(err, alerting_models.ErrAlertRuleFailedValidation) {
|
||||||
return ErrResp(http.StatusBadRequest, err, "")
|
return ErrResp(http.StatusBadRequest, err, "")
|
||||||
}
|
}
|
||||||
@ -419,11 +423,11 @@ func (srv *ProvisioningSrv) RoutePutAlertRuleGroup(c *contextmodel.ReqContext, a
|
|||||||
return response.JSON(http.StatusOK, ag)
|
return response.JSON(http.StatusOK, ag)
|
||||||
}
|
}
|
||||||
|
|
||||||
func determineProvenance(ctx *contextmodel.ReqContext) alerting_models.Provenance {
|
func determineProvenance(ctx *contextmodel.ReqContext) definitions.Provenance {
|
||||||
if _, disabled := ctx.Req.Header[disableProvenanceHeaderName]; disabled {
|
if _, disabled := ctx.Req.Header[disableProvenanceHeaderName]; disabled {
|
||||||
return alerting_models.ProvenanceNone
|
return definitions.Provenance(alerting_models.ProvenanceNone)
|
||||||
}
|
}
|
||||||
return alerting_models.ProvenanceAPI
|
return definitions.Provenance(alerting_models.ProvenanceAPI)
|
||||||
}
|
}
|
||||||
|
|
||||||
func exportResponse(c *contextmodel.ReqContext, body any) response.Response {
|
func exportResponse(c *contextmodel.ReqContext, body any) response.Response {
|
||||||
|
@ -272,12 +272,12 @@ func (service *AlertRuleService) ReplaceRuleGroup(ctx context.Context, orgID int
|
|||||||
|
|
||||||
updates := make([]models.UpdateRule, 0, len(delta.Update))
|
updates := make([]models.UpdateRule, 0, len(delta.Update))
|
||||||
for _, update := range delta.Update {
|
for _, update := range delta.Update {
|
||||||
// check that provenance is not changed in a invalid way
|
// check that provenance is not changed in an invalid way
|
||||||
storedProvenance, err := service.provenanceStore.GetProvenance(ctx, update.New, orgID)
|
storedProvenance, err := service.provenanceStore.GetProvenance(ctx, update.New, orgID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if storedProvenance != provenance && storedProvenance != models.ProvenanceNone {
|
if canUpdate := canUpdateProvenanceInRuleGroup(storedProvenance, provenance); !canUpdate {
|
||||||
return fmt.Errorf("cannot update with provided provenance '%s', needs '%s'", provenance, storedProvenance)
|
return fmt.Errorf("cannot update with provided provenance '%s', needs '%s'", provenance, storedProvenance)
|
||||||
}
|
}
|
||||||
updates = append(updates, models.UpdateRule{
|
updates = append(updates, models.UpdateRule{
|
||||||
@ -295,12 +295,12 @@ func (service *AlertRuleService) ReplaceRuleGroup(ctx context.Context, orgID int
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, delete := range delta.Delete {
|
for _, delete := range delta.Delete {
|
||||||
// check that provenance is not changed in a invalid way
|
// check that provenance is not changed in an invalid way
|
||||||
storedProvenance, err := service.provenanceStore.GetProvenance(ctx, delete, orgID)
|
storedProvenance, err := service.provenanceStore.GetProvenance(ctx, delete, orgID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if storedProvenance != provenance && storedProvenance != models.ProvenanceNone {
|
if canUpdate := canUpdateProvenanceInRuleGroup(storedProvenance, provenance); !canUpdate {
|
||||||
return fmt.Errorf("cannot update with provided provenance '%s', needs '%s'", provenance, storedProvenance)
|
return fmt.Errorf("cannot update with provided provenance '%s', needs '%s'", provenance, storedProvenance)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -316,16 +316,14 @@ func (service *AlertRuleService) ReplaceRuleGroup(ctx context.Context, orgID int
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateAlertRule creates a new alert rule. This function will ignore any
|
// UpdateAlertRule updates an alert rule.
|
||||||
// interval that is set in the rule struct and fetch the current group interval
|
|
||||||
// from database.
|
|
||||||
func (service *AlertRuleService) UpdateAlertRule(ctx context.Context, rule models.AlertRule, provenance models.Provenance) (models.AlertRule, error) {
|
func (service *AlertRuleService) UpdateAlertRule(ctx context.Context, rule models.AlertRule, provenance models.Provenance) (models.AlertRule, error) {
|
||||||
storedRule, storedProvenance, err := service.GetAlertRule(ctx, rule.OrgID, rule.UID)
|
storedRule, storedProvenance, err := service.GetAlertRule(ctx, rule.OrgID, rule.UID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return models.AlertRule{}, err
|
return models.AlertRule{}, err
|
||||||
}
|
}
|
||||||
if storedProvenance != provenance && storedProvenance != models.ProvenanceNone {
|
if storedProvenance != provenance && storedProvenance != models.ProvenanceNone {
|
||||||
return models.AlertRule{}, fmt.Errorf("cannot changed provenance from '%s' to '%s'", storedProvenance, provenance)
|
return models.AlertRule{}, fmt.Errorf("cannot change provenance from '%s' to '%s'", storedProvenance, provenance)
|
||||||
}
|
}
|
||||||
rule.Updated = time.Now()
|
rule.Updated = time.Now()
|
||||||
rule.ID = storedRule.ID
|
rule.ID = storedRule.ID
|
||||||
@ -357,7 +355,7 @@ func (service *AlertRuleService) DeleteAlertRule(ctx context.Context, orgID int6
|
|||||||
OrgID: orgID,
|
OrgID: orgID,
|
||||||
UID: ruleUID,
|
UID: ruleUID,
|
||||||
}
|
}
|
||||||
// check that provenance is not changed in a invalid way
|
// check that provenance is not changed in an invalid way
|
||||||
storedProvenance, err := service.provenanceStore.GetProvenance(ctx, rule, rule.OrgID)
|
storedProvenance, err := service.provenanceStore.GetProvenance(ctx, rule, rule.OrgID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -274,10 +274,10 @@ func TestAlertRuleService(t *testing.T) {
|
|||||||
errNil: false,
|
errNil: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "should not be able to update from provenance api to none",
|
name: "should be able to update from provenance api to none",
|
||||||
from: models.ProvenanceAPI,
|
from: models.ProvenanceAPI,
|
||||||
to: models.ProvenanceNone,
|
to: models.ProvenanceNone,
|
||||||
errNil: false,
|
errNil: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "should not be able to update from provenance file to api",
|
name: "should not be able to update from provenance file to api",
|
||||||
|
@ -261,13 +261,13 @@ func (ecp *ContactPointService) UpdateContactPoint(ctx context.Context, orgID in
|
|||||||
return fmt.Errorf("%w: %s", ErrValidation, err.Error())
|
return fmt.Errorf("%w: %s", ErrValidation, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
// check that provenance is not changed in a invalid way
|
// check that provenance is not changed in an invalid way
|
||||||
storedProvenance, err := ecp.provenanceStore.GetProvenance(ctx, &contactPoint, orgID)
|
storedProvenance, err := ecp.provenanceStore.GetProvenance(ctx, &contactPoint, orgID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if storedProvenance != provenance && storedProvenance != models.ProvenanceNone {
|
if storedProvenance != provenance && storedProvenance != models.ProvenanceNone {
|
||||||
return fmt.Errorf("cannot changed provenance from '%s' to '%s'", storedProvenance, provenance)
|
return fmt.Errorf("cannot change provenance from '%s' to '%s'", storedProvenance, provenance)
|
||||||
}
|
}
|
||||||
// transform to internal model
|
// transform to internal model
|
||||||
extractedSecrets, err := RemoveSecretsForContactPoint(&contactPoint)
|
extractedSecrets, err := RemoveSecretsForContactPoint(&contactPoint)
|
||||||
|
@ -2,6 +2,7 @@ package provisioning
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/prometheus/alertmanager/config"
|
"github.com/prometheus/alertmanager/config"
|
||||||
@ -144,78 +145,76 @@ func TestContactPointService(t *testing.T) {
|
|||||||
require.Equal(t, models.ProvenanceNone, models.Provenance(cps[0].Provenance))
|
require.Equal(t, models.ProvenanceNone, models.Provenance(cps[0].Provenance))
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("it's possible to update provenance from none to API", func(t *testing.T) {
|
t.Run("contact point provenance should be correctly checked", func(t *testing.T) {
|
||||||
sut := createContactPointServiceSut(secretsService)
|
tests := []struct {
|
||||||
newCp := createTestContactPoint()
|
name string
|
||||||
|
from models.Provenance
|
||||||
|
to models.Provenance
|
||||||
|
errNil bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "should be able to update from provenance none to api",
|
||||||
|
from: models.ProvenanceNone,
|
||||||
|
to: models.ProvenanceAPI,
|
||||||
|
errNil: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "should be able to update from provenance none to file",
|
||||||
|
from: models.ProvenanceNone,
|
||||||
|
to: models.ProvenanceFile,
|
||||||
|
errNil: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "should not be able to update from provenance api to file",
|
||||||
|
from: models.ProvenanceAPI,
|
||||||
|
to: models.ProvenanceFile,
|
||||||
|
errNil: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "should not be able to update from provenance api to none",
|
||||||
|
from: models.ProvenanceAPI,
|
||||||
|
to: models.ProvenanceNone,
|
||||||
|
errNil: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "should not be able to update from provenance file to api",
|
||||||
|
from: models.ProvenanceFile,
|
||||||
|
to: models.ProvenanceAPI,
|
||||||
|
errNil: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "should not be able to update from provenance file to none",
|
||||||
|
from: models.ProvenanceFile,
|
||||||
|
to: models.ProvenanceNone,
|
||||||
|
errNil: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
sut := createContactPointServiceSut(secretsService)
|
||||||
|
newCp := createTestContactPoint()
|
||||||
|
|
||||||
newCp, err := sut.CreateContactPoint(context.Background(), 1, newCp, models.ProvenanceNone)
|
newCp, err := sut.CreateContactPoint(context.Background(), 1, newCp, test.from)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
cps, err := sut.GetContactPoints(context.Background(), cpsQuery(1))
|
cps, err := sut.GetContactPoints(context.Background(), cpsQuery(1))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, newCp.UID, cps[1].UID)
|
require.Equal(t, newCp.UID, cps[1].UID)
|
||||||
require.Equal(t, models.ProvenanceNone, models.Provenance(cps[1].Provenance))
|
require.Equal(t, test.from, models.Provenance(cps[1].Provenance))
|
||||||
|
|
||||||
err = sut.UpdateContactPoint(context.Background(), 1, newCp, models.ProvenanceAPI)
|
err = sut.UpdateContactPoint(context.Background(), 1, newCp, test.to)
|
||||||
require.NoError(t, err)
|
if test.errNil {
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
cps, err = sut.GetContactPoints(context.Background(), cpsQuery(1))
|
cps, err = sut.GetContactPoints(context.Background(), cpsQuery(1))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, newCp.UID, cps[1].UID)
|
require.Equal(t, newCp.UID, cps[1].UID)
|
||||||
require.Equal(t, models.ProvenanceAPI, models.Provenance(cps[1].Provenance))
|
require.Equal(t, test.to, models.Provenance(cps[1].Provenance))
|
||||||
})
|
} else {
|
||||||
|
require.Error(t, err, fmt.Sprintf("cannot change provenance from '%s' to '%s'", test.from, test.to))
|
||||||
t.Run("it's possible to update provenance from none to File", func(t *testing.T) {
|
}
|
||||||
sut := createContactPointServiceSut(secretsService)
|
})
|
||||||
newCp := createTestContactPoint()
|
}
|
||||||
|
|
||||||
newCp, err := sut.CreateContactPoint(context.Background(), 1, newCp, models.ProvenanceNone)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
cps, err := sut.GetContactPoints(context.Background(), cpsQuery(1))
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, newCp.UID, cps[1].UID)
|
|
||||||
require.Equal(t, models.ProvenanceNone, models.Provenance(cps[1].Provenance))
|
|
||||||
|
|
||||||
err = sut.UpdateContactPoint(context.Background(), 1, newCp, models.ProvenanceFile)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
cps, err = sut.GetContactPoints(context.Background(), cpsQuery(1))
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, newCp.UID, cps[1].UID)
|
|
||||||
require.Equal(t, models.ProvenanceFile, models.Provenance(cps[1].Provenance))
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("it's not possible to update provenance from File to API", func(t *testing.T) {
|
|
||||||
sut := createContactPointServiceSut(secretsService)
|
|
||||||
newCp := createTestContactPoint()
|
|
||||||
|
|
||||||
newCp, err := sut.CreateContactPoint(context.Background(), 1, newCp, models.ProvenanceFile)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
cps, err := sut.GetContactPoints(context.Background(), cpsQuery(1))
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, newCp.UID, cps[1].UID)
|
|
||||||
require.Equal(t, models.ProvenanceFile, models.Provenance(cps[1].Provenance))
|
|
||||||
|
|
||||||
err = sut.UpdateContactPoint(context.Background(), 1, newCp, models.ProvenanceAPI)
|
|
||||||
require.Error(t, err)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("it's not possible to update provenance from API to File", func(t *testing.T) {
|
|
||||||
sut := createContactPointServiceSut(secretsService)
|
|
||||||
newCp := createTestContactPoint()
|
|
||||||
|
|
||||||
newCp, err := sut.CreateContactPoint(context.Background(), 1, newCp, models.ProvenanceAPI)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
cps, err := sut.GetContactPoints(context.Background(), cpsQuery(1))
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, newCp.UID, cps[1].UID)
|
|
||||||
require.Equal(t, models.ProvenanceAPI, models.Provenance(cps[1].Provenance))
|
|
||||||
|
|
||||||
err = sut.UpdateContactPoint(context.Background(), 1, newCp, models.ProvenanceFile)
|
|
||||||
require.Error(t, err)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("service respects concurrency token when updating", func(t *testing.T) {
|
t.Run("service respects concurrency token when updating", func(t *testing.T) {
|
||||||
|
13
pkg/services/ngalert/provisioning/provenance.go
Normal file
13
pkg/services/ngalert/provisioning/provenance.go
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
package provisioning
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
// canUpdateProvenanceInRuleGroup checks if a provenance can be updated for a rule group and its alerts.
|
||||||
|
// ReplaceRuleGroup function intends to replace an entire rule group: inserting, updating, and removing rules.
|
||||||
|
func canUpdateProvenanceInRuleGroup(storedProvenance, provenance models.Provenance) bool {
|
||||||
|
return storedProvenance == provenance ||
|
||||||
|
storedProvenance == models.ProvenanceNone ||
|
||||||
|
(storedProvenance == models.ProvenanceAPI && provenance == models.ProvenanceNone)
|
||||||
|
}
|
Reference in New Issue
Block a user