QueryTypes: Add feature toggle to show query types in datasource apiservers (#88213)

* initial attempt

* show query types

* show query types

* with formatting

* with formatting

* more cleanup

* add feature toggle

* fix build
This commit is contained in:
Ryan McKinley
2024-05-23 18:46:28 +02:00
committed by GitHub
parent 006d0021e3
commit 42b0f802de
17 changed files with 590 additions and 244 deletions
@@ -0,0 +1,235 @@
package queryschema
import (
"fmt"
"strings"
"k8s.io/kube-openapi/pkg/spec3"
"k8s.io/kube-openapi/pkg/validation/spec"
"github.com/grafana/grafana-plugin-sdk-go/experimental/schemabuilder"
data "github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1"
query "github.com/grafana/grafana/pkg/apis/query/v0alpha1"
"github.com/grafana/grafana/pkg/plugins"
)
const QueryRequestSchemaKey = "QueryRequestSchema"
// const QueryPayloadSchemaKey = "QueryPayloadSchema"
// const QuerySaveModelSchemaKey = "QuerySaveModelSchema"
type OASQueryOptions struct {
Swagger *spec3.OpenAPI
PluginJSON *plugins.JSONData
QueryTypes *query.QueryTypeDefinitionList
Root string
QueryPath string // eg "namespaces/{namespace}/query/{name}"
QueryDescription string
QueryExamples map[string]*spec3.Example
}
func AddQueriesToOpenAPI(options OASQueryOptions) error {
oas := options.Swagger
root := options.Root
examples := options.QueryExamples
resourceName := query.QueryTypeDefinitionResourceInfo.GroupResource().Resource
builder := schemabuilder.QuerySchemaOptions{
PluginID: []string{""},
QueryTypes: []data.QueryTypeDefinition{},
}
if options.PluginJSON != nil {
builder.PluginID = []string{options.PluginJSON.ID}
if options.PluginJSON.AliasIDs != nil {
builder.PluginID = append(builder.PluginID, options.PluginJSON.AliasIDs...)
}
}
if options.QueryTypes != nil {
// The SDK type and api type are not the same so we just recreate it here
for _, qt := range options.QueryTypes.Items {
builder.QueryTypes = append(builder.QueryTypes, data.QueryTypeDefinition{
ObjectMeta: data.ObjectMeta{
Name: qt.Name,
},
Spec: qt.Spec,
})
}
if examples == nil {
examples = getExamples(options.QueryTypes)
}
}
// Rewrite the query path
query := oas.Paths.Paths[root+options.QueryPath]
if query != nil && query.Post != nil {
query.Post.Tags = []string{"Query"}
query.Parameters = []*spec3.Parameter{
{
ParameterProps: spec3.ParameterProps{
Name: "namespace",
In: "path",
Description: "object name and auth scope, such as for teams and projects",
Example: "default",
Required: true,
Schema: spec.StringProperty().UniqueValues(),
},
},
}
query.Post.Description = options.QueryDescription
query.Post.Parameters = nil //
query.Post.RequestBody = &spec3.RequestBody{
RequestBodyProps: spec3.RequestBodyProps{
Content: map[string]*spec3.MediaType{
"application/json": {
MediaTypeProps: spec3.MediaTypeProps{
Schema: spec.RefSchema("#/components/schemas/" + QueryRequestSchemaKey),
Examples: examples,
},
},
},
},
}
// Remove the {name} hack from from the query
if strings.HasSuffix(options.QueryPath, "/{name}") {
delete(oas.Paths.Paths, root+options.QueryPath)
oas.Paths.Paths[root+strings.TrimSuffix(options.QueryPath, "/{name}")] = query
}
}
// Update the validate endpoint
validate := oas.Paths.Paths[root+resourceName+"/{name}/validate"]
if validate != nil && validate.Post != nil {
validate.Post.Description = "Verify if a query payload matches the expected value and return a clean version"
validate.Parameters = []*spec3.Parameter{
{
ParameterProps: spec3.ParameterProps{
Name: "name",
In: "path",
Description: "The query type name, or {any}",
Example: "{any}",
Required: true,
Schema: spec.StringProperty().UniqueValues(),
},
},
}
// Accept the same payload as the query type
validate.Post.RequestBody = query.Post.RequestBody
}
// Query Request
builder.Mode = schemabuilder.SchemaTypeQueryRequest
s, err := schemabuilder.GetQuerySchema(builder)
if err != nil {
return err
}
// The schema requires some munging to pass validation
// This should likely be fixed in the upstream "GetQuerySchema" function
removeSchemaRefs(s)
s.Description = "Schema for a set of queries sent to the query method"
oas.Components.Schemas[QueryRequestSchemaKey] = s
// // Query Payload (is this useful?)
// opts.Mode = schemabuilder.SchemaTypeQueryPayload
// s, err = schemabuilder.GetQuerySchema(opts)
// if err != nil {
// return err
// }
// delete(s.ExtraProps, "$schema")
// s.Description = "Schema for a single query object including all runtime properties"
// oas.Components.Schemas[QueryPayloadSchemaKey] = s
// // Query Save Model
// opts.Mode = schemabuilder.SchemaTypeSaveModel
// s, err = schemabuilder.GetQuerySchema(opts)
// if err != nil {
// return err
// }
// s.Extensions = nil // remove the $schema
// s.Description = "Valid save model for a single query target"
// oas.Components.Schemas[QuerySaveModelSchemaKey] = s
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
}
func removeSchemaRefs(s *spec.Schema) {
if s == nil {
return
}
if s.Schema != "" {
s.Schema = ""
}
// Examples is invalid -- only use the first example
examples, ok := s.ExtraProps["examples"]
if ok && examples != nil {
//fmt.Printf("TODO, use reflection to get first element from: %+v\n", examples)
//s.Example = examples[0]
delete(s.ExtraProps, "examples")
}
removeSchemaRefs(s.Not)
for idx := range s.AllOf {
removeSchemaRefs(&s.AllOf[idx])
}
for idx := range s.AnyOf {
removeSchemaRefs(&s.AnyOf[idx])
}
for k := range s.Properties {
v := s.Properties[k]
removeSchemaRefs(&v)
s.Properties[k] = v
}
if s.Items != nil {
removeSchemaRefs(s.Items.Schema)
for idx := range s.Items.Schemas {
removeSchemaRefs(&s.Items.Schemas[idx])
}
}
if s.AdditionalProperties != nil {
removeSchemaRefs(s.AdditionalProperties.Schema)
}
if s.AdditionalItems != nil {
removeSchemaRefs(s.AdditionalItems.Schema)
}
}
@@ -0,0 +1,147 @@
package queryschema
import (
"context"
"net/http"
"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"
"github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1"
common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
query "github.com/grafana/grafana/pkg/apis/query/v0alpha1"
)
var (
_ rest.Storage = (*queryTypeStorage)(nil)
_ rest.Scoper = (*queryTypeStorage)(nil)
_ rest.SingularNameProvider = (*queryTypeStorage)(nil)
_ rest.Lister = (*queryTypeStorage)(nil)
_ rest.Getter = (*queryTypeStorage)(nil)
// The connectors
_ = rest.Connecter(&queryValidationREST{})
)
type queryTypeStorage struct {
resourceInfo *common.ResourceInfo
tableConverter rest.TableConvertor
registry query.QueryTypeDefinitionList
}
type queryValidationREST struct {
qt *queryTypeStorage
}
func RegisterQueryTypes(queryTypes *query.QueryTypeDefinitionList, storage map[string]rest.Storage) error {
if queryTypes == nil {
return nil // NO error
}
resourceInfo := query.QueryTypeDefinitionResourceInfo
store := &queryTypeStorage{
resourceInfo: &resourceInfo,
tableConverter: rest.NewDefaultTableConvertor(resourceInfo.GroupResource()),
registry: *queryTypes,
}
// Supports list+get for all query types
storage[resourceInfo.StoragePath()] = store
// Adds a query validation endpoint for each query type
// We will also support the "<any>" or "*" name
storage[resourceInfo.StoragePath("validate")] = &queryValidationREST{store}
return nil
}
func (s *queryTypeStorage) New() runtime.Object {
return s.resourceInfo.NewFunc()
}
func (s *queryTypeStorage) Destroy() {}
func (s *queryTypeStorage) NamespaceScoped() bool {
return false
}
func (s *queryTypeStorage) GetSingularName() string {
return s.resourceInfo.GetSingularName()
}
func (s *queryTypeStorage) NewList() runtime.Object {
return s.resourceInfo.NewListFunc()
}
func (s *queryTypeStorage) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) {
return s.tableConverter.ConvertToTable(ctx, object, tableOptions)
}
func (s *queryTypeStorage) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) {
return &s.registry, nil
}
func (s *queryTypeStorage) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) {
for idx, qt := range s.registry.Items {
if qt.Name == name {
return &s.registry.Items[idx], nil
}
}
return nil, s.resourceInfo.NewNotFound(name)
}
//----------------------------------------------------
// The validation processor
//----------------------------------------------------
func (r *queryValidationREST) New() runtime.Object {
return &query.QueryDataRequest{}
}
func (r *queryValidationREST) Destroy() {
}
func (r *queryValidationREST) ConnectMethods() []string {
return []string{"POST"}
}
func (r *queryValidationREST) ProducesMIMETypes(verb string) []string {
return []string{"application/json"}
}
func (r *queryValidationREST) ProducesObject(verb string) interface{} {
return &query.QueryDataRequest{}
}
func (r *queryValidationREST) NewConnectOptions() (runtime.Object, bool, string) {
return nil, false, "" // true means you can use the trailing path as a variable
}
func (r *queryValidationREST) Connect(ctx context.Context, name string, opts runtime.Object, responder rest.Responder) (http.Handler, error) {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
// TODO -- validate/mutate the query
// should we return the DQR, or raw validation response?
qdr := &query.QueryDataRequest{
QueryDataRequest: v0alpha1.QueryDataRequest{
Debug: true,
},
}
if name == "*" || name == "{any}" {
qdr.Queries = []v0alpha1.DataQuery{
v0alpha1.NewDataQuery(
map[string]any{
"refId": "???",
"HELLO": "world",
"TODO": "parse any query",
},
),
}
}
responder.Object(http.StatusOK, qdr)
}), nil
}