From 0c120db84d6a6019e2eb9ce3456225afd7830c6e Mon Sep 17 00:00:00 2001 From: Mariell Hoversholm Date: Thu, 30 Jan 2025 08:11:50 +0100 Subject: [PATCH] Provisioning: Setup server and storage (#99757) feat: setup server and storage This simply sets up the API server and its storage for `provisioning.grafana.app`. Features will be added eventually. --- pkg/apiserver/registry/generic/storage.go | 6 + pkg/apiserver/registry/generic/store.go | 5 +- pkg/registry/apis/provisioning/register.go | 160 +++++++++++++++++++++ pkg/registry/apis/wireset.go | 2 + 4 files changed, 172 insertions(+), 1 deletion(-) create mode 100644 pkg/registry/apis/provisioning/register.go diff --git a/pkg/apiserver/registry/generic/storage.go b/pkg/apiserver/registry/generic/storage.go index 945d51fbd58..67b0efecb8d 100644 --- a/pkg/apiserver/registry/generic/storage.go +++ b/pkg/apiserver/registry/generic/storage.go @@ -29,3 +29,9 @@ func NewRegistryStore(scheme *runtime.Scheme, resourceInfo utils.ResourceInfo, o } return store, nil } + +func NewRegistryStatusStore(scheme *runtime.Scheme, specStore *registry.Store) *StatusREST { + gv := specStore.New().GetObjectKind().GroupVersionKind().GroupVersion() + strategy := NewStatusStrategy(scheme, gv) + return NewStatusREST(specStore, strategy) +} diff --git a/pkg/apiserver/registry/generic/store.go b/pkg/apiserver/registry/generic/store.go index 3cc50bd5445..d8eb09d8cf7 100644 --- a/pkg/apiserver/registry/generic/store.go +++ b/pkg/apiserver/registry/generic/store.go @@ -26,7 +26,10 @@ type StatusREST struct { store *genericregistry.Store } -var _ = rest.Patcher(&StatusREST{}) +var ( + _ rest.Patcher = (*StatusREST)(nil) + _ rest.Storage = (*StatusREST)(nil) +) // New creates a new DataPlaneService object. func (r *StatusREST) New() runtime.Object { diff --git a/pkg/registry/apis/provisioning/register.go b/pkg/registry/apis/provisioning/register.go new file mode 100644 index 00000000000..3171f55c66a --- /dev/null +++ b/pkg/registry/apis/provisioning/register.go @@ -0,0 +1,160 @@ +package provisioning + +import ( + "context" + "fmt" + + provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" + grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic" + "github.com/grafana/grafana/pkg/services/apiserver/builder" + "github.com/grafana/grafana/pkg/services/featuremgmt" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/validation/field" + "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" +) + +var ( + _ builder.APIGroupBuilder = (*APIBuilder)(nil) +) + +type APIBuilder struct{} + +// NewAPIBuilder creates an API builder. +// It avoids anything that is core to Grafana, such that it can be used in a multi-tenant service down the line. +// This means there are no hidden dependencies, and no use of e.g. *settings.Cfg. +func NewAPIBuilder() *APIBuilder { + return &APIBuilder{} +} + +// RegisterAPIService returns an API builder, from [NewAPIBuilder]. It is called by Wire. +// This function happily uses services core to Grafana, and does not need to be multi-tenancy-compatible. +func RegisterAPIService( + features featuremgmt.FeatureToggles, + apiregistration builder.APIRegistrar, +) (*APIBuilder, error) { + if !features.IsEnabledGlobally(featuremgmt.FlagProvisioning) && + !features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) { + return nil, nil // skip registration unless opting into experimental apis OR the feature specifically + } + + builder := NewAPIBuilder() + apiregistration.RegisterAPI(builder) + return builder, nil +} + +func (b *APIBuilder) GetAuthorizer() authorizer.Authorizer { + return authorizer.AuthorizerFunc( + func(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) { + // TODO: Implement a webhook authoriser somehow. + + // fallback to the standard authorizer + return authorizer.DecisionNoOpinion, "", nil + }) +} + +func (b *APIBuilder) GetGroupVersion() schema.GroupVersion { + return provisioning.SchemeGroupVersion +} + +func (b *APIBuilder) InstallSchema(scheme *runtime.Scheme) error { + err := provisioning.AddToScheme(scheme) + if err != nil { + return err + } + + // This is required for --server-side apply + err = provisioning.AddKnownTypes(provisioning.InternalGroupVersion, scheme) + if err != nil { + return err + } + + metav1.AddToGroupVersion(scheme, provisioning.SchemeGroupVersion) + // Only 1 version (for now?) + return scheme.SetVersionPriority(provisioning.SchemeGroupVersion) +} + +func (b *APIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIGroupInfo, opts builder.APIGroupOptions) error { + repositoryStorage, err := grafanaregistry.NewRegistryStore(opts.Scheme, provisioning.RepositoryResourceInfo, opts.OptsGetter) + if err != nil { + return fmt.Errorf("failed to create repository storage: %w", err) + } + + repositoryStatusStorage := grafanaregistry.NewRegistryStatusStore(opts.Scheme, repositoryStorage) + + storage := map[string]rest.Storage{} + storage[provisioning.RepositoryResourceInfo.StoragePath()] = repositoryStorage + storage[provisioning.RepositoryResourceInfo.StoragePath("status")] = repositoryStatusStorage + apiGroupInfo.VersionedResourcesStorageMap[provisioning.VERSION] = storage + return nil +} + +func (b *APIBuilder) Mutate(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces) error { + obj := a.GetObject() + + if obj == nil || a.GetOperation() == admission.Connect { + return nil // This is normal for sub-resource + } + + r, ok := obj.(*provisioning.Repository) + if !ok { + return fmt.Errorf("expected repository configuration") + } + + // TODO: Do something based on the resource we got. + _ = r + + return nil +} + +func (b *APIBuilder) Validate(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces) (err error) { + obj := a.GetObject() + if obj == nil || a.GetOperation() == admission.Connect { + return nil // This is normal for sub-resource + } + + var list field.ErrorList + // TODO: Fill the list with validation errors. + + if len(list) > 0 { + return apierrors.NewInvalid( + provisioning.RepositoryResourceInfo.GroupVersionKind().GroupKind(), + a.GetName(), list) + } + return nil +} + +func (b *APIBuilder) GetOpenAPIDefinitions() common.GetOpenAPIDefinitions { + return provisioning.GetOpenAPIDefinitions +} + +func (b *APIBuilder) GetPostStartHooks() (map[string]genericapiserver.PostStartHookFunc, error) { + postStartHooks := map[string]genericapiserver.PostStartHookFunc{ + "grafana-provisioning": func(postStartHookCtx genericapiserver.PostStartHookContext) error { + // TODO: Set up a shared informer for a controller and a watcher with workers. + return nil + }, + } + return postStartHooks, nil +} + +func (b *APIBuilder) PostProcessOpenAPI(oas *spec3.OpenAPI) (*spec3.OpenAPI, error) { + oas.Info.Description = "Provisioning" + + root := "/apis/" + b.GetGroupVersion().String() + "/" + + // The root API discovery list + sub := oas.Paths.Paths[root] + if sub != nil && sub.Get != nil { + sub.Get.Tags = []string{"API Discovery"} // sorts first in the list + } + + return oas, nil +} diff --git a/pkg/registry/apis/wireset.go b/pkg/registry/apis/wireset.go index 7cedee7d208..8c4ec829b3b 100644 --- a/pkg/registry/apis/wireset.go +++ b/pkg/registry/apis/wireset.go @@ -14,6 +14,7 @@ import ( "github.com/grafana/grafana/pkg/registry/apis/folders" "github.com/grafana/grafana/pkg/registry/apis/iam" "github.com/grafana/grafana/pkg/registry/apis/peakq" + "github.com/grafana/grafana/pkg/registry/apis/provisioning" "github.com/grafana/grafana/pkg/registry/apis/query" "github.com/grafana/grafana/pkg/registry/apis/scope" "github.com/grafana/grafana/pkg/registry/apis/service" @@ -40,6 +41,7 @@ var WireSet = wire.NewSet( folders.RegisterAPIService, iam.RegisterAPIService, peakq.RegisterAPIService, + provisioning.RegisterAPIService, service.RegisterAPIService, query.RegisterAPIService, scope.RegisterAPIService,