LibraryPanels: Support CRUD via apiserver (#113035)
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user