Skip to content

Commit a8003ef

Browse files
authored
Experimental: Add query type definition and schemas (#897)
1 parent 12ee831 commit a8003ef

34 files changed

+3743
-18
lines changed

experimental/apis/README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
## APIServer APIs
2+
3+
This package aims to expose types from the plugins-sdk in the grafana apiserver.
4+
5+
Currently, the types are not useable directly so we can avoid adding a dependency on k8s.io/apimachinery
6+
until it is more necessary. See https://github.com/grafana/grafana-plugin-sdk-go/pull/909
7+
8+
The "v0alpha1" version should be considered experimental and is subject to change at any time without notice.
9+
Once it is more stable, it will be released as a versioned API (v1)
10+
11+
12+
### Codegen
13+
14+
The file [apis/data/v0alpha1/zz_generated.deepcopy.go](data/v0alpha1/zz_generated.deepcopy.go) was generated by copying the folder structure into
15+
https://github.com/grafana/grafana/tree/main/pkg/apis and then run `hack/update-codegen.sh data` in [hack scripts](https://github.com/grafana/grafana/tree/v10.3.3/hack).
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package v0alpha1
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"encoding/json"
7+
"net/http"
8+
9+
"github.com/grafana/grafana-plugin-sdk-go/backend"
10+
"github.com/grafana/grafana-plugin-sdk-go/data/utils/jsoniter"
11+
)
12+
13+
type QueryDataClient interface {
14+
QueryData(ctx context.Context, req QueryDataRequest) (int, *backend.QueryDataResponse, error)
15+
}
16+
17+
type simpleHTTPClient struct {
18+
url string
19+
client *http.Client
20+
headers map[string]string
21+
}
22+
23+
func NewQueryDataClient(url string, client *http.Client, headers map[string]string) QueryDataClient {
24+
if client == nil {
25+
client = http.DefaultClient
26+
}
27+
return &simpleHTTPClient{
28+
url: url,
29+
client: client,
30+
headers: headers,
31+
}
32+
}
33+
34+
func (c *simpleHTTPClient) QueryData(ctx context.Context, query QueryDataRequest) (int, *backend.QueryDataResponse, error) {
35+
body, err := json.Marshal(query)
36+
if err != nil {
37+
return http.StatusBadRequest, nil, err
38+
}
39+
40+
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.url, bytes.NewBuffer(body))
41+
if err != nil {
42+
return http.StatusBadRequest, nil, err
43+
}
44+
for k, v := range c.headers {
45+
req.Header.Set(k, v)
46+
}
47+
req.Header.Set("Content-Type", "application/json")
48+
49+
rsp, err := c.client.Do(req)
50+
if err != nil {
51+
return rsp.StatusCode, nil, err
52+
}
53+
defer rsp.Body.Close()
54+
55+
qdr := &backend.QueryDataResponse{}
56+
iter, err := jsoniter.Parse(jsoniter.ConfigCompatibleWithStandardLibrary, rsp.Body, 1024*10)
57+
if err == nil {
58+
err = iter.ReadVal(qdr)
59+
}
60+
return rsp.StatusCode, qdr, err
61+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package v0alpha1_test
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"net/http"
8+
"testing"
9+
10+
"github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1"
11+
"github.com/stretchr/testify/require"
12+
)
13+
14+
func TestQueryClient(t *testing.T) {
15+
t.Skip()
16+
17+
client := v0alpha1.NewQueryDataClient("http://localhost:3000/api/ds/query", nil,
18+
map[string]string{
19+
"Authorization": "Bearer XYZ",
20+
})
21+
body := `{
22+
"from": "",
23+
"to": "",
24+
"queries": [
25+
{
26+
"refId": "X",
27+
"scenarioId": "csv_content",
28+
"datasource": {
29+
"type": "grafana-testdata-datasource",
30+
"uid": "PD8C576611E62080A"
31+
},
32+
"csvContent": "a,b,c\n1,hello,true",
33+
"hide": true
34+
}
35+
]
36+
}`
37+
qdr := v0alpha1.QueryDataRequest{}
38+
err := json.Unmarshal([]byte(body), &qdr)
39+
require.NoError(t, err)
40+
41+
code, rsp, err := client.QueryData(context.Background(), qdr)
42+
require.NoError(t, err)
43+
require.Equal(t, http.StatusOK, code)
44+
45+
r, ok := rsp.Responses["X"]
46+
require.True(t, ok)
47+
48+
for _, frame := range r.Frames {
49+
txt, err := frame.StringTable(20, 10)
50+
require.NoError(t, err)
51+
fmt.Printf("%s\n", txt)
52+
}
53+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// +k8s:deepcopy-gen=package
2+
// +k8s:openapi-gen=true
3+
// +k8s:defaulter-gen=TypeMeta
4+
// +groupName=data.grafana.com
5+
6+
package v0alpha1
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package v0alpha1
2+
3+
// ObjectMeta is a struct that aims to "look" like a real kubernetes object when
4+
// written to JSON, however it does not require the pile of dependencies
5+
// This is really an internal helper until we decide which dependencies make sense
6+
// to require within the SDK
7+
type ObjectMeta struct {
8+
// The name is for k8s and description, but not used in the schema
9+
Name string `json:"name,omitempty"`
10+
// Changes indicate that *something * changed
11+
ResourceVersion string `json:"resourceVersion,omitempty"`
12+
// Timestamp
13+
CreationTimestamp string `json:"creationTimestamp,omitempty"`
14+
}
15+
16+
type TypeMeta struct {
17+
Kind string `json:"kind"` // "QueryTypeDefinitionList",
18+
APIVersion string `json:"apiVersion"` // "query.grafana.app/v0alpha1",
19+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package v0alpha1
2+
3+
import (
4+
"embed"
5+
6+
"k8s.io/kube-openapi/pkg/common"
7+
spec "k8s.io/kube-openapi/pkg/validation/spec"
8+
)
9+
10+
//go:embed query.schema.json query.definition.schema.json
11+
var f embed.FS
12+
13+
func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition {
14+
return map[string]common.OpenAPIDefinition{
15+
"github.com/grafana/grafana-plugin-sdk-go/backend.DataResponse": schemaDataResponse(ref),
16+
"github.com/grafana/grafana-plugin-sdk-go/data.Frame": schemaDataFrame(ref),
17+
"github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1.DataQuery": schemaDataQuery(ref),
18+
"github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1.QueryTypeDefinitionSpec": schemaQueryTypeDefinitionSpec(ref),
19+
}
20+
}
21+
22+
// Individual response
23+
func schemaDataResponse(_ common.ReferenceCallback) common.OpenAPIDefinition {
24+
return common.OpenAPIDefinition{
25+
Schema: spec.Schema{
26+
SchemaProps: spec.SchemaProps{
27+
Description: "todo... improve schema",
28+
Type: []string{"object"},
29+
AdditionalProperties: &spec.SchemaOrBool{Allows: true},
30+
},
31+
},
32+
}
33+
}
34+
35+
func schemaDataFrame(_ common.ReferenceCallback) common.OpenAPIDefinition {
36+
return common.OpenAPIDefinition{
37+
Schema: spec.Schema{
38+
SchemaProps: spec.SchemaProps{
39+
Description: "any object for now",
40+
Type: []string{"object"},
41+
Properties: map[string]spec.Schema{},
42+
AdditionalProperties: &spec.SchemaOrBool{Allows: true},
43+
},
44+
},
45+
}
46+
}
47+
48+
func schemaQueryTypeDefinitionSpec(_ common.ReferenceCallback) common.OpenAPIDefinition {
49+
s, _ := loadSchema("query.definition.schema.json")
50+
if s == nil {
51+
s = &spec.Schema{}
52+
}
53+
return common.OpenAPIDefinition{
54+
Schema: *s,
55+
}
56+
}
57+
58+
func schemaDataQuery(_ common.ReferenceCallback) common.OpenAPIDefinition {
59+
s, _ := DataQuerySchema()
60+
if s == nil {
61+
s = &spec.Schema{}
62+
}
63+
s.SchemaProps.Type = []string{"object"}
64+
s.SchemaProps.AdditionalProperties = &spec.SchemaOrBool{Allows: true}
65+
return common.OpenAPIDefinition{Schema: *s}
66+
}
67+
68+
// Get the cached feature list (exposed as a k8s resource)
69+
func DataQuerySchema() (*spec.Schema, error) {
70+
return loadSchema("query.schema.json")
71+
}
72+
73+
// Get the cached feature list (exposed as a k8s resource)
74+
func loadSchema(path string) (*spec.Schema, error) {
75+
body, err := f.ReadFile(path)
76+
if err != nil {
77+
return nil, err
78+
}
79+
s := &spec.Schema{}
80+
err = s.UnmarshalJSON(body)
81+
return s, err
82+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package v0alpha1
2+
3+
import (
4+
"encoding/json"
5+
"os"
6+
"testing"
7+
8+
"github.com/stretchr/testify/assert"
9+
"github.com/stretchr/testify/require"
10+
"k8s.io/kube-openapi/pkg/validation/spec"
11+
"k8s.io/kube-openapi/pkg/validation/strfmt"
12+
"k8s.io/kube-openapi/pkg/validation/validate"
13+
)
14+
15+
func TestOpenAPI(t *testing.T) {
16+
//nolint:gocritic
17+
defs := GetOpenAPIDefinitions(func(path string) spec.Ref { // (unlambda: replace ¯\_(ツ)_/¯)
18+
return spec.MustCreateRef(path) // placeholder for tests
19+
})
20+
21+
def, ok := defs["github.com/grafana/grafana-plugin-sdk-go/backend.DataResponse"]
22+
require.True(t, ok)
23+
require.Empty(t, def.Dependencies) // not yet supported!
24+
25+
validator := validate.NewSchemaValidator(&def.Schema, nil, "data", strfmt.Default)
26+
27+
body, err := os.ReadFile("./testdata/sample_query_results.json")
28+
require.NoError(t, err)
29+
unstructured := make(map[string]any)
30+
err = json.Unmarshal(body, &unstructured)
31+
require.NoError(t, err)
32+
33+
result := validator.Validate(unstructured)
34+
for _, err := range result.Errors {
35+
assert.NoError(t, err, "validation error")
36+
}
37+
for _, err := range result.Warnings {
38+
assert.NoError(t, err, "validation warning")
39+
}
40+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
{
2+
"$schema": "https://json-schema.org/draft-04/schema#",
3+
"properties": {
4+
"discriminators": {
5+
"items": {
6+
"properties": {
7+
"field": {
8+
"type": "string",
9+
"description": "DiscriminatorField is the field used to link behavior to this specific\nquery type. It is typically \"queryType\", but can be another field if necessary"
10+
},
11+
"value": {
12+
"type": "string",
13+
"description": "The discriminator value"
14+
}
15+
},
16+
"additionalProperties": false,
17+
"type": "object",
18+
"required": [
19+
"field",
20+
"value"
21+
]
22+
},
23+
"type": "array",
24+
"description": "Multiple schemas can be defined using discriminators"
25+
},
26+
"description": {
27+
"type": "string",
28+
"description": "Describe whe the query type is for"
29+
},
30+
"schema": {
31+
"$ref": "https://json-schema.org/draft-04/schema#",
32+
"type": "object",
33+
"description": "The query schema represents the properties that can be sent to the API\nIn many cases, this may be the same properties that are saved in a dashboard\nIn the case where the save model is different, we must also specify a save model"
34+
},
35+
"examples": {
36+
"items": {
37+
"properties": {
38+
"name": {
39+
"type": "string",
40+
"description": "Version identifier or empty if only one exists"
41+
},
42+
"description": {
43+
"type": "string",
44+
"description": "Optionally explain why the example is interesting"
45+
},
46+
"saveModel": {
47+
"additionalProperties": true,
48+
"type": "object",
49+
"description": "An example value saved that can be saved in a dashboard"
50+
}
51+
},
52+
"additionalProperties": false,
53+
"type": "object"
54+
},
55+
"type": "array",
56+
"description": "Examples (include a wrapper) ideally a template!"
57+
},
58+
"changelog": {
59+
"items": {
60+
"type": "string"
61+
},
62+
"type": "array",
63+
"description": "Changelog defines the changed from the previous version\nAll changes in the same version *must* be backwards compatible\nOnly notable changes will be shown here, for the full version history see git!"
64+
}
65+
},
66+
"additionalProperties": false,
67+
"type": "object",
68+
"required": [
69+
"schema",
70+
"examples"
71+
]
72+
}

0 commit comments

Comments
 (0)