From ea38c4ad5ad512a85d6ed835939ab24bfd91e49a Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Thu, 3 Jul 2025 13:03:41 -0700 Subject: [PATCH] custom jsonschema for testdata --- pkg/apis/datasource/v0alpha1/config.go | 7 + pkg/apis/datasource/v0alpha1/doc.go | 3 - pkg/apis/datasource/v0alpha1/openapi.go | 35 +++++ pkg/apis/datasource/v0alpha1/types.go | 2 + .../v0alpha1/zz_generated.defaults.go | 19 --- pkg/apis/secret/v0alpha1/inline.go | 12 +- .../secret/v0alpha1/zz_generated.openapi.go | 14 +- ...enerated.openapi_violation_exceptions.list | 1 - pkg/registry/apis/datasource/converter.go | 29 ++-- pkg/registry/apis/datasource/openapi.go | 131 ++++++++++++++++++ pkg/registry/apis/datasource/register.go | 70 ---------- 11 files changed, 207 insertions(+), 116 deletions(-) create mode 100644 pkg/apis/datasource/v0alpha1/openapi.go delete mode 100644 pkg/apis/datasource/v0alpha1/zz_generated.defaults.go create mode 100644 pkg/registry/apis/datasource/openapi.go diff --git a/pkg/apis/datasource/v0alpha1/config.go b/pkg/apis/datasource/v0alpha1/config.go index 5b8c026059a..f34db889cbe 100644 --- a/pkg/apis/datasource/v0alpha1/config.go +++ b/pkg/apis/datasource/v0alpha1/config.go @@ -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"` diff --git a/pkg/apis/datasource/v0alpha1/doc.go b/pkg/apis/datasource/v0alpha1/doc.go index 43d5863c3c0..e7a4bf9c23e 100644 --- a/pkg/apis/datasource/v0alpha1/doc.go +++ b/pkg/apis/datasource/v0alpha1/doc.go @@ -1,6 +1,3 @@ -// +k8s:deepcopy-gen=package -// +k8s:openapi-gen=true -// +k8s:defaulter-gen=TypeMeta // +groupName=datasource.grafana.com package v0alpha1 diff --git a/pkg/apis/datasource/v0alpha1/openapi.go b/pkg/apis/datasource/v0alpha1/openapi.go new file mode 100644 index 00000000000..3c544211c5c --- /dev/null +++ b/pkg/apis/datasource/v0alpha1/openapi.go @@ -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"` +} diff --git a/pkg/apis/datasource/v0alpha1/types.go b/pkg/apis/datasource/v0alpha1/types.go index 53dc1e8821e..f41ce9f0757 100644 --- a/pkg/apis/datasource/v0alpha1/types.go +++ b/pkg/apis/datasource/v0alpha1/types.go @@ -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"` diff --git a/pkg/apis/datasource/v0alpha1/zz_generated.defaults.go b/pkg/apis/datasource/v0alpha1/zz_generated.defaults.go deleted file mode 100644 index 238fc2f4edc..00000000000 --- a/pkg/apis/datasource/v0alpha1/zz_generated.defaults.go +++ /dev/null @@ -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 -} diff --git a/pkg/apis/secret/v0alpha1/inline.go b/pkg/apis/secret/v0alpha1/inline.go index c852a90d026..c9557503500 100644 --- a/pkg/apis/secret/v0alpha1/inline.go +++ b/pkg/apis/secret/v0alpha1/inline.go @@ -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 diff --git a/pkg/apis/secret/v0alpha1/zz_generated.openapi.go b/pkg/apis/secret/v0alpha1/zz_generated.openapi.go index 0b2d71e8fb9..a4c4a7ff7e6 100644 --- a/pkg/apis/secret/v0alpha1/zz_generated.openapi.go +++ b/pkg/apis/secret/v0alpha1/zz_generated.openapi.go @@ -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"}, }, }, } diff --git a/pkg/apis/secret/v0alpha1/zz_generated.openapi_violation_exceptions.list b/pkg/apis/secret/v0alpha1/zz_generated.openapi_violation_exceptions.list index 0c626eea748..679e6938ae0 100644 --- a/pkg/apis/secret/v0alpha1/zz_generated.openapi_violation_exceptions.list +++ b/pkg/apis/secret/v0alpha1/zz_generated.openapi_violation_exceptions.list @@ -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 diff --git a/pkg/registry/apis/datasource/converter.go b/pkg/registry/apis/datasource/converter.go index ab9cc369420..53f832489af 100644 --- a/pkg/registry/apis/datasource/converter.go +++ b/pkg/registry/apis/datasource/converter.go @@ -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 } diff --git a/pkg/registry/apis/datasource/openapi.go b/pkg/registry/apis/datasource/openapi.go new file mode 100644 index 00000000000..993ccca3674 --- /dev/null +++ b/pkg/registry/apis/datasource/openapi.go @@ -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 +} diff --git a/pkg/registry/apis/datasource/register.go b/pkg/registry/apis/datasource/register.go index 014e4a88ad8..8a540230a64 100644 --- a/pkg/registry/apis/datasource/register.go +++ b/pkg/registry/apis/datasource/register.go @@ -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 -}