[v9.5.x] Alerting: Allow provenance disable in alerting provisioning API (#66761)
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>
(cherry picked from commit f64a89727e)
Co-authored-by: Alex Moreno <alexander.moreno@grafana.com>
This commit is contained in:
co-authored by
Alex Moreno
parent
81b78f3af4
commit
3fb37e26b5
@@ -80,7 +80,8 @@ func (srv *ProvisioningSrv) RouteGetPolicyTree(c *contextmodel.ReqContext) respo
|
||||
}
|
||||
|
||||
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) {
|
||||
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 {
|
||||
// TODO: provenance is hardcoded for now, change it later to make it more flexible
|
||||
contactPoint, err := srv.contactPointService.CreateContactPoint(c.Req.Context(), c.OrgID, cp, alerting_models.ProvenanceAPI)
|
||||
provenance := determineProvenance(c)
|
||||
contactPoint, err := srv.contactPointService.CreateContactPoint(c.Req.Context(), c.OrgID, cp, alerting_models.Provenance(provenance))
|
||||
if errors.Is(err, provisioning.ErrValidation) {
|
||||
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 {
|
||||
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) {
|
||||
return ErrResp(http.StatusBadRequest, err, "")
|
||||
}
|
||||
@@ -176,7 +178,7 @@ func (srv *ProvisioningSrv) RoutePutTemplate(c *contextmodel.ReqContext, body de
|
||||
tmpl := definitions.NotificationTemplate{
|
||||
Name: name,
|
||||
Template: body.Template,
|
||||
Provenance: definitions.Provenance(alerting_models.ProvenanceAPI),
|
||||
Provenance: determineProvenance(c),
|
||||
}
|
||||
modified, err := srv.templates.SetTemplate(c.Req.Context(), c.OrgID, tmpl)
|
||||
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 {
|
||||
mt.Provenance = definitions.Provenance(alerting_models.ProvenanceAPI)
|
||||
mt.Provenance = determineProvenance(c)
|
||||
created, err := srv.muteTimings.CreateMuteTiming(c.Req.Context(), mt, c.OrgID)
|
||||
if err != nil {
|
||||
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 {
|
||||
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)
|
||||
if err != nil {
|
||||
if errors.Is(err, provisioning.ErrValidation) {
|
||||
@@ -276,7 +278,7 @@ func (srv *ProvisioningSrv) RoutePostAlertRule(c *contextmodel.ReqContext, ar de
|
||||
return ErrResp(http.StatusBadRequest, err, "")
|
||||
}
|
||||
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) {
|
||||
return ErrResp(http.StatusBadRequest, err, "")
|
||||
}
|
||||
@@ -290,7 +292,7 @@ func (srv *ProvisioningSrv) RoutePostAlertRule(c *contextmodel.ReqContext, ar de
|
||||
return ErrResp(http.StatusInternalServerError, err, "")
|
||||
}
|
||||
|
||||
resp := ProvisionedAlertRuleFromAlertRule(createdAlertRule, provenance)
|
||||
resp := ProvisionedAlertRuleFromAlertRule(createdAlertRule, alerting_models.Provenance(provenance))
|
||||
return response.JSON(http.StatusCreated, resp)
|
||||
}
|
||||
|
||||
@@ -302,7 +304,7 @@ func (srv *ProvisioningSrv) RoutePutAlertRule(c *contextmodel.ReqContext, ar def
|
||||
updated.OrgID = c.OrgID
|
||||
updated.UID = UID
|
||||
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) {
|
||||
return response.Empty(http.StatusNotFound)
|
||||
}
|
||||
@@ -316,12 +318,13 @@ func (srv *ProvisioningSrv) RoutePutAlertRule(c *contextmodel.ReqContext, ar def
|
||||
return ErrResp(http.StatusInternalServerError, err, "")
|
||||
}
|
||||
|
||||
resp := ProvisionedAlertRuleFromAlertRule(updatedAlertRule, provenance)
|
||||
resp := ProvisionedAlertRuleFromAlertRule(updatedAlertRule, alerting_models.Provenance(provenance))
|
||||
return response.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
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 {
|
||||
return ErrResp(http.StatusInternalServerError, err, "")
|
||||
}
|
||||
@@ -406,7 +409,8 @@ func (srv *ProvisioningSrv) RoutePutAlertRuleGroup(c *contextmodel.ReqContext, a
|
||||
if err != nil {
|
||||
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) {
|
||||
return ErrResp(http.StatusBadRequest, err, "")
|
||||
}
|
||||
@@ -419,11 +423,11 @@ func (srv *ProvisioningSrv) RoutePutAlertRuleGroup(c *contextmodel.ReqContext, a
|
||||
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 {
|
||||
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 {
|
||||
|
||||
@@ -272,12 +272,12 @@ func (service *AlertRuleService) ReplaceRuleGroup(ctx context.Context, orgID int
|
||||
|
||||
updates := make([]models.UpdateRule, 0, len(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)
|
||||
if err != nil {
|
||||
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)
|
||||
}
|
||||
updates = append(updates, models.UpdateRule{
|
||||
@@ -295,12 +295,12 @@ func (service *AlertRuleService) ReplaceRuleGroup(ctx context.Context, orgID int
|
||||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -316,16 +316,14 @@ func (service *AlertRuleService) ReplaceRuleGroup(ctx context.Context, orgID int
|
||||
})
|
||||
}
|
||||
|
||||
// CreateAlertRule creates a new alert rule. This function will ignore any
|
||||
// interval that is set in the rule struct and fetch the current group interval
|
||||
// from database.
|
||||
// UpdateAlertRule updates an alert rule.
|
||||
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)
|
||||
if err != nil {
|
||||
return models.AlertRule{}, err
|
||||
}
|
||||
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.ID = storedRule.ID
|
||||
@@ -357,7 +355,7 @@ func (service *AlertRuleService) DeleteAlertRule(ctx context.Context, orgID int6
|
||||
OrgID: orgID,
|
||||
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)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -274,10 +274,10 @@ func TestAlertRuleService(t *testing.T) {
|
||||
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,
|
||||
to: models.ProvenanceNone,
|
||||
errNil: false,
|
||||
errNil: true,
|
||||
},
|
||||
{
|
||||
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())
|
||||
}
|
||||
|
||||
// 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)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
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
|
||||
extractedSecrets, err := RemoveSecretsForContactPoint(&contactPoint)
|
||||
|
||||
@@ -2,6 +2,7 @@ package provisioning
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/prometheus/alertmanager/config"
|
||||
@@ -144,78 +145,76 @@ func TestContactPointService(t *testing.T) {
|
||||
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) {
|
||||
sut := createContactPointServiceSut(secretsService)
|
||||
newCp := createTestContactPoint()
|
||||
t.Run("contact point provenance should be correctly checked", func(t *testing.T) {
|
||||
tests := []struct {
|
||||
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)
|
||||
require.NoError(t, err)
|
||||
newCp, err := sut.CreateContactPoint(context.Background(), 1, newCp, test.from)
|
||||
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))
|
||||
cps, err := sut.GetContactPoints(context.Background(), cpsQuery(1))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, newCp.UID, cps[1].UID)
|
||||
require.Equal(t, test.from, models.Provenance(cps[1].Provenance))
|
||||
|
||||
err = sut.UpdateContactPoint(context.Background(), 1, newCp, models.ProvenanceAPI)
|
||||
require.NoError(t, err)
|
||||
err = sut.UpdateContactPoint(context.Background(), 1, newCp, test.to)
|
||||
if test.errNil {
|
||||
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))
|
||||
})
|
||||
|
||||
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)
|
||||
cps, err = sut.GetContactPoints(context.Background(), cpsQuery(1))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, newCp.UID, cps[1].UID)
|
||||
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("service respects concurrency token when updating", func(t *testing.T) {
|
||||
|
||||
@@ -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