K8s/Testdata: Expose testdata in standalone apiserver (#80248)

This commit is contained in:
Ryan McKinley 2024-01-10 07:45:23 -08:00 committed by GitHub
parent 42f1059bc9
commit 6be6724433
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 93 additions and 24 deletions

4
.vscode/launch.json vendored
View File

@ -12,14 +12,14 @@
"args": ["server", "--homepath", "${workspaceFolder}", "--packaging", "dev"] "args": ["server", "--homepath", "${workspaceFolder}", "--packaging", "dev"]
}, },
{ {
"name": "Run API Server (k8s)", "name": "Run API Server (testdata)",
"type": "go", "type": "go",
"request": "launch", "request": "launch",
"mode": "auto", "mode": "auto",
"program": "${workspaceFolder}/pkg/cmd/grafana/", "program": "${workspaceFolder}/pkg/cmd/grafana/",
"env": {}, "env": {},
"cwd": "${workspaceFolder}", "cwd": "${workspaceFolder}",
"args": ["apiserver", "example.grafana.app"] "args": ["apiserver", "testdata.datasource.grafana.app"]
}, },
{ {
"name": "Attach to Chrome", "name": "Attach to Chrome",

View File

@ -4,18 +4,17 @@ import (
"os" "os"
"path" "path"
"github.com/spf13/cobra"
genericapiserver "k8s.io/apiserver/pkg/server"
"k8s.io/apiserver/pkg/server/options"
"k8s.io/client-go/tools/clientcmd" "k8s.io/client-go/tools/clientcmd"
"k8s.io/component-base/cli"
"k8s.io/klog/v2" "k8s.io/klog/v2"
aggregatorscheme "k8s.io/kube-aggregator/pkg/apiserver/scheme"
"github.com/grafana/grafana/pkg/aggregator" "github.com/grafana/grafana/pkg/aggregator"
grafanaapiserver "github.com/grafana/grafana/pkg/services/grafana-apiserver" grafanaapiserver "github.com/grafana/grafana/pkg/services/grafana-apiserver"
"github.com/grafana/grafana/pkg/services/grafana-apiserver/utils" "github.com/grafana/grafana/pkg/services/grafana-apiserver/utils"
"github.com/spf13/cobra"
genericapiserver "k8s.io/apiserver/pkg/server"
"k8s.io/apiserver/pkg/server/options"
"k8s.io/component-base/cli"
aggregatorscheme "k8s.io/kube-aggregator/pkg/apiserver/scheme"
) )
const ( const (
@ -34,7 +33,7 @@ func newCommandStartExampleAPIServer(o *APIServerOptions, stopCh <-chan struct{}
Example: "grafana apiserver example.grafana.app", Example: "grafana apiserver example.grafana.app",
RunE: func(c *cobra.Command, args []string) error { RunE: func(c *cobra.Command, args []string) error {
// Load each group from the args // Load each group from the args
if err := o.LoadAPIGroupBuilders(args[1:]); err != nil { if err := o.loadAPIGroupBuilders(args[1:]); err != nil {
return err return err
} }

View File

@ -12,6 +12,7 @@ import (
"k8s.io/client-go/tools/clientcmd" "k8s.io/client-go/tools/clientcmd"
netutils "k8s.io/utils/net" netutils "k8s.io/utils/net"
"github.com/grafana/grafana/pkg/registry/apis/datasource"
"github.com/grafana/grafana/pkg/registry/apis/example" "github.com/grafana/grafana/pkg/registry/apis/example"
grafanaAPIServer "github.com/grafana/grafana/pkg/services/grafana-apiserver" grafanaAPIServer "github.com/grafana/grafana/pkg/services/grafana-apiserver"
"github.com/grafana/grafana/pkg/services/grafana-apiserver/utils" "github.com/grafana/grafana/pkg/services/grafana-apiserver/utils"
@ -39,13 +40,19 @@ func newAPIServerOptions(out, errOut io.Writer) *APIServerOptions {
} }
} }
func (o *APIServerOptions) LoadAPIGroupBuilders(args []string) error { func (o *APIServerOptions) loadAPIGroupBuilders(args []string) error {
o.builders = []grafanaAPIServer.APIGroupBuilder{} o.builders = []grafanaAPIServer.APIGroupBuilder{}
for _, g := range args { for _, g := range args {
switch g { switch g {
// No dependencies for testing // No dependencies for testing
case "example.grafana.app": case "example.grafana.app":
o.builders = append(o.builders, example.NewTestingAPIBuilder()) o.builders = append(o.builders, example.NewTestingAPIBuilder())
case "testdata.datasource.grafana.app":
ds, err := datasource.NewStandaloneDatasource(g)
if err != nil {
return err
}
o.builders = append(o.builders, ds)
default: default:
return fmt.Errorf("unknown group: %s", g) return fmt.Errorf("unknown group: %s", g)
} }

View File

@ -2,7 +2,7 @@ Experimental!
This is exploring how to expose any datasource as a k8s aggregated API server. This is exploring how to expose any datasource as a k8s aggregated API server.
Unlike the other services, this will register other plugins as: Unlike the other services, this will register datasources as:
> {plugin}.datasource.grafana.app > {plugin}.datasource.grafana.app

View File

@ -7,6 +7,7 @@ import (
"k8s.io/apimachinery/pkg/apis/meta/internalversion" "k8s.io/apimachinery/pkg/apis/meta/internalversion"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/apiserver/pkg/registry/rest" "k8s.io/apiserver/pkg/registry/rest"
"github.com/grafana/grafana/pkg/apis" "github.com/grafana/grafana/pkg/apis"
@ -57,32 +58,38 @@ func (s *connectionAccess) ConvertToTable(ctx context.Context, object runtime.Ob
} }
func (s *connectionAccess) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) { func (s *connectionAccess) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) {
ns := request.NamespaceValue(ctx)
ds, err := s.builder.getDataSource(ctx, name) ds, err := s.builder.getDataSource(ctx, name)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return s.asConnection(ds), nil return s.asConnection(ds, ns), nil
} }
func (s *connectionAccess) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) { func (s *connectionAccess) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) {
ns := request.NamespaceValue(ctx)
if ns == "" {
// require a namespace so we do not need to support reverse mappings (yet)
return nil, fmt.Errorf("missing namespace in request URL")
}
result := &v0alpha1.DataSourceConnectionList{ result := &v0alpha1.DataSourceConnectionList{
Items: []v0alpha1.DataSourceConnection{}, Items: []v0alpha1.DataSourceConnection{},
} }
vals, err := s.builder.getDataSources(ctx) vals, err := s.builder.getDataSources(ctx)
if err == nil { if err == nil {
for _, ds := range vals { for _, ds := range vals {
result.Items = append(result.Items, *s.asConnection(ds)) result.Items = append(result.Items, *s.asConnection(ds, ns))
} }
} }
return result, err return result, err
} }
func (s *connectionAccess) asConnection(ds *datasources.DataSource) *v0alpha1.DataSourceConnection { func (s *connectionAccess) asConnection(ds *datasources.DataSource, ns string) *v0alpha1.DataSourceConnection {
v := &v0alpha1.DataSourceConnection{ v := &v0alpha1.DataSourceConnection{
TypeMeta: s.resourceInfo.TypeMeta(), TypeMeta: s.resourceInfo.TypeMeta(),
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Name: ds.UID, Name: ds.UID,
Namespace: s.builder.namespacer(ds.OrgID), Namespace: ns,
CreationTimestamp: metav1.NewTime(ds.Created), CreationTimestamp: metav1.NewTime(ds.Created),
ResourceVersion: fmt.Sprintf("%d", ds.Updated.UnixMilli()), ResourceVersion: fmt.Sprintf("%d", ds.Updated.UnixMilli()),
}, },

View File

@ -37,12 +37,11 @@ var _ grafanaapiserver.APIGroupBuilder = (*DataSourceAPIBuilder)(nil)
type DataSourceAPIBuilder struct { type DataSourceAPIBuilder struct {
connectionResourceInfo apis.ResourceInfo connectionResourceInfo apis.ResourceInfo
plugin pluginstore.Plugin plugin plugins.JSONData
client plugins.Client client plugins.Client
dsService datasources.DataSourceService dsService datasources.DataSourceService
dsCache datasources.CacheService dsCache datasources.CacheService
accessControl accesscontrol.AccessControl accessControl accesscontrol.AccessControl
namespacer request.NamespaceMapper
} }
func RegisterAPIService( func RegisterAPIService(
@ -67,13 +66,12 @@ func RegisterAPIService(
"grafana-testdata-datasource", "grafana-testdata-datasource",
} }
namespacer := request.GetNamespaceMapper(cfg)
for _, ds := range all { for _, ds := range all {
if !slices.Contains(ids, ds.ID) { if !slices.Contains(ids, ds.ID) {
continue // skip this one continue // skip this one
} }
builder, err = NewDataSourceAPIBuilder(ds, pluginClient, dsService, dsCache, accessControl, namespacer) builder, err = NewDataSourceAPIBuilder(ds.JSONData, pluginClient, dsService, dsCache, accessControl)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -83,12 +81,11 @@ func RegisterAPIService(
} }
func NewDataSourceAPIBuilder( func NewDataSourceAPIBuilder(
plugin pluginstore.Plugin, plugin plugins.JSONData,
client plugins.Client, client plugins.Client,
dsService datasources.DataSourceService, dsService datasources.DataSourceService,
dsCache datasources.CacheService, dsCache datasources.CacheService,
accessControl accesscontrol.AccessControl, accessControl accesscontrol.AccessControl) (*DataSourceAPIBuilder, error) {
namespacer request.NamespaceMapper) (*DataSourceAPIBuilder, error) {
group, err := getDatasourceGroupNameFromPluginID(plugin.ID) group, err := getDatasourceGroupNameFromPluginID(plugin.ID)
if err != nil { if err != nil {
return nil, err return nil, err
@ -100,7 +97,6 @@ func NewDataSourceAPIBuilder(
dsService: dsService, dsService: dsService,
dsCache: dsCache, dsCache: dsCache,
accessControl: accessControl, accessControl: accessControl,
namespacer: namespacer,
}, nil }, nil
} }

View File

@ -0,0 +1,51 @@
package datasource
import (
"fmt"
"time"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/services/accesscontrol/actest"
"github.com/grafana/grafana/pkg/services/datasources"
fakeDatasources "github.com/grafana/grafana/pkg/services/datasources/fakes"
testdatasource "github.com/grafana/grafana/pkg/tsdb/grafana-testdata-datasource"
)
// This is a helper function to create a new datasource API server for a group
// This currently has no dependencies and only works for testdata. In future iterations
// this will include here (or elsewhere) versions that can load config from HG api or
// the remote SQL directly
func NewStandaloneDatasource(group string) (*DataSourceAPIBuilder, error) {
if group != "testdata.datasource.grafana.app" {
return nil, fmt.Errorf("only testadata is currently supported")
}
orgId := int64(1)
pluginId := "grafana-testdata-datasource"
now := time.Now()
dss := []*datasources.DataSource{
{
OrgID: orgId, // default -- used in the list command
Type: pluginId,
UID: "builtin", // fake for now
Created: now,
Updated: now,
Name: "Testdata (builtin)",
},
{
OrgID: orgId, // default -- used in the list command
Type: pluginId,
UID: "PD8C576611E62080A", // match the gdev version
Created: now,
Updated: now,
Name: "gdev-testdata",
},
}
return NewDataSourceAPIBuilder(
plugins.JSONData{ID: pluginId}, testdatasource.ProvideService(),
&fakeDatasources.FakeDataSourceService{DataSources: dss},
&fakeDatasources.FakeCacheService{DataSources: dss},
// Always allow... but currently not called in standalone!
&actest.FakeAccessControl{ExpectedEvaluate: true},
)
}

View File

@ -45,7 +45,7 @@ func RegisterAPIService(features featuremgmt.FeatureToggles, apiregistration gra
return nil // skip registration unless opting into experimental apis return nil // skip registration unless opting into experimental apis
} }
builder := NewTestingAPIBuilder() builder := NewTestingAPIBuilder()
apiregistration.RegisterAPI(NewTestingAPIBuilder()) apiregistration.RegisterAPI(builder)
return builder return builder
} }

View File

@ -9,9 +9,13 @@ import (
"github.com/grafana/grafana-plugin-sdk-go/backend/log" "github.com/grafana/grafana-plugin-sdk-go/backend/log"
"github.com/grafana/grafana-plugin-sdk-go/backend/resource/httpadapter" "github.com/grafana/grafana-plugin-sdk-go/backend/resource/httpadapter"
"github.com/grafana/grafana-plugin-sdk-go/data" "github.com/grafana/grafana-plugin-sdk-go/data"
"github.com/grafana/grafana/pkg/tsdb/grafana-testdata-datasource/sims" "github.com/grafana/grafana/pkg/tsdb/grafana-testdata-datasource/sims"
) )
// ensures that testdata implements all client functions
// var _ plugins.Client = &Service{}
func ProvideService() *Service { func ProvideService() *Service {
s := &Service{ s := &Service{
queryMux: datasource.NewQueryTypeMux(), queryMux: datasource.NewQueryTypeMux(),
@ -66,3 +70,8 @@ func (s *Service) CheckHealth(_ context.Context, _ *backend.CheckHealthRequest)
Message: "Data source is working", Message: "Data source is working",
}, nil }, nil
} }
// CollectMetricsHandler handles metric collection.
func (s *Service) CollectMetrics(ctx context.Context, req *backend.CollectMetricsRequest) (*backend.CollectMetricsResult, error) {
return nil, nil
}