mirror of https://github.com/grafana/grafana.git
286 lines
9.0 KiB
Go
286 lines
9.0 KiB
Go
package folders
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/prometheus/client_golang/prometheus"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
|
"k8s.io/apiserver/pkg/admission"
|
|
"k8s.io/apiserver/pkg/authorization/authorizer"
|
|
"k8s.io/apiserver/pkg/registry/rest"
|
|
genericapiserver "k8s.io/apiserver/pkg/server"
|
|
"k8s.io/kube-openapi/pkg/common"
|
|
"k8s.io/kube-openapi/pkg/spec3"
|
|
|
|
authlib "github.com/grafana/authlib/types"
|
|
|
|
folders "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1"
|
|
"github.com/grafana/grafana/apps/iam/pkg/reconcilers"
|
|
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
|
grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic"
|
|
grafanarest "github.com/grafana/grafana/pkg/apiserver/rest"
|
|
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
|
"github.com/grafana/grafana/pkg/services/apiserver/builder"
|
|
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
|
|
"github.com/grafana/grafana/pkg/services/authz/zanzana"
|
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
|
"github.com/grafana/grafana/pkg/services/folder"
|
|
"github.com/grafana/grafana/pkg/setting"
|
|
"github.com/grafana/grafana/pkg/storage/unified/apistore"
|
|
"github.com/grafana/grafana/pkg/storage/unified/resource"
|
|
"github.com/grafana/grafana/pkg/storage/unified/resourcepb"
|
|
)
|
|
|
|
var _ builder.APIGroupBuilder = (*FolderAPIBuilder)(nil)
|
|
var _ builder.APIGroupValidation = (*FolderAPIBuilder)(nil)
|
|
|
|
var resourceInfo = folders.FolderResourceInfo
|
|
|
|
var errNoUser = errors.New("valid user is required")
|
|
var errNoResource = errors.New("resource name is required")
|
|
|
|
// This is used just so wire has something unique to return
|
|
type FolderAPIBuilder struct {
|
|
features featuremgmt.FeatureToggles
|
|
namespacer request.NamespaceMapper
|
|
folderSvc folder.LegacyService
|
|
folderPermissionsSvc accesscontrol.FolderPermissionsService
|
|
acService accesscontrol.Service
|
|
ac accesscontrol.AccessControl
|
|
storage grafanarest.Storage
|
|
permissionStore reconcilers.PermissionStore
|
|
|
|
authorizer authorizer.Authorizer
|
|
parents parentsGetter
|
|
|
|
searcher resourcepb.ResourceIndexClient
|
|
permissionsOnCreate bool
|
|
ignoreLegacy bool // skip legacy storage and only use unified storage
|
|
}
|
|
|
|
func RegisterAPIService(cfg *setting.Cfg,
|
|
features featuremgmt.FeatureToggles,
|
|
apiregistration builder.APIRegistrar,
|
|
folderSvc folder.LegacyService,
|
|
folderPermissionsSvc accesscontrol.FolderPermissionsService,
|
|
accessControl accesscontrol.AccessControl,
|
|
acService accesscontrol.Service,
|
|
accessClient authlib.AccessClient,
|
|
registerer prometheus.Registerer,
|
|
unified resource.ResourceClient,
|
|
zanzanaClient zanzana.Client,
|
|
) *FolderAPIBuilder {
|
|
builder := &FolderAPIBuilder{
|
|
features: features,
|
|
namespacer: request.GetNamespaceMapper(cfg),
|
|
folderSvc: folderSvc,
|
|
folderPermissionsSvc: folderPermissionsSvc,
|
|
acService: acService,
|
|
ac: accessControl,
|
|
permissionsOnCreate: cfg.RBAC.PermissionsOnCreation("folder"),
|
|
authorizer: newLegacyAuthorizer(accessControl),
|
|
searcher: unified,
|
|
permissionStore: reconcilers.NewZanzanaPermissionStore(zanzanaClient),
|
|
}
|
|
apiregistration.RegisterAPI(builder)
|
|
return builder
|
|
}
|
|
|
|
func NewAPIService(ac authlib.AccessClient, searcher resource.ResourceClient) *FolderAPIBuilder {
|
|
return &FolderAPIBuilder{
|
|
authorizer: newMultiTenantAuthorizer(ac),
|
|
searcher: searcher,
|
|
ignoreLegacy: true,
|
|
}
|
|
}
|
|
func (b *FolderAPIBuilder) GetGroupVersion() schema.GroupVersion {
|
|
return resourceInfo.GroupVersion()
|
|
}
|
|
|
|
func addKnownTypes(scheme *runtime.Scheme, gv schema.GroupVersion) {
|
|
scheme.AddKnownTypes(gv,
|
|
&folders.Folder{},
|
|
&folders.FolderList{},
|
|
&folders.FolderInfoList{},
|
|
&folders.DescendantCounts{},
|
|
&folders.FolderAccessInfo{},
|
|
)
|
|
}
|
|
|
|
func (b *FolderAPIBuilder) InstallSchema(scheme *runtime.Scheme) error {
|
|
gv := b.GetGroupVersion()
|
|
addKnownTypes(scheme, gv)
|
|
|
|
// Link this version to the internal representation.
|
|
// This is used for server-side-apply (PATCH), and avoids the error:
|
|
// "no kind is registered for the type"
|
|
addKnownTypes(scheme, schema.GroupVersion{
|
|
Group: gv.Group,
|
|
Version: runtime.APIVersionInternal,
|
|
})
|
|
|
|
// If multiple versions exist, then register conversions from zz_generated.conversion.go
|
|
// if err := playlist.RegisterConversions(scheme); err != nil {
|
|
// return err
|
|
// }
|
|
metav1.AddToGroupVersion(scheme, gv)
|
|
return scheme.SetVersionPriority(gv)
|
|
}
|
|
|
|
func (b *FolderAPIBuilder) AllowedV0Alpha1Resources() []string {
|
|
return nil
|
|
}
|
|
|
|
func (b *FolderAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIGroupInfo, opts builder.APIGroupOptions) error {
|
|
scheme := opts.Scheme
|
|
optsGetter := opts.OptsGetter
|
|
dualWriteBuilder := opts.DualWriteBuilder
|
|
storage := map[string]rest.Storage{}
|
|
|
|
if b.ignoreLegacy {
|
|
opts.StorageOptsRegister(resourceInfo.GroupResource(), apistore.StorageOptions{
|
|
EnableFolderSupport: true,
|
|
RequireDeprecatedInternalID: true})
|
|
|
|
store, err := grafanaregistry.NewRegistryStore(opts.Scheme, resourceInfo, opts.OptsGetter)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
storage[resourceInfo.StoragePath()] = store
|
|
apiGroupInfo.VersionedResourcesStorageMap[folders.VERSION] = storage
|
|
b.storage = storage[resourceInfo.StoragePath()].(grafanarest.Storage)
|
|
b.parents = newParentsGetter(store, folder.MaxNestedFolderDepth)
|
|
return nil
|
|
}
|
|
|
|
legacyStore := &legacyStorage{
|
|
service: b.folderSvc,
|
|
namespacer: b.namespacer,
|
|
tableConverter: resourceInfo.TableConverter(),
|
|
}
|
|
|
|
opts.StorageOptsRegister(resourceInfo.GroupResource(), apistore.StorageOptions{
|
|
EnableFolderSupport: true,
|
|
RequireDeprecatedInternalID: true})
|
|
|
|
folderStore := &folderStorage{
|
|
tableConverter: resourceInfo.TableConverter(),
|
|
folderPermissionsSvc: b.folderPermissionsSvc,
|
|
acService: b.acService,
|
|
permissionsOnCreate: b.permissionsOnCreate,
|
|
}
|
|
|
|
if optsGetter != nil && dualWriteBuilder != nil {
|
|
store, err := grafanaregistry.NewRegistryStore(scheme, resourceInfo, optsGetter)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if b.features.IsEnabledGlobally(featuremgmt.FlagZanzana) {
|
|
store.BeginCreate = b.beginCreate
|
|
store.BeginUpdate = b.beginUpdate
|
|
}
|
|
|
|
dw, err := dualWriteBuilder(resourceInfo.GroupResource(), legacyStore, store)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
folderStore.store = dw
|
|
}
|
|
storage[resourceInfo.StoragePath()] = folderStore
|
|
|
|
b.parents = newParentsGetter(folderStore, folder.MaxNestedFolderDepth) // used for validation
|
|
storage[resourceInfo.StoragePath("parents")] = &subParentsREST{
|
|
getter: folderStore,
|
|
parents: b.parents,
|
|
}
|
|
storage[resourceInfo.StoragePath("counts")] = &subCountREST{searcher: b.searcher}
|
|
storage[resourceInfo.StoragePath("access")] = &subAccessREST{folderStore, b.ac}
|
|
|
|
// Adds a path to return children of a given folder
|
|
storage[resourceInfo.StoragePath("children")] = &subChildrenREST{
|
|
lister: storage[resourceInfo.StoragePath()].(rest.Lister),
|
|
}
|
|
|
|
apiGroupInfo.VersionedResourcesStorageMap[folders.VERSION] = storage
|
|
b.storage = storage[resourceInfo.StoragePath()].(grafanarest.Storage)
|
|
return nil
|
|
}
|
|
|
|
func (b *FolderAPIBuilder) GetOpenAPIDefinitions() common.GetOpenAPIDefinitions {
|
|
return folders.GetOpenAPIDefinitions
|
|
}
|
|
|
|
func (b *FolderAPIBuilder) PostProcessOpenAPI(oas *spec3.OpenAPI) (*spec3.OpenAPI, error) {
|
|
oas.Info.Description = "Grafana folders"
|
|
return oas, nil
|
|
}
|
|
|
|
type authorizerParams struct {
|
|
user identity.Requester
|
|
evaluator accesscontrol.Evaluator
|
|
}
|
|
|
|
func (b *FolderAPIBuilder) GetAuthorizer() authorizer.Authorizer {
|
|
return b.authorizer
|
|
}
|
|
|
|
func (b *FolderAPIBuilder) Mutate(ctx context.Context, a admission.Attributes, _ admission.ObjectInterfaces) error {
|
|
verb := a.GetOperation()
|
|
if verb == admission.Create || verb == admission.Update {
|
|
obj := a.GetObject()
|
|
f, ok := obj.(*folders.Folder)
|
|
if !ok {
|
|
return fmt.Errorf("obj is not folders.Folder")
|
|
}
|
|
f.Spec.Title = strings.Trim(f.Spec.Title, " ")
|
|
return nil
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (b *FolderAPIBuilder) Validate(ctx context.Context, a admission.Attributes, _ admission.ObjectInterfaces) error {
|
|
var obj runtime.Object
|
|
verb := a.GetOperation()
|
|
|
|
switch verb {
|
|
case admission.Create, admission.Update:
|
|
obj = a.GetObject()
|
|
case admission.Delete:
|
|
obj = a.GetOldObject()
|
|
if obj == nil {
|
|
return fmt.Errorf("old object is nil for delete request")
|
|
}
|
|
case admission.Connect:
|
|
return nil
|
|
default:
|
|
obj = a.GetObject()
|
|
}
|
|
|
|
f, ok := obj.(*folders.Folder)
|
|
if !ok {
|
|
return fmt.Errorf("obj is not folders.Folder")
|
|
}
|
|
|
|
switch a.GetOperation() {
|
|
case admission.Create:
|
|
return validateOnCreate(ctx, f, b.parents, folder.MaxNestedFolderDepth)
|
|
case admission.Delete:
|
|
return validateOnDelete(ctx, f, b.searcher)
|
|
case admission.Update:
|
|
old, ok := a.GetOldObject().(*folders.Folder)
|
|
if !ok {
|
|
return fmt.Errorf("obj is not folders.Folder")
|
|
}
|
|
return validateOnUpdate(ctx, f, old, b.storage, b.parents, folder.MaxNestedFolderDepth)
|
|
default:
|
|
return nil
|
|
}
|
|
}
|