LibraryPanels: Support CRUD via apiserver (#113035)

This commit is contained in:
Ryan McKinley
2025-10-31 08:14:38 +03:00
committed by GitHub
parent cf242def3a
commit 53aed34652
22 changed files with 617 additions and 159 deletions
+64 -8
View File
@@ -5,16 +5,17 @@ import (
"fmt"
"strconv"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/apis/meta/internalversion"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/registry/rest"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/apimachinery/utils"
"github.com/grafana/grafana/pkg/registry/apis/dashboard/legacy"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"github.com/grafana/grafana/pkg/services/libraryelements"
)
var (
@@ -29,9 +30,9 @@ var (
)
type LibraryPanelStore struct {
Access legacy.DashboardAccess
ResourceInfo utils.ResourceInfo
AccessControl accesscontrol.AccessControl
Access legacy.DashboardAccess
ResourceInfo utils.ResourceInfo
service libraryelements.Service
}
func (s *LibraryPanelStore) New() runtime.Object {
@@ -57,15 +58,70 @@ func (s *LibraryPanelStore) ConvertToTable(ctx context.Context, object runtime.O
}
func (s *LibraryPanelStore) Create(ctx context.Context, obj runtime.Object, createValidation rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) {
return nil, fmt.Errorf("method not yet implemented")
user, err := identity.GetRequester(ctx)
if err != nil {
return nil, err
}
cmd, err := libraryelements.ToCreateLibraryElementCommand(obj)
if err != nil {
return nil, err
}
// NOTE: this includes all access control checks
out, err := s.service.CreateElement(ctx, user, *cmd)
if err != nil {
return nil, err
}
if out.UID == "" {
return nil, fmt.Errorf("created library panel has empty UID")
}
return s.Get(ctx, out.UID, &metav1.GetOptions{})
}
func (s *LibraryPanelStore) Update(ctx context.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc, forceAllowCreate bool, options *metav1.UpdateOptions) (runtime.Object, bool, error) {
return nil, false, fmt.Errorf("method not yet implemented")
user, err := identity.GetRequester(ctx)
if err != nil {
return nil, false, err
}
old, err := s.Get(ctx, name, &metav1.GetOptions{})
if err != nil {
return nil, false, err
}
// NOTE: this includes all access control checks
obj, err := objInfo.UpdatedObject(ctx, old)
if err != nil {
return nil, false, err
}
cmd, err := libraryelements.ToPatchLibraryElementCommand(obj)
if err != nil {
return nil, false, err
}
out, err := s.service.PatchLibraryElement(ctx, user, *cmd, name)
if err != nil {
return nil, false, err
}
if out.UID == "" {
return nil, false, fmt.Errorf("created library panel has empty UID")
}
obj, err = s.Get(ctx, out.UID, &metav1.GetOptions{})
return obj, false, err
}
func (s *LibraryPanelStore) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) {
return nil, false, fmt.Errorf("method not yet implemented")
user, err := identity.GetRequester(ctx)
if err != nil {
return nil, false, err
}
// NOTE: this includes all access control checks
_, err = s.service.DeleteLibraryElement(ctx, user, name)
if err != nil {
return nil, false, err
}
return nil, true, nil
}
func (s *LibraryPanelStore) DeleteCollection(ctx context.Context, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions, listOptions *internalversion.ListOptions) (runtime.Object, error) {
+13
View File
@@ -26,6 +26,19 @@ func (b *DashboardsAPIBuilder) Mutate(ctx context.Context, a admission.Attribute
if op != admission.Create && op != admission.Update {
return nil
}
switch a.GetResource().Resource {
case dashboardV0.DASHBOARD_RESOURCE:
return b.mutateDashboard(ctx, a)
case dashboardV0.LIBRARY_PANEL_RESOURCE:
return nil // nothing needed
}
return fmt.Errorf("unexpected resource: %+v", a.GetResource())
}
func (b *DashboardsAPIBuilder) mutateDashboard(ctx context.Context, a admission.Attributes) (err error) {
var internalID int64
obj := a.GetObject()
meta, err := utils.MetaAccessor(obj)
+2 -3
View File
@@ -7,7 +7,6 @@ import (
"github.com/stretchr/testify/require"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/admission"
dashv0 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1"
@@ -180,10 +179,10 @@ func TestDashboardAPIBuilder_Mutate(t *testing.T) {
err := b.Mutate(context.Background(), admission.NewAttributesRecord(
tt.inputObj,
nil,
schema.GroupVersionKind{},
dashv1.DashboardResourceInfo.GroupVersionKind(),
"",
"test",
schema.GroupVersionResource{},
dashv1.DashboardResourceInfo.GroupVersionResource(),
"",
tt.operation,
operationOptions,
+33 -17
View File
@@ -50,6 +50,7 @@ import (
dashsvc "github.com/grafana/grafana/pkg/services/dashboards/service"
"github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/libraryelements"
"github.com/grafana/grafana/pkg/services/librarypanels"
"github.com/grafana/grafana/pkg/services/provisioning"
"github.com/grafana/grafana/pkg/services/quota"
@@ -68,6 +69,8 @@ var (
_ builder.APIGroupVersionsProvider = (*DashboardsAPIBuilder)(nil)
_ builder.OpenAPIPostProcessor = (*DashboardsAPIBuilder)(nil)
_ builder.APIGroupRouteProvider = (*DashboardsAPIBuilder)(nil)
_ builder.APIGroupMutation = (*DashboardsAPIBuilder)(nil)
_ builder.APIGroupValidation = (*DashboardsAPIBuilder)(nil)
)
const (
@@ -108,6 +111,7 @@ type DashboardsAPIBuilder struct {
minRefreshInterval string
dualWriter dualwrite.Service
folderClientProvider client.K8sHandlerProvider
libraryPanels libraryelements.Service // for legacy library panels
isStandalone bool // skips any handling including anything to do with legacy storage
}
@@ -135,6 +139,7 @@ func RegisterAPIService(
libraryPanelSvc librarypanels.Service,
restConfigProvider apiserver.RestConfigProvider,
userService user.Service,
libraryPanels libraryelements.Service,
) *DashboardsAPIBuilder {
dbp := legacysql.NewDatabaseProvider(sql)
namespacer := request.GetNamespaceMapper(cfg)
@@ -157,6 +162,7 @@ func RegisterAPIService(
minRefreshInterval: cfg.MinRefreshInterval,
dualWriter: dual,
folderClientProvider: newSimpleFolderClientProvider(folderClient),
libraryPanels: libraryPanels,
legacy: &DashboardStorage{
Access: legacy.NewDashboardAccess(dbp, namespacer, dashStore, provisioning, libraryPanelSvc, sorter, dashboardPermissionsSvc, accessControl, features),
@@ -229,26 +235,35 @@ func (b *DashboardsAPIBuilder) InstallSchema(scheme *runtime.Scheme) error {
}
func (b *DashboardsAPIBuilder) AllowedV0Alpha1Resources() []string {
return []string{dashv0.DashboardKind().Plural()}
return []string{
dashv0.DashboardKind().Plural(),
dashv0.LIBRARY_PANEL_RESOURCE,
}
}
// Validate validates dashboard operations for the apiserver
func (b *DashboardsAPIBuilder) Validate(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces) (err error) {
op := a.GetOperation()
// Handle different operations
switch op {
case admission.Delete:
return b.validateDelete(ctx, a)
case admission.Create:
return b.validateCreate(ctx, a, o)
case admission.Update:
return b.validateUpdate(ctx, a, o)
case admission.Connect:
return nil
switch a.GetResource().Resource {
case dashv0.DASHBOARD_RESOURCE:
// Handle different operations
switch op {
case admission.Delete:
return b.validateDelete(ctx, a)
case admission.Create:
return b.validateCreate(ctx, a, o)
case admission.Update:
return b.validateUpdate(ctx, a, o)
case admission.Connect:
return nil
}
case dashv0.LIBRARY_PANEL_RESOURCE:
return nil // OK for now
}
return nil
return fmt.Errorf("unsupported validation: %+v", a.GetResource())
}
// validateDelete checks if a dashboard can be deleted
@@ -642,12 +657,13 @@ func (b *DashboardsAPIBuilder) storageForVersion(
return err
}
// Expose read only library panels
if libraryPanels != nil {
// Expose read library panels
//nolint:staticcheck // not yet migrated to OpenFeature
if libraryPanels != nil && b.features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) {
legacyLibraryStore := &LibraryPanelStore{
Access: b.legacy.Access,
ResourceInfo: *libraryPanels,
AccessControl: b.accessControl,
Access: b.legacy.Access,
ResourceInfo: *libraryPanels,
service: b.libraryPanels,
}
unifiedLibraryStore, err := grafanaregistry.NewRegistryStore(opts.Scheme, *libraryPanels, opts.OptsGetter)