mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
K8s/Testdata: Expose testdata in standalone apiserver (#80248)
This commit is contained in:
parent
42f1059bc9
commit
6be6724433
4
.vscode/launch.json
vendored
4
.vscode/launch.json
vendored
@ -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",
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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()),
|
||||||
},
|
},
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
51
pkg/registry/apis/datasource/standalone.go
Normal file
51
pkg/registry/apis/datasource/standalone.go
Normal 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},
|
||||||
|
)
|
||||||
|
}
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user