From 91754bcda56141fafe8afc9d48012c0d60feaed6 Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Tue, 6 Feb 2024 08:40:35 -0800 Subject: [PATCH] K8s: Refactor standalone apiserver initialization (#81932) --- .vscode/launch.json | 14 +- pkg/cmd/grafana/apiserver/cmd.go | 57 ++---- pkg/cmd/grafana/apiserver/cmd_test.go | 22 -- pkg/cmd/grafana/apiserver/server.go | 49 +---- pkg/registry/apis/datasource/standalone.go | 127 ------------ .../apis/datasource/standalone_services.go | 188 ------------------ pkg/server/wire.go | 9 +- pkg/server/wireexts_oss.go | 6 +- pkg/services/apiserver/standalone/factory.go | 154 ++++++++++++++ pkg/services/apiserver/standalone/runtime.go | 46 +++++ .../apiserver/standalone/runtime_test.go | 22 ++ 11 files changed, 256 insertions(+), 438 deletions(-) delete mode 100644 pkg/cmd/grafana/apiserver/cmd_test.go delete mode 100644 pkg/registry/apis/datasource/standalone.go delete mode 100644 pkg/registry/apis/datasource/standalone_services.go create mode 100644 pkg/services/apiserver/standalone/factory.go create mode 100644 pkg/services/apiserver/standalone/runtime.go create mode 100644 pkg/services/apiserver/standalone/runtime_test.go diff --git a/.vscode/launch.json b/.vscode/launch.json index f29f1d70114..0ff75cc92ac 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -19,17 +19,9 @@ "program": "${workspaceFolder}/pkg/cmd/grafana/", "env": {}, "cwd": "${workspaceFolder}", - "args": ["apiserver", "--secure-port=8443", "testdata.datasource.grafana.app"] - }, - { - "name": "Run API Server (aggregator)", - "type": "go", - "request": "launch", - "mode": "auto", - "program": "${workspaceFolder}/pkg/cmd/grafana/", - "env": {}, - "cwd": "${workspaceFolder}", - "args": ["aggregator", "--secure-port", "8443"] + "args": ["apiserver", + "--secure-port=8443", + "--runtime-config=testdata.datasource.grafana.app/v0alpha1=true"] }, { "name": "Attach to Chrome", diff --git a/pkg/cmd/grafana/apiserver/cmd.go b/pkg/cmd/grafana/apiserver/cmd.go index 50f6ee979bf..6f60d63cf2e 100644 --- a/pkg/cmd/grafana/apiserver/cmd.go +++ b/pkg/cmd/grafana/apiserver/cmd.go @@ -1,22 +1,28 @@ package apiserver import ( - "fmt" "os" - "strings" "github.com/spf13/cobra" genericapiserver "k8s.io/apiserver/pkg/server" "k8s.io/apiserver/pkg/server/options" "k8s.io/component-base/cli" + "github.com/grafana/grafana/pkg/server" grafanaapiserver "github.com/grafana/grafana/pkg/services/apiserver" + "github.com/grafana/grafana/pkg/services/apiserver/standalone" ) func newCommandStartExampleAPIServer(o *APIServerOptions, stopCh <-chan struct{}) *cobra.Command { devAcknowledgementNotice := "The apiserver command is in heavy development. The entire setup is subject to change without notice" runtimeConfig := "" + factory, err := server.InitializeAPIServerFactory() + if err != nil { + return nil + } + o.factory = factory + cmd := &cobra.Command{ Use: "apiserver [api group(s)]", Short: "Run the grafana apiserver", @@ -24,7 +30,11 @@ func newCommandStartExampleAPIServer(o *APIServerOptions, stopCh <-chan struct{} devAcknowledgementNotice, Example: "grafana apiserver example.grafana.app", RunE: func(c *cobra.Command, args []string) error { - apis, err := readRuntimeConfig(runtimeConfig) + runtime, err := standalone.ReadRuntimeConfig(runtimeConfig) + if err != nil { + return err + } + apis, err := o.factory.GetEnabled(runtime) if err != nil { return err } @@ -52,6 +62,7 @@ func newCommandStartExampleAPIServer(o *APIServerOptions, stopCh <-chan struct{} } cmd.Flags().StringVar(&runtimeConfig, "runtime-config", "", "A set of key=value pairs that enable or disable built-in APIs.") + o.factory.InitFlags(cmd.Flags()) // Register standard k8s flags with the command line o.RecommendedOptions = options.NewRecommendedOptions( @@ -71,43 +82,3 @@ func RunCLI() int { return cli.Run(cmd) } - -type apiConfig struct { - group string - version string - enabled bool -} - -func (a apiConfig) String() string { - return fmt.Sprintf("%s/%s=%v", a.group, a.version, a.enabled) -} - -// Supported options are: -// -// /=true|false for a specific API group and version (e.g. dashboards.grafana.app/v0alpha1=true) -// api/all=true|false controls all API versions -// api/ga=true|false controls all API versions of the form v[0-9]+ -// api/beta=true|false controls all API versions of the form v[0-9]+beta[0-9]+ -// api/alpha=true|false controls all API versions of the form v[0-9]+alpha[0-9]+`) -// -// See: https://kubernetes.io/docs/reference/command-line-tools-reference/kube-apiserver/ -func readRuntimeConfig(cfg string) ([]apiConfig, error) { - if cfg == "" { - return nil, fmt.Errorf("missing --runtime-config={apiservers}") - } - parts := strings.Split(cfg, ",") - apis := make([]apiConfig, len(parts)) - for i, part := range parts { - idx0 := strings.Index(part, "/") - idx1 := strings.LastIndex(part, "=") - if idx1 < idx0 || idx0 < 0 { - return nil, fmt.Errorf("expected values in the form: group/version=true") - } - apis[i] = apiConfig{ - group: part[:idx0], - version: part[idx0+1 : idx1], - enabled: part[idx1+1:] == "true", - } - } - return apis, nil -} diff --git a/pkg/cmd/grafana/apiserver/cmd_test.go b/pkg/cmd/grafana/apiserver/cmd_test.go deleted file mode 100644 index bf9b4b920c7..00000000000 --- a/pkg/cmd/grafana/apiserver/cmd_test.go +++ /dev/null @@ -1,22 +0,0 @@ -package apiserver - -import ( - "fmt" - "testing" - - "github.com/stretchr/testify/require" -) - -func TestAdminAPIEndpoint(t *testing.T) { - out, err := readRuntimeConfig("all/all=true,dashboards.grafana.app/v0alpha1=false") - require.NoError(t, err) - require.Equal(t, []apiConfig{ - {group: "all", version: "all", enabled: true}, - {group: "dashboards.grafana.app", version: "v0alpha1", enabled: false}, - }, out) - require.Equal(t, "all/all=true", fmt.Sprintf("%v", out[0])) - - // Empty is an error - _, err = readRuntimeConfig("") - require.Error(t, err) -} diff --git a/pkg/cmd/grafana/apiserver/server.go b/pkg/cmd/grafana/apiserver/server.go index ed9c4df6946..07059d9c857 100644 --- a/pkg/cmd/grafana/apiserver/server.go +++ b/pkg/cmd/grafana/apiserver/server.go @@ -6,23 +6,17 @@ import ( "net" "path" + "k8s.io/apimachinery/pkg/runtime/schema" utilerrors "k8s.io/apimachinery/pkg/util/errors" genericapiserver "k8s.io/apiserver/pkg/server" "k8s.io/apiserver/pkg/server/options" "k8s.io/client-go/tools/clientcmd" netutils "k8s.io/utils/net" - "github.com/grafana/grafana/pkg/registry/apis/example" - "github.com/grafana/grafana/pkg/registry/apis/featuretoggle" - "github.com/grafana/grafana/pkg/registry/apis/query" - "github.com/grafana/grafana/pkg/registry/apis/query/runner" - "github.com/grafana/grafana/pkg/server" - "github.com/grafana/grafana/pkg/services/accesscontrol/actest" grafanaAPIServer "github.com/grafana/grafana/pkg/services/apiserver" "github.com/grafana/grafana/pkg/services/apiserver/builder" + "github.com/grafana/grafana/pkg/services/apiserver/standalone" "github.com/grafana/grafana/pkg/services/apiserver/utils" - "github.com/grafana/grafana/pkg/services/featuremgmt" - "github.com/grafana/grafana/pkg/setting" ) const ( @@ -32,6 +26,7 @@ const ( // APIServerOptions contains the state for the apiserver type APIServerOptions struct { + factory standalone.APIServerFactory builders []builder.APIGroupBuilder RecommendedOptions *options.RecommendedOptions AlternateDNS []string @@ -47,40 +42,14 @@ func newAPIServerOptions(out, errOut io.Writer) *APIServerOptions { } } -func (o *APIServerOptions) loadAPIGroupBuilders(runtime []apiConfig) error { +func (o *APIServerOptions) loadAPIGroupBuilders(apis []schema.GroupVersion) error { o.builders = []builder.APIGroupBuilder{} - for _, gv := range runtime { - if !gv.enabled { - return fmt.Errorf("disabling apis is not yet supported") - } - switch gv.group { - case "all": - return fmt.Errorf("managing all APIs is not yet supported") - case "example.grafana.app": - o.builders = append(o.builders, example.NewTestingAPIBuilder()) - // Only works with testdata - case "query.grafana.app": - o.builders = append(o.builders, query.NewQueryAPIBuilder( - featuremgmt.WithFeatures(), - runner.NewDummyTestRunner(), - runner.NewDummyRegistry(), - )) - case "featuretoggle.grafana.app": - o.builders = append(o.builders, - featuretoggle.NewFeatureFlagAPIBuilder( - featuremgmt.WithFeatureManager(setting.FeatureMgmtSettings{}, nil), // none... for now - &actest.FakeAccessControl{ExpectedEvaluate: false}, - ), - ) - case "testdata.datasource.grafana.app": - ds, err := server.InitializeDataSourceAPIServer(gv.group) - if err != nil { - return err - } - o.builders = append(o.builders, ds) - default: - return fmt.Errorf("unsupported runtime-config: %v", gv) + for _, gv := range apis { + api, err := o.factory.MakeAPIServer(gv) + if err != nil { + return err } + o.builders = append(o.builders, api) } if len(o.builders) < 1 { diff --git a/pkg/registry/apis/datasource/standalone.go b/pkg/registry/apis/datasource/standalone.go deleted file mode 100644 index fed03c18e13..00000000000 --- a/pkg/registry/apis/datasource/standalone.go +++ /dev/null @@ -1,127 +0,0 @@ -package datasource - -import ( - "context" - "fmt" - - "github.com/grafana/grafana-plugin-sdk-go/backend" - v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - - "github.com/grafana/grafana/pkg/apis/datasource/v0alpha1" - "github.com/grafana/grafana/pkg/plugins" - "github.com/grafana/grafana/pkg/services/accesscontrol/actest" - "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" - "github.com/grafana/grafana/pkg/services/featuremgmt" - "github.com/grafana/grafana/pkg/setting" - testdatasource "github.com/grafana/grafana/pkg/tsdb/grafana-testdata-datasource" -) - -// NewTestDataAPIServer is a helper function to create a new datasource API server for a group. -// This currently builds its dependencies manually and only works for testdata. -func NewTestDataAPIServer(group string) (*DataSourceAPIBuilder, error) { - pluginID := "grafana-testdata-datasource" - features := featuremgmt.WithFeatures() // None for now! - - if group != "testdata.datasource.grafana.app" { - return nil, fmt.Errorf("only %s is currently supported", pluginID) - } - - // Run standalone with zero dependencies - if true { - return NewDataSourceAPIBuilder( - plugins.JSONData{ - ID: pluginID, - }, - testdatasource.ProvideService(), // the client - &pluginDatasourceImpl{ - startup: v1.Now(), - }, - &pluginDatasourceImpl{}, // stub - &actest.FakeAccessControl{ExpectedEvaluate: true}, - ) - } - - // Otherwise manually wire up access to testdata - cfg, err := setting.NewCfgFromArgs(setting.CommandLineArgs{ - // TODO: Add support for args? - }) - if err != nil { - return nil, err - } - - accessControl, pluginStore, dsService, dsCache, err := apiBuilderServices(cfg, features, pluginID) - if err != nil { - return nil, err - } - - td, exists := pluginStore.Plugin(context.Background(), pluginID) - if !exists { - return nil, fmt.Errorf("plugin %s not found", pluginID) - } - - return NewDataSourceAPIBuilder( - td.JSONData, - testdatasource.ProvideService(), // the client - &defaultPluginDatasourceProvider{ - dsService: dsService, - dsCache: dsCache, - }, - &pluginDatasourceImpl{}, // stub - accessControl, - ) -} - -// Simple stub for standalone testing -type pluginDatasourceImpl struct { - startup v1.Time -} - -var ( - _ PluginDatasourceProvider = (*pluginDatasourceImpl)(nil) -) - -// Get implements PluginDatasourceProvider. -func (p *pluginDatasourceImpl) Get(ctx context.Context, pluginID string, uid string) (*v0alpha1.DataSourceConnection, error) { - all, err := p.List(ctx, pluginID) - if err != nil { - return nil, err - } - for idx, v := range all.Items { - if v.Name == uid { - return &all.Items[idx], nil - } - } - return nil, fmt.Errorf("not found") -} - -// List implements PluginConfigProvider. -func (p *pluginDatasourceImpl) List(ctx context.Context, pluginID string) (*v0alpha1.DataSourceConnectionList, error) { - info, err := request.NamespaceInfoFrom(ctx, true) - if err != nil { - return nil, err - } - - return &v0alpha1.DataSourceConnectionList{ - TypeMeta: v0alpha1.GenericConnectionResourceInfo.TypeMeta(), - Items: []v0alpha1.DataSourceConnection{ - { - ObjectMeta: v1.ObjectMeta{ - Name: "PD8C576611E62080A", - Namespace: info.Value, // the raw namespace value - CreationTimestamp: p.startup, - }, - Title: "gdev-testdata", - }, - }, - }, nil -} - -// PluginContextForDataSource implements PluginConfigProvider. -func (*pluginDatasourceImpl) GetInstanceSettings(ctx context.Context, pluginID, uid string) (*backend.DataSourceInstanceSettings, error) { - return &backend.DataSourceInstanceSettings{}, nil -} - -// PluginContextWrapper -func (*pluginDatasourceImpl) PluginContextForDataSource(ctx context.Context, datasourceSettings *backend.DataSourceInstanceSettings) (backend.PluginContext, error) { - return backend.PluginContext{DataSourceInstanceSettings: datasourceSettings}, nil -} diff --git a/pkg/registry/apis/datasource/standalone_services.go b/pkg/registry/apis/datasource/standalone_services.go deleted file mode 100644 index 642c9a17c92..00000000000 --- a/pkg/registry/apis/datasource/standalone_services.go +++ /dev/null @@ -1,188 +0,0 @@ -package datasource - -import ( - "context" - "path/filepath" - - "github.com/grafana/grafana/pkg/api/routing" - "github.com/grafana/grafana/pkg/bus" - "github.com/grafana/grafana/pkg/infra/kvstore" - "github.com/grafana/grafana/pkg/infra/localcache" - "github.com/grafana/grafana/pkg/infra/tracing" - "github.com/grafana/grafana/pkg/infra/usagestats/service" - "github.com/grafana/grafana/pkg/plugins" - pCfg "github.com/grafana/grafana/pkg/plugins/config" - "github.com/grafana/grafana/pkg/plugins/manager/loader" - "github.com/grafana/grafana/pkg/plugins/manager/pipeline/bootstrap" - "github.com/grafana/grafana/pkg/plugins/manager/pipeline/discovery" - "github.com/grafana/grafana/pkg/plugins/manager/pipeline/initialization" - "github.com/grafana/grafana/pkg/plugins/manager/pipeline/termination" - "github.com/grafana/grafana/pkg/plugins/manager/pipeline/validation" - "github.com/grafana/grafana/pkg/plugins/manager/registry" - "github.com/grafana/grafana/pkg/plugins/manager/signature" - "github.com/grafana/grafana/pkg/plugins/manager/sources" - "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" - "github.com/grafana/grafana/pkg/services/accesscontrol/ossaccesscontrol" - "github.com/grafana/grafana/pkg/services/datasources/guardian" - datasourceService "github.com/grafana/grafana/pkg/services/datasources/service" - "github.com/grafana/grafana/pkg/services/encryption/provider" - encryptionService "github.com/grafana/grafana/pkg/services/encryption/service" - "github.com/grafana/grafana/pkg/services/featuremgmt" - "github.com/grafana/grafana/pkg/services/kmsproviders/osskmsproviders" - "github.com/grafana/grafana/pkg/services/org/orgimpl" - "github.com/grafana/grafana/pkg/services/pluginsintegration/config" - "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore" - "github.com/grafana/grafana/pkg/services/quota/quotaimpl" - "github.com/grafana/grafana/pkg/services/secrets/database" - kvstoreService "github.com/grafana/grafana/pkg/services/secrets/kvstore" - "github.com/grafana/grafana/pkg/services/secrets/manager" - "github.com/grafana/grafana/pkg/services/sqlstore" - "github.com/grafana/grafana/pkg/services/sqlstore/migrations" - "github.com/grafana/grafana/pkg/services/supportbundles/bundleregistry" - "github.com/grafana/grafana/pkg/services/team/teamimpl" - "github.com/grafana/grafana/pkg/services/user/userimpl" - "github.com/grafana/grafana/pkg/setting" -) - -func apiBuilderServices(cfg *setting.Cfg, features featuremgmt.FeatureToggles, pluginID string) ( - *acimpl.AccessControl, - *pluginstore.Service, - *datasourceService.Service, - *datasourceService.CacheServiceImpl, - error, -) { - accessControl := acimpl.ProvideAccessControl(cfg) - cacheService := localcache.ProvideService() - tracingService, err := tracing.ProvideService(cfg) - if err != nil { - return nil, nil, nil, nil, err - } - routeRegisterImpl := routing.ProvideRegister() - featureManager, err := featuremgmt.ProvideManagerService(cfg) - if err != nil { - return nil, nil, nil, nil, err - } - - inProcBus := bus.ProvideBus(tracingService) - ossMigrations := migrations.ProvideOSSMigrations(features) - sqlStore, err := sqlstore.ProvideService(cfg, features, ossMigrations, inProcBus, tracingService) - if err != nil { - return nil, nil, nil, nil, err - } - - kvStore := kvstore.ProvideService(sqlStore) - featureToggles := featuremgmt.ProvideToggles(featureManager) - bundleRegistry := bundleregistry.ProvideService() - - quota := quotaimpl.ProvideService(sqlStore, cfg) - orgService, err := orgimpl.ProvideService(sqlStore, cfg, quota) - if err != nil { - return nil, nil, nil, nil, err - } - teamService := teamimpl.ProvideService(sqlStore, cfg) - userService, err := userimpl.ProvideService(sqlStore, orgService, cfg, teamService, cacheService, quota, bundleRegistry) - if err != nil { - return nil, nil, nil, nil, err - } - - acimplService, err := acimpl.ProvideService(cfg, sqlStore, routeRegisterImpl, cacheService, accessControl, userService, featureToggles) - if err != nil { - return nil, nil, nil, nil, err - } - usageStats, err := service.ProvideService(cfg, kvStore, routeRegisterImpl, tracingService, accessControl, acimplService, bundleRegistry) - if err != nil { - return nil, nil, nil, nil, err - } - secretsStoreImpl := database.ProvideSecretsStore(sqlStore) - providerProvider := provider.ProvideEncryptionProvider() - serviceService, err := encryptionService.ProvideEncryptionService(providerProvider, usageStats, cfg) - if err != nil { - return nil, nil, nil, nil, err - } - kmsProviders := osskmsproviders.ProvideService(serviceService, cfg, featureToggles) - secretsService, err := manager.ProvideSecretsService(secretsStoreImpl, kmsProviders, serviceService, cfg, featureToggles, usageStats) - if err != nil { - return nil, nil, nil, nil, err - } - ossImpl := setting.ProvideProvider(cfg) - pluginCfg, err := config.ProvideConfig(ossImpl, cfg, featureToggles) - if err != nil { - return nil, nil, nil, nil, err - } - pluginRegistry := registry.ProvideService() - quotaService := quotaimpl.ProvideService(sqlStore, cfg) - pluginLoader, err := createLoader(pluginCfg, pluginRegistry) - if err != nil { - return nil, nil, nil, nil, err - } - pluginStore, err := pluginstore.ProvideService(pluginRegistry, newPluginSource(cfg, pluginID), pluginLoader) - if err != nil { - return nil, nil, nil, nil, err - } - secretsKVStore, err := kvstoreService.ProvideService(sqlStore, secretsService, pluginStore, kvStore, featureToggles, cfg) - if err != nil { - return nil, nil, nil, nil, err - } - dsPermissionsService := ossaccesscontrol.ProvideDatasourcePermissionsService() - dsService, err := datasourceService.ProvideService(sqlStore, secretsService, secretsKVStore, cfg, featureToggles, accessControl, dsPermissionsService, quotaService, pluginStore) - if err != nil { - return nil, nil, nil, nil, err - } - - ossProvider := guardian.ProvideGuardian() - cacheServiceImpl := datasourceService.ProvideCacheService(cacheService, sqlStore, ossProvider) - - return accessControl, pluginStore, dsService, cacheServiceImpl, nil -} - -var _ sources.Registry = (*pluginSource)(nil) - -type pluginSource struct { - cfg *setting.Cfg - pluginID string -} - -func newPluginSource(cfg *setting.Cfg, pluginID string) *pluginSource { - return &pluginSource{ - cfg: cfg, - pluginID: pluginID, - } -} - -func (t *pluginSource) List(_ context.Context) []plugins.PluginSource { - p := filepath.Join(t.cfg.StaticRootPath, "app/plugins/datasource", t.pluginID) - return []plugins.PluginSource{sources.NewLocalSource(plugins.ClassCore, []string{p})} -} - -func createLoader(cfg *pCfg.Cfg, pr registry.Service) (loader.Service, error) { - d := discovery.New(cfg, discovery.Opts{ - FindFilterFuncs: []discovery.FindFilterFunc{ - func(ctx context.Context, _ plugins.Class, b []*plugins.FoundBundle) ([]*plugins.FoundBundle, error) { - return discovery.NewDuplicatePluginFilterStep(pr).Filter(ctx, b) - }, - }, - }) - b := bootstrap.New(cfg, bootstrap.Opts{ - DecorateFuncs: []bootstrap.DecorateFunc{}, // no decoration required - }) - v := validation.New(cfg, validation.Opts{ - ValidateFuncs: []validation.ValidateFunc{ - validation.SignatureValidationStep(signature.NewValidator(signature.NewUnsignedAuthorizer(cfg))), - }, - }) - i := initialization.New(cfg, initialization.Opts{ - InitializeFuncs: []initialization.InitializeFunc{ - initialization.PluginRegistrationStep(pr), - }, - }) - t, err := termination.New(cfg, termination.Opts{ - TerminateFuncs: []termination.TerminateFunc{ - termination.DeregisterStep(pr), - }, - }) - if err != nil { - return nil, err - } - - return loader.New(d, b, v, i, t), nil -} diff --git a/pkg/server/wire.go b/pkg/server/wire.go index 6e7c1629129..44d0b7bc5ba 100644 --- a/pkg/server/wire.go +++ b/pkg/server/wire.go @@ -35,7 +35,6 @@ import ( "github.com/grafana/grafana/pkg/middleware/csrf" "github.com/grafana/grafana/pkg/middleware/loggermw" apiregistry "github.com/grafana/grafana/pkg/registry/apis" - "github.com/grafana/grafana/pkg/registry/apis/datasource" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" "github.com/grafana/grafana/pkg/services/accesscontrol/ossaccesscontrol" @@ -45,6 +44,7 @@ import ( "github.com/grafana/grafana/pkg/services/anonymous/anonimpl/anonstore" "github.com/grafana/grafana/pkg/services/apikey/apikeyimpl" grafanaapiserver "github.com/grafana/grafana/pkg/services/apiserver" + "github.com/grafana/grafana/pkg/services/apiserver/standalone" "github.com/grafana/grafana/pkg/services/auth" "github.com/grafana/grafana/pkg/services/auth/idimpl" "github.com/grafana/grafana/pkg/services/auth/jwt" @@ -465,7 +465,8 @@ func InitializeModuleServer(cfg *setting.Cfg, opts Options, apiOpts api.ServerOp return &ModuleServer{}, nil } -func InitializeDataSourceAPIServer(group string) (*datasource.DataSourceAPIBuilder, error) { - wire.Build(wireExtsDataSourceApiServerSet) - return &datasource.DataSourceAPIBuilder{}, nil +// Initialize the standalone APIServer factory +func InitializeAPIServerFactory() (standalone.APIServerFactory, error) { + wire.Build(wireExtsStandaloneAPIServerSet) + return &standalone.DummyAPIFactory{}, nil // Wire will replace this with a real interface } diff --git a/pkg/server/wireexts_oss.go b/pkg/server/wireexts_oss.go index 898f08d9912..6b9cb1407b1 100644 --- a/pkg/server/wireexts_oss.go +++ b/pkg/server/wireexts_oss.go @@ -11,7 +11,6 @@ import ( "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/manager" "github.com/grafana/grafana/pkg/registry" - "github.com/grafana/grafana/pkg/registry/apis/datasource" "github.com/grafana/grafana/pkg/registry/backgroundsvcs" "github.com/grafana/grafana/pkg/registry/usagestatssvcs" "github.com/grafana/grafana/pkg/services/accesscontrol" @@ -19,6 +18,7 @@ import ( "github.com/grafana/grafana/pkg/services/accesscontrol/ossaccesscontrol" "github.com/grafana/grafana/pkg/services/anonymous" "github.com/grafana/grafana/pkg/services/anonymous/anonimpl" + "github.com/grafana/grafana/pkg/services/apiserver/standalone" "github.com/grafana/grafana/pkg/services/auth" "github.com/grafana/grafana/pkg/services/auth/authimpl" "github.com/grafana/grafana/pkg/services/auth/idimpl" @@ -139,6 +139,6 @@ var wireExtsModuleServerSet = wire.NewSet( wireExtsBaseCLISet, ) -var wireExtsDataSourceApiServerSet = wire.NewSet( - datasource.NewTestDataAPIServer, +var wireExtsStandaloneAPIServerSet = wire.NewSet( + standalone.GetDummyAPIFactory, ) diff --git a/pkg/services/apiserver/standalone/factory.go b/pkg/services/apiserver/standalone/factory.go new file mode 100644 index 00000000000..0768109a0d1 --- /dev/null +++ b/pkg/services/apiserver/standalone/factory.go @@ -0,0 +1,154 @@ +package standalone + +import ( + "context" + "fmt" + + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/spf13/pflag" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + + "github.com/grafana/grafana/pkg/apis/datasource/v0alpha1" + "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/registry/apis/datasource" + "github.com/grafana/grafana/pkg/registry/apis/example" + "github.com/grafana/grafana/pkg/registry/apis/featuretoggle" + "github.com/grafana/grafana/pkg/registry/apis/query" + "github.com/grafana/grafana/pkg/registry/apis/query/runner" + "github.com/grafana/grafana/pkg/services/accesscontrol/actest" + "github.com/grafana/grafana/pkg/services/apiserver/builder" + "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" + "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/setting" + testdatasource "github.com/grafana/grafana/pkg/tsdb/grafana-testdata-datasource" +) + +type APIServerFactory interface { + // Called before the groups are loaded so any custom command can be registered + InitFlags(flags *pflag.FlagSet) + + // Given the flags, what can we produce + GetEnabled(runtime []RuntimeConfig) ([]schema.GroupVersion, error) + + // Make an API server for a given group+version + MakeAPIServer(gv schema.GroupVersion) (builder.APIGroupBuilder, error) +} + +// Zero dependency provider for testing +func GetDummyAPIFactory() APIServerFactory { + return &DummyAPIFactory{} +} + +type DummyAPIFactory struct{} + +func (p *DummyAPIFactory) InitFlags(flags *pflag.FlagSet) {} + +func (p *DummyAPIFactory) GetEnabled(runtime []RuntimeConfig) ([]schema.GroupVersion, error) { + gv := []schema.GroupVersion{} + for _, cfg := range runtime { + if !cfg.Enabled { + return nil, fmt.Errorf("only enabled supported now") + } + if cfg.Group == "all" { + return nil, fmt.Errorf("all not yet supported") + } + gv = append(gv, schema.GroupVersion{Group: cfg.Group, Version: cfg.Version}) + } + return gv, nil +} + +func (p *DummyAPIFactory) MakeAPIServer(gv schema.GroupVersion) (builder.APIGroupBuilder, error) { + if gv.Version != "v0alpha1" { + return nil, fmt.Errorf("only alpha supported now") + } + + switch gv.Group { + case "example.grafana.app": + return example.NewTestingAPIBuilder(), nil + + // Only works with testdata + case "query.grafana.app": + return query.NewQueryAPIBuilder( + featuremgmt.WithFeatures(), + runner.NewDummyTestRunner(), + runner.NewDummyRegistry(), + ), nil + + case "featuretoggle.grafana.app": + return featuretoggle.NewFeatureFlagAPIBuilder( + featuremgmt.WithFeatureManager(setting.FeatureMgmtSettings{}, nil), // none... for now + &actest.FakeAccessControl{ExpectedEvaluate: false}, + ), nil + + case "testdata.datasource.grafana.app": + return datasource.NewDataSourceAPIBuilder( + plugins.JSONData{ + ID: "grafana-testdata-datasource", + }, + testdatasource.ProvideService(), // the client + &pluginDatasourceImpl{ + startup: v1.Now(), + }, + &pluginDatasourceImpl{}, // stub + &actest.FakeAccessControl{ExpectedEvaluate: true}, + ) + } + + return nil, fmt.Errorf("unsupported group") +} + +// Simple stub for standalone datasource testing +type pluginDatasourceImpl struct { + startup v1.Time +} + +var ( + _ datasource.PluginDatasourceProvider = (*pluginDatasourceImpl)(nil) +) + +// Get implements PluginDatasourceProvider. +func (p *pluginDatasourceImpl) Get(ctx context.Context, pluginID string, uid string) (*v0alpha1.DataSourceConnection, error) { + all, err := p.List(ctx, pluginID) + if err != nil { + return nil, err + } + for idx, v := range all.Items { + if v.Name == uid { + return &all.Items[idx], nil + } + } + return nil, fmt.Errorf("not found") +} + +// List implements PluginConfigProvider. +func (p *pluginDatasourceImpl) List(ctx context.Context, pluginID string) (*v0alpha1.DataSourceConnectionList, error) { + info, err := request.NamespaceInfoFrom(ctx, true) + if err != nil { + return nil, err + } + + return &v0alpha1.DataSourceConnectionList{ + TypeMeta: v0alpha1.GenericConnectionResourceInfo.TypeMeta(), + Items: []v0alpha1.DataSourceConnection{ + { + ObjectMeta: v1.ObjectMeta{ + Name: "PD8C576611E62080A", + Namespace: info.Value, // the raw namespace value + CreationTimestamp: p.startup, + }, + Title: "gdev-testdata", + }, + }, + }, nil +} + +// PluginContextForDataSource implements PluginConfigProvider. +func (*pluginDatasourceImpl) GetInstanceSettings(ctx context.Context, pluginID, uid string) (*backend.DataSourceInstanceSettings, error) { + return &backend.DataSourceInstanceSettings{}, nil +} + +// PluginContextWrapper +func (*pluginDatasourceImpl) PluginContextForDataSource(ctx context.Context, datasourceSettings *backend.DataSourceInstanceSettings) (backend.PluginContext, error) { + return backend.PluginContext{DataSourceInstanceSettings: datasourceSettings}, nil +} diff --git a/pkg/services/apiserver/standalone/runtime.go b/pkg/services/apiserver/standalone/runtime.go new file mode 100644 index 00000000000..7b8abcb6d93 --- /dev/null +++ b/pkg/services/apiserver/standalone/runtime.go @@ -0,0 +1,46 @@ +package standalone + +import ( + "fmt" + "strings" +) + +type RuntimeConfig struct { + Group string + Version string + Enabled bool +} + +func (a RuntimeConfig) String() string { + return fmt.Sprintf("%s/%s=%v", a.Group, a.Version, a.Enabled) +} + +// Supported options are: +// +// /=true|false for a specific API group and version (e.g. dashboards.grafana.app/v0alpha1=true) +// api/all=true|false controls all API versions +// api/ga=true|false controls all API versions of the form v[0-9]+ +// api/beta=true|false controls all API versions of the form v[0-9]+beta[0-9]+ +// api/alpha=true|false controls all API versions of the form v[0-9]+alpha[0-9]+`) +// +// See: https://kubernetes.io/docs/reference/command-line-tools-reference/kube-apiserver/ +func ReadRuntimeConfig(cfg string) ([]RuntimeConfig, error) { + if cfg == "" { + return nil, fmt.Errorf("missing --runtime-config={apiservers}") + } + parts := strings.Split(cfg, ",") + apis := make([]RuntimeConfig, len(parts)) + for i, part := range parts { + idx0 := strings.Index(part, "/") + idx1 := strings.LastIndex(part, "=") + if idx1 < idx0 || idx0 < 0 { + return nil, fmt.Errorf("expected values in the form: group/version=true") + } + apis[i] = RuntimeConfig{ + Group: part[:idx0], + Version: part[idx0+1 : idx1], + Enabled: part[idx1+1:] == "true", + } + } + return apis, nil +} diff --git a/pkg/services/apiserver/standalone/runtime_test.go b/pkg/services/apiserver/standalone/runtime_test.go new file mode 100644 index 00000000000..374920cfd0e --- /dev/null +++ b/pkg/services/apiserver/standalone/runtime_test.go @@ -0,0 +1,22 @@ +package standalone + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestReadRuntimeCOnfig(t *testing.T) { + out, err := ReadRuntimeConfig("all/all=true,dashboards.grafana.app/v0alpha1=false") + require.NoError(t, err) + require.Equal(t, []RuntimeConfig{ + {Group: "all", Version: "all", Enabled: true}, + {Group: "dashboards.grafana.app", Version: "v0alpha1", Enabled: false}, + }, out) + require.Equal(t, "all/all=true", fmt.Sprintf("%v", out[0])) + + // Empty is an error + _, err = ReadRuntimeConfig("") + require.Error(t, err) +}