This commit is contained in:
Matt Jacobson
2026-01-07 12:00:49 -05:00
parent d220d765b8
commit 0fa9f3a247
23 changed files with 898 additions and 283 deletions
@@ -16,13 +16,28 @@ import (
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
gapiutil "github.com/grafana/grafana/pkg/services/apiserver/utils"
"github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
"github.com/grafana/grafana/pkg/services/ngalert/notifier/legacy_storage"
"github.com/grafana/grafana/pkg/util"
)
func ConvertToK8sResource(orgID int64, r definitions.Route, version string, namespacer request.NamespaceMapper) (*model.RoutingTree, error) {
func ConvertToK8sResources(orgID int64, routes legacy_storage.ManagedRoutes, namespacer request.NamespaceMapper) (*model.RoutingTreeList, error) {
result := &model.RoutingTreeList{
Items: make([]model.RoutingTree, 0, len(routes)),
}
for _, r := range routes {
k8sResource, err := ConvertToK8sResource(orgID, r, namespacer)
if err != nil {
return nil, fmt.Errorf("failed to convert route %q to k8s resource: %w", r.Name, err)
}
result.Items = append(result.Items, *k8sResource)
}
return result, nil
}
func ConvertToK8sResource(orgID int64, r *legacy_storage.ManagedRoute, namespacer request.NamespaceMapper) (*model.RoutingTree, error) {
spec := model.RoutingTreeSpec{
Defaults: model.RoutingTreeRouteDefaults{
GroupBy: r.GroupByStr,
GroupBy: r.GroupBy,
GroupWait: optionalPrometheusDurationToString(r.GroupWait),
GroupInterval: optionalPrometheusDurationToString(r.GroupInterval),
RepeatInterval: optionalPrometheusDurationToString(r.RepeatInterval),
@@ -39,9 +54,9 @@ func ConvertToK8sResource(orgID int64, r definitions.Route, version string, name
var result = &model.RoutingTree{
ObjectMeta: metav1.ObjectMeta{
Name: model.UserDefinedRoutingTreeName,
Name: r.Name,
Namespace: namespacer(orgID),
ResourceVersion: version,
ResourceVersion: r.Version,
},
Spec: spec,
}
@@ -15,6 +15,7 @@ import (
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
"github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
alerting_models "github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/ngalert/notifier/legacy_storage"
)
var (
@@ -22,9 +23,11 @@ var (
)
type RouteService 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) (definitions.Route, string, error)
ResetPolicyTree(ctx context.Context, orgID int64, p alerting_models.Provenance) (definitions.Route, error)
GetManagedRoutes(ctx context.Context, orgID int64) (legacy_storage.ManagedRoutes, error)
GetManagedRoute(ctx context.Context, orgID int64, name string) (legacy_storage.ManagedRoute, error)
DeleteManagedRoute(ctx context.Context, orgID int64, name string, p alerting_models.Provenance, version string) error
CreateManagedRoute(ctx context.Context, orgID int64, name string, subtree definitions.Route, p alerting_models.Provenance) (*legacy_storage.ManagedRoute, error)
UpdateManagedRoute(ctx context.Context, orgID int64, name string, subtree definitions.Route, p alerting_models.Provenance, version string) (*legacy_storage.ManagedRoute, error)
}
type legacyStorage struct {
@@ -55,56 +58,76 @@ func (s *legacyStorage) ConvertToTable(ctx context.Context, object runtime.Objec
return s.tableConverter.ConvertToTable(ctx, object, tableOptions)
}
func (s *legacyStorage) getUserDefinedRoutingTree(ctx context.Context) (*model.RoutingTree, error) {
func (s *legacyStorage) List(ctx context.Context, _ *internalversion.ListOptions) (runtime.Object, error) {
orgId, err := request.OrgIDForList(ctx)
if err != nil {
return nil, err
}
res, version, err := s.service.GetPolicyTree(ctx, orgId)
managedRoutes, err := s.service.GetManagedRoutes(ctx, orgId)
if err != nil {
return nil, err
}
return ConvertToK8sResource(orgId, res, version, s.namespacer)
}
func (s *legacyStorage) List(ctx context.Context, _ *internalversion.ListOptions) (runtime.Object, error) {
user, err := s.getUserDefinedRoutingTree(ctx)
if err != nil {
return nil, err
}
return &model.RoutingTreeList{
Items: []model.RoutingTree{
*user,
},
}, nil
return ConvertToK8sResources(orgId, managedRoutes, s.namespacer)
}
func (s *legacyStorage) Get(ctx context.Context, name string, _ *metav1.GetOptions) (runtime.Object, error) {
if name != model.UserDefinedRoutingTreeName {
return nil, errors.NewNotFound(ResourceInfo.GroupResource(), name)
info, err := request.NamespaceInfoFrom(ctx, true)
if err != nil {
return nil, err
}
return s.getUserDefinedRoutingTree(ctx)
managedRoute, err := s.service.GetManagedRoute(ctx, info.OrgID, name)
if err != nil {
return nil, err
}
return ConvertToK8sResource(info.OrgID, &managedRoute, s.namespacer)
}
func (s *legacyStorage) Create(_ context.Context,
_ runtime.Object,
_ rest.ValidateObjectFunc,
func (s *legacyStorage) Create(ctx context.Context,
obj runtime.Object,
createValidation rest.ValidateObjectFunc,
_ *metav1.CreateOptions,
) (runtime.Object, error) {
return nil, errors.NewMethodNotSupported(ResourceInfo.GroupResource(), "create")
info, err := request.NamespaceInfoFrom(ctx, true)
if err != nil {
return nil, err
}
if createValidation != nil {
if err := createValidation(ctx, obj.DeepCopyObject()); err != nil {
return nil, err
}
}
p, ok := obj.(*model.RoutingTree)
if !ok {
return nil, fmt.Errorf("expected %s but got %s", ResourceInfo.GroupVersionKind(), obj.GetObjectKind().GroupVersionKind())
}
domainModel, _, err := convertToDomainModel(p)
if err != nil {
return nil, err
}
created, err := s.service.CreateManagedRoute(ctx, info.OrgID, p.Name, domainModel, alerting_models.ProvenanceNone)
if err != nil {
return nil, err
}
return ConvertToK8sResource(info.OrgID, created, s.namespacer)
}
func (s *legacyStorage) Update(ctx context.Context, name string, objInfo rest.UpdatedObjectInfo, _ rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc, _ bool, options *metav1.UpdateOptions) (runtime.Object, bool, error) {
if name != model.UserDefinedRoutingTreeName {
return nil, false, errors.NewNotFound(ResourceInfo.GroupResource(), name)
}
func (s *legacyStorage) Update(
ctx context.Context,
name string,
objInfo rest.UpdatedObjectInfo,
_ rest.ValidateObjectFunc,
updateValidation rest.ValidateObjectUpdateFunc,
_ bool,
_ *metav1.UpdateOptions,
) (runtime.Object, bool, error) {
info, err := request.NamespaceInfoFrom(ctx, true)
if err != nil {
return nil, false, err
}
old, err := s.Get(ctx, model.UserDefinedRoutingTreeName, nil)
old, err := s.Get(ctx, name, nil)
if err != nil {
return old, false, err
}
@@ -122,24 +145,26 @@ func (s *legacyStorage) Update(ctx context.Context, name string, objInfo rest.Up
return nil, false, fmt.Errorf("expected %s but got %s", ResourceInfo.GroupVersionKind(), obj.GetObjectKind().GroupVersionKind())
}
model, version, err := convertToDomainModel(p)
domainModel, version, err := convertToDomainModel(p)
if err != nil {
return nil, false, err
}
updated, updatedVersion, err := s.service.UpdatePolicyTree(ctx, info.OrgID, model, alerting_models.ProvenanceNone, version)
updated, err := s.service.UpdateManagedRoute(ctx, info.OrgID, p.Name, domainModel, alerting_models.ProvenanceNone, version)
if err != nil {
return nil, false, err
}
obj, err = ConvertToK8sResource(info.OrgID, updated, updatedVersion, s.namespacer)
obj, err = ConvertToK8sResource(info.OrgID, updated, s.namespacer)
return obj, false, err
}
// Delete implements rest.GracefulDeleter. It is needed for API server to not crash when it registers DeleteCollection method
func (s *legacyStorage) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, opts *metav1.DeleteOptions) (runtime.Object, bool, error) {
if name != model.UserDefinedRoutingTreeName {
return nil, false, errors.NewNotFound(ResourceInfo.GroupResource(), name)
}
func (s *legacyStorage) Delete(
ctx context.Context,
name string,
deleteValidation rest.ValidateObjectFunc,
options *metav1.DeleteOptions,
) (runtime.Object, bool, error) {
info, err := request.NamespaceInfoFrom(ctx, true)
if err != nil {
return nil, false, err
@@ -155,10 +180,14 @@ func (s *legacyStorage) Delete(ctx context.Context, name string, deleteValidatio
return nil, false, err
}
}
_, err = s.service.ResetPolicyTree(ctx, info.OrgID, alerting_models.ProvenanceNone) // TODO add support for dry-run option
version := ""
if options.Preconditions != nil && options.Preconditions.ResourceVersion != nil {
version = *options.Preconditions.ResourceVersion
}
err = s.service.DeleteManagedRoute(ctx, info.OrgID, name, alerting_models.ProvenanceNone, version) // TODO add support for dry-run option
return old, false, err
}
func (s *legacyStorage) DeleteCollection(_ context.Context, _ rest.ValidateObjectFunc, _ *metav1.DeleteOptions, _ *internalversion.ListOptions) (runtime.Object, error) {
return nil, errors.NewMethodNotSupported(ResourceInfo.GroupResource(), "delete")
return nil, errors.NewMethodNotSupported(ResourceInfo.GroupResource(), "deleteCollection")
}
@@ -1,8 +1,10 @@
package routingtree
import (
"fmt"
"strings"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
model "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1"
@@ -14,5 +16,18 @@ var ResourceInfo = utils.NewResourceInfo(kind.Group(), kind.Version(),
kind.GroupVersionResource().Resource, strings.ToLower(kind.Kind()), kind.Kind(),
func() runtime.Object { return kind.ZeroValue() },
func() runtime.Object { return kind.ZeroListValue() },
utils.TableColumns{},
utils.TableColumns{
Definition: []metav1.TableColumnDefinition{
{Name: "Name", Type: "string", Format: "name"},
},
Reader: func(obj any) ([]interface{}, error) {
r, ok := obj.(*model.RoutingTree)
if !ok {
return nil, fmt.Errorf("expected resource or info")
}
return []interface{}{
r.Name,
}, nil
},
},
)
+24 -6
View File
@@ -20,6 +20,7 @@ import (
"github.com/grafana/grafana/pkg/services/ngalert/api/hcl"
"github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
alerting_models "github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/ngalert/notifier/legacy_storage"
"github.com/grafana/grafana/pkg/services/ngalert/provisioning"
"github.com/grafana/grafana/pkg/services/ngalert/store"
"github.com/grafana/grafana/pkg/util"
@@ -58,6 +59,8 @@ 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) (definitions.Route, string, error)
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)
}
type MuteTimingService interface {
@@ -96,15 +99,27 @@ func (srv *ProvisioningSrv) RouteGetPolicyTree(c *contextmodel.ReqContext) respo
}
func (srv *ProvisioningSrv) RouteGetPolicyTreeExport(c *contextmodel.ReqContext) response.Response {
policies, _, err := srv.policies.GetPolicyTree(c.Req.Context(), c.GetOrgID())
if err != nil {
if errors.Is(err, store.ErrNoAlertmanagerConfiguration) {
return ErrResp(http.StatusNotFound, err, "")
routeName := c.Query("routeName")
var policy definitions.Route
if routeName == "" {
var err error
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, "")
}
return ErrResp(http.StatusInternalServerError, 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()
}
e, err := AlertingFileExportFromRoute(c.GetOrgID(), policies)
e, err := AlertingFileExportFromRoute(c.GetOrgID(), routeName, policy)
if err != nil {
return ErrResp(http.StatusInternalServerError, err, "failed to create alerting file export")
}
@@ -593,6 +608,9 @@ func escapeAlertingFileExport(body definitions.AlertingFileExport) definitions.A
}
func escapeRouteExport(r *definitions.RouteExport) {
if r.Name != nil {
r.Name = util.Pointer(addEscapeCharactersToString(*r.Name))
}
r.Receiver = addEscapeCharactersToString(r.Receiver)
if r.GroupByStr != nil {
groupByStr := make([]string, len(*r.GroupByStr))
@@ -2248,6 +2248,7 @@ func createTestRequestCtx() contextmodel.ReqContext {
}
type fakeNotificationPolicyService struct {
NotificationPolicyService
tree definitions.Route
prov models.Provenance
}
@@ -2318,7 +2319,9 @@ func (f *fakeNotificationPolicyService) ResetPolicyTree(ctx context.Context, org
return f.tree, nil
}
type fakeFailingNotificationPolicyService struct{}
type fakeFailingNotificationPolicyService struct {
NotificationPolicyService
}
func (f *fakeFailingNotificationPolicyService) GetPolicyTree(ctx context.Context, orgID int64) (definitions.Route, string, error) {
return definitions.Route{}, "", fmt.Errorf("something went wrong")
@@ -2332,7 +2335,9 @@ func (f *fakeFailingNotificationPolicyService) ResetPolicyTree(ctx context.Conte
return definitions.Route{}, fmt.Errorf("something went wrong")
}
type fakeRejectingNotificationPolicyService struct{}
type fakeRejectingNotificationPolicyService struct {
NotificationPolicyService
}
type fakeRejectingNotificationSettingsValidatorProvider struct{}
+4 -2
View File
@@ -327,12 +327,14 @@ func ReceiverExportFromEmbeddedContactPoint(contact definitions.EmbeddedContactP
}
// AlertingFileExportFromRoute creates a definitions.AlertingFileExport DTO from definitions.Route.
func AlertingFileExportFromRoute(orgID int64, route definitions.Route) (definitions.AlertingFileExport, error) {
func AlertingFileExportFromRoute(orgID int64, name string, route definitions.Route) (definitions.AlertingFileExport, error) {
export := RouteExportFromRoute(&route)
export.Name = OmitDefault(util.Pointer(name))
f := definitions.AlertingFileExport{
APIVersion: 1,
Policies: []definitions.NotificationPolicyExport{{
OrgID: orgID,
RouteExport: RouteExportFromRoute(&route),
RouteExport: export,
}},
}
return f, nil
@@ -775,10 +775,11 @@ func fromPrometheusConfig(prometheusConfig config.Config) PostableApiAlertingCon
// swagger:model
type PostableUserConfig struct {
TemplateFiles map[string]string `yaml:"template_files" json:"template_files"`
AlertmanagerConfig PostableApiAlertingConfig `yaml:"alertmanager_config" json:"alertmanager_config"`
ExtraConfigs []ExtraConfiguration `yaml:"extra_config,omitempty" json:"extra_config,omitempty"`
amSimple map[string]interface{} `yaml:"-" json:"-"`
TemplateFiles map[string]string `yaml:"template_files" json:"template_files"`
AlertmanagerConfig PostableApiAlertingConfig `yaml:"alertmanager_config" json:"alertmanager_config"`
ExtraConfigs []ExtraConfiguration `yaml:"extra_config,omitempty" json:"extra_config,omitempty"`
ManagedRoutes map[string]*definition.Route `yaml:"managed_routes,omitempty" json:"managed_routes,omitempty"` // TODO: Move to ConfigRevision?
amSimple map[string]interface{} `yaml:"-" json:"-"`
}
func (c *PostableUserConfig) GetMergedAlertmanagerConfig() (MergeResult, error) {
@@ -70,7 +70,8 @@ type NotificationPolicyExport struct {
// RouteExport is the provisioned file export of definitions.Route. This is needed to hide fields that aren't useable in
// provisioning file format. An alternative would be to define a custom MarshalJSON and MarshalYAML that excludes them.
type RouteExport struct {
Receiver string `yaml:"receiver,omitempty" json:"receiver,omitempty" hcl:"contact_point"`
Name *string `yaml:"name,omitempty" json:"name,omitempty" hcl:"name,optional"`
Receiver string `yaml:"receiver,omitempty" json:"receiver,omitempty" hcl:"contact_point"`
GroupByStr *[]string `yaml:"group_by,omitempty" json:"group_by,omitempty" hcl:"group_by"`
// Deprecated. Remove before v1.0 release.
+1 -1
View File
@@ -422,7 +422,7 @@ func (ng *AlertNG) init() error {
)
// Provisioning
policyService := provisioning.NewNotificationPolicyService(configStore, ng.store, ng.store, ng.Cfg.UnifiedAlerting, ng.Log)
policyService := provisioning.NewNotificationPolicyService(configStore, ng.store, ng.store, ng.Cfg.UnifiedAlerting, ng.FeatureToggles, ng.Log)
contactPointService := provisioning.NewContactPointService(configStore, ng.SecretsService, ng.store, ng.store, provisioningReceiverService, ng.Log, ng.store, ng.ResourcePermissions)
templateService := provisioning.NewTemplateService(configStore, ng.store, ng.store, ng.Log)
muteTimingService := provisioning.NewMuteTimingService(configStore, ng.store, ng.store, ng.Log, ng.store)
@@ -21,6 +21,7 @@ import (
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
"github.com/grafana/grafana/pkg/services/ngalert/metrics"
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/ngalert/notifier/legacy_storage"
"github.com/grafana/grafana/pkg/services/ngalert/store"
"github.com/grafana/grafana/pkg/services/notifications"
"github.com/grafana/grafana/pkg/setting"
@@ -336,6 +337,9 @@ func (am *alertmanager) applyConfig(ctx context.Context, cfg *apimodels.Postable
return false, fmt.Errorf("failed to decrypt external configurations: %w", err)
}
// Add managed routes to the configuration.
cfg.AlertmanagerConfig.Route = legacy_storage.WithManagedRoutes(cfg.AlertmanagerConfig.Route, cfg.ManagedRoutes)
mergeResult, err := cfg.GetMergedAlertmanagerConfig()
if err != nil {
return false, fmt.Errorf("failed to get full alertmanager configuration: %w", err)
@@ -12,6 +12,12 @@ var (
"Invalid receiver: '{{ .Public.Reason }}'",
errutil.WithPublic("Invalid receiver: '{{ .Public.Reason }}'"),
)
ErrRouteExists = errutil.Conflict("alerting.notifications.routes.exists", errutil.WithPublicMessage("Route with this name already exists. Use a different name or update an existing one."))
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."),
)
)
func makeErrBadAlertmanagerConfiguration(err error) error {
@@ -33,3 +39,12 @@ func MakeErrReceiverInvalid(err error) error {
}
return ErrReceiverInvalid.Build(data)
}
func MakeErrRouteInvalidFormat(err error) error {
return ErrRouteInvalidFormat.Build(errutil.TemplateData{
Public: map[string]any{
"Error": err.Error(),
},
Error: err,
})
}
@@ -0,0 +1,380 @@
package legacy_storage
import (
"encoding/binary"
"fmt"
"hash"
"hash/fnv"
"maps"
"slices"
"strings"
"unsafe"
"github.com/grafana/alerting/definition"
"github.com/prometheus/alertmanager/dispatch"
"github.com/prometheus/alertmanager/pkg/labels"
"github.com/prometheus/common/model"
"github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
"github.com/grafana/grafana/pkg/services/ngalert/models"
)
const UserDefinedRoutingTreeName = "user-defined"
const NamedRouteMatcher = "__grafana_managed_route__"
type ManagedRoute struct {
Name string
Version string
Receiver string
GroupBy []string
GroupWait *model.Duration
GroupInterval *model.Duration
RepeatInterval *model.Duration
Routes []*definition.Route
Provenance models.Provenance
}
func (r *ManagedRoute) AsAMRoute() definition.Route {
// Only need to copy the fields that are valid for a root route.
return definition.Route{
Receiver: r.Receiver,
GroupByStr: r.GroupBy,
GroupWait: r.GroupWait,
GroupInterval: r.GroupInterval,
RepeatInterval: r.RepeatInterval,
Routes: r.Routes,
Provenance: definitions.Provenance(r.Provenance),
}
}
func (r *ManagedRoute) GeneratedSubRoute() *definition.Route {
amRoute := r.AsAMRoute()
// It's important that the generated sub-route is fully defined so that they will never rely on the values of the root.
defaultOpts := dispatch.DefaultRouteOpts
if amRoute.GroupWait == nil {
gw := model.Duration(defaultOpts.GroupWait)
amRoute.GroupWait = &gw
}
if amRoute.GroupInterval == nil {
gi := model.Duration(defaultOpts.GroupInterval)
amRoute.GroupInterval = &gi
}
if amRoute.RepeatInterval == nil {
ri := model.Duration(defaultOpts.RepeatInterval)
amRoute.RepeatInterval = &ri
}
if r.Name != UserDefinedRoutingTreeName {
// Set label matcher.
amRoute.ObjectMatchers = definitions.ObjectMatchers{managedRouteMatcher(r.Name)}
}
return &amRoute
}
func (r *ManagedRoute) ResourceType() string {
return (&definition.Route{}).ResourceType()
}
func (r *ManagedRoute) ResourceID() string {
if r.Name == UserDefinedRoutingTreeName {
// Backwards compatibility with legacy user-defined routing tree.
return ""
}
return r.Name
}
func NewManagedRoute(name string, r *definition.Route) *ManagedRoute {
return &ManagedRoute{
Name: name,
Version: CalculateRouteFingerprint(*r),
Receiver: r.Receiver,
GroupBy: r.GroupByStr,
GroupWait: r.GroupWait,
GroupInterval: r.GroupInterval,
RepeatInterval: r.RepeatInterval,
Routes: r.Routes,
Provenance: models.Provenance(r.Provenance),
}
}
func managedRouteMatcher(name string) *labels.Matcher {
return &labels.Matcher{
Type: labels.MatchEqual,
Name: NamedRouteMatcher,
Value: name,
}
}
type ManagedRoutes []*ManagedRoute
func (m ManagedRoutes) Sort() {
// Sort the keys of the map to ensure consistent ordering. Always ensure that the legacy user-defined routing tree is last.
slices.SortFunc(m, func(a, b *ManagedRoute) int {
if a.Name == UserDefinedRoutingTreeName {
return 1
}
if b.Name == UserDefinedRoutingTreeName {
return -1
}
return strings.Compare(a.Name, b.Name)
})
}
func WithManagedRoutes(root *definitions.Route, managedRoutes map[string]*definition.Route) *definitions.Route {
if len(managedRoutes) == 0 {
// If there are no managed routes, we just return the original root.
return root
}
newRoot := *root
newManagedRoutes := make([]*definition.Route, 0, len(newRoot.Routes)+len(managedRoutes))
for _, k := range slices.Sorted(maps.Keys(managedRoutes)) {
// On the off chance that the route is nil or invalid managed route with the restricted name, we skip it.
if managedRoutes[k] == nil || k == UserDefinedRoutingTreeName {
continue
}
newManagedRoutes = append(newManagedRoutes, NewManagedRoute(k, managedRoutes[k]).GeneratedSubRoute())
}
// Add the user-defined routing tree at the end.
newManagedRoutes = append(newManagedRoutes, newRoot.Routes...)
newRoot.Routes = newManagedRoutes
return &newRoot
}
func (rev *ConfigRevision) GetManagedRoute(name string) *ManagedRoute {
if name == UserDefinedRoutingTreeName {
return NewManagedRoute(UserDefinedRoutingTreeName, rev.Config.AlertmanagerConfig.Route)
}
route, ok := rev.Config.ManagedRoutes[name]
if !ok {
return nil
}
return NewManagedRoute(name, route)
}
func (rev *ConfigRevision) GetManagedRoutes() ManagedRoutes {
managedRoutes := make(ManagedRoutes, 0, len(rev.Config.ManagedRoutes)+1)
for _, k := range slices.Sorted(maps.Keys(rev.Config.ManagedRoutes)) {
// On the off chance that the route is nil or invalid managed route with the restricted name, we skip it.
if rev.Config.ManagedRoutes[k] == nil || k == UserDefinedRoutingTreeName {
continue
}
managedRoutes = append(managedRoutes, NewManagedRoute(k, rev.Config.ManagedRoutes[k]))
}
managedRoutes = append(managedRoutes, NewManagedRoute(UserDefinedRoutingTreeName, rev.Config.AlertmanagerConfig.Route))
return managedRoutes
}
func (rev *ConfigRevision) DeleteManagedRoute(name string) {
// Intentionally does not consider if name == UserDefinedRoutingTreeName as it should only be Reset via Update.
delete(rev.Config.ManagedRoutes, name)
}
func (rev *ConfigRevision) CreateManagedRoute(name string, subtree definitions.Route) (*ManagedRoute, error) {
if name == "" {
return nil, fmt.Errorf("route name is required")
}
if name == UserDefinedRoutingTreeName {
return nil, fmt.Errorf("cannot create a managed route with the name %q, this name is reserved for the user-defined routing tree", UserDefinedRoutingTreeName)
}
if _, exists := rev.Config.ManagedRoutes[name]; exists {
return nil, ErrRouteExists.Errorf("")
}
managedRoute := NewManagedRoute(name, &subtree)
amRoute := managedRoute.AsAMRoute()
err := rev.ValidateRoute(amRoute)
if err != nil {
return nil, MakeErrRouteInvalidFormat(err)
}
if rev.Config.ManagedRoutes == nil {
rev.Config.ManagedRoutes = make(map[string]*definition.Route, 1)
}
rev.Config.ManagedRoutes[name] = &amRoute
return managedRoute, nil
}
func (rev *ConfigRevision) UpdateNamedRoute(name string, subtree definitions.Route) (*ManagedRoute, error) {
if name == "" {
return nil, fmt.Errorf("route name is required")
}
if existing := rev.GetManagedRoute(name); existing == nil {
return nil, fmt.Errorf("managed route %q not found", name)
}
managedRoute := NewManagedRoute(name, &subtree)
amRoute := managedRoute.AsAMRoute()
err := rev.ValidateRoute(amRoute)
if err != nil {
return nil, MakeErrRouteInvalidFormat(err)
}
if name == UserDefinedRoutingTreeName {
rev.Config.AlertmanagerConfig.Route = &amRoute
} else {
if rev.Config.ManagedRoutes == nil {
rev.Config.ManagedRoutes = make(map[string]*definition.Route, 1)
}
rev.Config.ManagedRoutes[name] = &amRoute
}
return managedRoute, nil
}
func (rev *ConfigRevision) ResetUserDefinedRoute(defaultCfg *definitions.PostableUserConfig) (*ManagedRoute, error) {
// Ensure the new default receiver exists and if not, create it.
if err := rev.validateReceiverReferences(*defaultCfg.AlertmanagerConfig.Route); err != nil {
// Default receiver doesn't exist, create it.
var defaultRcv *definitions.PostableApiReceiver
for _, rcv := range defaultCfg.AlertmanagerConfig.Receivers {
if rcv.Name == defaultCfg.AlertmanagerConfig.Route.Receiver {
defaultRcv = rcv
break
}
}
if defaultRcv == nil {
return nil, fmt.Errorf("inconsistent default configuration: default receiver %q not found", defaultCfg.AlertmanagerConfig.Route.Receiver)
}
rev.Config.AlertmanagerConfig.Receivers = append(rev.Config.AlertmanagerConfig.Receivers, defaultRcv)
}
return rev.UpdateNamedRoute(UserDefinedRoutingTreeName, *defaultCfg.AlertmanagerConfig.Route)
}
func (rev *ConfigRevision) ValidateRoute(route definitions.Route) error {
err := route.Validate()
if err != nil {
return err
}
err = rev.validateReceiverReferences(route)
if err != nil {
return err
}
err = rev.validateTimeIntervalReferences(route)
if err != nil {
return err
}
return nil
}
func (rev *ConfigRevision) validateReceiverReferences(route definitions.Route) error {
receivers := rev.GetReceiversNames()
receivers[""] = struct{}{} // Allow empty receiver (inheriting from parent)
return route.ValidateReceivers(receivers)
}
func (rev *ConfigRevision) validateTimeIntervalReferences(route definitions.Route) error {
timeIntervals := map[string]struct{}{}
for _, mt := range rev.Config.AlertmanagerConfig.MuteTimeIntervals {
timeIntervals[mt.Name] = struct{}{}
}
for _, mt := range rev.Config.AlertmanagerConfig.TimeIntervals {
timeIntervals[mt.Name] = struct{}{}
}
return route.ValidateTimeIntervals(timeIntervals)
}
func CalculateRouteFingerprint(route definitions.Route) string {
sum := fnv.New64a()
writeToHash(sum, &route)
return fmt.Sprintf("%016x", sum.Sum64())
}
func writeToHash(sum hash.Hash, r *definitions.Route) {
writeBytes := func(b []byte) {
_, _ = sum.Write(b)
// add a byte sequence that cannot happen in UTF-8 strings.
_, _ = sum.Write([]byte{255})
}
writeString := func(s string) {
if len(s) == 0 {
writeBytes(nil)
return
}
// #nosec G103
// avoid allocation when converting string to byte slice
writeBytes(unsafe.Slice(unsafe.StringData(s), len(s)))
}
// this temp slice is used to convert ints to bytes.
tmp := make([]byte, 8)
writeInt := func(u int64) {
binary.LittleEndian.PutUint64(tmp, uint64(u))
writeBytes(tmp)
}
writeBool := func(b bool) {
if b {
writeInt(1)
} else {
writeInt(0)
}
}
writeDuration := func(d *model.Duration) {
if d == nil {
_, _ = sum.Write([]byte{255})
} else {
binary.LittleEndian.PutUint64(tmp, uint64(*d))
_, _ = sum.Write(tmp)
_, _ = sum.Write([]byte{255})
}
}
writeString(r.Receiver)
for _, s := range r.GroupByStr {
writeString(s)
}
for _, labelName := range r.GroupBy {
writeString(string(labelName))
}
writeBool(r.GroupByAll)
if len(r.Match) > 0 {
for _, key := range slices.Sorted(maps.Keys(r.Match)) {
writeString(key)
writeString(r.Match[key])
}
}
if len(r.MatchRE) > 0 {
for _, key := range slices.Sorted(maps.Keys(r.MatchRE)) {
writeString(key)
str, err := r.MatchRE[key].MarshalJSON()
if err != nil {
writeString(fmt.Sprintf("%+v", r.MatchRE))
}
writeBytes(str)
}
}
for _, matcher := range r.Matchers {
writeString(matcher.String())
}
for _, matcher := range r.ObjectMatchers {
writeString(matcher.String())
}
for _, timeInterval := range r.MuteTimeIntervals {
writeString(timeInterval)
}
for _, timeInterval := range r.ActiveTimeIntervals {
writeString(timeInterval)
}
writeBool(r.Continue)
writeDuration(r.GroupWait)
writeDuration(r.GroupInterval)
writeDuration(r.RepeatInterval)
for _, route := range r.Routes {
writeToHash(sum, route)
}
}
@@ -41,6 +41,8 @@ var (
ErrRouteConflictingMatchers = errutil.BadRequest("alerting.notifications.routes.conflictingMatchers").MustTemplate("Routing tree conflicts with the external configuration",
errutil.WithPublic("Cannot add\\update route: matchers conflict with an external routing tree merging matchers {{ .Public.Matchers }}, making the added\\updated route unreachable."),
)
ErrRouteNotFound = errutil.NotFound("alerting.notifications.routes.notFound", errutil.WithPublicMessage("Route not found"))
)
// MakeErrTimeIntervalInvalid creates an error with the ErrTimeIntervalInvalid template
@@ -2,19 +2,13 @@ package provisioning
import (
"context"
"encoding/binary"
"errors"
"fmt"
"hash"
"hash/fnv"
"slices"
"unsafe"
"github.com/grafana/alerting/definition"
"github.com/prometheus/common/model"
"golang.org/x/exp/maps"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"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"
@@ -29,92 +23,138 @@ type NotificationPolicyService struct {
log log.Logger
settings setting.UnifiedAlertingSettings
validator validation.ProvenanceStatusTransitionValidator
FeatureToggles featuremgmt.FeatureToggles
}
func NewNotificationPolicyService(am alertmanagerConfigStore, prov ProvisioningStore,
xact TransactionManager, settings setting.UnifiedAlertingSettings, log log.Logger) *NotificationPolicyService {
func NewNotificationPolicyService(
am alertmanagerConfigStore,
prov ProvisioningStore,
xact TransactionManager,
settings setting.UnifiedAlertingSettings,
features featuremgmt.FeatureToggles,
log log.Logger,
) *NotificationPolicyService {
return &NotificationPolicyService{
configStore: am,
provenanceStore: prov,
xact: xact,
log: log,
settings: settings,
FeatureToggles: features,
validator: validation.ValidateProvenanceRelaxed,
}
}
func (nps *NotificationPolicyService) GetPolicyTree(ctx context.Context, orgID int64) (definitions.Route, string, error) {
func (nps *NotificationPolicyService) GetManagedRoute(ctx context.Context, orgID int64, name string) (legacy_storage.ManagedRoute, error) {
// TODO: Keep this?
if name == "" {
name = legacy_storage.UserDefinedRoutingTreeName
}
// Backwards compatibility when managed routes FF is disabled. Only allow the default route.
if !nps.FeatureToggles.IsEnabledGlobally(featuremgmt.FlagAlertingMultiplePolicies) && name != legacy_storage.UserDefinedRoutingTreeName {
return legacy_storage.ManagedRoute{}, ErrRouteNotFound.Errorf("route %q not found", name)
}
rev, err := nps.configStore.Get(ctx, orgID)
if err != nil {
return definitions.Route{}, "", err
return legacy_storage.ManagedRoute{}, err
}
if rev.Config.AlertmanagerConfig.Route == nil {
return definitions.Route{}, "", fmt.Errorf("no route present in current alertmanager config")
route := rev.GetManagedRoute(name)
if route == nil {
return legacy_storage.ManagedRoute{}, ErrRouteNotFound.Errorf("route %q not found", name)
}
provenance, err := nps.provenanceStore.GetProvenance(ctx, rev.Config.AlertmanagerConfig.Route, orgID)
provenance, err := nps.provenanceStore.GetProvenance(ctx, route, orgID)
if err != nil {
return definitions.Route{}, "", err
return legacy_storage.ManagedRoute{}, err
}
result := *rev.Config.AlertmanagerConfig.Route
result.Provenance = definitions.Provenance(provenance)
version := calculateRouteFingerprint(result)
return result, version, nil
route.Provenance = provenance
return *route, nil
}
func (nps *NotificationPolicyService) UpdatePolicyTree(ctx context.Context, orgID int64, tree definitions.Route, p models.Provenance, version string) (definitions.Route, string, error) {
err := tree.Validate()
func (nps *NotificationPolicyService) GetManagedRoutes(ctx context.Context, orgID int64) (legacy_storage.ManagedRoutes, error) {
rev, err := nps.configStore.Get(ctx, orgID)
if err != nil {
return definitions.Route{}, "", MakeErrRouteInvalidFormat(err)
return nil, err
}
provenances, err := nps.provenanceStore.GetProvenances(ctx, orgID, (&legacy_storage.ManagedRoute{}).ResourceType())
if err != nil {
return nil, err
}
managedRoutesDisabled := !nps.FeatureToggles.IsEnabledGlobally(featuremgmt.FlagAlertingMultiplePolicies)
managedRoutes := rev.GetManagedRoutes()
for _, mr := range managedRoutes {
// Backwards compatibility when managed routes FF is disabled. Don't include any custom managed routes.
if managedRoutesDisabled && mr.Name != legacy_storage.UserDefinedRoutingTreeName {
continue
}
provenance, ok := provenances[mr.ResourceID()]
if !ok {
provenance = models.ProvenanceNone
}
mr.Provenance = provenance
}
managedRoutes.Sort()
return managedRoutes, nil
}
func (nps *NotificationPolicyService) UpdateManagedRoute(ctx context.Context, orgID int64, name string, subtree definitions.Route, p models.Provenance, version string) (*legacy_storage.ManagedRoute, error) {
// TODO: Keep this?
if name == "" {
name = legacy_storage.UserDefinedRoutingTreeName
}
// Backwards compatibility when managed routes FF is disabled. Only allow the default route.
if !nps.FeatureToggles.IsEnabledGlobally(featuremgmt.FlagAlertingMultiplePolicies) && name != legacy_storage.UserDefinedRoutingTreeName {
return nil, ErrRouteNotFound.Errorf("route %q not found", name)
}
err := subtree.Validate()
if err != nil {
return nil, MakeErrRouteInvalidFormat(err)
}
revision, err := nps.configStore.Get(ctx, orgID)
if err != nil {
return definitions.Route{}, "", err
return nil, err
}
err = nps.checkOptimisticConcurrency(*revision.Config.AlertmanagerConfig.Route, p, version, "update")
existing := revision.GetManagedRoute(name)
if existing == nil {
return nil, ErrRouteNotFound.Errorf("route %q not found", name)
}
err = nps.checkOptimisticConcurrency(existing, p, version, "update")
if err != nil {
return definitions.Route{}, "", err
return nil, err
}
// check that provenance is not changed in an invalid way
storedProvenance, err := nps.provenanceStore.GetProvenance(ctx, &tree, orgID)
storedProvenance, err := nps.provenanceStore.GetProvenance(ctx, existing, orgID)
if err != nil {
return definitions.Route{}, "", err
return nil, err
}
if err := nps.validator(storedProvenance, p); err != nil {
return definitions.Route{}, "", err
return nil, err
}
receivers := revision.GetReceiversNames()
receivers[""] = struct{}{} // Allow empty receiver (inheriting from parent)
err = tree.ValidateReceivers(receivers)
updated, err := revision.UpdateNamedRoute(name, subtree)
if err != nil {
return definitions.Route{}, "", MakeErrRouteInvalidFormat(err)
return nil, err
}
timeIntervals := map[string]struct{}{}
for _, mt := range revision.Config.AlertmanagerConfig.MuteTimeIntervals {
timeIntervals[mt.Name] = struct{}{}
}
for _, mt := range revision.Config.AlertmanagerConfig.TimeIntervals {
timeIntervals[mt.Name] = struct{}{}
}
err = tree.ValidateTimeIntervals(timeIntervals)
if err != nil {
return definitions.Route{}, "", MakeErrRouteInvalidFormat(err)
}
revision.Config.AlertmanagerConfig.Route = &tree
updated.Provenance = storedProvenance
_, err = revision.Config.GetMergedAlertmanagerConfig()
if err != nil {
if errors.Is(err, definition.ErrSubtreeMatchersConflict) {
// TODO temporarily get the conflicting matchers
return definitions.Route{}, "", MakeErrRouteConflictingMatchers(fmt.Sprintf("%s", revision.Config.ExtraConfigs[0].MergeMatchers))
return nil, MakeErrRouteConflictingMatchers(fmt.Sprintf("%s", revision.Config.ExtraConfigs[0].MergeMatchers))
}
nps.log.Warn("Unable to validate the combined routing tree because of an error during merging. This could be a sign of broken external configuration. Skipping", "error", err)
}
@@ -123,169 +163,147 @@ func (nps *NotificationPolicyService) UpdatePolicyTree(ctx context.Context, orgI
if err := nps.configStore.Save(ctx, revision, orgID); err != nil {
return err
}
return nps.provenanceStore.SetProvenance(ctx, &tree, orgID, p)
return nps.provenanceStore.SetProvenance(ctx, updated, orgID, p)
})
if err != nil {
return definitions.Route{}, "", err
return nil, err
}
return tree, calculateRouteFingerprint(tree), nil
return updated, nil
}
func (nps *NotificationPolicyService) ResetPolicyTree(ctx context.Context, orgID int64, provenance models.Provenance) (definitions.Route, error) {
storedProvenance, err := nps.provenanceStore.GetProvenance(ctx, &definitions.Route{}, orgID)
if err != nil {
return definitions.Route{}, err
}
if err := nps.validator(storedProvenance, provenance); err != nil {
return definitions.Route{}, err
func (nps *NotificationPolicyService) DeleteManagedRoute(ctx context.Context, orgID int64, name string, p models.Provenance, version string) error {
// TODO: Keep this?
if name == "" {
name = legacy_storage.UserDefinedRoutingTreeName
}
defaultCfg, err := legacy_storage.DeserializeAlertmanagerConfig([]byte(nps.settings.DefaultConfiguration))
if err != nil {
nps.log.Error("Failed to parse default alertmanager config: %w", err)
return definitions.Route{}, fmt.Errorf("failed to parse default alertmanager config: %w", err)
// Backwards compatibility when managed routes FF is disabled. Only allow the default route.
if !nps.FeatureToggles.IsEnabledGlobally(featuremgmt.FlagAlertingMultiplePolicies) && name != legacy_storage.UserDefinedRoutingTreeName {
return ErrRouteNotFound.Errorf("route %q not found", name)
}
route := defaultCfg.AlertmanagerConfig.Route
revision, err := nps.configStore.Get(ctx, orgID)
if err != nil {
return definitions.Route{}, err
return err
}
revision.Config.AlertmanagerConfig.Route = route
err = nps.ensureDefaultReceiverExists(revision.Config, defaultCfg)
existing := revision.GetManagedRoute(name)
if existing == nil {
return ErrRouteNotFound.Errorf("route %q not found", name)
}
err = nps.checkOptimisticConcurrency(existing, p, version, "delete")
if err != nil {
return definitions.Route{}, err
return err
}
storedProvenance, err := nps.provenanceStore.GetProvenance(ctx, existing, orgID)
if err != nil {
return err
}
if err := nps.validator(storedProvenance, p); err != nil {
return err
}
if name == legacy_storage.UserDefinedRoutingTreeName {
defaultCfg, err := legacy_storage.DeserializeAlertmanagerConfig([]byte(nps.settings.DefaultConfiguration))
if err != nil {
nps.log.Error("Failed to parse default alertmanager config: %w", err)
return fmt.Errorf("failed to parse default alertmanager config: %w", err)
}
_, err = revision.ResetUserDefinedRoute(defaultCfg)
if err != nil {
return err
}
} else {
revision.DeleteManagedRoute(name)
}
_, err = revision.Config.GetMergedAlertmanagerConfig()
if err != nil {
return fmt.Errorf("new routing tree is not compatible with extra configuration: %w", err)
}
return nps.xact.InTransaction(ctx, func(ctx context.Context) error {
if err := nps.configStore.Save(ctx, revision, orgID); err != nil {
return err
}
return nps.provenanceStore.DeleteProvenance(ctx, existing, orgID)
})
}
func (nps *NotificationPolicyService) CreateManagedRoute(ctx context.Context, orgID int64, name string, subtree definitions.Route, p models.Provenance) (*legacy_storage.ManagedRoute, error) {
// Backwards compatibility when managed routes FF is disabled. This is not allowed.
if !nps.FeatureToggles.IsEnabledGlobally(featuremgmt.FlagAlertingMultiplePolicies) {
return nil, fmt.Errorf("managed routes are not enabled, see feature toggle %s", featuremgmt.FlagAlertingMultiplePolicies)
}
err := subtree.Validate()
if err != nil {
return nil, MakeErrRouteInvalidFormat(err)
}
revision, err := nps.configStore.Get(ctx, orgID)
if err != nil {
return nil, err
}
created, err := revision.CreateManagedRoute(name, subtree)
if err != nil {
return nil, err
}
_, err = revision.Config.GetMergedAlertmanagerConfig()
if err != nil {
return nil, fmt.Errorf("new routing tree is not compatible with extra configuration: %w", err)
}
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.DeleteProvenance(ctx, route, orgID)
return nps.provenanceStore.SetProvenance(ctx, created, orgID, p)
})
if err != nil {
return definitions.Route{}, nil
} // TODO should be error?
return *route, nil
return nil, err
}
return created, nil
}
func (nps *NotificationPolicyService) ensureDefaultReceiverExists(cfg *definitions.PostableUserConfig, defaultCfg *definitions.PostableUserConfig) error {
defaultRcv := cfg.AlertmanagerConfig.Route.Receiver
for _, rcv := range cfg.AlertmanagerConfig.Receivers {
if rcv.Name == defaultRcv {
return nil
}
// TODO: Remove this method once the all callers support named routes.
func (nps *NotificationPolicyService) GetPolicyTree(ctx context.Context, orgID int64) (definitions.Route, string, error) {
r, err := nps.GetManagedRoute(ctx, orgID, legacy_storage.UserDefinedRoutingTreeName)
if err != nil {
return definitions.Route{}, "", err
}
for _, rcv := range defaultCfg.AlertmanagerConfig.Receivers {
if rcv.Name == defaultRcv {
cfg.AlertmanagerConfig.Receivers = append(cfg.AlertmanagerConfig.Receivers, rcv)
return nil
}
}
nps.log.Error("Grafana Alerting has been configured with a default configuration that is internally inconsistent! The default configuration's notification policy must have a corresponding receiver.")
return fmt.Errorf("inconsistent default configuration")
return r.AsAMRoute(), r.Version, nil
}
func calculateRouteFingerprint(route definitions.Route) string {
sum := fnv.New64a()
writeToHash(sum, &route)
return fmt.Sprintf("%016x", sum.Sum64())
// TODO: Remove this method once the all callers support named routes.
func (nps *NotificationPolicyService) UpdatePolicyTree(ctx context.Context, orgID int64, tree definitions.Route, p models.Provenance, version string) (definitions.Route, string, error) {
r, err := nps.UpdateManagedRoute(ctx, orgID, legacy_storage.UserDefinedRoutingTreeName, tree, p, version)
if err != nil {
return definitions.Route{}, "", err
}
return r.AsAMRoute(), r.Version, nil
}
func writeToHash(sum hash.Hash, r *definitions.Route) {
writeBytes := func(b []byte) {
_, _ = sum.Write(b)
// add a byte sequence that cannot happen in UTF-8 strings.
_, _ = sum.Write([]byte{255})
// TODO: Remove this method once the all callers support named routes.
func (nps *NotificationPolicyService) ResetPolicyTree(ctx context.Context, orgID int64, provenance models.Provenance) (definitions.Route, error) {
err := nps.DeleteManagedRoute(ctx, orgID, legacy_storage.UserDefinedRoutingTreeName, provenance, "")
if err != nil {
return definitions.Route{}, err
}
writeString := func(s string) {
if len(s) == 0 {
writeBytes(nil)
return
}
// #nosec G103
// avoid allocation when converting string to byte slice
writeBytes(unsafe.Slice(unsafe.StringData(s), len(s)))
}
// this temp slice is used to convert ints to bytes.
tmp := make([]byte, 8)
writeInt := func(u int64) {
binary.LittleEndian.PutUint64(tmp, uint64(u))
writeBytes(tmp)
}
writeBool := func(b bool) {
if b {
writeInt(1)
} else {
writeInt(0)
}
}
writeDuration := func(d *model.Duration) {
if d == nil {
_, _ = sum.Write([]byte{255})
} else {
binary.LittleEndian.PutUint64(tmp, uint64(*d))
_, _ = sum.Write(tmp)
_, _ = sum.Write([]byte{255})
}
}
writeString(r.Receiver)
for _, s := range r.GroupByStr {
writeString(s)
}
for _, labelName := range r.GroupBy {
writeString(string(labelName))
}
writeBool(r.GroupByAll)
if len(r.Match) > 0 {
keys := maps.Keys(r.Match)
slices.Sort(keys)
for _, key := range keys {
writeString(key)
writeString(r.Match[key])
}
}
if len(r.MatchRE) > 0 {
keys := maps.Keys(r.MatchRE)
slices.Sort(keys)
for _, key := range keys {
writeString(key)
str, err := r.MatchRE[key].MarshalJSON()
if err != nil {
writeString(fmt.Sprintf("%+v", r.MatchRE))
}
writeBytes(str)
}
}
for _, matcher := range r.Matchers {
writeString(matcher.String())
}
for _, matcher := range r.ObjectMatchers {
writeString(matcher.String())
}
for _, timeInterval := range r.MuteTimeIntervals {
writeString(timeInterval)
}
for _, timeInterval := range r.ActiveTimeIntervals {
writeString(timeInterval)
}
writeBool(r.Continue)
writeDuration(r.GroupWait)
writeDuration(r.GroupInterval)
writeDuration(r.RepeatInterval)
for _, route := range r.Routes {
writeToHash(sum, route)
defaultCfg, err := legacy_storage.DeserializeAlertmanagerConfig([]byte(nps.settings.DefaultConfiguration))
if err != nil {
nps.log.Error("Failed to parse default alertmanager config: %w", err)
return definitions.Route{}, fmt.Errorf("failed to parse default alertmanager config: %w", err)
}
return *defaultCfg.AlertmanagerConfig.Route, nil
}
func (nps *NotificationPolicyService) checkOptimisticConcurrency(current definitions.Route, provenance models.Provenance, desiredVersion string, action string) error {
func (nps *NotificationPolicyService) checkOptimisticConcurrency(current *legacy_storage.ManagedRoute, provenance models.Provenance, desiredVersion string, action string) error {
if desiredVersion == "" {
if provenance != models.ProvenanceFile {
// if version is not specified and it's not a file provisioning, emit a log message to reflect that optimistic concurrency is disabled for this request
@@ -293,9 +311,8 @@ func (nps *NotificationPolicyService) checkOptimisticConcurrency(current definit
}
return nil
}
currentVersion := calculateRouteFingerprint(current)
if currentVersion != desiredVersion {
return ErrVersionConflict.Errorf("provided version %s of routing tree does not match current version %s", desiredVersion, currentVersion)
if current.Version != desiredVersion {
return ErrVersionConflict.Errorf("provided version %s of routing tree does not match current version %s", desiredVersion, current.Version)
}
return nil
}
@@ -16,6 +16,7 @@ import (
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"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"
@@ -29,7 +30,7 @@ func TestGetPolicyTree(t *testing.T) {
rev := getDefaultConfigRevision()
expectedRoute := *rev.Config.AlertmanagerConfig.Route
expectedRoute.Provenance = definitions.Provenance(models.ProvenanceAPI)
expectedVersion := calculateRouteFingerprint(expectedRoute)
expectedVersion := legacy_storage.CalculateRouteFingerprint(expectedRoute)
sut, store, prov := createNotificationPolicyServiceSut()
store.GetFn = func(ctx context.Context, orgID int64) (*legacy_storage.ConfigRevision, error) {
@@ -52,7 +53,7 @@ func TestGetPolicyTree(t *testing.T) {
assert.Len(t, prov.Calls, 1)
assert.Equal(t, "GetProvenance", prov.Calls[0].MethodName)
assert.IsType(t, &definitions.Route{}, prov.Calls[0].Arguments[1])
assert.Equal(t, (&definitions.Route{}).ResourceType(), prov.Calls[0].Arguments[1].(models.Provisionable).ResourceType())
assert.Equal(t, orgID, prov.Calls[0].Arguments[2])
}
@@ -60,7 +61,7 @@ func TestUpdatePolicyTree(t *testing.T) {
orgID := int64(1)
rev := getDefaultConfigRevision()
defaultVersion := calculateRouteFingerprint(*rev.Config.AlertmanagerConfig.Route)
defaultVersion := legacy_storage.CalculateRouteFingerprint(*rev.Config.AlertmanagerConfig.Route)
newRoute := definitions.Route{
Receiver: rev.Config.AlertmanagerConfig.Receivers[0].Name,
@@ -185,7 +186,7 @@ func TestUpdatePolicyTree(t *testing.T) {
assert.Len(t, prov.Calls, 1)
assert.Equal(t, "GetProvenance", prov.Calls[0].MethodName)
assert.IsType(t, &definitions.Route{}, prov.Calls[0].Arguments[1])
assert.Equal(t, (&definitions.Route{}).ResourceType(), prov.Calls[0].Arguments[1].(models.Provisionable).ResourceType())
assert.Equal(t, orgID, prov.Calls[0].Arguments[2].(int64))
})
@@ -273,7 +274,7 @@ func TestUpdatePolicyTree(t *testing.T) {
result, version, err := sut.UpdatePolicyTree(context.Background(), orgID, route, models.ProvenanceAPI, defaultVersion)
require.NoError(t, err)
assert.Equal(t, route, result)
assert.Equal(t, calculateRouteFingerprint(route), version)
assert.Equal(t, legacy_storage.CalculateRouteFingerprint(route), version)
})
t.Run("updates Route and sets provenance in transaction if route is valid and version matches", func(t *testing.T) {
@@ -289,7 +290,7 @@ func TestUpdatePolicyTree(t *testing.T) {
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.Equal(t, legacy_storage.CalculateRouteFingerprint(newRoute), version)
assert.Len(t, store.Calls, 2)
assert.Equal(t, "Save", store.Calls[1].Method)
@@ -298,12 +299,12 @@ func TestUpdatePolicyTree(t *testing.T) {
c := prov.Calls[0]
assert.Equal(t, "GetProvenance", c.MethodName)
assert.IsType(t, &definitions.Route{}, c.Arguments[1])
assert.Equal(t, (&definitions.Route{}).ResourceType(), c.Arguments[1].(models.Provisionable).ResourceType())
assert.Equal(t, orgID, c.Arguments[2].(int64))
c = prov.Calls[1]
assert.Equal(t, "SetProvenance", c.MethodName)
assertInTransaction(t, c.Arguments[0].(context.Context))
assert.IsType(t, &definitions.Route{}, c.Arguments[1])
assert.Equal(t, (&definitions.Route{}).ResourceType(), c.Arguments[1].(models.Provisionable).ResourceType())
assert.Equal(t, orgID, c.Arguments[2].(int64))
assert.Equal(t, models.ProvenanceAPI, c.Arguments[3].(models.Provenance))
})
@@ -321,7 +322,7 @@ func TestUpdatePolicyTree(t *testing.T) {
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.Equal(t, legacy_storage.CalculateRouteFingerprint(newRoute), version)
assert.Len(t, store.Calls, 2)
assert.Equal(t, "Save", store.Calls[1].Method)
@@ -332,7 +333,7 @@ func TestUpdatePolicyTree(t *testing.T) {
c := prov.Calls[1]
assert.Equal(t, "SetProvenance", c.MethodName)
assertInTransaction(t, c.Arguments[0].(context.Context))
assert.IsType(t, &definitions.Route{}, c.Arguments[1])
assert.Equal(t, (&definitions.Route{}).ResourceType(), c.Arguments[1].(models.Provisionable).ResourceType())
assert.Equal(t, orgID, c.Arguments[2].(int64))
assert.Equal(t, models.ProvenanceAPI, c.Arguments[3].(models.Provenance))
})
@@ -430,12 +431,12 @@ func TestResetPolicyTree(t *testing.T) {
assert.Len(t, prov.Calls, 2)
c := prov.Calls[0]
assert.Equal(t, "GetProvenance", c.MethodName)
assert.IsType(t, &definitions.Route{}, c.Arguments[1])
assert.Equal(t, (&definitions.Route{}).ResourceType(), c.Arguments[1].(models.Provisionable).ResourceType())
assert.Equal(t, orgID, c.Arguments[2].(int64))
c = prov.Calls[1]
assert.Equal(t, "DeleteProvenance", c.MethodName)
assertInTransaction(t, c.Arguments[0].(context.Context))
assert.IsType(t, &definitions.Route{}, c.Arguments[1])
assert.Equal(t, (&definitions.Route{}).ResourceType(), c.Arguments[1].(models.Provisionable).ResourceType())
assert.Equal(t, orgID, c.Arguments[2])
})
}
@@ -516,10 +517,10 @@ func TestRoute_Fingerprint(t *testing.T) {
t.Run("stable across code changes", func(t *testing.T) {
expectedFingerprint := "450c06a7f4a66675" // If this is a valid fingerprint generation change, update the expected value.
assert.Equal(t, expectedFingerprint, calculateRouteFingerprint(baseRouteGen()))
assert.Equal(t, expectedFingerprint, legacy_storage.CalculateRouteFingerprint(baseRouteGen()))
})
t.Run("unstable across field modification", func(t *testing.T) {
fingerprint := calculateRouteFingerprint(baseRouteGen())
fingerprint := legacy_storage.CalculateRouteFingerprint(baseRouteGen())
excludedFields := map[string]struct{}{
"Routes": {},
"Provenance": {},
@@ -548,7 +549,7 @@ func TestRoute_Fingerprint(t *testing.T) {
// Set the field to the value of the completelyDifferentRoute.
vf.Set(otherField)
f2 := calculateRouteFingerprint(cp)
f2 := legacy_storage.CalculateRouteFingerprint(cp)
assert.NotEqualf(t, fingerprint, f2, "Route field %s does not seem to be used in fingerprint", field)
}
})
@@ -573,6 +574,7 @@ func createNotificationPolicyServiceSut() (*NotificationPolicyService, *legacy_s
validator: func(from, to models.Provenance) error {
return nil
},
FeatureToggles: featuremgmt.WithFeatures(),
}, configStore, prov
}
@@ -2,6 +2,7 @@ package alerting
import (
"context"
"errors"
"fmt"
"github.com/grafana/grafana/pkg/infra/log"
@@ -31,7 +32,18 @@ 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,
if _, err := c.notificationPolicyService.GetManagedRoute(ctx, np.OrgID, np.Name); err != nil {
if errors.Is(err, provisioning.ErrRouteNotFound) {
_, err := c.notificationPolicyService.CreateManagedRoute(ctx, np.OrgID, np.Name,
np.Policy, models.ProvenanceFile)
if err != nil {
return fmt.Errorf("%s: %w", file.Filename, err)
}
continue
}
return err
}
_, err := c.notificationPolicyService.UpdateManagedRoute(ctx, np.OrgID, np.Name,
np.Policy, models.ProvenanceFile, "")
if err != nil {
return fmt.Errorf("%s: %w", file.Filename, err)
@@ -44,8 +56,8 @@ func (c *defaultNotificationPolicyProvisioner) Provision(ctx context.Context,
func (c *defaultNotificationPolicyProvisioner) Unprovision(ctx context.Context,
files []*AlertingFile) error {
for _, file := range files {
for _, orgID := range file.ResetPolicies {
_, err := c.notificationPolicyService.ResetPolicyTree(ctx, int64(orgID), models.ProvenanceFile)
for _, deletePolicy := range file.DeletePolicies {
err := c.notificationPolicyService.DeleteManagedRoute(ctx, deletePolicy.OrgID, deletePolicy.Name, models.ProvenanceFile, "")
if err != nil {
return fmt.Errorf("%s: %w", file.Filename, err)
}
@@ -2,15 +2,19 @@ package alerting
import (
"encoding/json"
"errors"
"strings"
"github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
"github.com/grafana/grafana/pkg/services/ngalert/notifier/legacy_storage"
"github.com/grafana/grafana/pkg/services/provisioning/values"
)
type NotificiationPolicyV1 struct {
OrgID values.Int64Value `json:"orgId" yaml:"orgId"`
// We use JSONValue here, as we want to have interpolation the values.
Policy values.JSONValue `json:"-" yaml:"-"`
Policy values.JSONValue `json:"-" yaml:"-"`
Name values.StringValue `json:"name" yaml:"name"`
}
func (v1 *NotificiationPolicyV1) UnmarshalYAML(unmarshal func(any) error) error {
@@ -30,6 +34,10 @@ func (v1 *NotificiationPolicyV1) mapToModel() (NotificiationPolicy, error) {
if orgID < 1 {
orgID = 1
}
name := v1.Name.Value()
if name == "" {
name = legacy_storage.UserDefinedRoutingTreeName
}
var route definitions.Route
// We need the string json representation, so we marshal the policy back
// as a string and interpolate it at the same time.
@@ -48,10 +56,37 @@ func (v1 *NotificiationPolicyV1) mapToModel() (NotificiationPolicy, error) {
return NotificiationPolicy{
OrgID: orgID,
Policy: route,
Name: strings.TrimSpace(name),
}, nil
}
type NotificiationPolicy struct {
OrgID int64
Name string
Policy definitions.Route
}
type DeleteNotificationPolicyV1 struct {
OrgID values.Int64Value `json:"orgId" yaml:"orgId"`
Name values.StringValue `json:"name" yaml:"name"`
}
func (v1 DeleteNotificationPolicyV1) mapToModel() (DeleteNotificationPolicy, error) {
name := strings.TrimSpace(v1.Name.Value())
if name == "" {
return DeleteNotificationPolicy{}, errors.New("delete policy missing name")
}
orgID := v1.OrgID.Value()
if orgID < 1 {
orgID = 1
}
return DeleteNotificationPolicy{
OrgID: orgID,
Name: name,
}, nil
}
type DeleteNotificationPolicy struct {
OrgID int64
Name string
}
@@ -5,6 +5,8 @@ import (
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"
"github.com/grafana/grafana/pkg/services/ngalert/notifier/legacy_storage"
)
func TestNotificationPolicy(t *testing.T) {
@@ -27,6 +29,33 @@ repeat_interval: ${NOTIFIER_EMAIL_REMINDER_FREQUENCY}
require.NoError(t, err)
require.Equal(t, int64(123), np.OrgID)
require.Equal(t, "test", np.Policy.Receiver)
require.Equal(t, legacy_storage.UserDefinedRoutingTreeName, np.Name)
require.True(t, np.Policy.Continue)
require.Equal(t, envValue, np.Policy.RepeatInterval.String())
}
func TestNotificationPolicyWithName(t *testing.T) {
const (
envKey = "NOTIFIER_EMAIL_REMINDER_FREQUENCY"
envValue = "4h"
)
t.Setenv(envKey, envValue)
data := `orgId: 123
receiver: test
continue: true
name: "test-policy"
repeat_interval: ${NOTIFIER_EMAIL_REMINDER_FREQUENCY}
`
var model NotificiationPolicyV1
err := yaml.Unmarshal([]byte(data), &model)
require.NoError(t, err)
np, err := model.mapToModel()
require.NoError(t, err)
require.Equal(t, int64(123), np.OrgID)
require.Equal(t, "test", np.Policy.Receiver)
require.Equal(t, "test-policy", np.Name)
require.True(t, np.Policy.Continue)
require.Equal(t, envValue, np.Policy.RepeatInterval.String())
}
+25 -12
View File
@@ -4,6 +4,7 @@ import (
"fmt"
"github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/ngalert/notifier/legacy_storage"
"github.com/grafana/grafana/pkg/services/provisioning/values"
)
@@ -21,7 +22,7 @@ type AlertingFile struct {
ContactPoints []ContactPoint
DeleteContactPoints []DeleteContactPoint
Policies []NotificiationPolicy
ResetPolicies []OrgID
DeletePolicies []DeleteNotificationPolicy
MuteTimes []MuteTime
DeleteMuteTimes []DeleteMuteTime
Templates []Template
@@ -31,16 +32,17 @@ type AlertingFile struct {
type AlertingFileV1 struct {
configVersion
Filename string
Groups []AlertRuleGroupV1 `json:"groups" yaml:"groups"`
DeleteRules []RuleDeleteV1 `json:"deleteRules" yaml:"deleteRules"`
ContactPoints []ContactPointV1 `json:"contactPoints" yaml:"contactPoints"`
DeleteContactPoints []DeleteContactPointV1 `json:"deleteContactPoints" yaml:"deleteContactPoints"`
Policies []NotificiationPolicyV1 `json:"policies" yaml:"policies"`
ResetPolicies []values.Int64Value `json:"resetPolicies" yaml:"resetPolicies"`
MuteTimes []MuteTimeV1 `json:"muteTimes" yaml:"muteTimes"`
DeleteMuteTimes []DeleteMuteTimeV1 `json:"deleteMuteTimes" yaml:"deleteMuteTimes"`
Templates []TemplateV1 `json:"templates" yaml:"templates"`
DeleteTemplates []DeleteTemplateV1 `json:"deleteTemplates" yaml:"deleteTemplates"`
Groups []AlertRuleGroupV1 `json:"groups" yaml:"groups"`
DeleteRules []RuleDeleteV1 `json:"deleteRules" yaml:"deleteRules"`
ContactPoints []ContactPointV1 `json:"contactPoints" yaml:"contactPoints"`
DeleteContactPoints []DeleteContactPointV1 `json:"deleteContactPoints" yaml:"deleteContactPoints"`
Policies []NotificiationPolicyV1 `json:"policies" yaml:"policies"`
ResetPolicies []values.Int64Value `json:"resetPolicies" yaml:"resetPolicies"` // Legacy field, use DeletePolicies instead.
DeletePolicies []DeleteNotificationPolicyV1 `json:"deletePolicies" yaml:"deletePolicies"`
MuteTimes []MuteTimeV1 `json:"muteTimes" yaml:"muteTimes"`
DeleteMuteTimes []DeleteMuteTimeV1 `json:"deleteMuteTimes" yaml:"deleteMuteTimes"`
Templates []TemplateV1 `json:"templates" yaml:"templates"`
DeleteTemplates []DeleteTemplateV1 `json:"deleteTemplates" yaml:"deleteTemplates"`
}
func (fileV1 *AlertingFileV1) MapToModel() (AlertingFile, error) {
@@ -101,7 +103,18 @@ func (fileV1 *AlertingFileV1) mapPolicies(alertingFile *AlertingFile) error {
alertingFile.Policies = append(alertingFile.Policies, np)
}
for _, orgIDV1 := range fileV1.ResetPolicies {
alertingFile.ResetPolicies = append(alertingFile.ResetPolicies, OrgID(orgIDV1.Value()))
alertingFile.DeletePolicies = append(alertingFile.DeletePolicies, DeleteNotificationPolicy{
OrgID: orgIDV1.Value(),
Name: legacy_storage.UserDefinedRoutingTreeName,
})
}
for _, deleteV1 := range fileV1.DeletePolicies {
delReq, err := deleteV1.mapToModel()
if err != nil {
return err
}
alertingFile.DeletePolicies = append(alertingFile.DeletePolicies, delReq)
}
return nil
}
+1 -1
View File
@@ -336,7 +336,7 @@ func (ps *ProvisioningServiceImpl) ProvisionAlerting(ctx context.Context) error
contactPointService := provisioning.NewContactPointService(configStore, ps.secretService,
ps.alertingStore, ps.SQLStore, receiverSvc, ps.log, ps.alertingStore, ps.resourcePermissions)
notificationPolicyService := provisioning.NewNotificationPolicyService(configStore,
ps.alertingStore, ps.SQLStore, ps.Cfg.UnifiedAlerting, ps.log)
ps.alertingStore, ps.SQLStore, ps.Cfg.UnifiedAlerting, ps.alertingStore.FeatureToggles, ps.log)
mutetimingsService := provisioning.NewMuteTimingService(configStore, ps.alertingStore, ps.alertingStore, ps.log, ps.alertingStore)
templateService := provisioning.NewTemplateService(configStore, ps.alertingStore, ps.alertingStore, ps.log)
cfg := prov_alerting.ProvisionerConfig{
@@ -23,7 +23,6 @@ import (
"k8s.io/apimachinery/pkg/types"
"github.com/grafana/alerting/notify"
"github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1"
common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
"github.com/grafana/grafana/pkg/bus"
@@ -38,6 +37,7 @@ import (
"github.com/grafana/grafana/pkg/services/ngalert/api"
"github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/ngalert/notifier/legacy_storage"
"github.com/grafana/grafana/pkg/services/ngalert/store"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/tests/api/alerting"
@@ -809,7 +809,9 @@ func TestIntegrationInUseMetadata(t *testing.T) {
// Removing the new extra route should leave only 1.
amConfig.AlertmanagerConfig.Route.Routes = amConfig.AlertmanagerConfig.Route.Routes[:1]
v1Route, err := routingtree.ConvertToK8sResource(helper.Org1.AdminServiceAccount.OrgId, *amConfig.AlertmanagerConfig.Route, "", func(int64) string { return "default" })
managedRoute := legacy_storage.NewManagedRoute(v0alpha1.UserDefinedRoutingTreeName, amConfig.AlertmanagerConfig.Route)
managedRoute.Version = "" // Avoid version conflict.
v1Route, err := routingtree.ConvertToK8sResource(helper.Org1.AdminServiceAccount.OrgId, managedRoute, func(int64) string { return "default" })
require.NoError(t, err)
routeAdminClient := test_common.NewRoutingTreeClient(t, helper.Org1.Admin)
_, err = routeAdminClient.Update(ctx, v1Route, v1.UpdateOptions{})
@@ -831,7 +833,9 @@ func TestIntegrationInUseMetadata(t *testing.T) {
// Remove the remaining routes.
amConfig.AlertmanagerConfig.Route.Routes = nil
v1route, err := routingtree.ConvertToK8sResource(1, *amConfig.AlertmanagerConfig.Route, "", func(int64) string { return "default" })
managedRoute = legacy_storage.NewManagedRoute(v0alpha1.UserDefinedRoutingTreeName, amConfig.AlertmanagerConfig.Route)
managedRoute.Version = "" // Avoid version conflict.
v1route, err := routingtree.ConvertToK8sResource(1, managedRoute, func(int64) string { return "default" })
require.NoError(t, err)
_, err = routeAdminClient.Update(ctx, v1route, v1.UpdateOptions{})
require.NoError(t, err)
@@ -1139,9 +1143,15 @@ func TestIntegrationReferentialIntegrity(t *testing.T) {
}
updatedRoute := legacyCli.GetRoute(t)
for _, route := range updatedRoute.Routes {
assert.Equalf(t, expectedTitle, route.Receiver, "time receiver in routes should have been renamed but it did not")
}
// Sanity check to make sure at least some titles updated.
assert.Error(t, updatedRoute.ValidateReceivers(map[string]struct{}{
"grafana-default-email": {},
}))
// Make sure all references are either to the default receiver or the renamed one.
assert.NoError(t, updatedRoute.ValidateReceivers(map[string]struct{}{
"grafana-default-email": {},
expectedTitle: {},
}))
actual, err = adminClient.Get(ctx, actual.Name, v1.GetOptions{})
require.NoError(t, err)
@@ -1498,7 +1508,9 @@ func persistInitialConfig(t *testing.T, amConfig definitions.PostableUserConfig)
nsMapper := func(_ int64) string { return "default" }
routeClient := test_common.NewRoutingTreeClient(t, helper.Org1.Admin)
v1route, err := routingtree.ConvertToK8sResource(helper.Org1.AdminServiceAccount.OrgId, *amConfig.AlertmanagerConfig.Route, "", nsMapper)
managedRoute := legacy_storage.NewManagedRoute(v0alpha1.UserDefinedRoutingTreeName, amConfig.AlertmanagerConfig.Route)
managedRoute.Version = "" // Avoid version conflict.
v1route, err := routingtree.ConvertToK8sResource(helper.Org1.AdminServiceAccount.OrgId, managedRoute, nsMapper)
require.NoError(t, err)
_, err = routeClient.Update(ctx, v1route, v1.UpdateOptions{})
require.NoError(t, err)
@@ -18,6 +18,7 @@ import (
"github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1"
"github.com/grafana/grafana/pkg/registry/apps/alerting/notifications/routingtree"
"github.com/grafana/grafana/pkg/services/ngalert/notifier/legacy_storage"
"github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/fakes"
"github.com/grafana/grafana/pkg/bus"
@@ -48,6 +49,8 @@ func getTestHelper(t *testing.T) *apis.K8sTestHelper {
}
func TestIntegrationNotAllowedMethods(t *testing.T) {
// TODO: Add more tests.
t.Skip("No longer applies, need real tests.")
testutil.SkipIntegrationTestInShortMode(t)
ctx := context.Background()
@@ -387,7 +390,9 @@ func TestIntegrationDataConsistency(t *testing.T) {
createRoute := func(t *testing.T, route definitions.Route) {
t.Helper()
routeClient := common.NewRoutingTreeClient(t, helper.Org1.Admin)
v1Route, err := routingtree.ConvertToK8sResource(helper.Org1.Admin.Identity.GetOrgID(), route, "", func(int64) string { return "default" })
managedRoute := legacy_storage.NewManagedRoute(v0alpha1.UserDefinedRoutingTreeName, &route)
managedRoute.Version = "" // Avoid version conflict.
v1Route, err := routingtree.ConvertToK8sResource(helper.Org1.Admin.Identity.GetOrgID(), managedRoute, func(int64) string { return "default" })
require.NoError(t, err)
_, err = routeClient.Update(ctx, v1Route, v1.UpdateOptions{})
require.NoError(t, err)
@@ -30,6 +30,7 @@ import (
"github.com/grafana/grafana/pkg/services/folder/foldertest"
"github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/ngalert/notifier/legacy_storage"
"github.com/grafana/grafana/pkg/services/ngalert/store"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/tests/api/alerting"
@@ -656,7 +657,9 @@ func TestIntegrationTimeIntervalReferentialIntegrity(t *testing.T) {
}
routeClient := common.NewRoutingTreeClient(t, helper.Org1.Admin)
v1route, err := routingtree.ConvertToK8sResource(helper.Org1.Admin.Identity.GetOrgID(), *amConfig.AlertmanagerConfig.Route, "", func(int64) string { return "default" })
route := legacy_storage.NewManagedRoute(v0alpha1.UserDefinedRoutingTreeName, amConfig.AlertmanagerConfig.Route)
route.Version = "" // Avoid version conflict.
v1route, err := routingtree.ConvertToK8sResource(helper.Org1.Admin.Identity.GetOrgID(), route, func(int64) string { return "default" })
require.NoError(t, err)
_, err = routeClient.Update(ctx, v1route, v1.UpdateOptions{})
require.NoError(t, err)