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
},
},
)