Backend
This commit is contained in:
@@ -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
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
@@ -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{}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user