K8s: Add basic query service (#80325)

This commit is contained in:
Ryan McKinley
2024-01-31 20:36:51 +02:00
committed by GitHub
parent d1b938ba15
commit e013cd427c
38 changed files with 2143 additions and 203 deletions
+83
View File
@@ -0,0 +1,83 @@
package query
import (
"fmt"
"github.com/grafana/grafana/pkg/apis/query/v0alpha1"
"github.com/grafana/grafana/pkg/expr"
)
type parsedQueryRequest struct {
// The queries broken into requests
Requests []groupedQueries
// Optionally show the additional query properties
Expressions []v0alpha1.GenericDataQuery
}
type groupedQueries struct {
// the plugin type
pluginId string
// The datasource name/uid
uid string
// The raw backend query objects
query []v0alpha1.GenericDataQuery
}
// Internally define what makes this request unique (eventually may include the apiVersion)
func (d *groupedQueries) key() string {
return fmt.Sprintf("%s/%s", d.pluginId, d.uid)
}
func parseQueryRequest(raw v0alpha1.GenericQueryRequest) (parsedQueryRequest, error) {
mixed := make(map[string]*groupedQueries)
parsed := parsedQueryRequest{}
refIds := make(map[string]bool)
for _, original := range raw.Queries {
if refIds[original.RefID] {
return parsed, fmt.Errorf("invalid query, duplicate refId: " + original.RefID)
}
refIds[original.RefID] = true
q := original
if q.TimeRange == nil && raw.From != "" {
q.TimeRange = &v0alpha1.TimeRange{
From: raw.From,
To: raw.To,
}
}
// Extract out the expressions queries earlier
if expr.IsDataSource(q.Datasource.Type) || expr.IsDataSource(q.Datasource.UID) {
parsed.Expressions = append(parsed.Expressions, q)
continue
}
g := &groupedQueries{pluginId: q.Datasource.Type, uid: q.Datasource.UID}
group, ok := mixed[g.key()]
if !ok || group == nil {
group = g
mixed[g.key()] = g
}
group.query = append(group.query, q)
}
for _, q := range parsed.Expressions {
// TODO: parse and build tree, for now just fail fast on unknown commands
_, err := expr.GetExpressionCommandType(q.AdditionalProperties())
if err != nil {
return parsed, err
}
}
// Add each request
for _, v := range mixed {
parsed.Requests = append(parsed.Requests, *v)
}
return parsed, nil
}
+62
View File
@@ -0,0 +1,62 @@
package query
import (
"context"
"k8s.io/apimachinery/pkg/apis/meta/internalversion"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/registry/rest"
common "github.com/grafana/grafana/pkg/apis/common/v0alpha1"
example "github.com/grafana/grafana/pkg/apis/example/v0alpha1"
query "github.com/grafana/grafana/pkg/apis/query/v0alpha1"
)
var (
_ rest.Storage = (*pluginsStorage)(nil)
_ rest.Scoper = (*pluginsStorage)(nil)
_ rest.SingularNameProvider = (*pluginsStorage)(nil)
_ rest.Lister = (*pluginsStorage)(nil)
)
type pluginsStorage struct {
resourceInfo *common.ResourceInfo
tableConverter rest.TableConvertor
registry query.DataSourceApiServerRegistry
}
func newPluginsStorage(reg query.DataSourceApiServerRegistry) *pluginsStorage {
var resourceInfo = query.DataSourceApiServerResourceInfo
return &pluginsStorage{
resourceInfo: &resourceInfo,
tableConverter: rest.NewDefaultTableConvertor(resourceInfo.GroupResource()),
registry: reg,
}
}
func (s *pluginsStorage) New() runtime.Object {
return s.resourceInfo.NewFunc()
}
func (s *pluginsStorage) Destroy() {}
func (s *pluginsStorage) NamespaceScoped() bool {
return false
}
func (s *pluginsStorage) GetSingularName() string {
return example.DummyResourceInfo.GetSingularName()
}
func (s *pluginsStorage) NewList() runtime.Object {
return s.resourceInfo.NewListFunc()
}
func (s *pluginsStorage) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) {
return s.tableConverter.ConvertToTable(ctx, object, tableOptions)
}
func (s *pluginsStorage) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) {
return s.registry.GetDatasourceApiServers(ctx)
}
+157
View File
@@ -0,0 +1,157 @@
package query
import (
"context"
"encoding/json"
"fmt"
"net/http"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"golang.org/x/sync/errgroup"
"github.com/grafana/grafana/pkg/apis/query/v0alpha1"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/middleware/requestmeta"
"github.com/grafana/grafana/pkg/util/errutil/errhttp"
"github.com/grafana/grafana/pkg/web"
)
func (b *QueryAPIBuilder) handleQuery(w http.ResponseWriter, r *http.Request) {
reqDTO := v0alpha1.GenericQueryRequest{}
if err := web.Bind(r, &reqDTO); err != nil {
errhttp.Write(r.Context(), err, w)
return
}
parsed, err := parseQueryRequest(reqDTO)
if err != nil {
errhttp.Write(r.Context(), err, w)
return
}
ctx := r.Context()
qdr, err := b.processRequest(ctx, parsed)
if err != nil {
errhttp.Write(r.Context(), err, w)
return
}
statusCode := http.StatusOK
for _, res := range qdr.Responses {
if res.Error != nil {
statusCode = http.StatusBadRequest
if b.returnMultiStatus {
statusCode = http.StatusMultiStatus
}
}
}
if statusCode != http.StatusOK {
requestmeta.WithDownstreamStatusSource(ctx)
}
w.Header().Set("Content-Type", "application/json")
err = json.NewEncoder(w).Encode(qdr)
if err != nil {
errhttp.Write(r.Context(), err, w)
}
}
// See:
// https://github.com/grafana/grafana/blob/v10.2.3/pkg/services/query/query.go#L88
func (b *QueryAPIBuilder) processRequest(ctx context.Context, req parsedQueryRequest) (qdr *backend.QueryDataResponse, err error) {
switch len(req.Requests) {
case 0:
break // nothing to do
case 1:
qdr, err = b.handleQuerySingleDatasource(ctx, req.Requests[0])
default:
qdr, err = b.executeConcurrentQueries(ctx, req.Requests)
}
if len(req.Expressions) > 0 {
return b.handleExpressions(ctx, qdr, req.Expressions)
}
return qdr, err
}
// Process a single request
// See: https://github.com/grafana/grafana/blob/v10.2.3/pkg/services/query/query.go#L242
func (b *QueryAPIBuilder) handleQuerySingleDatasource(ctx context.Context, req groupedQueries) (*backend.QueryDataResponse, error) {
gv, err := b.registry.GetDatasourceGroupVersion(req.pluginId)
if err != nil {
return nil, err
}
return b.runner.ExecuteQueryData(ctx, gv, req.uid, req.query)
}
// buildErrorResponses applies the provided error to each query response in the list. These queries should all belong to the same datasource.
func buildErrorResponse(err error, req groupedQueries) *backend.QueryDataResponse {
rsp := backend.NewQueryDataResponse()
for _, query := range req.query {
rsp.Responses[query.RefID] = backend.DataResponse{
Error: err,
}
}
return rsp
}
// executeConcurrentQueries executes queries to multiple datasources concurrently and returns the aggregate result.
func (b *QueryAPIBuilder) executeConcurrentQueries(ctx context.Context, requests []groupedQueries) (*backend.QueryDataResponse, error) {
g, ctx := errgroup.WithContext(ctx)
g.SetLimit(b.concurrentQueryLimit) // prevent too many concurrent requests
rchan := make(chan *backend.QueryDataResponse, len(requests))
// Create panic recovery function for loop below
recoveryFn := func(req groupedQueries) {
if r := recover(); r != nil {
var err error
b.log.Error("query datasource panic", "error", r, "stack", log.Stack(1))
if theErr, ok := r.(error); ok {
err = theErr
} else if theErrString, ok := r.(string); ok {
err = fmt.Errorf(theErrString)
} else {
err = fmt.Errorf("unexpected error - %s", b.userFacingDefaultError)
}
// Due to the panic, there is no valid response for any query for this datasource. Append an error for each one.
rchan <- buildErrorResponse(err, req)
}
}
// Query each datasource concurrently
for idx := range requests {
req := requests[idx]
g.Go(func() error {
defer recoveryFn(req)
dqr, err := b.handleQuerySingleDatasource(ctx, req)
if err == nil {
rchan <- dqr
} else {
rchan <- buildErrorResponse(err, req)
}
return nil
})
}
if err := g.Wait(); err != nil {
return nil, err
}
close(rchan)
// Merge the results from each response
resp := backend.NewQueryDataResponse()
for result := range rchan {
for refId, dataResponse := range result.Responses {
resp.Responses[refId] = dataResponse
}
}
return resp, nil
}
// NOTE the upstream queries have already been executed
// https://github.com/grafana/grafana/blob/v10.2.3/pkg/services/query/query.go#L242
func (b *QueryAPIBuilder) handleExpressions(ctx context.Context, qdr *backend.QueryDataResponse, expressions []v0alpha1.GenericDataQuery) (*backend.QueryDataResponse, error) {
return qdr, fmt.Errorf("expressions are not implemented yet")
}
+260
View File
@@ -0,0 +1,260 @@
package query
import (
"encoding/json"
"net/http"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/apiserver/pkg/registry/generic"
"k8s.io/apiserver/pkg/registry/rest"
genericapiserver "k8s.io/apiserver/pkg/server"
common "k8s.io/kube-openapi/pkg/common"
"k8s.io/kube-openapi/pkg/spec3"
"k8s.io/kube-openapi/pkg/validation/spec"
"github.com/grafana/grafana/pkg/apis/query/v0alpha1"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/registry/apis/query/runner"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/featuremgmt"
grafanaapiserver "github.com/grafana/grafana/pkg/services/grafana-apiserver"
"github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
)
var _ grafanaapiserver.APIGroupBuilder = (*QueryAPIBuilder)(nil)
type QueryAPIBuilder struct {
log log.Logger
concurrentQueryLimit int
userFacingDefaultError string
returnMultiStatus bool // from feature toggle
runner v0alpha1.QueryRunner
registry v0alpha1.DataSourceApiServerRegistry
}
func NewQueryAPIBuilder(features featuremgmt.FeatureToggles,
runner v0alpha1.QueryRunner,
registry v0alpha1.DataSourceApiServerRegistry,
) *QueryAPIBuilder {
return &QueryAPIBuilder{
concurrentQueryLimit: 4, // from config?
log: log.New("query_apiserver"),
returnMultiStatus: features.IsEnabledGlobally(featuremgmt.FlagDatasourceQueryMultiStatus),
runner: runner,
registry: registry,
}
}
func RegisterAPIService(features featuremgmt.FeatureToggles,
apiregistration grafanaapiserver.APIRegistrar,
dataSourcesService datasources.DataSourceService,
pluginStore pluginstore.Store,
accessControl accesscontrol.AccessControl,
pluginClient plugins.Client,
pCtxProvider *plugincontext.Provider,
) *QueryAPIBuilder {
if !features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) {
return nil // skip registration unless opting into experimental apis
}
builder := NewQueryAPIBuilder(
features,
runner.NewDirectQueryRunner(pluginClient, pCtxProvider),
runner.NewDirectRegistry(pluginStore, dataSourcesService),
)
// ONLY testdata...
if false {
builder = NewQueryAPIBuilder(
features,
runner.NewDummyTestRunner(),
runner.NewDummyRegistry(),
)
}
apiregistration.RegisterAPI(builder)
return builder
}
func (b *QueryAPIBuilder) GetGroupVersion() schema.GroupVersion {
return v0alpha1.SchemeGroupVersion
}
func addKnownTypes(scheme *runtime.Scheme, gv schema.GroupVersion) {
scheme.AddKnownTypes(gv,
&v0alpha1.DataSourceApiServer{},
&v0alpha1.DataSourceApiServerList{},
&v0alpha1.QueryDataResponse{},
)
}
func (b *QueryAPIBuilder) InstallSchema(scheme *runtime.Scheme) error {
addKnownTypes(scheme, v0alpha1.SchemeGroupVersion)
metav1.AddToGroupVersion(scheme, v0alpha1.SchemeGroupVersion)
return scheme.SetVersionPriority(v0alpha1.SchemeGroupVersion)
}
func (b *QueryAPIBuilder) GetAPIGroupInfo(
scheme *runtime.Scheme,
codecs serializer.CodecFactory, // pointer?
optsGetter generic.RESTOptionsGetter,
) (*genericapiserver.APIGroupInfo, error) {
gv := v0alpha1.SchemeGroupVersion
apiGroupInfo := genericapiserver.NewDefaultAPIGroupInfo(gv.Group, scheme, metav1.ParameterCodec, codecs)
plugins := newPluginsStorage(b.registry)
storage := map[string]rest.Storage{}
storage[plugins.resourceInfo.StoragePath()] = plugins
apiGroupInfo.VersionedResourcesStorageMap[gv.Version] = storage
return &apiGroupInfo, nil
}
func (b *QueryAPIBuilder) GetOpenAPIDefinitions() common.GetOpenAPIDefinitions {
return v0alpha1.GetOpenAPIDefinitions
}
// Register additional routes with the server
func (b *QueryAPIBuilder) GetAPIRoutes() *grafanaapiserver.APIRoutes {
defs := v0alpha1.GetOpenAPIDefinitions(func(path string) spec.Ref { return spec.Ref{} })
querySchema := defs["github.com/grafana/grafana/pkg/apis/query/v0alpha1.QueryRequest"].Schema
responseSchema := defs["github.com/grafana/grafana/pkg/apis/query/v0alpha1.QueryDataResponse"].Schema
var randomWalkQuery any
var randomWalkTable any
_ = json.Unmarshal([]byte(`{
"queries": [
{
"refId": "A",
"scenarioId": "random_walk",
"seriesCount": 1,
"datasource": {
"type": "grafana-testdata-datasource",
"uid": "PD8C576611E62080A"
},
"intervalMs": 60000,
"maxDataPoints": 20
}
],
"from": "1704893381544",
"to": "1704914981544"
}`), &randomWalkQuery)
_ = json.Unmarshal([]byte(`{
"queries": [
{
"refId": "A",
"scenarioId": "random_walk_table",
"seriesCount": 1,
"datasource": {
"type": "grafana-testdata-datasource",
"uid": "PD8C576611E62080A"
},
"intervalMs": 60000,
"maxDataPoints": 20
}
],
"from": "1704893381544",
"to": "1704914981544"
}`), &randomWalkTable)
return &grafanaapiserver.APIRoutes{
Root: []grafanaapiserver.APIRouteHandler{},
Namespace: []grafanaapiserver.APIRouteHandler{
{
Path: "query",
Spec: &spec3.PathProps{
Post: &spec3.Operation{
OperationProps: spec3.OperationProps{
Tags: []string{"query"},
Description: "query across multiple datasources with expressions. This api matches the legacy /ds/query endpoint",
Parameters: []*spec3.Parameter{
{
ParameterProps: spec3.ParameterProps{
Name: "namespace",
Description: "object name and auth scope, such as for teams and projects",
In: "path",
Required: true,
Schema: spec.StringProperty(),
Example: "default",
},
},
},
RequestBody: &spec3.RequestBody{
RequestBodyProps: spec3.RequestBodyProps{
Required: true,
Description: "the query array",
Content: map[string]*spec3.MediaType{
"application/json": {
MediaTypeProps: spec3.MediaTypeProps{
Schema: querySchema.WithExample(randomWalkQuery),
Examples: map[string]*spec3.Example{
"random_walk": {
ExampleProps: spec3.ExampleProps{
Summary: "random walk",
Value: randomWalkQuery,
},
},
"random_walk_table": {
ExampleProps: spec3.ExampleProps{
Summary: "random walk (table)",
Value: randomWalkTable,
},
},
},
},
},
},
},
},
Responses: &spec3.Responses{
ResponsesProps: spec3.ResponsesProps{
StatusCodeResponses: map[int]*spec3.Response{
http.StatusOK: {
ResponseProps: spec3.ResponseProps{
Description: "Query results",
Content: map[string]*spec3.MediaType{
"application/json": {
MediaTypeProps: spec3.MediaTypeProps{
Schema: &responseSchema,
},
},
},
},
},
http.StatusMultiStatus: {
ResponseProps: spec3.ResponseProps{
Description: "Errors exist in the downstream results",
Content: map[string]*spec3.MediaType{
"application/json": {
MediaTypeProps: spec3.MediaTypeProps{
Schema: &responseSchema,
},
},
},
},
},
},
},
},
},
},
},
Handler: b.handleQuery,
},
},
}
}
func (b *QueryAPIBuilder) GetAuthorizer() authorizer.Authorizer {
return nil // default is OK
}
+179
View File
@@ -0,0 +1,179 @@
package runner
import (
"context"
"fmt"
"sync"
"time"
"github.com/grafana/grafana-plugin-sdk-go/backend"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"github.com/grafana/grafana/pkg/apis/query/v0alpha1"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/tsdb/legacydata"
)
type directRunner struct {
pluginClient plugins.Client
pCtxProvider *plugincontext.Provider
}
type directRegistry struct {
pluginsMu sync.Mutex
plugins *v0alpha1.DataSourceApiServerList
apis map[string]schema.GroupVersion
groupToPlugin map[string]string
pluginStore pluginstore.Store
// called on demand
dataSourcesService datasources.DataSourceService
}
var _ v0alpha1.QueryRunner = (*directRunner)(nil)
var _ v0alpha1.DataSourceApiServerRegistry = (*directRegistry)(nil)
// NewDummyTestRunner creates a runner that only works with testdata
func NewDirectQueryRunner(
pluginClient plugins.Client,
pCtxProvider *plugincontext.Provider) v0alpha1.QueryRunner {
return &directRunner{
pluginClient: pluginClient,
pCtxProvider: pCtxProvider,
}
}
func NewDirectRegistry(pluginStore pluginstore.Store,
dataSourcesService datasources.DataSourceService,
) v0alpha1.DataSourceApiServerRegistry {
return &directRegistry{
pluginStore: pluginStore,
dataSourcesService: dataSourcesService,
}
}
// ExecuteQueryData implements QueryHelper.
func (d *directRunner) ExecuteQueryData(ctx context.Context,
// The k8s group for the datasource (pluginId)
datasource schema.GroupVersion,
// The datasource name/uid
name string,
// The raw backend query objects
query []v0alpha1.GenericDataQuery,
) (*backend.QueryDataResponse, error) {
queries, dsRef, err := legacydata.ToDataSourceQueries(v0alpha1.GenericQueryRequest{
Queries: query,
})
if err != nil {
return nil, err
}
if dsRef != nil && dsRef.UID != name {
return nil, fmt.Errorf("expected query body datasource and request to match")
}
// NOTE: this depends on uid unique across datasources
settings, err := d.pCtxProvider.GetDataSourceInstanceSettings(ctx, name)
if err != nil {
return nil, err
}
pCtx, err := d.pCtxProvider.PluginContextForDataSource(ctx, settings)
if err != nil {
return nil, err
}
return d.pluginClient.QueryData(ctx, &backend.QueryDataRequest{
PluginContext: pCtx,
Queries: queries,
})
}
// GetDatasourceAPI implements DataSourceRegistry.
func (d *directRegistry) GetDatasourceGroupVersion(pluginId string) (schema.GroupVersion, error) {
d.pluginsMu.Lock()
defer d.pluginsMu.Unlock()
if d.plugins == nil {
err := d.updatePlugins()
if err != nil {
return schema.GroupVersion{}, err
}
}
var err error
gv, ok := d.apis[pluginId]
if !ok {
err = fmt.Errorf("no API found for id: " + pluginId)
}
return gv, err
}
// GetDatasourcePlugins no namespace? everything that is available
func (d *directRegistry) GetDatasourceApiServers(ctx context.Context) (*v0alpha1.DataSourceApiServerList, error) {
d.pluginsMu.Lock()
defer d.pluginsMu.Unlock()
if d.plugins == nil {
err := d.updatePlugins()
if err != nil {
return nil, err
}
}
return d.plugins, nil
}
// This should be called when plugins change
func (d *directRegistry) updatePlugins() error {
groupToPlugin := map[string]string{}
apis := map[string]schema.GroupVersion{}
result := &v0alpha1.DataSourceApiServerList{
ListMeta: metav1.ListMeta{
ResourceVersion: fmt.Sprintf("%d", time.Now().UnixMilli()),
},
}
// TODO? only backend plugins
for _, dsp := range d.pluginStore.Plugins(context.Background(), plugins.TypeDataSource) {
ts := setting.BuildStamp * 1000
if dsp.Info.Build.Time > 0 {
ts = dsp.Info.Build.Time
}
group, err := plugins.GetDatasourceGroupNameFromPluginID(dsp.ID)
if err != nil {
return err
}
gv := schema.GroupVersion{Group: group, Version: "v0alpha1"} // default version
apis[dsp.ID] = gv
for _, alias := range dsp.AliasIDs {
apis[alias] = gv
}
groupToPlugin[group] = dsp.ID
ds := v0alpha1.DataSourceApiServer{
ObjectMeta: metav1.ObjectMeta{
Name: dsp.ID,
CreationTimestamp: metav1.NewTime(time.UnixMilli(ts)),
},
Title: dsp.Name,
AliasIDs: dsp.AliasIDs,
GroupVersion: gv.String(),
Description: dsp.Info.Description,
}
result.Items = append(result.Items, ds)
}
d.plugins = result
d.apis = apis
d.groupToPlugin = groupToPlugin
return nil
}
+88
View File
@@ -0,0 +1,88 @@
package runner
import (
"context"
"fmt"
"time"
"github.com/grafana/grafana-plugin-sdk-go/backend"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"github.com/grafana/grafana/pkg/apis/query/v0alpha1"
testdata "github.com/grafana/grafana/pkg/tsdb/grafana-testdata-datasource"
"github.com/grafana/grafana/pkg/tsdb/legacydata"
)
type testdataDummy struct{}
var _ v0alpha1.QueryRunner = (*testdataDummy)(nil)
var _ v0alpha1.DataSourceApiServerRegistry = (*testdataDummy)(nil)
// NewDummyTestRunner creates a runner that only works with testdata
func NewDummyTestRunner() v0alpha1.QueryRunner {
return &testdataDummy{}
}
func NewDummyRegistry() v0alpha1.DataSourceApiServerRegistry {
return &testdataDummy{}
}
// ExecuteQueryData implements QueryHelper.
func (d *testdataDummy) ExecuteQueryData(ctx context.Context,
// The k8s group for the datasource (pluginId)
datasource schema.GroupVersion,
// The datasource name/uid
name string,
// The raw backend query objects
query []v0alpha1.GenericDataQuery,
) (*backend.QueryDataResponse, error) {
if datasource.Group != "testdata.datasource.grafana.app" {
return nil, fmt.Errorf("expecting testdata requests")
}
queries, _, err := legacydata.ToDataSourceQueries(v0alpha1.GenericQueryRequest{
Queries: query,
})
if err != nil {
return nil, err
}
return testdata.ProvideService().QueryData(ctx, &backend.QueryDataRequest{
Queries: queries,
})
}
// GetDatasourceAPI implements DataSourceRegistry.
func (*testdataDummy) GetDatasourceGroupVersion(pluginId string) (schema.GroupVersion, error) {
if pluginId == "testdata" || pluginId == "grafana-testdata-datasource" {
return schema.GroupVersion{
Group: "testdata.datasource.grafana.app",
Version: "v0alpha1",
}, nil
}
return schema.GroupVersion{}, fmt.Errorf("unsupported plugin (only testdata for now)")
}
// GetDatasourcePlugins implements QueryHelper.
func (d *testdataDummy) GetDatasourceApiServers(ctx context.Context) (*v0alpha1.DataSourceApiServerList, error) {
return &v0alpha1.DataSourceApiServerList{
ListMeta: metav1.ListMeta{
ResourceVersion: fmt.Sprintf("%d", time.Now().UnixMilli()),
},
Items: []v0alpha1.DataSourceApiServer{
{
ObjectMeta: metav1.ObjectMeta{
Name: "grafana-testdata-datasource",
CreationTimestamp: metav1.Now(),
},
Title: "Test Data",
GroupVersion: "testdata.datasource.grafana.app/v0alpha1",
AliasIDs: []string{"testdata"},
},
},
}, nil
}