custom jsonschema for testdata

This commit is contained in:
Ryan McKinley
2025-07-03 13:03:41 -07:00
parent 01926ab3c8
commit ea38c4ad5a
11 changed files with 207 additions and 116 deletions
+7
View File
@@ -7,6 +7,8 @@ import (
secret "github.com/grafana/grafana/pkg/apis/secret/v0alpha1"
)
// +k8s:deepcopy-gen=true
// +k8s:openapi-gen=true
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type DataSource struct {
metav1.TypeMeta `json:",inline"`
@@ -20,6 +22,7 @@ type DataSource struct {
}
// DsAccess represents how the datasource connects to the remote service
// +k8s:openapi-gen=true
// +enum
type DsAccess string
@@ -36,6 +39,8 @@ func (dsa DsAccess) String() string {
return string(dsa)
}
// +k8s:deepcopy-gen=true
// +k8s:openapi-gen=true
type DataSourceSpec struct {
// The diplay name (previously saved as the "name" property)
Title string `json:"title"`
@@ -57,6 +62,8 @@ type DataSourceSpec struct {
JsonData common.Unstructured `json:"jsonData,omitzero"`
}
// +k8s:deepcopy-gen=true
// +k8s:openapi-gen=true
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type DataSourceList struct {
metav1.TypeMeta `json:",inline"`
-3
View File
@@ -1,6 +1,3 @@
// +k8s:deepcopy-gen=package
// +k8s:openapi-gen=true
// +k8s:defaulter-gen=TypeMeta
// +groupName=datasource.grafana.com
package v0alpha1
+35
View File
@@ -0,0 +1,35 @@
package v0alpha1
import (
"k8s.io/kube-openapi/pkg/spec3"
"k8s.io/kube-openapi/pkg/validation/spec"
)
// Optional extensions for an explict datasource type
type DataSourceOpenAPIExtension struct {
// When specified, this will replace the default spec
DataSourceSpec *spec.Schema `json:"spec,omitempty"`
// The raw value is never returned in an API response
SecureValues []SecureValueInfo `json:"secureValues"`
// Additional Schemas added to the response
Schemas map[string]*spec.Schema `json:"schemas,omitempty"`
// Resource routes
Routes *spec3.Paths `json:"routes,omitempty"`
// Proxy routes
Proxy *spec3.Paths `json:"proxy,omitempty"`
}
type SecureValueInfo struct {
// The key
Key string `json:"string"`
// Description
Description string `json:"description,omitempty"`
// Required secure values
Required bool `json:"required,omitempty"`
}
+2
View File
@@ -6,6 +6,8 @@ import (
common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
)
// +k8s:deepcopy-gen=true
// +k8s:openapi-gen=true
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type HealthCheckResult struct {
metav1.TypeMeta `json:",inline"`
@@ -1,19 +0,0 @@
//go:build !ignore_autogenerated
// +build !ignore_autogenerated
// SPDX-License-Identifier: AGPL-3.0-only
// Code generated by defaulter-gen. DO NOT EDIT.
package v0alpha1
import (
runtime "k8s.io/apimachinery/pkg/runtime"
)
// RegisterDefaults adds defaulters functions to the given scheme.
// Public to allow building arbitrary schemes.
// All generated defaulters are covering - they call all nested defaulters.
func RegisterDefaults(scheme *runtime.Scheme) error {
return nil
}
+6 -6
View File
@@ -3,19 +3,19 @@ package v0alpha1
// Access secure values inside any resource
// +k8s:openapi-gen=true
type InlineSecureValue struct {
// Create a secure value
// Create a secure value -- this is only used for POST/PUT
// +k8s:validation:minLength=1
// +k8s:validation:maxLength=24576
Create ExposedSecureValue `json:"create,omitempty"`
// Reference a shared secret (enterprise only)
Reference string `json:"ref,omitempty"`
// Name in the secret service (reference)
Name string `json:"name,omitempty"`
// The resolved UID within the secret service
UID string `json:"uid,omitempty"`
// The secret is shared (enterprise only)
Shared bool `json:"shared,omitempty"`
// Remove this value -- cascading delete to the secret service if necessary
Remove bool `json:"remove,omitzero"`
Remove bool `json:"remove,omitempty,omitzero"`
}
// Collection of secure values
@@ -354,37 +354,35 @@ func schema_pkg_apis_secret_v0alpha1_InlineSecureValue(ref common.ReferenceCallb
Properties: map[string]spec.Schema{
"create": {
SchemaProps: spec.SchemaProps{
Description: "Create a secure value",
Description: "Create a secure value -- this is only used for POST/PUT",
MinLength: ptr.To[int64](1),
MaxLength: ptr.To[int64](24576),
Type: []string{"string"},
Format: "",
},
},
"ref": {
"name": {
SchemaProps: spec.SchemaProps{
Description: "Reference a shared secret (enterprise only)",
Description: "Name in the secret service (reference)",
Type: []string{"string"},
Format: "",
},
},
"uid": {
"shared": {
SchemaProps: spec.SchemaProps{
Description: "The resolved UID within the secret service",
Type: []string{"string"},
Description: "The secret is shared (enterprise only)",
Type: []string{"boolean"},
Format: "",
},
},
"remove": {
SchemaProps: spec.SchemaProps{
Description: "Remove this value -- cascading delete to the secret service if necessary",
Default: false,
Type: []string{"boolean"},
Format: "",
},
},
},
Required: []string{"remove"},
},
},
}
@@ -3,7 +3,6 @@ API rule violation: names_match,github.com/grafana/grafana/pkg/apis/secret/v0alp
API rule violation: names_match,github.com/grafana/grafana/pkg/apis/secret/v0alpha1,AzureCredentials,ClientID
API rule violation: names_match,github.com/grafana/grafana/pkg/apis/secret/v0alpha1,AzureCredentials,TenantID
API rule violation: names_match,github.com/grafana/grafana/pkg/apis/secret/v0alpha1,GCPCredentials,ProjectID
API rule violation: names_match,github.com/grafana/grafana/pkg/apis/secret/v0alpha1,InlineSecureValue,Reference
API rule violation: names_match,github.com/grafana/grafana/pkg/apis/secret/v0alpha1,KeeperSpec,Azure
API rule violation: names_match,github.com/grafana/grafana/pkg/apis/secret/v0alpha1,KeeperSpec,HashiCorp
API rule violation: names_match,github.com/grafana/grafana/pkg/apis/secret/v0alpha1,SecureValueStatus,ExternalID
+20 -9
View File
@@ -1,6 +1,8 @@
package datasource
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"strconv"
"strings"
@@ -64,8 +66,15 @@ func (r *converter) asDataSource(ds *datasources.DataSource) (*datasourceV0.Data
if ds.SecureJsonData != nil {
cfg.Secure = make(secretV0.InlineSecureValues)
for k := range ds.SecureJsonData {
h := sha256.New()
h.Write([]byte(ds.Type)) // group+resource
h.Write([]byte("|"))
h.Write([]byte(ds.UID)) // name
h.Write([]byte("|"))
h.Write([]byte(k)) // property
n := hex.EncodeToString(h.Sum(nil))
cfg.Secure[k] = secretV0.InlineSecureValue{
UID: "????", // ????
Name: "@" + n[0:10], // ??????
}
}
}
@@ -103,9 +112,8 @@ func (r *converter) toAddCommand(ds *datasourceV0.DataSource) (*datasources.AddD
cmd.JsonData = simplejson.NewFromAny(ds.Spec.JsonData.Object)
}
cmd.SecureJsonData = toSecureJsonData(ds)
return cmd, nil
cmd.SecureJsonData, err = toSecureJsonData(ds)
return cmd, err
}
func (r *converter) toUpdateCommand(ds *datasourceV0.DataSource) (*datasources.UpdateDataSourceCommand, error) {
@@ -140,17 +148,20 @@ func (r *converter) toUpdateCommand(ds *datasourceV0.DataSource) (*datasources.U
if len(ds.Spec.JsonData.Object) > 0 {
cmd.JsonData = simplejson.NewFromAny(ds.Spec.JsonData.Object)
}
cmd.SecureJsonData = toSecureJsonData(ds)
return cmd, nil
cmd.SecureJsonData, err = toSecureJsonData(ds)
return cmd, err
}
func toSecureJsonData(ds *datasourceV0.DataSource) map[string]string {
func toSecureJsonData(ds *datasourceV0.DataSource) (map[string]string, error) {
if ds == nil || len(ds.Secure) < 1 {
return nil
return nil, nil
}
secure := map[string]string{}
for k, v := range ds.Secure {
if v.Shared {
return nil, fmt.Errorf("shared secrets not yet supported (%s)", k)
}
if v.Create != "" {
secure[k] = v.Create.DangerouslyExposeAndConsumeValue()
}
@@ -158,5 +169,5 @@ func toSecureJsonData(ds *datasourceV0.DataSource) map[string]string {
secure[k] = "" // Weirdly, this is the best we can do with the legacy API :(
}
}
return secure
return secure, nil
}
+131
View File
@@ -0,0 +1,131 @@
package datasource
import (
"fmt"
"maps"
"k8s.io/kube-openapi/pkg/spec3"
"k8s.io/kube-openapi/pkg/validation/spec"
datasourceV0 "github.com/grafana/grafana/pkg/apis/datasource/v0alpha1"
secretsV0 "github.com/grafana/grafana/pkg/apis/secret/v0alpha1"
"github.com/grafana/grafana/pkg/registry/apis/query/queryschema"
)
func (b *DataSourceAPIBuilder) PostProcessOpenAPI(oas *spec3.OpenAPI) (*spec3.OpenAPI, error) {
// The plugin description
oas.Info.Description = b.pluginJSON.Info.Description
// The root api URL
root := "/apis/" + b.datasourceResourceInfo.GroupVersion().String() + "/"
// Add queries to the request properties
err := queryschema.AddQueriesToOpenAPI(queryschema.OASQueryOptions{
Swagger: oas,
PluginJSON: &b.pluginJSON,
QueryTypes: b.queryTypes,
Root: root,
QueryPath: "namespaces/{namespace}/datasources/{name}/query",
QueryDescription: fmt.Sprintf("Query the %s datasources", b.pluginJSON.Name),
})
// Set explicit apiVersion and kind on the datasource
ds, ok := oas.Components.Schemas["com.github.grafana.grafana.pkg.apis.datasource.v0alpha1.DataSource"]
if !ok {
return nil, fmt.Errorf("missing DS type")
}
ds.Properties["apiVersion"] = *spec.StringProperty().WithEnum(b.GetGroupVersion().String())
ds.Properties["kind"] = *spec.StringProperty().WithEnum("DataSource")
custom, err := getCustomOpenAPI(b.pluginJSON.ID)
if err != nil {
return nil, err
}
if custom == nil {
return oas, nil // nothing special
}
// Add custom schemas
maps.Copy(oas.Components.Schemas, custom.Schemas)
// Replace the generic DataSourceSpec with the explicit one
if custom.DataSourceSpec != nil {
oas.Components.Schemas["DataSourceSpec"] = custom.DataSourceSpec
ds.Properties["spec"] = spec.Schema{
SchemaProps: spec.SchemaProps{
Ref: spec.MustCreateRef("#/components/schemas/DataSourceSpec"),
},
}
}
if custom.SecureValues != nil {
example := secretsV0.InlineSecureValues{}
ref := spec.MustCreateRef("#/components/schemas/com.github.grafana.grafana.pkg.apis.secret.v0alpha1.InlineSecureValue")
secure := &spec.Schema{
SchemaProps: spec.SchemaProps{
Properties: make(map[string]spec.Schema),
AdditionalProperties: &spec.SchemaOrBool{Allows: false},
}}
secure.Description = "custom secure value definition"
for _, v := range custom.SecureValues {
secure.Properties[v.Key] = spec.Schema{
SchemaProps: spec.SchemaProps{
Description: v.Description,
Ref: ref,
},
}
if v.Required {
secure.Required = append(secure.Required, v.Key)
example[v.Key] = secretsV0.InlineSecureValue{Create: "***"}
}
}
if len(example) > 0 {
secure.Example = example
}
// Link the explicit secure values in the resource
oas.Components.Schemas["SecureValues"] = secure
ds.Properties["secure"] = spec.Schema{
SchemaProps: spec.SchemaProps{
Ref: spec.MustCreateRef("#/components/schemas/SecureValues"),
},
}
}
return oas, err
}
func getCustomOpenAPI(plugin string) (*datasourceV0.DataSourceOpenAPIExtension, error) {
if plugin == "grafana-testdata-datasource" {
oas := &datasourceV0.DataSourceOpenAPIExtension{
SecureValues: []datasourceV0.SecureValueInfo{{
Key: "aaa",
Description: "describe aaa",
Required: true,
}, {
Key: "bbb",
Description: "describe bbb",
}},
}
// Dummy spec
p := &spec.Schema{} //SchemaProps: spec.SchemaProps{Type: []string{"object"}}}
p.Description = "HELLO!"
p.Required = []string{"url"}
p.AdditionalProperties = &spec.SchemaOrBool{Allows: false}
p.Properties = map[string]spec.Schema{
"url": *spec.StringProperty(),
"str": *spec.StringProperty(), // ??? must this be under jsonData?
"int64": *spec.Int64Property(), // ??? must this be under jsonData?
}
p.Example = map[string]any{
"url": "http://xxxx",
"int64": 1234,
}
oas.DataSourceSpec = p
return oas, nil
}
return nil, nil
}
-70
View File
@@ -3,7 +3,6 @@ package datasource
import (
"context"
"encoding/json"
"fmt"
"github.com/prometheus/client_golang/prometheus"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -13,15 +12,12 @@ import (
"k8s.io/apiserver/pkg/registry/rest"
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"
"github.com/grafana/grafana/pkg/apimachinery/utils"
datasourceV0 "github.com/grafana/grafana/pkg/apis/datasource/v0alpha1"
queryV0 "github.com/grafana/grafana/pkg/apis/query/v0alpha1"
secretsV0 "github.com/grafana/grafana/pkg/apis/secret/v0alpha1"
grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/plugins"
@@ -261,69 +257,3 @@ func (b *DataSourceAPIBuilder) GetOpenAPIDefinitions() openapi.GetOpenAPIDefinit
return defs
}
}
func (b *DataSourceAPIBuilder) PostProcessOpenAPI(oas *spec3.OpenAPI) (*spec3.OpenAPI, error) {
// The plugin description
oas.Info.Description = b.pluginJSON.Info.Description
// The root api URL
root := "/apis/" + b.datasourceResourceInfo.GroupVersion().String() + "/"
// Add queries to the request properties
err := queryschema.AddQueriesToOpenAPI(queryschema.OASQueryOptions{
Swagger: oas,
PluginJSON: &b.pluginJSON,
QueryTypes: b.queryTypes,
Root: root,
QueryPath: "namespaces/{namespace}/datasources/{name}/query",
QueryDescription: fmt.Sprintf("Query the %s datasources", b.pluginJSON.Name),
})
ds, ok := oas.Components.Schemas["com.github.grafana.grafana.pkg.apis.datasource.v0alpha1.DataSource"]
if !ok {
return nil, fmt.Errorf("missing DS type")
}
// Spec
p := spec.MapProperty(nil)
p.Description = "HELLO!"
p.Required = []string{"x", "y"}
p.Example = map[string]any{
"x": 10,
"y": "hello",
"url": "http://xxxx",
}
oas.Components.Schemas["DataSourceSpec"] = p
ds.Properties["spec"] = spec.Schema{
SchemaProps: spec.SchemaProps{
Ref: spec.MustCreateRef("#/components/schemas/DataSourceSpec"),
},
}
// Secure fields
p = spec.MapProperty(&spec.Schema{
SchemaProps: spec.SchemaProps{
Ref: spec.MustCreateRef("#/components/schemas/com.github.grafana.grafana.pkg.apis.secret.v0alpha1.InlineSecureValue"),
},
})
p.Required = []string{"a", "b"}
oas.Components.Schemas["SecureFields"] = p
ds.Properties["secure"] = spec.Schema{
SchemaProps: spec.SchemaProps{
Ref: spec.MustCreateRef("#/components/schemas/SecureFields"),
},
}
p.Example = secretsV0.InlineSecureValues{
"a": secretsV0.InlineSecureValue{
Create: "ExampleA",
},
"b": secretsV0.InlineSecureValue{
Create: "ExampleB",
},
}
ds.Properties["apiVersion"] = *spec.StringProperty().WithEnum(b.GetGroupVersion().String())
ds.Properties["kind"] = *spec.StringProperty().WithEnum("DataSource")
return oas, err
}