Alerting: Notifications Routes API (#91550)

* Introduce new models RoutingTree, RouteDefaults and Route and api-server to serve them that is backed by provisioning notification policy service.

* update method UpdatePolicyTree of notification policy service to return route and new version

* declare new actions alert.notifications.routes:read and alert.notifications.routes:write and two corresponding fixed roles.

---------

Co-authored-by: Tom Ratcliffe <tom.ratcliffe@grafana.com>
Co-authored-by: Matthew Jacobson <matthew.jacobson@grafana.com>
This commit is contained in:
Yuri Tseretyan
2024-10-24 13:53:03 -04:00
committed by GitHub
co-authored by Tom Ratcliffe Matthew Jacobson
parent fb0221d561
commit 2deced7d40
41 changed files with 2809 additions and 54 deletions
@@ -28,6 +28,11 @@ var (
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."))
ErrRouteInvalidFormat = errutil.BadRequest("alerting.notifications.routes.invalidFormat").MustTemplate(
"Invalid format of the submitted route.",
errutil.WithPublic("Invalid format of the submitted route: {{.Public.Error}}. Correct the payload and try again."),
)
)
// MakeErrTimeIntervalInvalid creates an error with the ErrTimeIntervalInvalid template
@@ -90,3 +95,12 @@ func MakeErrTimeIntervalDependentResourcesProvenance(usedByRoutes bool, rules []
Public: data,
})
}
func MakeErrRouteInvalidFormat(err error) error {
return ErrRouteInvalidFormat.Build(errutil.TemplateData{
Public: map[string]any{
"Error": err.Error(),
},
Error: err,
})
}
@@ -61,40 +61,40 @@ func (nps *NotificationPolicyService) GetPolicyTree(ctx context.Context, orgID i
return result, version, nil
}
func (nps *NotificationPolicyService) UpdatePolicyTree(ctx context.Context, orgID int64, tree definitions.Route, p models.Provenance, version string) error {
func (nps *NotificationPolicyService) UpdatePolicyTree(ctx context.Context, orgID int64, tree definitions.Route, p models.Provenance, version string) (definitions.Route, string, error) {
err := tree.Validate()
if err != nil {
return fmt.Errorf("%w: %s", ErrValidation, err.Error())
return definitions.Route{}, "", MakeErrRouteInvalidFormat(err)
}
revision, err := nps.configStore.Get(ctx, orgID)
if err != nil {
return err
return definitions.Route{}, "", err
}
err = nps.checkOptimisticConcurrency(*revision.Config.AlertmanagerConfig.Route, p, version, "update")
if err != nil {
return err
return definitions.Route{}, "", err
}
// check that provenance is not changed in an invalid way
storedProvenance, err := nps.provenanceStore.GetProvenance(ctx, &tree, orgID)
if err != nil {
return err
return definitions.Route{}, "", err
}
if err := nps.validator(storedProvenance, p); err != nil {
return err
}
receivers, err := nps.receiversToMap(revision.Config.AlertmanagerConfig.Receivers)
if err != nil {
return err
return definitions.Route{}, "", err
}
receivers := map[string]struct{}{}
receivers[""] = struct{}{} // Allow empty receiver (inheriting from parent)
for _, receiver := range revision.GetReceivers(nil) {
receivers[receiver.Name] = struct{}{}
}
err = tree.ValidateReceivers(receivers)
if err != nil {
return fmt.Errorf("%w: %s", ErrValidation, err.Error())
return definitions.Route{}, "", MakeErrRouteInvalidFormat(err)
}
timeIntervals := map[string]struct{}{}
@@ -106,17 +106,21 @@ func (nps *NotificationPolicyService) UpdatePolicyTree(ctx context.Context, orgI
}
err = tree.ValidateMuteTimes(timeIntervals)
if err != nil {
return fmt.Errorf("%w: %s", ErrValidation, err.Error())
return definitions.Route{}, "", MakeErrRouteInvalidFormat(err)
}
revision.Config.AlertmanagerConfig.Config.Route = &tree
return nps.xact.InTransaction(ctx, func(ctx context.Context) error {
err = nps.xact.InTransaction(ctx, func(ctx context.Context) error {
if err := nps.configStore.Save(ctx, revision, orgID); err != nil {
return err
}
return nps.provenanceStore.SetProvenance(ctx, &tree, orgID, p)
})
if err != nil {
return definitions.Route{}, "", err
}
return tree, calculateRouteFingerprint(tree), nil
}
func (nps *NotificationPolicyService) ResetPolicyTree(ctx context.Context, orgID int64, provenance models.Provenance) (definitions.Route, error) {
@@ -159,14 +163,6 @@ func (nps *NotificationPolicyService) ResetPolicyTree(ctx context.Context, orgID
return *route, nil
}
func (nps *NotificationPolicyService) receiversToMap(records []*definitions.PostableApiReceiver) (map[string]struct{}, error) {
receivers := map[string]struct{}{}
for _, receiver := range records {
receivers[receiver.Name] = struct{}{}
}
return receivers, nil
}
func (nps *NotificationPolicyService) ensureDefaultReceiverExists(cfg *definitions.PostableUserConfig, defaultCfg *definitions.PostableUserConfig) error {
defaultRcv := cfg.AlertmanagerConfig.Route.Receiver
@@ -84,8 +84,8 @@ func TestUpdatePolicyTree(t *testing.T) {
"not-existing",
},
}
err := sut.UpdatePolicyTree(context.Background(), orgID, newRoute, models.ProvenanceNone, defaultVersion)
require.ErrorIs(t, err, ErrValidation)
_, _, err := sut.UpdatePolicyTree(context.Background(), orgID, newRoute, models.ProvenanceNone, defaultVersion)
require.ErrorIs(t, err, ErrRouteInvalidFormat)
})
t.Run("ErrValidation if root route has no receiver", func(t *testing.T) {
@@ -97,8 +97,8 @@ func TestUpdatePolicyTree(t *testing.T) {
newRoute := definitions.Route{
Receiver: "",
}
err := sut.UpdatePolicyTree(context.Background(), orgID, newRoute, models.ProvenanceNone, defaultVersion)
require.ErrorIs(t, err, ErrValidation)
_, _, err := sut.UpdatePolicyTree(context.Background(), orgID, newRoute, models.ProvenanceNone, defaultVersion)
require.ErrorIs(t, err, ErrRouteInvalidFormat)
})
t.Run("ErrValidation if referenced receiver does not exist", func(t *testing.T) {
@@ -113,8 +113,8 @@ func TestUpdatePolicyTree(t *testing.T) {
newRoute := definitions.Route{
Receiver: "unknown",
}
err := sut.UpdatePolicyTree(context.Background(), orgID, newRoute, models.ProvenanceNone, defaultVersion)
require.ErrorIs(t, err, ErrValidation)
_, _, err := sut.UpdatePolicyTree(context.Background(), orgID, newRoute, models.ProvenanceNone, defaultVersion)
require.ErrorIs(t, err, ErrRouteInvalidFormat)
t.Run("including sub-routes", func(t *testing.T) {
newRoute := definitions.Route{
@@ -123,8 +123,8 @@ func TestUpdatePolicyTree(t *testing.T) {
{Receiver: "unknown"},
},
}
err := sut.UpdatePolicyTree(context.Background(), orgID, newRoute, models.ProvenanceNone, defaultVersion)
require.ErrorIs(t, err, ErrValidation)
_, _, err := sut.UpdatePolicyTree(context.Background(), orgID, newRoute, models.ProvenanceNone, defaultVersion)
require.ErrorIs(t, err, ErrRouteInvalidFormat)
})
})
@@ -137,7 +137,7 @@ func TestUpdatePolicyTree(t *testing.T) {
newRoute := definitions.Route{
Receiver: rev.Config.AlertmanagerConfig.Receivers[0].Name,
}
err := sut.UpdatePolicyTree(context.Background(), orgID, newRoute, models.ProvenanceNone, "wrong-version")
_, _, err := sut.UpdatePolicyTree(context.Background(), orgID, newRoute, models.ProvenanceNone, "wrong-version")
require.ErrorIs(t, err, ErrVersionConflict)
})
@@ -161,7 +161,7 @@ func TestUpdatePolicyTree(t *testing.T) {
return expectedErr
}
err := sut.UpdatePolicyTree(context.Background(), orgID, newRoute, models.ProvenanceNone, defaultVersion)
_, _, err := sut.UpdatePolicyTree(context.Background(), orgID, newRoute, models.ProvenanceNone, defaultVersion)
require.ErrorIs(t, err, expectedErr)
assert.Len(t, prov.Calls, 1)
@@ -180,8 +180,10 @@ func TestUpdatePolicyTree(t *testing.T) {
expectedRev.ConcurrencyToken = rev.ConcurrencyToken
expectedRev.Config.AlertmanagerConfig.Route = &route
err := sut.UpdatePolicyTree(context.Background(), orgID, newRoute, models.ProvenanceAPI, defaultVersion)
result, version, err := sut.UpdatePolicyTree(context.Background(), orgID, newRoute, models.ProvenanceAPI, defaultVersion)
require.NoError(t, err)
assert.Equal(t, newRoute, result)
assert.Equal(t, calculateRouteFingerprint(newRoute), version)
assert.Len(t, store.Calls, 2)
assert.Equal(t, "Save", store.Calls[1].Method)
@@ -210,8 +212,10 @@ func TestUpdatePolicyTree(t *testing.T) {
expectedRev.Config.AlertmanagerConfig.Route = &newRoute
expectedRev.ConcurrencyToken = rev.ConcurrencyToken
err := sut.UpdatePolicyTree(context.Background(), orgID, newRoute, models.ProvenanceAPI, "")
result, version, err := sut.UpdatePolicyTree(context.Background(), orgID, newRoute, models.ProvenanceAPI, "")
require.NoError(t, err)
assert.Equal(t, newRoute, result)
assert.Equal(t, calculateRouteFingerprint(newRoute), version)
assert.Len(t, store.Calls, 2)
assert.Equal(t, "Save", store.Calls[1].Method)