mirror of
https://github.com/grafana/grafana.git
synced 2026-07-29 15:59:50 -05:00
K8s: Add basic query service (#80325)
This commit is contained in:
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/registry/apis/featuretoggle"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/folders"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/playlist"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/query"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -27,6 +28,7 @@ func ProvideRegistryServiceSink(
|
||||
_ *featuretoggle.FeatureFlagAPIBuilder,
|
||||
_ *datasource.DataSourceAPIBuilder,
|
||||
_ *folders.FolderAPIBuilder,
|
||||
_ *query.QueryAPIBuilder,
|
||||
) *Service {
|
||||
return &Service{}
|
||||
}
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
package datasource
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
|
||||
"github.com/grafana/grafana/pkg/tsdb/legacydata"
|
||||
)
|
||||
|
||||
// Copied from: https://github.com/grafana/grafana/blob/main/pkg/api/dtos/models.go#L62
|
||||
type rawMetricRequest struct {
|
||||
// From Start time in epoch timestamps in milliseconds or relative using Grafana time units.
|
||||
// required: true
|
||||
// example: now-1h
|
||||
From string `json:"from"`
|
||||
// To End time in epoch timestamps in milliseconds or relative using Grafana time units.
|
||||
// required: true
|
||||
// example: now
|
||||
To string `json:"to"`
|
||||
// queries.refId – Specifies an identifier of the query. Is optional and default to “A”.
|
||||
// queries.datasourceId – Specifies the data source to be queried. Each query in the request must have an unique datasourceId.
|
||||
// queries.maxDataPoints - Species maximum amount of data points that dashboard panel can render. Is optional and default to 100.
|
||||
// queries.intervalMs - Specifies the time interval in milliseconds of time series. Is optional and defaults to 1000.
|
||||
// required: true
|
||||
// example: [ { "refId": "A", "intervalMs": 86400000, "maxDataPoints": 1092, "datasource":{ "uid":"PD8C576611E62080A" }, "rawSql": "SELECT 1 as valueOne, 2 as valueTwo", "format": "table" } ]
|
||||
Queries []rawDataQuery `json:"queries"`
|
||||
// required: false
|
||||
Debug bool `json:"debug"`
|
||||
}
|
||||
|
||||
type rawDataQuery = map[string]interface{}
|
||||
|
||||
func readQueries(in []byte) ([]backend.DataQuery, error) {
|
||||
reqDTO := &rawMetricRequest{}
|
||||
err := json.Unmarshal(in, &reqDTO)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(reqDTO.Queries) == 0 {
|
||||
return nil, fmt.Errorf("expected queries")
|
||||
}
|
||||
|
||||
tr := legacydata.NewDataTimeRange(reqDTO.From, reqDTO.To)
|
||||
backendTr := backend.TimeRange{
|
||||
From: tr.MustGetFrom(),
|
||||
To: tr.MustGetTo(),
|
||||
}
|
||||
queries := make([]backend.DataQuery, 0)
|
||||
|
||||
for _, query := range reqDTO.Queries {
|
||||
dataQuery := backend.DataQuery{
|
||||
TimeRange: backendTr,
|
||||
}
|
||||
|
||||
v, ok := query["refId"]
|
||||
if ok {
|
||||
dataQuery.RefID, ok = v.(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("expeted string refId")
|
||||
}
|
||||
}
|
||||
|
||||
v, ok = query["queryType"]
|
||||
if ok {
|
||||
dataQuery.QueryType, ok = v.(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("expeted string queryType")
|
||||
}
|
||||
}
|
||||
|
||||
v, ok = query["maxDataPoints"]
|
||||
if ok {
|
||||
vInt, ok := v.(float64)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("expected float64 maxDataPoints")
|
||||
}
|
||||
|
||||
dataQuery.MaxDataPoints = int64(vInt)
|
||||
}
|
||||
|
||||
v, ok = query["intervalMs"]
|
||||
if ok {
|
||||
vInt, ok := v.(float64)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("expected float64 intervalMs")
|
||||
}
|
||||
|
||||
dataQuery.Interval = time.Duration(vInt)
|
||||
}
|
||||
|
||||
dataQuery.JSON, err = json.Marshal(query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
queries = append(queries, dataQuery)
|
||||
}
|
||||
|
||||
return queries, nil
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
package datasource
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestParseQueriesIntoQueryDataRequest(t *testing.T) {
|
||||
request := []byte(`{
|
||||
"queries": [
|
||||
{
|
||||
"refId": "A",
|
||||
"datasource": {
|
||||
"type": "grafana-googlesheets-datasource",
|
||||
"uid": "b1808c48-9fc9-4045-82d7-081781f8a553"
|
||||
},
|
||||
"cacheDurationSeconds": 300,
|
||||
"spreadsheet": "spreadsheetID",
|
||||
"range": "",
|
||||
"datasourceId": 4,
|
||||
"intervalMs": 30000,
|
||||
"maxDataPoints": 794
|
||||
}
|
||||
],
|
||||
"from": "1692624667389",
|
||||
"to": "1692646267389"
|
||||
}`)
|
||||
|
||||
parsedDataQuery, err := readQueries(request)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, len(parsedDataQuery), 1)
|
||||
}
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
|
||||
common "github.com/grafana/grafana/pkg/apis/common/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/apis/datasource/v0alpha1"
|
||||
query "github.com/grafana/grafana/pkg/apis/query/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
@@ -112,7 +113,8 @@ func addKnownTypes(scheme *runtime.Scheme, gv schema.GroupVersion) {
|
||||
&v0alpha1.DataSourceConnectionList{},
|
||||
&v0alpha1.HealthCheckResult{},
|
||||
&unstructured.Unstructured{},
|
||||
// Added for subresource stubs
|
||||
// Query handler
|
||||
&query.QueryDataResponse{},
|
||||
&metav1.Status{},
|
||||
)
|
||||
}
|
||||
@@ -138,7 +140,7 @@ func (b *DataSourceAPIBuilder) InstallSchema(scheme *runtime.Scheme) error {
|
||||
}
|
||||
|
||||
func resourceFromPluginID(pluginID string) (common.ResourceInfo, error) {
|
||||
group, err := getDatasourceGroupNameFromPluginID(pluginID)
|
||||
group, err := plugins.GetDatasourceGroupNameFromPluginID(pluginID)
|
||||
if err != nil {
|
||||
return common.ResourceInfo{}, err
|
||||
}
|
||||
|
||||
@@ -3,14 +3,18 @@ package datasource
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
"strconv"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apiserver/pkg/registry/rest"
|
||||
|
||||
"github.com/grafana/grafana/pkg/apis/query/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/middleware/requestmeta"
|
||||
"github.com/grafana/grafana/pkg/tsdb/legacydata"
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
)
|
||||
|
||||
type subQueryREST struct {
|
||||
@@ -20,11 +24,10 @@ type subQueryREST struct {
|
||||
var _ = rest.Connecter(&subQueryREST{})
|
||||
|
||||
func (r *subQueryREST) New() runtime.Object {
|
||||
return &metav1.Status{}
|
||||
return &v0alpha1.QueryDataResponse{}
|
||||
}
|
||||
|
||||
func (r *subQueryREST) Destroy() {
|
||||
}
|
||||
func (r *subQueryREST) Destroy() {}
|
||||
|
||||
func (r *subQueryREST) ConnectMethods() []string {
|
||||
return []string{"POST", "GET"}
|
||||
@@ -34,40 +37,39 @@ func (r *subQueryREST) NewConnectOptions() (runtime.Object, bool, string) {
|
||||
return nil, false, ""
|
||||
}
|
||||
|
||||
func (r *subQueryREST) readQueries(req *http.Request) ([]backend.DataQuery, error) {
|
||||
func (r *subQueryREST) readQueries(req *http.Request) ([]backend.DataQuery, *v0alpha1.DataSourceRef, error) {
|
||||
reqDTO := v0alpha1.GenericQueryRequest{}
|
||||
// Simple URL to JSON mapping
|
||||
if req.Method == http.MethodGet {
|
||||
body := make(map[string]any, 0)
|
||||
for k, v := range req.URL.Query() {
|
||||
switch len(v) {
|
||||
case 0:
|
||||
body[k] = true
|
||||
case 1:
|
||||
body[k] = v[0] // TODO, convert numbers
|
||||
query := v0alpha1.GenericDataQuery{
|
||||
RefID: "A",
|
||||
MaxDataPoints: 1000,
|
||||
IntervalMS: 10,
|
||||
}
|
||||
params := req.URL.Query()
|
||||
for k := range params {
|
||||
v := params.Get(k) // the singular value
|
||||
switch k {
|
||||
case "to":
|
||||
reqDTO.To = v
|
||||
case "from":
|
||||
reqDTO.From = v
|
||||
case "maxDataPoints":
|
||||
query.MaxDataPoints, _ = strconv.ParseInt(v, 10, 64)
|
||||
case "intervalMs":
|
||||
query.IntervalMS, _ = strconv.ParseFloat(v, 64)
|
||||
case "queryType":
|
||||
query.QueryType = v
|
||||
default:
|
||||
body[k] = v // TODO, convert numbers
|
||||
query.AdditionalProperties()[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
var err error
|
||||
dq := backend.DataQuery{
|
||||
RefID: "A",
|
||||
TimeRange: backend.TimeRange{
|
||||
From: time.Now().Add(-1 * time.Hour), // last hour
|
||||
To: time.Now(),
|
||||
},
|
||||
MaxDataPoints: 1000,
|
||||
Interval: time.Second * 10,
|
||||
}
|
||||
dq.JSON, err = json.Marshal(body)
|
||||
return []backend.DataQuery{dq}, err
|
||||
reqDTO.Queries = []v0alpha1.GenericDataQuery{query}
|
||||
} else if err := web.Bind(req, &reqDTO); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(req.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return readQueries(body)
|
||||
return legacydata.ToDataSourceQueries(reqDTO)
|
||||
}
|
||||
|
||||
func (r *subQueryREST) Connect(ctx context.Context, name string, opts runtime.Object, responder rest.Responder) (http.Handler, error) {
|
||||
@@ -77,27 +79,56 @@ func (r *subQueryREST) Connect(ctx context.Context, name string, opts runtime.Ob
|
||||
}
|
||||
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
queries, err := r.readQueries(req)
|
||||
queries, dsRef, err := r.readQueries(req)
|
||||
if err != nil {
|
||||
responder.Error(err)
|
||||
return
|
||||
}
|
||||
if dsRef != nil && dsRef.UID != name {
|
||||
responder.Error(fmt.Errorf("expected the datasource in the request url and body to match"))
|
||||
return
|
||||
}
|
||||
|
||||
queryResponse, err := r.builder.client.QueryData(ctx, &backend.QueryDataRequest{
|
||||
qdr, err := r.builder.client.QueryData(ctx, &backend.QueryDataRequest{
|
||||
PluginContext: pluginCtx,
|
||||
Queries: queries,
|
||||
// Headers: // from context
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
jsonRsp, err := json.Marshal(queryResponse)
|
||||
if err != nil {
|
||||
responder.Error(err)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(200)
|
||||
_, _ = w.Write(jsonRsp)
|
||||
|
||||
statusCode := http.StatusOK
|
||||
for _, res := range qdr.Responses {
|
||||
if res.Error != nil {
|
||||
statusCode = http.StatusMultiStatus
|
||||
}
|
||||
}
|
||||
if statusCode != http.StatusOK {
|
||||
requestmeta.WithDownstreamStatusSource(ctx)
|
||||
}
|
||||
|
||||
// TODO... someday :) can return protobuf for machine-machine communication
|
||||
// will avoid some hops the current response workflow (for external plugins)
|
||||
// 1. Plugin:
|
||||
// creates: golang structs
|
||||
// returns: arrow + protobuf |
|
||||
// 2. Client: | direct when local/non grpc
|
||||
// reads: protobuf+arrow V
|
||||
// returns: golang structs
|
||||
// 3. Datasource Server (eg right here):
|
||||
// reads: golang structs
|
||||
// returns: JSON
|
||||
// 4. Query service (alerting etc):
|
||||
// reads: JSON? (TODO! raw output from 1???)
|
||||
// returns: JSON (after more operations)
|
||||
// 5. Browser
|
||||
// reads: JSON
|
||||
w.WriteHeader(statusCode)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
err = json.NewEncoder(w).Encode(qdr)
|
||||
if err != nil {
|
||||
responder.Error(err)
|
||||
}
|
||||
}), nil
|
||||
}
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
package datasource
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func getDatasourceGroupNameFromPluginID(pluginId string) (string, error) {
|
||||
if pluginId == "" {
|
||||
return "", fmt.Errorf("bad pluginID (empty)")
|
||||
}
|
||||
parts := strings.Split(pluginId, "-")
|
||||
if len(parts) == 1 {
|
||||
return fmt.Sprintf("%s.datasource.grafana.app", parts[0]), nil
|
||||
}
|
||||
|
||||
last := parts[len(parts)-1]
|
||||
if last != "datasource" {
|
||||
return "", fmt.Errorf("bad pluginID (%s)", pluginId)
|
||||
}
|
||||
if parts[0] == "grafana" {
|
||||
parts = parts[1:] // strip the first value
|
||||
}
|
||||
return fmt.Sprintf("%s.datasource.grafana.app", strings.Join(parts[:len(parts)-1], "-")), nil
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
package datasource
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestUtils(t *testing.T) {
|
||||
// multiple flavors of the same idea
|
||||
require.Equal(t, "tempo.datasource.grafana.app", getIDIgnoreError("tempo"))
|
||||
require.Equal(t, "tempo.datasource.grafana.app", getIDIgnoreError("grafana-tempo-datasource"))
|
||||
require.Equal(t, "tempo.datasource.grafana.app", getIDIgnoreError("tempo-datasource"))
|
||||
|
||||
// Multiple dashes in the name
|
||||
require.Equal(t, "org-name.datasource.grafana.app", getIDIgnoreError("org-name-datasource"))
|
||||
require.Equal(t, "org-name-more.datasource.grafana.app", getIDIgnoreError("org-name-more-datasource"))
|
||||
require.Equal(t, "org-name-more-more.datasource.grafana.app", getIDIgnoreError("org-name-more-more-datasource"))
|
||||
|
||||
require.Error(t, getErrorIgnoreValue("graph-panel"))
|
||||
require.Error(t, getErrorIgnoreValue("anything-notdatasource"))
|
||||
}
|
||||
|
||||
func getIDIgnoreError(id string) string {
|
||||
v, _ := getDatasourceGroupNameFromPluginID(id)
|
||||
return v
|
||||
}
|
||||
|
||||
func getErrorIgnoreValue(id string) error {
|
||||
_, err := getDatasourceGroupNameFromPluginID(id)
|
||||
return err
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/registry/apis/featuretoggle"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/folders"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/playlist"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/query"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext"
|
||||
)
|
||||
|
||||
@@ -27,4 +28,5 @@ var WireSet = wire.NewSet(
|
||||
featuretoggle.RegisterAPIService,
|
||||
datasource.RegisterAPIService,
|
||||
folders.RegisterAPIService,
|
||||
query.RegisterAPIService,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user