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:
co-authored by
Tom Ratcliffe
Matthew Jacobson
parent
fb0221d561
commit
2deced7d40
@@ -460,6 +460,10 @@ const (
|
||||
ActionAlertingReceiversPermissionsRead = "receivers.permissions:read"
|
||||
ActionAlertingReceiversPermissionsWrite = "receivers.permissions:write"
|
||||
|
||||
// Alerting routes policies actions
|
||||
ActionAlertingRoutesRead = "alert.notifications.routes:read"
|
||||
ActionAlertingRoutesWrite = "alert.notifications.routes:write"
|
||||
|
||||
// External alerting rule actions. We can only narrow it down to writes or reads, as we don't control the atomicity in the external system.
|
||||
ActionAlertingRuleExternalWrite = "alert.rules.external:write"
|
||||
ActionAlertingRuleExternalRead = "alert.rules.external:read"
|
||||
|
||||
@@ -258,6 +258,6 @@ func updateNotificationPolicyTree(t *testing.T, ctx context.Context, service *Se
|
||||
Routes: []*definition.Route{&child},
|
||||
}
|
||||
|
||||
err := service.ngAlert.Api.Policies.UpdatePolicyTree(ctx, user.GetOrgID(), tree, "", "")
|
||||
_, _, err := service.ngAlert.Api.Policies.UpdatePolicyTree(ctx, user.GetOrgID(), tree, "", "")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
@@ -204,15 +204,39 @@ var (
|
||||
},
|
||||
}
|
||||
|
||||
routesReaderRole = accesscontrol.RoleRegistration{
|
||||
Role: accesscontrol.RoleDTO{
|
||||
Name: accesscontrol.FixedRolePrefix + "alerting.routes:reader",
|
||||
DisplayName: "Notification Policies Reader",
|
||||
Description: "Read all notification policies in Grafana alerting",
|
||||
Group: AlertRolesGroup,
|
||||
Permissions: accesscontrol.ConcatPermissions([]accesscontrol.Permission{
|
||||
{Action: accesscontrol.ActionAlertingRoutesRead},
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
routesWriterRole = accesscontrol.RoleRegistration{
|
||||
Role: accesscontrol.RoleDTO{
|
||||
Name: accesscontrol.FixedRolePrefix + "alerting.routes:writer",
|
||||
DisplayName: "Notification Policies Writer",
|
||||
Description: "Update and reset notification policies in Grafana alerting",
|
||||
Group: AlertRolesGroup,
|
||||
Permissions: accesscontrol.ConcatPermissions(routesReaderRole.Role.Permissions, []accesscontrol.Permission{
|
||||
{Action: accesscontrol.ActionAlertingRoutesWrite},
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
notificationsReaderRole = accesscontrol.RoleRegistration{
|
||||
Role: accesscontrol.RoleDTO{
|
||||
Name: accesscontrol.FixedRolePrefix + "alerting.notifications:reader",
|
||||
DisplayName: "Notifications Reader",
|
||||
Description: "Read notification policies and contact points in Grafana and external providers",
|
||||
Group: AlertRolesGroup,
|
||||
Permissions: accesscontrol.ConcatPermissions(receiversReaderRole.Role.Permissions, templatesReaderRole.Role.Permissions, timeIntervalsReaderRole.Role.Permissions, []accesscontrol.Permission{
|
||||
Permissions: accesscontrol.ConcatPermissions(receiversReaderRole.Role.Permissions, templatesReaderRole.Role.Permissions, timeIntervalsReaderRole.Role.Permissions, routesReaderRole.Role.Permissions, []accesscontrol.Permission{
|
||||
{
|
||||
Action: accesscontrol.ActionAlertingNotificationsRead,
|
||||
Action: accesscontrol.ActionAlertingNotificationsRead, // TODO remove when we decide tò limit access to raw config API
|
||||
},
|
||||
{
|
||||
Action: accesscontrol.ActionAlertingNotificationsExternalRead,
|
||||
@@ -228,9 +252,9 @@ var (
|
||||
DisplayName: "Notifications Writer",
|
||||
Description: "Add, update, and delete contact points and notification policies in Grafana and external providers",
|
||||
Group: AlertRolesGroup,
|
||||
Permissions: accesscontrol.ConcatPermissions(notificationsReaderRole.Role.Permissions, receiversWriterRole.Role.Permissions, templatesWriterRole.Role.Permissions, timeIntervalsWriterRole.Role.Permissions, []accesscontrol.Permission{
|
||||
Permissions: accesscontrol.ConcatPermissions(notificationsReaderRole.Role.Permissions, receiversWriterRole.Role.Permissions, templatesWriterRole.Role.Permissions, timeIntervalsWriterRole.Role.Permissions, routesWriterRole.Role.Permissions, []accesscontrol.Permission{
|
||||
{
|
||||
Action: accesscontrol.ActionAlertingNotificationsWrite,
|
||||
Action: accesscontrol.ActionAlertingNotificationsWrite, // TODO remove when we decide tò limit access to raw config API
|
||||
},
|
||||
{
|
||||
Action: accesscontrol.ActionAlertingNotificationsExternalWrite,
|
||||
@@ -360,7 +384,7 @@ func DeclareFixedRoles(service accesscontrol.Service, features featuremgmt.Featu
|
||||
}
|
||||
|
||||
if features.IsEnabledGlobally(featuremgmt.FlagAlertingApiServer) {
|
||||
fixedRoles = append(fixedRoles, receiversReaderRole, receiversCreatorRole, receiversWriterRole, templatesReaderRole, templatesWriterRole, timeIntervalsReaderRole, timeIntervalsWriterRole)
|
||||
fixedRoles = append(fixedRoles, receiversReaderRole, receiversCreatorRole, receiversWriterRole, templatesReaderRole, templatesWriterRole, timeIntervalsReaderRole, timeIntervalsWriterRole, routesReaderRole, routesWriterRole)
|
||||
}
|
||||
|
||||
return service.DeclareFixedRoles(fixedRoles...)
|
||||
|
||||
@@ -58,7 +58,10 @@ func (srv AlertmanagerSrv) k8sApiServiceGuard(currentConfig apimodels.GettableUs
|
||||
|
||||
func checkRoutes(currentConfig apimodels.GettableUserConfig, newConfig apimodels.PostableUserConfig) error {
|
||||
reporter := cmputil.DiffReporter{}
|
||||
options := []cmp.Option{cmp.Reporter(&reporter), cmpopts.EquateEmpty(), cmpopts.IgnoreUnexported(labels.Matcher{})}
|
||||
options := []cmp.Option{cmp.Reporter(&reporter), cmpopts.EquateEmpty(), cmpopts.IgnoreUnexported(labels.Matcher{}), cmp.Transformer("", func(regexp amConfig.Regexp) any {
|
||||
r, _ := regexp.MarshalYAML()
|
||||
return r
|
||||
})}
|
||||
routesEqual := cmp.Equal(currentConfig.AlertmanagerConfig.Route, newConfig.AlertmanagerConfig.Route, options...)
|
||||
if !routesEqual && currentConfig.AlertmanagerConfig.Route.Provenance != apimodels.Provenance(ngmodels.ProvenanceNone) {
|
||||
return fmt.Errorf("policies were provisioned and cannot be changed through the UI")
|
||||
|
||||
@@ -52,7 +52,7 @@ type TemplateService interface {
|
||||
|
||||
type NotificationPolicyService interface {
|
||||
GetPolicyTree(ctx context.Context, orgID int64) (definitions.Route, string, error)
|
||||
UpdatePolicyTree(ctx context.Context, orgID int64, tree definitions.Route, p alerting_models.Provenance, version string) error
|
||||
UpdatePolicyTree(ctx context.Context, orgID int64, tree definitions.Route, p alerting_models.Provenance, version string) (definitions.Route, string, error)
|
||||
ResetPolicyTree(ctx context.Context, orgID int64, provenance alerting_models.Provenance) (definitions.Route, error)
|
||||
}
|
||||
|
||||
@@ -109,7 +109,7 @@ func (srv *ProvisioningSrv) RouteGetPolicyTreeExport(c *contextmodel.ReqContext)
|
||||
|
||||
func (srv *ProvisioningSrv) RoutePutPolicyTree(c *contextmodel.ReqContext, tree definitions.Route) response.Response {
|
||||
provenance := determineProvenance(c)
|
||||
err := srv.policies.UpdatePolicyTree(c.Req.Context(), c.SignedInUser.GetOrgID(), tree, alerting_models.Provenance(provenance), "")
|
||||
_, _, err := srv.policies.UpdatePolicyTree(c.Req.Context(), c.SignedInUser.GetOrgID(), tree, alerting_models.Provenance(provenance), "")
|
||||
if errors.Is(err, store.ErrNoAlertmanagerConfiguration) {
|
||||
return ErrResp(http.StatusNotFound, err, "")
|
||||
}
|
||||
|
||||
@@ -2035,13 +2035,13 @@ func (f *fakeNotificationPolicyService) GetPolicyTree(ctx context.Context, orgID
|
||||
return result, "", nil
|
||||
}
|
||||
|
||||
func (f *fakeNotificationPolicyService) UpdatePolicyTree(ctx context.Context, orgID int64, tree definitions.Route, p models.Provenance, _ string) error {
|
||||
func (f *fakeNotificationPolicyService) UpdatePolicyTree(ctx context.Context, orgID int64, tree definitions.Route, p models.Provenance, version string) (definitions.Route, string, error) {
|
||||
if orgID != 1 {
|
||||
return store.ErrNoAlertmanagerConfiguration
|
||||
return definitions.Route{}, "", store.ErrNoAlertmanagerConfiguration
|
||||
}
|
||||
f.tree = tree
|
||||
f.prov = p
|
||||
return nil
|
||||
return tree, "some", nil
|
||||
}
|
||||
|
||||
func (f *fakeNotificationPolicyService) ResetPolicyTree(ctx context.Context, orgID int64, provenance models.Provenance) (definitions.Route, error) {
|
||||
@@ -2055,8 +2055,8 @@ func (f *fakeFailingNotificationPolicyService) GetPolicyTree(ctx context.Context
|
||||
return definitions.Route{}, "", fmt.Errorf("something went wrong")
|
||||
}
|
||||
|
||||
func (f *fakeFailingNotificationPolicyService) UpdatePolicyTree(ctx context.Context, orgID int64, tree definitions.Route, p models.Provenance, _ string) error {
|
||||
return fmt.Errorf("something went wrong")
|
||||
func (f *fakeFailingNotificationPolicyService) UpdatePolicyTree(ctx context.Context, orgID int64, tree definitions.Route, p models.Provenance, version string) (definitions.Route, string, error) {
|
||||
return definitions.Route{}, "", fmt.Errorf("something went wrong")
|
||||
}
|
||||
|
||||
func (f *fakeFailingNotificationPolicyService) ResetPolicyTree(ctx context.Context, orgID int64, provenance models.Provenance) (definitions.Route, error) {
|
||||
@@ -2069,8 +2069,8 @@ func (f *fakeRejectingNotificationPolicyService) GetPolicyTree(ctx context.Conte
|
||||
return definitions.Route{}, "", nil
|
||||
}
|
||||
|
||||
func (f *fakeRejectingNotificationPolicyService) UpdatePolicyTree(ctx context.Context, orgID int64, tree definitions.Route, p models.Provenance, _ string) error {
|
||||
return fmt.Errorf("%w: invalid policy tree", provisioning.ErrValidation)
|
||||
func (f *fakeRejectingNotificationPolicyService) UpdatePolicyTree(ctx context.Context, orgID int64, tree definitions.Route, p models.Provenance, version string) (definitions.Route, string, error) {
|
||||
return definitions.Route{}, "", fmt.Errorf("%w: invalid policy tree", provisioning.ErrValidation)
|
||||
}
|
||||
|
||||
func (f *fakeRejectingNotificationPolicyService) ResetPolicyTree(ctx context.Context, orgID int64, provenance models.Provenance) (definitions.Route, error) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -31,7 +31,7 @@ func (c *defaultNotificationPolicyProvisioner) Provision(ctx context.Context,
|
||||
files []*AlertingFile) error {
|
||||
for _, file := range files {
|
||||
for _, np := range file.Policies {
|
||||
err := c.notificationPolicyService.UpdatePolicyTree(ctx, np.OrgID,
|
||||
_, _, err := c.notificationPolicyService.UpdatePolicyTree(ctx, np.OrgID,
|
||||
np.Policy, models.ProvenanceFile, "")
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: %w", file.Filename, err)
|
||||
|
||||
Reference in New Issue
Block a user