QueryService: Use types from sdk (#84029)
This commit is contained in:
@@ -3,8 +3,12 @@ package datasource
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
data "github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/experimental/schemabuilder"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
@@ -15,10 +19,9 @@ import (
|
||||
genericapiserver "k8s.io/apiserver/pkg/server"
|
||||
openapi "k8s.io/kube-openapi/pkg/common"
|
||||
"k8s.io/kube-openapi/pkg/spec3"
|
||||
"k8s.io/kube-openapi/pkg/validation/spec"
|
||||
"k8s.io/utils/strings/slices"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
|
||||
common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
|
||||
datasource "github.com/grafana/grafana/pkg/apis/datasource/v0alpha1"
|
||||
query "github.com/grafana/grafana/pkg/apis/query/v0alpha1"
|
||||
@@ -30,6 +33,9 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
|
||||
)
|
||||
|
||||
const QueryRequestSchemaKey = "QueryRequestSchema"
|
||||
const QueryPayloadSchemaKey = "QueryPayloadSchema"
|
||||
|
||||
var _ builder.APIGroupBuilder = (*DataSourceAPIBuilder)(nil)
|
||||
|
||||
// DataSourceAPIBuilder is used just so wire has something unique to return
|
||||
@@ -41,6 +47,7 @@ type DataSourceAPIBuilder struct {
|
||||
datasources PluginDatasourceProvider
|
||||
contextProvider PluginContextWrapper
|
||||
accessControl accesscontrol.AccessControl
|
||||
queryTypes *query.QueryTypeDefinitionList
|
||||
}
|
||||
|
||||
func RegisterAPIService(
|
||||
@@ -62,6 +69,7 @@ func RegisterAPIService(
|
||||
all := pluginStore.Plugins(context.Background(), plugins.TypeDataSource)
|
||||
ids := []string{
|
||||
"grafana-testdata-datasource",
|
||||
// "prometheus",
|
||||
}
|
||||
|
||||
for _, ds := range all {
|
||||
@@ -123,6 +131,7 @@ func addKnownTypes(scheme *runtime.Scheme, gv schema.GroupVersion) {
|
||||
&datasource.HealthCheckResult{},
|
||||
&unstructured.Unstructured{},
|
||||
// Query handler
|
||||
&query.QueryDataRequest{},
|
||||
&query.QueryDataResponse{},
|
||||
&metav1.Status{},
|
||||
)
|
||||
@@ -238,15 +247,108 @@ func (b *DataSourceAPIBuilder) PostProcessOpenAPI(oas *spec3.OpenAPI) (*spec3.Op
|
||||
// Hide the ability to list all connections across tenants
|
||||
delete(oas.Paths.Paths, root+b.connectionResourceInfo.GroupResource().Resource)
|
||||
|
||||
var err error
|
||||
opts := schemabuilder.QuerySchemaOptions{
|
||||
PluginID: []string{b.pluginJSON.ID},
|
||||
QueryTypes: []data.QueryTypeDefinition{},
|
||||
Mode: schemabuilder.SchemaTypeQueryPayload,
|
||||
}
|
||||
if b.pluginJSON.AliasIDs != nil {
|
||||
opts.PluginID = append(opts.PluginID, b.pluginJSON.AliasIDs...)
|
||||
}
|
||||
if b.queryTypes != nil {
|
||||
for _, qt := range b.queryTypes.Items {
|
||||
// The SDK type and api type are not the same so we recreate it here
|
||||
opts.QueryTypes = append(opts.QueryTypes, data.QueryTypeDefinition{
|
||||
ObjectMeta: data.ObjectMeta{
|
||||
Name: qt.Name,
|
||||
},
|
||||
Spec: qt.Spec,
|
||||
})
|
||||
}
|
||||
}
|
||||
oas.Components.Schemas[QueryPayloadSchemaKey], err = schemabuilder.GetQuerySchema(opts)
|
||||
if err != nil {
|
||||
return oas, err
|
||||
}
|
||||
opts.Mode = schemabuilder.SchemaTypeQueryRequest
|
||||
oas.Components.Schemas[QueryRequestSchemaKey], err = schemabuilder.GetQuerySchema(opts)
|
||||
if err != nil {
|
||||
return oas, err
|
||||
}
|
||||
|
||||
// Update the request object
|
||||
sub := oas.Paths.Paths[root+"namespaces/{namespace}/connections/{name}/query"]
|
||||
if sub != nil && sub.Post != nil {
|
||||
sub.Post.Description = "Execute queries"
|
||||
sub.Post.RequestBody = &spec3.RequestBody{
|
||||
RequestBodyProps: spec3.RequestBodyProps{
|
||||
Required: true,
|
||||
Content: map[string]*spec3.MediaType{
|
||||
"application/json": {
|
||||
MediaTypeProps: spec3.MediaTypeProps{
|
||||
Schema: spec.RefSchema("#/components/schemas/" + QueryRequestSchemaKey),
|
||||
Examples: getExamples(b.queryTypes),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
okrsp, ok := sub.Post.Responses.StatusCodeResponses[200]
|
||||
if ok {
|
||||
sub.Post.Responses.StatusCodeResponses[http.StatusMultiStatus] = &spec3.Response{
|
||||
ResponseProps: spec3.ResponseProps{
|
||||
Description: "Query executed, but errors may exist in the datasource. See the payload for more details.",
|
||||
Content: okrsp.Content,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The root API discovery list
|
||||
sub := oas.Paths.Paths[root]
|
||||
sub = oas.Paths.Paths[root]
|
||||
if sub != nil && sub.Get != nil {
|
||||
sub.Get.Tags = []string{"API Discovery"} // sorts first in the list
|
||||
}
|
||||
return oas, nil
|
||||
return oas, err
|
||||
}
|
||||
|
||||
// Register additional routes with the server
|
||||
func (b *DataSourceAPIBuilder) GetAPIRoutes() *builder.APIRoutes {
|
||||
return nil
|
||||
}
|
||||
|
||||
func getExamples(queryTypes *query.QueryTypeDefinitionList) map[string]*spec3.Example {
|
||||
if queryTypes == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
tr := data.TimeRange{From: "now-1h", To: "now"}
|
||||
examples := map[string]*spec3.Example{}
|
||||
for _, queryType := range queryTypes.Items {
|
||||
for idx, example := range queryType.Spec.Examples {
|
||||
q := data.NewDataQuery(example.SaveModel.Object)
|
||||
q.RefID = "A"
|
||||
for _, dis := range queryType.Spec.Discriminators {
|
||||
_ = q.Set(dis.Field, dis.Value)
|
||||
}
|
||||
if q.MaxDataPoints < 1 {
|
||||
q.MaxDataPoints = 1000
|
||||
}
|
||||
if q.IntervalMS < 1 {
|
||||
q.IntervalMS = 5000 // 5s
|
||||
}
|
||||
examples[fmt.Sprintf("%s-%d", example.Name, idx)] = &spec3.Example{
|
||||
ExampleProps: spec3.ExampleProps{
|
||||
Summary: example.Name,
|
||||
Description: example.Description,
|
||||
Value: data.QueryDataRequest{
|
||||
TimeRange: tr,
|
||||
Queries: []data.DataQuery{q},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
return examples
|
||||
}
|
||||
|
||||
@@ -2,16 +2,15 @@ package datasource
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
data "github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apiserver/pkg/registry/rest"
|
||||
|
||||
query "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"
|
||||
)
|
||||
@@ -20,28 +19,33 @@ type subQueryREST struct {
|
||||
builder *DataSourceAPIBuilder
|
||||
}
|
||||
|
||||
var _ = rest.Connecter(&subQueryREST{})
|
||||
var (
|
||||
_ rest.Storage = (*subQueryREST)(nil)
|
||||
_ rest.Connecter = (*subQueryREST)(nil)
|
||||
_ rest.StorageMetadata = (*subQueryREST)(nil)
|
||||
)
|
||||
|
||||
func (r *subQueryREST) New() runtime.Object {
|
||||
// This is added as the "ResponseType" regarless what ProducesObject() says :)
|
||||
return &query.QueryDataResponse{}
|
||||
}
|
||||
|
||||
func (r *subQueryREST) Destroy() {}
|
||||
|
||||
func (r *subQueryREST) ProducesMIMETypes(verb string) []string {
|
||||
return []string{"application/json"} // and parquet!
|
||||
}
|
||||
|
||||
func (r *subQueryREST) ProducesObject(verb string) interface{} {
|
||||
return &query.QueryDataResponse{}
|
||||
}
|
||||
|
||||
func (r *subQueryREST) ConnectMethods() []string {
|
||||
return []string{"POST"}
|
||||
}
|
||||
|
||||
func (r *subQueryREST) NewConnectOptions() (runtime.Object, bool, string) {
|
||||
return nil, false, ""
|
||||
}
|
||||
|
||||
func (r *subQueryREST) readQueries(req *http.Request) ([]backend.DataQuery, *query.DataSourceRef, error) {
|
||||
reqDTO := query.GenericQueryRequest{}
|
||||
if err := web.Bind(req, &reqDTO); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return legacydata.ToDataSourceQueries(reqDTO)
|
||||
return nil, false, "" // true means you can use the trailing path as a variable
|
||||
}
|
||||
|
||||
func (r *subQueryREST) Connect(ctx context.Context, name string, opts runtime.Object, responder rest.Responder) (http.Handler, error) {
|
||||
@@ -49,59 +53,35 @@ func (r *subQueryREST) Connect(ctx context.Context, name string, opts runtime.Ob
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ctx = backend.WithGrafanaConfig(ctx, pluginCtx.GrafanaConfig)
|
||||
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
queries, dsRef, err := r.readQueries(req)
|
||||
dqr := data.QueryDataRequest{}
|
||||
err := web.Bind(req, &dqr)
|
||||
if err != nil {
|
||||
responder.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
queries, dsRef, err := legacydata.ToDataSourceQueries(dqr)
|
||||
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
|
||||
responder.Error(fmt.Errorf("expected query body datasource and request to match"))
|
||||
}
|
||||
|
||||
qdr, err := r.builder.client.QueryData(ctx, &backend.QueryDataRequest{
|
||||
PluginContext: pluginCtx,
|
||||
ctx = backend.WithGrafanaConfig(ctx, pluginCtx.GrafanaConfig)
|
||||
rsp, err := r.builder.client.QueryData(ctx, &backend.QueryDataRequest{
|
||||
Queries: queries,
|
||||
PluginContext: pluginCtx,
|
||||
})
|
||||
if err != nil {
|
||||
responder.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
responder.Object(query.GetResponseCode(rsp),
|
||||
&query.QueryDataResponse{QueryDataResponse: *rsp},
|
||||
)
|
||||
}), nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user