From 38103a2ff026e880d76db1989aa797a21cd8910b Mon Sep 17 00:00:00 2001 From: Matt Jacobson Date: Thu, 6 Nov 2025 14:32:48 -0500 Subject: [PATCH] Fix Export All policies Broken when moved to ManagedRoutes field on PostableUserConfig --- pkg/services/ngalert/api/api_provisioning.go | 31 ++++++++++++------ pkg/services/ngalert/api/compat/compat.go | 34 +++++++++++++++++--- 2 files changed, 52 insertions(+), 13 deletions(-) diff --git a/pkg/services/ngalert/api/api_provisioning.go b/pkg/services/ngalert/api/api_provisioning.go index bff1e692bdb..e50a8112d7c 100644 --- a/pkg/services/ngalert/api/api_provisioning.go +++ b/pkg/services/ngalert/api/api_provisioning.go @@ -61,6 +61,7 @@ type NotificationPolicyService interface { ResetPolicyTree(ctx context.Context, orgID int64, provenance alerting_models.Provenance) (definitions.Route, error) GetManagedRoute(ctx context.Context, orgID int64, name string) (legacy_storage.ManagedRoute, error) + GetManagedRoutes(ctx context.Context, orgID int64) (legacy_storage.ManagedRoutes, error) } type MuteTimingService interface { @@ -99,31 +100,43 @@ func (srv *ProvisioningSrv) RouteGetPolicyTree(c *contextmodel.ReqContext) respo } func (srv *ProvisioningSrv) RouteGetPolicyTreeExport(c *contextmodel.ReqContext) response.Response { - routeName := c.Query("routeName") - - var policy definitions.Route - if routeName == "" { - var err error - policy, _, err = srv.policies.GetPolicyTree(c.Req.Context(), c.GetOrgID()) + if !srv.featureManager.IsEnabledGlobally(featuremgmt.FlagAlertingMultiplePolicies) { + // Default to the old behavior of exporting the single user-defined policy tree without a "name" field. + policy, _, err := srv.policies.GetPolicyTree(c.Req.Context(), c.GetOrgID()) if err != nil { if errors.Is(err, store.ErrNoAlertmanagerConfiguration) { return ErrResp(http.StatusNotFound, err, "") } return ErrResp(http.StatusInternalServerError, err, "") } + e, err := AlertingFileExportFromRoute(c.GetOrgID(), policy) + if err != nil { + return ErrResp(http.StatusInternalServerError, err, "failed to create alerting file export") + } + return exportResponse(c, e) + } + + routeName := c.Query("routeName") + var routesToExport legacy_storage.ManagedRoutes + if routeName == "" { + // Interpreted as Export All. + var err error + routesToExport, err = srv.policies.GetManagedRoutes(c.Req.Context(), c.GetOrgID()) + if err != nil { + return response.ErrOrFallback(http.StatusInternalServerError, "failed to export all notification policy trees", err) + } } else { managedRoute, err := srv.policies.GetManagedRoute(c.Req.Context(), c.GetOrgID(), routeName) if err != nil { return response.ErrOrFallback(http.StatusInternalServerError, "failed to export notification policy tree", err) } - policy = managedRoute.AsAMRoute() + routesToExport = legacy_storage.ManagedRoutes{&managedRoute} } - e, err := AlertingFileExportFromRoute(c.GetOrgID(), routeName, policy) + e, err := AlertingFileExportFromManagedRoutes(c.GetOrgID(), routesToExport) if err != nil { return ErrResp(http.StatusInternalServerError, err, "failed to create alerting file export") } - return exportResponse(c, e) } diff --git a/pkg/services/ngalert/api/compat/compat.go b/pkg/services/ngalert/api/compat/compat.go index 1136ed64540..6bd70c8ab64 100644 --- a/pkg/services/ngalert/api/compat/compat.go +++ b/pkg/services/ngalert/api/compat/compat.go @@ -10,6 +10,7 @@ import ( "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/models" + "github.com/grafana/grafana/pkg/services/ngalert/notifier/legacy_storage" "github.com/grafana/grafana/pkg/util" ) @@ -327,19 +328,32 @@ func ReceiverExportFromEmbeddedContactPoint(contact definitions.EmbeddedContactP } // AlertingFileExportFromRoute creates a definitions.AlertingFileExport DTO from definitions.Route. -func AlertingFileExportFromRoute(orgID int64, name string, route definitions.Route) (definitions.AlertingFileExport, error) { - export := RouteExportFromRoute(&route) - export.Name = OmitDefault(util.Pointer(name)) +func AlertingFileExportFromRoute(orgID int64, route definitions.Route) (definitions.AlertingFileExport, error) { f := definitions.AlertingFileExport{ APIVersion: 1, Policies: []definitions.NotificationPolicyExport{{ OrgID: orgID, - RouteExport: export, + RouteExport: RouteExportFromRoute(&route), }}, } return f, nil } +// AlertingFileExportFromManagedRoutes creates a definitions.AlertingFileExport DTO from one or more legacy_storage.ManagedRoute. +func AlertingFileExportFromManagedRoutes(orgID int64, routes legacy_storage.ManagedRoutes) (definitions.AlertingFileExport, error) { + f := definitions.AlertingFileExport{ + APIVersion: 1, + Policies: make([]definitions.NotificationPolicyExport, 0, len(routes)), + } + for _, route := range routes { + f.Policies = append(f.Policies, definitions.NotificationPolicyExport{ + OrgID: orgID, + RouteExport: RouteExportFromManagedRoute(route), + }) + } + return f, nil +} + // RouteExportFromRoute creates a definitions.RouteExport DTO from definitions.Route. func RouteExportFromRoute(route *definitions.Route) *definitions.RouteExport { toStringIfNotNil := func(d *model.Duration) *string { @@ -385,6 +399,18 @@ func RouteExportFromRoute(route *definitions.Route) *definitions.RouteExport { return &export } +// RouteExportFromManagedRoute creates a definitions.RouteExport DTO from legacy_storage.ManagedRoute. +func RouteExportFromManagedRoute(route *legacy_storage.ManagedRoute) *definitions.RouteExport { + amRoute := route.AsAMRoute() + export := RouteExportFromRoute(&amRoute) + if route.Name == legacy_storage.UserDefinedRoutingTreeName { + export.Name = nil // Functionally this shouldn't matter, aesthetically this prefers an empty name over "user-defined". + } else { + export.Name = OmitDefault(util.Pointer(route.Name)) + } + return export +} + // OmitDefault returns nil if the value is the default. func OmitDefault[T comparable](v *T) *T { var def T