[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:
Grot (@grafanabot)
2023-04-18 15:34:44 +02:00
committed by GitHub
co-authored by Alex Moreno
parent 81b78f3af4
commit 3fb37e26b5
8 changed files with 157 additions and 136 deletions
+20 -16
View File
@@ -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)
}