diff --git a/go.work.sum b/go.work.sum index 5422650d2c7..d7e4f2a81f2 100644 --- a/go.work.sum +++ b/go.work.sum @@ -1485,7 +1485,6 @@ github.com/twmb/murmur3 v1.1.8 h1:8Yt9taO/WN3l08xErzjeschgZU2QSrwm1kclYq+0aRg= github.com/twmb/murmur3 v1.1.8/go.mod h1:Qq/R7NUyOfr65zD+6Q5IHKsJLwP7exErjN6lyyq3OSQ= github.com/uber-go/atomic v1.4.0 h1:yOuPqEq4ovnhEjpHmfFwsqBXDYbQeT6Nb0bwD6XnD5o= github.com/uber-go/atomic v1.4.0/go.mod h1:/Ct5t2lcmbJ4OSe/waGBoaVvVqtO0bmtfVNex1PFV8g= -github.com/urfave/cli/v2 v2.27.6/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/vertica/vertica-sql-go v1.3.3 h1:fL+FKEAEy5ONmsvya2WH5T8bhkvY27y/Ik3ReR2T+Qw= diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 57472bf05f4..2523471d4a4 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -301,10 +301,6 @@ export interface FeatureToggles { */ queryService?: boolean; /** - * Adds datasource connections to the query service - */ - queryServiceWithConnections?: boolean; - /** * Rewrite requests targeting /ds/query to the query service */ queryServiceRewrite?: boolean; diff --git a/pkg/aggregator/apiserver/plugin/handler.go b/pkg/aggregator/apiserver/plugin/handler.go index 0cce5708623..cd09bf45efb 100644 --- a/pkg/aggregator/apiserver/plugin/handler.go +++ b/pkg/aggregator/apiserver/plugin/handler.go @@ -5,9 +5,9 @@ import ( "net/http" "path" + "github.com/grafana/grafana-plugin-sdk-go/backend" "k8s.io/apimachinery/pkg/runtime/serializer" - "github.com/grafana/grafana-plugin-sdk-go/backend" aggregationv0alpha1 "github.com/grafana/grafana/pkg/aggregator/apis/aggregation/v0alpha1" "github.com/grafana/grafana/pkg/aggregator/apiserver/plugin/admission" ) @@ -64,7 +64,7 @@ func (h *PluginHandler) registerRoutes() { case aggregationv0alpha1.DataSourceProxyServiceType: // TODO: implement in future PR case aggregationv0alpha1.QueryServiceType: - h.mux.Handle(proxyPath("/namespaces/{namespace}/datasources/{uid}/query"), h.QueryDataHandler()) + h.mux.Handle(proxyPath("/namespaces/{namespace}/connections/{uid}/query"), h.QueryDataHandler()) case aggregationv0alpha1.RouteServiceType: // TODO: implement in future PR case aggregationv0alpha1.StreamServiceType: diff --git a/pkg/aggregator/apiserver/plugin/query.go b/pkg/aggregator/apiserver/plugin/query.go index c7750b6dc93..b790514ef31 100644 --- a/pkg/aggregator/apiserver/plugin/query.go +++ b/pkg/aggregator/apiserver/plugin/query.go @@ -6,15 +6,15 @@ import ( "fmt" "net/http" + "github.com/grafana/grafana-plugin-sdk-go/backend" + data "github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1" + grafanasemconv "github.com/grafana/grafana/pkg/semconv" semconv "go.opentelemetry.io/otel/semconv/v1.21.0" "k8s.io/component-base/tracing" "k8s.io/klog/v2" - "github.com/grafana/grafana-plugin-sdk-go/backend" - data "github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1" aggregationv0alpha1 "github.com/grafana/grafana/pkg/aggregator/apis/aggregation/v0alpha1" "github.com/grafana/grafana/pkg/aggregator/apiserver/util" - grafanasemconv "github.com/grafana/grafana/pkg/semconv" ) func (h *PluginHandler) QueryDataHandler() http.HandlerFunc { diff --git a/pkg/aggregator/apiserver/plugin/query_test.go b/pkg/aggregator/apiserver/plugin/query_test.go index 0a9a040a0b6..97bfabb3a08 100644 --- a/pkg/aggregator/apiserver/plugin/query_test.go +++ b/pkg/aggregator/apiserver/plugin/query_test.go @@ -10,14 +10,13 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/data" datav0alpha1 "github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1" "github.com/grafana/grafana/pkg/aggregator/apis/aggregation/v0alpha1" "github.com/grafana/grafana/pkg/aggregator/apiserver/plugin/fakes" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestQueryDataHandler(t *testing.T) { @@ -88,7 +87,7 @@ func TestQueryDataHandler(t *testing.T) { buf := bytes.NewBuffer(nil) assert.NoError(t, json.NewEncoder(buf).Encode(qdr)) - req, err := http.NewRequest("POST", "/apis/testds.example.com/v1/namespaces/default/datasources/123/query", buf) + req, err := http.NewRequest("POST", "/apis/testds.example.com/v1/namespaces/default/connections/123/query", buf) assert.NoError(t, err) rr := httptest.NewRecorder() @@ -114,7 +113,7 @@ func TestQueryDataHandler(t *testing.T) { buf := bytes.NewBuffer(nil) assert.NoError(t, json.NewEncoder(buf).Encode(qdr)) - req, err := http.NewRequest("POST", "/apis/testds.example.com/v1/namespaces/default/datasources/123/query", buf) + req, err := http.NewRequest("POST", "/apis/testds.example.com/v1/namespaces/default/connections/123/query", buf) assert.NoError(t, err) rr := httptest.NewRecorder() @@ -142,7 +141,7 @@ func TestQueryDataHandler(t *testing.T) { buf := bytes.NewBuffer(nil) assert.NoError(t, json.NewEncoder(buf).Encode(qdr)) - req, err := http.NewRequest("POST", "/apis/testds.example.com/v1/namespaces/default/datasources/abc/query", buf) + req, err := http.NewRequest("POST", "/apis/testds.example.com/v1/namespaces/default/connections/abc/query", buf) assert.NoError(t, err) rr := httptest.NewRecorder() @@ -166,7 +165,7 @@ func TestQueryDataHandler(t *testing.T) { }) t.Run("should return delegate response if group does not match", func(t *testing.T) { - req, err := http.NewRequest("POST", "/apis/wrongds.example.com/v1/namespaces/default/datasources/abc/query", bytes.NewBuffer(nil)) + req, err := http.NewRequest("POST", "/apis/wrongds.example.com/v1/namespaces/default/connections/abc/query", bytes.NewBuffer(nil)) assert.NoError(t, err) rr := httptest.NewRecorder() diff --git a/pkg/apis/datasource/v0alpha1/datasource.go b/pkg/apis/datasource/v0alpha1/datasource.go deleted file mode 100644 index 54368f4b9ee..00000000000 --- a/pkg/apis/datasource/v0alpha1/datasource.go +++ /dev/null @@ -1,73 +0,0 @@ -package v0alpha1 - -import ( - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - - 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 DataSource struct { - metav1.TypeMeta `json:",inline"` - metav1.ObjectMeta `json:"metadata"` - - // DataSource configuration -- these properties are all visible - // to anyone able to query the data source from their browser - Spec UnstructuredSpec `json:"spec"` - - // Secure values allows setting values that are never shown to users - // The returned properties are only the names of the configured values - Secure common.InlineSecureValues `json:"secure,omitzero,omitempty"` -} - -// DsAccess represents how the datasource connects to the remote service -// +k8s:openapi-gen=true -// +enum -type DsAccess string - -const ( - // The frontend can connect directly to the remote URL - // This method is discouraged - DsAccessDirect DsAccess = "direct" - - // Connect to the remote datasource through the grafana backend - DsAccessProxy DsAccess = "proxy" -) - -func (dsa DsAccess) String() string { - return string(dsa) -} - -// +k8s:openapi-gen=true -type GenericDataSourceSpec struct { - // The display name (previously saved as the "name" property) - Title string `json:"title"` - - Access DsAccess `json:"access,omitempty"` - ReadOnly bool `json:"readOnly,omitempty"` - IsDefault bool `json:"isDefault,omitempty"` - - // Server URL - URL string `json:"url,omitempty"` - - User string `json:"user,omitempty"` - Database string `json:"database,omitempty"` - BasicAuth bool `json:"basicAuth,omitempty"` - BasicAuthUser string `json:"basicAuthUser,omitempty"` - WithCredentials bool `json:"withCredentials,omitempty"` - - // Generic unstructured configuration settings - 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"` - metav1.ListMeta `json:"metadata"` - - Items []DataSource `json:"items"` -} diff --git a/pkg/apis/datasource/v0alpha1/doc.go b/pkg/apis/datasource/v0alpha1/doc.go index e7a4bf9c23e..43d5863c3c0 100644 --- a/pkg/apis/datasource/v0alpha1/doc.go +++ b/pkg/apis/datasource/v0alpha1/doc.go @@ -1,3 +1,6 @@ +// +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/register.go b/pkg/apis/datasource/v0alpha1/register.go index 735207452f9..2333aa0d996 100644 --- a/pkg/apis/datasource/v0alpha1/register.go +++ b/pkg/apis/datasource/v0alpha1/register.go @@ -4,10 +4,9 @@ import ( "fmt" "time" + "github.com/grafana/grafana/pkg/apimachinery/utils" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" - - "github.com/grafana/grafana/pkg/apimachinery/utils" ) const ( @@ -15,24 +14,26 @@ const ( VERSION = "v0alpha1" ) -var DataSourceResourceInfo = utils.NewResourceInfo(GROUP, VERSION, - "datasources", "datasource", "DataSource", - func() runtime.Object { return &DataSource{} }, - func() runtime.Object { return &DataSourceList{} }, +var GenericConnectionResourceInfo = utils.NewResourceInfo(GROUP, VERSION, + "connections", "connection", "DataSourceConnection", + func() runtime.Object { return &DataSourceConnection{} }, + func() runtime.Object { return &DataSourceConnectionList{} }, utils.TableColumns{ Definition: []metav1.TableColumnDefinition{ {Name: "Name", Type: "string", Format: "name"}, - {Name: "Title", Type: "string", Format: "string", Description: "Title"}, + {Name: "Title", Type: "string", Format: "string", Description: "The datasource title"}, + {Name: "APIVersion", Type: "string", Format: "string", Description: "API Version"}, {Name: "Created At", Type: "date"}, }, - Reader: func(obj any) ([]any, error) { - m, ok := obj.(*DataSource) + Reader: func(obj any) ([]interface{}, error) { + m, ok := obj.(*DataSourceConnection) if !ok { return nil, fmt.Errorf("expected connection") } - return []any{ + return []interface{}{ m.Name, - m.Spec.Object["title"], + m.Title, + m.APIVersion, m.CreationTimestamp.UTC().Format(time.RFC3339), }, nil }, diff --git a/pkg/apis/datasource/v0alpha1/types.go b/pkg/apis/datasource/v0alpha1/types.go index f41ce9f0757..6fe4d14dd71 100644 --- a/pkg/apis/datasource/v0alpha1/types.go +++ b/pkg/apis/datasource/v0alpha1/types.go @@ -6,8 +6,26 @@ 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 DataSourceConnection struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + // The display name + Title string `json:"title"` + + // Optional description for the data source (does not exist yet) + Description string `json:"description,omitempty"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type DataSourceConnectionList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + + Items []DataSourceConnection `json:"items"` +} + // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object type HealthCheckResult struct { metav1.TypeMeta `json:",inline"` diff --git a/pkg/apis/datasource/v0alpha1/unstructured.go b/pkg/apis/datasource/v0alpha1/unstructured.go deleted file mode 100644 index 67bd2b14530..00000000000 --- a/pkg/apis/datasource/v0alpha1/unstructured.go +++ /dev/null @@ -1,173 +0,0 @@ -package v0alpha1 - -import ( - "encoding/json" - - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - openapi "k8s.io/kube-openapi/pkg/common" - spec "k8s.io/kube-openapi/pkg/validation/spec" - - common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" -) - -// UnstructuredSpec allows any property to be saved into the spec -// Validation will happen from the dynamically loaded schemas for each datasource -// +k8s:deepcopy-gen=true -// +k8s:openapi-gen=true -type UnstructuredSpec common.Unstructured - -func (u *UnstructuredSpec) GetString(key string) string { - if u.Object == nil { - return "" - } - v := u.Object[key] - str, _ := v.(string) - return str -} - -func (u *UnstructuredSpec) Set(key string, val any) *UnstructuredSpec { - if u.Object == nil { - u.Object = make(map[string]any) - } - if val == nil || val == "" || val == false { - delete(u.Object, key) - } else { - u.Object[key] = val - } - return u -} - -func (u *UnstructuredSpec) Title() string { - return u.GetString("title") -} - -func (u *UnstructuredSpec) SetTitle(v string) *UnstructuredSpec { - return u.Set("title", v) -} - -func (u *UnstructuredSpec) URL() string { - return u.GetString("url") -} - -func (u *UnstructuredSpec) SetURL(v string) *UnstructuredSpec { - return u.Set("url", v) -} - -func (u *UnstructuredSpec) Database() string { - return u.GetString("database") -} - -func (u *UnstructuredSpec) SetDatabase(v string) *UnstructuredSpec { - return u.Set("database", v) -} - -func (u *UnstructuredSpec) Access() DsAccess { - return DsAccess(u.GetString("access")) -} - -func (u *UnstructuredSpec) SetAccess(v string) *UnstructuredSpec { - return u.Set("access", v) -} - -func (u *UnstructuredSpec) User() string { - return u.GetString("user") -} - -func (u *UnstructuredSpec) SetUser(v string) *UnstructuredSpec { - return u.Set("user", v) -} - -func (u *UnstructuredSpec) BasicAuth() bool { - v, _, _ := unstructured.NestedBool(u.Object, "basicAuth") - return v -} - -func (u *UnstructuredSpec) SetBasicAuth(v bool) *UnstructuredSpec { - return u.Set("basicAuth", v) -} - -func (u *UnstructuredSpec) BasicAuthUser() string { - return u.GetString("basicAuthUser") -} - -func (u *UnstructuredSpec) SetBasicAuthUser(v string) *UnstructuredSpec { - return u.Set("basicAuthUser", v) -} - -func (u *UnstructuredSpec) WithCredentials() bool { - v, _, _ := unstructured.NestedBool(u.Object, "withCredentials") - return v -} - -func (u *UnstructuredSpec) SetWithCredentials(v bool) *UnstructuredSpec { - return u.Set("withCredentials", v) -} - -func (u *UnstructuredSpec) IsDefault() bool { - v, _, _ := unstructured.NestedBool(u.Object, "isDefault") - return v -} - -func (u *UnstructuredSpec) SetIsDefault(v bool) *UnstructuredSpec { - return u.Set("isDefault", v) -} - -func (u *UnstructuredSpec) ReadOnly() bool { - v, _, _ := unstructured.NestedBool(u.Object, "readOnly") - return v -} - -func (u *UnstructuredSpec) SetReadOnly(v bool) *UnstructuredSpec { - return u.Set("readOnly", v) -} - -func (u *UnstructuredSpec) JSONData() any { - return u.Object["jsonData"] -} - -func (u *UnstructuredSpec) SetJSONData(v any) *UnstructuredSpec { - return u.Set("jsonData", v) -} - -// The OpenAPI spec uses the generated values from GenericDataSourceSpec, except that it: -// 1. Allows additional properties at the root -// 2. The jsonData field *may* be an raw value OR a map -func (UnstructuredSpec) OpenAPIDefinition() openapi.OpenAPIDefinition { - s := schema_pkg_apis_datasource_v0alpha1_GenericDataSourceSpec(func(path string) spec.Ref { - return spec.MustCreateRef(path) - }) - s.Schema.AdditionalProperties = &spec.SchemaOrBool{ - Allows: true, - } - return s -} - -// MarshalJSON ensures that the unstructured object produces proper -// JSON when passed to Go's standard JSON library. -func (u *UnstructuredSpec) MarshalJSON() ([]byte, error) { - return json.Marshal(u.Object) -} - -// UnmarshalJSON ensures that the unstructured object properly decodes -// JSON when passed to Go's standard JSON library. -func (u *UnstructuredSpec) UnmarshalJSON(b []byte) error { - return json.Unmarshal(b, &u.Object) -} - -func (u *UnstructuredSpec) DeepCopy() *UnstructuredSpec { - if u == nil { - return nil - } - out := new(UnstructuredSpec) - *out = *u - - tmp := common.Unstructured{Object: u.Object} - copy := tmp.DeepCopy() - out.Object = copy.Object - return out -} - -func (u *UnstructuredSpec) DeepCopyInto(out *UnstructuredSpec) { - clone := u.DeepCopy() - *out = *clone -} diff --git a/pkg/apis/datasource/v0alpha1/zz_generated.deepcopy.go b/pkg/apis/datasource/v0alpha1/zz_generated.deepcopy.go index 4488fa37f74..5f9e41d7ad2 100644 --- a/pkg/apis/datasource/v0alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/datasource/v0alpha1/zz_generated.deepcopy.go @@ -8,38 +8,29 @@ package v0alpha1 import ( - commonv0alpha1 "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" runtime "k8s.io/apimachinery/pkg/runtime" ) // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *DataSource) DeepCopyInto(out *DataSource) { +func (in *DataSourceConnection) DeepCopyInto(out *DataSourceConnection) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - in.Spec.DeepCopyInto(&out.Spec) - if in.Secure != nil { - in, out := &in.Secure, &out.Secure - *out = make(map[string]commonv0alpha1.InlineSecureValue, len(*in)) - for key, val := range *in { - (*out)[key] = val - } - } return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DataSource. -func (in *DataSource) DeepCopy() *DataSource { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DataSourceConnection. +func (in *DataSourceConnection) DeepCopy() *DataSourceConnection { if in == nil { return nil } - out := new(DataSource) + out := new(DataSourceConnection) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *DataSource) DeepCopyObject() runtime.Object { +func (in *DataSourceConnection) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } @@ -47,13 +38,13 @@ func (in *DataSource) DeepCopyObject() runtime.Object { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *DataSourceList) DeepCopyInto(out *DataSourceList) { +func (in *DataSourceConnectionList) DeepCopyInto(out *DataSourceConnectionList) { *out = *in out.TypeMeta = in.TypeMeta in.ListMeta.DeepCopyInto(&out.ListMeta) if in.Items != nil { in, out := &in.Items, &out.Items - *out = make([]DataSource, len(*in)) + *out = make([]DataSourceConnection, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } @@ -61,18 +52,18 @@ func (in *DataSourceList) DeepCopyInto(out *DataSourceList) { return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DataSourceList. -func (in *DataSourceList) DeepCopy() *DataSourceList { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DataSourceConnectionList. +func (in *DataSourceConnectionList) DeepCopy() *DataSourceConnectionList { if in == nil { return nil } - out := new(DataSourceList) + out := new(DataSourceConnectionList) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *DataSourceList) DeepCopyObject() runtime.Object { +func (in *DataSourceConnectionList) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } diff --git a/pkg/apis/datasource/v0alpha1/zz_generated.defaults.go b/pkg/apis/datasource/v0alpha1/zz_generated.defaults.go new file mode 100644 index 00000000000..238fc2f4edc --- /dev/null +++ b/pkg/apis/datasource/v0alpha1/zz_generated.defaults.go @@ -0,0 +1,19 @@ +//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/datasource/v0alpha1/zz_generated.openapi.go b/pkg/apis/datasource/v0alpha1/zz_generated.openapi.go index 7e78e455bae..8573e1080df 100644 --- a/pkg/apis/datasource/v0alpha1/zz_generated.openapi.go +++ b/pkg/apis/datasource/v0alpha1/zz_generated.openapi.go @@ -14,15 +14,13 @@ import ( func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition { return map[string]common.OpenAPIDefinition{ - "github.com/grafana/grafana/pkg/apis/datasource/v0alpha1.DataSource": schema_pkg_apis_datasource_v0alpha1_DataSource(ref), - "github.com/grafana/grafana/pkg/apis/datasource/v0alpha1.DataSourceList": schema_pkg_apis_datasource_v0alpha1_DataSourceList(ref), - "github.com/grafana/grafana/pkg/apis/datasource/v0alpha1.GenericDataSourceSpec": schema_pkg_apis_datasource_v0alpha1_GenericDataSourceSpec(ref), - "github.com/grafana/grafana/pkg/apis/datasource/v0alpha1.HealthCheckResult": schema_pkg_apis_datasource_v0alpha1_HealthCheckResult(ref), - "github.com/grafana/grafana/pkg/apis/datasource/v0alpha1.UnstructuredSpec": UnstructuredSpec{}.OpenAPIDefinition(), + "github.com/grafana/grafana/pkg/apis/datasource/v0alpha1.DataSourceConnection": schema_pkg_apis_datasource_v0alpha1_DataSourceConnection(ref), + "github.com/grafana/grafana/pkg/apis/datasource/v0alpha1.DataSourceConnectionList": schema_pkg_apis_datasource_v0alpha1_DataSourceConnectionList(ref), + "github.com/grafana/grafana/pkg/apis/datasource/v0alpha1.HealthCheckResult": schema_pkg_apis_datasource_v0alpha1_HealthCheckResult(ref), } } -func schema_pkg_apis_datasource_v0alpha1_DataSource(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_datasource_v0alpha1_DataSourceConnection(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -48,37 +46,31 @@ func schema_pkg_apis_datasource_v0alpha1_DataSource(ref common.ReferenceCallback Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"), }, }, - "spec": { + "title": { SchemaProps: spec.SchemaProps{ - Description: "DataSource configuration -- these properties are all visible to anyone able to query the data source from their browser", - Ref: ref("github.com/grafana/grafana/pkg/apis/datasource/v0alpha1.UnstructuredSpec"), + Description: "The display name", + Default: "", + Type: []string{"string"}, + Format: "", }, }, - "secure": { + "description": { SchemaProps: spec.SchemaProps{ - Description: "Secure values allows setting values that are never shown to users The returned properties are only the names of the configured values", - Type: []string{"object"}, - AdditionalProperties: &spec.SchemaOrBool{ - Allows: true, - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.InlineSecureValue"), - }, - }, - }, + Description: "Optional description for the data source (does not exist yet)", + Type: []string{"string"}, + Format: "", }, }, }, - Required: []string{"metadata", "spec"}, + Required: []string{"title"}, }, }, Dependencies: []string{ - "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.InlineSecureValue", "github.com/grafana/grafana/pkg/apis/datasource/v0alpha1.UnstructuredSpec", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, + "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, } } -func schema_pkg_apis_datasource_v0alpha1_DataSourceList(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_datasource_v0alpha1_DataSourceConnectionList(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -111,104 +103,18 @@ func schema_pkg_apis_datasource_v0alpha1_DataSourceList(ref common.ReferenceCall Schema: &spec.Schema{ SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/pkg/apis/datasource/v0alpha1.DataSource"), + Ref: ref("github.com/grafana/grafana/pkg/apis/datasource/v0alpha1.DataSourceConnection"), }, }, }, }, }, }, - Required: []string{"metadata", "items"}, + Required: []string{"items"}, }, }, Dependencies: []string{ - "github.com/grafana/grafana/pkg/apis/datasource/v0alpha1.DataSource", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, - } -} - -func schema_pkg_apis_datasource_v0alpha1_GenericDataSourceSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "title": { - SchemaProps: spec.SchemaProps{ - Description: "The display name (previously saved as the \"name\" property)", - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - "access": { - SchemaProps: spec.SchemaProps{ - Description: "Possible enum values:\n - `\"direct\"` The frontend can connect directly to the remote URL This method is discouraged\n - `\"proxy\"` Connect to the remote datasource through the grafana backend", - Type: []string{"string"}, - Format: "", - Enum: []interface{}{"direct", "proxy"}, - }, - }, - "readOnly": { - SchemaProps: spec.SchemaProps{ - Type: []string{"boolean"}, - Format: "", - }, - }, - "isDefault": { - SchemaProps: spec.SchemaProps{ - Type: []string{"boolean"}, - Format: "", - }, - }, - "url": { - SchemaProps: spec.SchemaProps{ - Description: "Server URL", - Type: []string{"string"}, - Format: "", - }, - }, - "user": { - SchemaProps: spec.SchemaProps{ - Type: []string{"string"}, - Format: "", - }, - }, - "database": { - SchemaProps: spec.SchemaProps{ - Type: []string{"string"}, - Format: "", - }, - }, - "basicAuth": { - SchemaProps: spec.SchemaProps{ - Type: []string{"boolean"}, - Format: "", - }, - }, - "basicAuthUser": { - SchemaProps: spec.SchemaProps{ - Type: []string{"string"}, - Format: "", - }, - }, - "withCredentials": { - SchemaProps: spec.SchemaProps{ - Type: []string{"boolean"}, - Format: "", - }, - }, - "jsonData": { - SchemaProps: spec.SchemaProps{ - Description: "Generic unstructured configuration settings", - Ref: ref("github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.Unstructured"), - }, - }, - }, - Required: []string{"title", "jsonData"}, - }, - }, - Dependencies: []string{ - "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.Unstructured"}, + "github.com/grafana/grafana/pkg/apis/datasource/v0alpha1.DataSourceConnection", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, } } diff --git a/pkg/apis/datasource/v0alpha1/zz_generated.openapi_violation_exceptions.list b/pkg/apis/datasource/v0alpha1/zz_generated.openapi_violation_exceptions.list deleted file mode 100644 index 918518966c7..00000000000 --- a/pkg/apis/datasource/v0alpha1/zz_generated.openapi_violation_exceptions.list +++ /dev/null @@ -1,2 +0,0 @@ -API rule violation: names_match,github.com/grafana/grafana/pkg/apis/datasource/v0alpha1,UnstructuredSpec,Object -API rule violation: streaming_list_type_json_tags,github.com/grafana/grafana/pkg/apis/datasource/v0alpha1,DataSourceList,ListMeta diff --git a/pkg/apis/query/v0alpha1/connection.go b/pkg/apis/query/v0alpha1/datasource.go similarity index 54% rename from pkg/apis/query/v0alpha1/connection.go rename to pkg/apis/query/v0alpha1/datasource.go index ced60d0d106..04c236113e3 100644 --- a/pkg/apis/query/v0alpha1/connection.go +++ b/pkg/apis/query/v0alpha1/datasource.go @@ -7,40 +7,6 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" ) -// Connection to a datasource instance -// The connection name must be '{group}:{name}' -// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object -type DataSourceConnection struct { - metav1.TypeMeta `json:",inline"` - metav1.ObjectMeta `json:"metadata,omitzero,omitempty"` - - // The configured display name - Title string `json:"title"` - - // Reference to the kubernets datasource - Datasource DataSourceConnectionRef `json:"datasource"` -} - -type DataSourceConnectionRef struct { - Group string `json:"group"` - Version string `json:"version"` - Name string `json:"name"` -} - -// The valid connection name for a group + identifier -func DataSourceConnectionName(group, name string) string { - return group + ":" + name -} - -// List of all datasource instances across all datasource apiservers -// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object -type DataSourceConnectionList struct { - metav1.TypeMeta `json:",inline"` - metav1.ListMeta `json:"metadata,omitzero,omitempty"` - - Items []DataSourceConnection `json:"items"` -} - type DataSourceApiServerRegistry interface { // Get the group and preferred version for a plugin GetDatasourceGroupVersion(pluginId string) (schema.GroupVersion, error) @@ -58,7 +24,7 @@ type DataSourceApiServerRegistry interface { // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object type DataSourceApiServer struct { metav1.TypeMeta `json:",inline"` - metav1.ObjectMeta `json:"metadata,omitzero,omitempty"` + metav1.ObjectMeta `json:"metadata,omitempty"` // The display name Title string `json:"title"` @@ -77,7 +43,7 @@ type DataSourceApiServer struct { // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object type DataSourceApiServerList struct { metav1.TypeMeta `json:",inline"` - metav1.ListMeta `json:"metadata,omitzero,omitempty"` + metav1.ListMeta `json:"metadata,omitempty"` Items []DataSourceApiServer `json:"items"` } diff --git a/pkg/apis/query/v0alpha1/register.go b/pkg/apis/query/v0alpha1/register.go index 6eb75675cdd..14ba3560315 100644 --- a/pkg/apis/query/v0alpha1/register.go +++ b/pkg/apis/query/v0alpha1/register.go @@ -1,10 +1,6 @@ package v0alpha1 import ( - "fmt" - "time" - - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" @@ -17,32 +13,6 @@ const ( APIVERSION = GROUP + "/" + VERSION ) -var ConnectionResourceInfo = utils.NewResourceInfo(GROUP, VERSION, - "connections", "connection", "DataSourceConnection", - func() runtime.Object { return &DataSourceConnection{} }, - func() runtime.Object { return &DataSourceConnectionList{} }, - utils.TableColumns{ - Definition: []metav1.TableColumnDefinition{ - {Name: "Name", Type: "string", Format: "name"}, - {Name: "Title", Type: "string", Format: "string", Description: "The datasource title"}, - {Name: "APIVersion", Type: "string", Format: "string", Description: "API Version"}, - {Name: "Created At", Type: "date"}, - }, - Reader: func(obj any) ([]interface{}, error) { - m, ok := obj.(*DataSourceConnection) - if !ok { - return nil, fmt.Errorf("expected connection") - } - return []interface{}{ - m.Name, - m.Title, - m.APIVersion, - m.CreationTimestamp.UTC().Format(time.RFC3339), - }, nil - }, - }, -) - var DataSourceApiServerResourceInfo = utils.NewResourceInfo(GROUP, VERSION, "datasourceapiservers", "datasourceapiserver", "DataSourceApiServer", func() runtime.Object { return &DataSourceApiServer{} }, diff --git a/pkg/apis/query/v0alpha1/zz_generated.deepcopy.go b/pkg/apis/query/v0alpha1/zz_generated.deepcopy.go index 38a7bd0b115..8f36003313d 100644 --- a/pkg/apis/query/v0alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/query/v0alpha1/zz_generated.deepcopy.go @@ -75,82 +75,6 @@ func (in *DataSourceApiServerList) DeepCopyObject() runtime.Object { return nil } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *DataSourceConnection) DeepCopyInto(out *DataSourceConnection) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Datasource = in.Datasource - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DataSourceConnection. -func (in *DataSourceConnection) DeepCopy() *DataSourceConnection { - if in == nil { - return nil - } - out := new(DataSourceConnection) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *DataSourceConnection) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *DataSourceConnectionList) DeepCopyInto(out *DataSourceConnectionList) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ListMeta.DeepCopyInto(&out.ListMeta) - if in.Items != nil { - in, out := &in.Items, &out.Items - *out = make([]DataSourceConnection, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DataSourceConnectionList. -func (in *DataSourceConnectionList) DeepCopy() *DataSourceConnectionList { - if in == nil { - return nil - } - out := new(DataSourceConnectionList) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *DataSourceConnectionList) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *DataSourceConnectionRef) DeepCopyInto(out *DataSourceConnectionRef) { - *out = *in - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DataSourceConnectionRef. -func (in *DataSourceConnectionRef) DeepCopy() *DataSourceConnectionRef { - if in == nil { - return nil - } - out := new(DataSourceConnectionRef) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *QueryDataRequest) DeepCopyInto(out *QueryDataRequest) { *out = *in diff --git a/pkg/apis/query/v0alpha1/zz_generated.openapi.go b/pkg/apis/query/v0alpha1/zz_generated.openapi.go index 2ee9ed95395..b4089bd94f1 100644 --- a/pkg/apis/query/v0alpha1/zz_generated.openapi.go +++ b/pkg/apis/query/v0alpha1/zz_generated.openapi.go @@ -14,15 +14,12 @@ import ( func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition { return map[string]common.OpenAPIDefinition{ - "github.com/grafana/grafana/pkg/apis/query/v0alpha1.DataSourceApiServer": schema_pkg_apis_query_v0alpha1_DataSourceApiServer(ref), - "github.com/grafana/grafana/pkg/apis/query/v0alpha1.DataSourceApiServerList": schema_pkg_apis_query_v0alpha1_DataSourceApiServerList(ref), - "github.com/grafana/grafana/pkg/apis/query/v0alpha1.DataSourceConnection": schema_pkg_apis_query_v0alpha1_DataSourceConnection(ref), - "github.com/grafana/grafana/pkg/apis/query/v0alpha1.DataSourceConnectionList": schema_pkg_apis_query_v0alpha1_DataSourceConnectionList(ref), - "github.com/grafana/grafana/pkg/apis/query/v0alpha1.DataSourceConnectionRef": schema_pkg_apis_query_v0alpha1_DataSourceConnectionRef(ref), - "github.com/grafana/grafana/pkg/apis/query/v0alpha1.QueryDataRequest": schema_pkg_apis_query_v0alpha1_QueryDataRequest(ref), - "github.com/grafana/grafana/pkg/apis/query/v0alpha1.QueryDataResponse": schema_pkg_apis_query_v0alpha1_QueryDataResponse(ref), - "github.com/grafana/grafana/pkg/apis/query/v0alpha1.QueryTypeDefinition": schema_pkg_apis_query_v0alpha1_QueryTypeDefinition(ref), - "github.com/grafana/grafana/pkg/apis/query/v0alpha1.QueryTypeDefinitionList": schema_pkg_apis_query_v0alpha1_QueryTypeDefinitionList(ref), + "github.com/grafana/grafana/pkg/apis/query/v0alpha1.DataSourceApiServer": schema_pkg_apis_query_v0alpha1_DataSourceApiServer(ref), + "github.com/grafana/grafana/pkg/apis/query/v0alpha1.DataSourceApiServerList": schema_pkg_apis_query_v0alpha1_DataSourceApiServerList(ref), + "github.com/grafana/grafana/pkg/apis/query/v0alpha1.QueryDataRequest": schema_pkg_apis_query_v0alpha1_QueryDataRequest(ref), + "github.com/grafana/grafana/pkg/apis/query/v0alpha1.QueryDataResponse": schema_pkg_apis_query_v0alpha1_QueryDataResponse(ref), + "github.com/grafana/grafana/pkg/apis/query/v0alpha1.QueryTypeDefinition": schema_pkg_apis_query_v0alpha1_QueryTypeDefinition(ref), + "github.com/grafana/grafana/pkg/apis/query/v0alpha1.QueryTypeDefinitionList": schema_pkg_apis_query_v0alpha1_QueryTypeDefinitionList(ref), } } @@ -149,140 +146,6 @@ func schema_pkg_apis_query_v0alpha1_DataSourceApiServerList(ref common.Reference } } -func schema_pkg_apis_query_v0alpha1_DataSourceConnection(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Description: "Connection to a datasource instance The connection name must be '{group}:{name}'", - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "kind": { - SchemaProps: spec.SchemaProps{ - Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", - Type: []string{"string"}, - Format: "", - }, - }, - "apiVersion": { - SchemaProps: spec.SchemaProps{ - Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", - Type: []string{"string"}, - Format: "", - }, - }, - "metadata": { - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"), - }, - }, - "title": { - SchemaProps: spec.SchemaProps{ - Description: "The configured display name", - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - "datasource": { - SchemaProps: spec.SchemaProps{ - Description: "Reference to the kubernets datasource", - Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/pkg/apis/query/v0alpha1.DataSourceConnectionRef"), - }, - }, - }, - Required: []string{"title", "datasource"}, - }, - }, - Dependencies: []string{ - "github.com/grafana/grafana/pkg/apis/query/v0alpha1.DataSourceConnectionRef", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, - } -} - -func schema_pkg_apis_query_v0alpha1_DataSourceConnectionList(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Description: "List of all datasource instances across all datasource apiservers", - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "kind": { - SchemaProps: spec.SchemaProps{ - Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", - Type: []string{"string"}, - Format: "", - }, - }, - "apiVersion": { - SchemaProps: spec.SchemaProps{ - Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", - Type: []string{"string"}, - Format: "", - }, - }, - "metadata": { - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"), - }, - }, - "items": { - SchemaProps: spec.SchemaProps{ - Type: []string{"array"}, - Items: &spec.SchemaOrArray{ - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/pkg/apis/query/v0alpha1.DataSourceConnection"), - }, - }, - }, - }, - }, - }, - Required: []string{"items"}, - }, - }, - Dependencies: []string{ - "github.com/grafana/grafana/pkg/apis/query/v0alpha1.DataSourceConnection", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, - } -} - -func schema_pkg_apis_query_v0alpha1_DataSourceConnectionRef(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "group": { - SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - "version": { - SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - "name": { - SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - }, - Required: []string{"group", "version", "name"}, - }, - }, - } -} - func schema_pkg_apis_query_v0alpha1_QueryDataRequest(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ diff --git a/pkg/apis/query/v0alpha1/zz_generated.openapi_violation_exceptions.list b/pkg/apis/query/v0alpha1/zz_generated.openapi_violation_exceptions.list index f2a822d96dc..85c5a768907 100644 --- a/pkg/apis/query/v0alpha1/zz_generated.openapi_violation_exceptions.list +++ b/pkg/apis/query/v0alpha1/zz_generated.openapi_violation_exceptions.list @@ -1,3 +1 @@ API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/query/v0alpha1,DataSourceApiServer,AliasIDs -API rule violation: streaming_list_type_json_tags,github.com/grafana/grafana/pkg/apis/query/v0alpha1,DataSourceApiServerList,ListMeta -API rule violation: streaming_list_type_json_tags,github.com/grafana/grafana/pkg/apis/query/v0alpha1,DataSourceConnectionList,ListMeta diff --git a/pkg/registry/apis/datasource/authorizer.go b/pkg/registry/apis/datasource/authorizer.go index d862655c749..570b438d808 100644 --- a/pkg/registry/apis/datasource/authorizer.go +++ b/pkg/registry/apis/datasource/authorizer.go @@ -25,7 +25,7 @@ func (b *DataSourceAPIBuilder) GetAuthorizer() authorizer.Authorizer { uidScope := datasources.ScopeProvider.GetResourceScopeUID(attr.GetName()) // Must have query access to see a connection - if attr.GetResource() == b.datasourceResourceInfo.GroupResource().Resource { + if attr.GetResource() == b.connectionResourceInfo.GroupResource().Resource { scopes := []string{} if attr.GetName() != "" { scopes = []string{uidScope} diff --git a/pkg/registry/apis/datasource/connections.go b/pkg/registry/apis/datasource/connections.go new file mode 100644 index 00000000000..ec79393095d --- /dev/null +++ b/pkg/registry/apis/datasource/connections.go @@ -0,0 +1,59 @@ +package datasource + +import ( + "context" + + "github.com/grafana/grafana/pkg/apimachinery/utils" + "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" +) + +var ( + _ rest.Scoper = (*connectionAccess)(nil) + _ rest.SingularNameProvider = (*connectionAccess)(nil) + _ rest.Getter = (*connectionAccess)(nil) + _ rest.Lister = (*connectionAccess)(nil) + _ rest.Storage = (*connectionAccess)(nil) +) + +type connectionAccess struct { + resourceInfo utils.ResourceInfo + tableConverter rest.TableConvertor + datasources PluginDatasourceProvider +} + +func (s *connectionAccess) New() runtime.Object { + return s.resourceInfo.NewFunc() +} + +func (s *connectionAccess) Destroy() {} + +func (s *connectionAccess) NamespaceScoped() bool { + return true +} + +func (s *connectionAccess) GetSingularName() string { + return s.resourceInfo.GetSingularName() +} + +func (s *connectionAccess) ShortNames() []string { + return s.resourceInfo.GetShortNames() +} + +func (s *connectionAccess) NewList() runtime.Object { + return s.resourceInfo.NewListFunc() +} + +func (s *connectionAccess) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) { + return s.tableConverter.ConvertToTable(ctx, object, tableOptions) +} + +func (s *connectionAccess) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) { + return s.datasources.Get(ctx, name) +} + +func (s *connectionAccess) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) { + return s.datasources.List(ctx) +} diff --git a/pkg/registry/apis/datasource/converter.go b/pkg/registry/apis/datasource/converter.go deleted file mode 100644 index f1db08f42b5..00000000000 --- a/pkg/registry/apis/datasource/converter.go +++ /dev/null @@ -1,192 +0,0 @@ -package datasource - -import ( - "crypto/sha256" - "encoding/hex" - "fmt" - "iter" - "maps" - "slices" - "strconv" - "strings" - "time" - - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - - "github.com/grafana/authlib/types" - common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" - "github.com/grafana/grafana/pkg/apimachinery/utils" - datasourceV0 "github.com/grafana/grafana/pkg/apis/datasource/v0alpha1" - "github.com/grafana/grafana/pkg/components/simplejson" - "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" - gapiutil "github.com/grafana/grafana/pkg/services/apiserver/utils" - "github.com/grafana/grafana/pkg/services/datasources" -) - -type converter struct { - mapper request.NamespaceMapper - group string // the expected group - plugin string // the expected pluginId - alias []string // optional alias for the pluginId -} - -func (r *converter) asDataSource(ds *datasources.DataSource) (*datasourceV0.DataSource, error) { - if ds.Type != r.plugin && !slices.Contains(r.alias, ds.Type) { - return nil, fmt.Errorf("expected datasource type: %s %v // not: %s", r.plugin, r.alias, ds.Type) - } - - obj := &datasourceV0.DataSource{ - ObjectMeta: metav1.ObjectMeta{ - Name: ds.UID, - Namespace: r.mapper(ds.OrgID), - Generation: int64(ds.Version), - }, - Spec: datasourceV0.UnstructuredSpec{}, - Secure: ToInlineSecureValues(ds.Type, ds.UID, maps.Keys(ds.SecureJsonData)), - } - obj.UID = gapiutil.CalculateClusterWideUID(obj) - obj.Spec.SetTitle(ds.Name). - SetAccess(string(ds.Access)). - SetURL(ds.URL). - SetDatabase(ds.Database). - SetUser(ds.User). - SetDatabase(ds.Database). - SetBasicAuth(ds.BasicAuth). - SetBasicAuthUser(ds.BasicAuthUser). - SetWithCredentials(ds.WithCredentials). - SetIsDefault(ds.IsDefault). - SetReadOnly(ds.ReadOnly). - SetJSONData(ds.JsonData) - - if !ds.Created.IsZero() { - obj.CreationTimestamp = metav1.NewTime(ds.Created) - } - if !ds.Updated.IsZero() { - obj.ResourceVersion = fmt.Sprintf("%d", ds.Updated.UnixMilli()) - obj.Annotations = map[string]string{ - utils.AnnoKeyUpdatedTimestamp: ds.Updated.Format(time.RFC3339), - } - } - - if ds.APIVersion != "" { - obj.APIVersion = fmt.Sprintf("%s/%s", r.group, ds.APIVersion) - } - - if ds.ID > 0 { - obj.Labels = map[string]string{ - utils.LabelKeyDeprecatedInternalID: strconv.FormatInt(ds.ID, 10), - } - } - return obj, nil -} - -// ToInlineSecureValues converts secure json into InlineSecureValues with reference names -// The names are predictable and can be used while we implement dual writing for secrets -func ToInlineSecureValues(dsType string, dsUID string, keys iter.Seq[string]) common.InlineSecureValues { - values := make(common.InlineSecureValues) - for k := range keys { - h := sha256.New() - h.Write([]byte(dsType)) // plugin id - h.Write([]byte("|")) - h.Write([]byte(dsUID)) // unique identifier - h.Write([]byte("|")) - h.Write([]byte(k)) // property name - n := hex.EncodeToString(h.Sum(nil)) - values[k] = common.InlineSecureValue{ - Name: "ds-" + n[0:10], // predictable name for dual writing - } - } - if len(values) == 0 { - return nil - } - return values -} - -func (r *converter) toAddCommand(ds *datasourceV0.DataSource) (*datasources.AddDataSourceCommand, error) { - if r.group != "" && ds.APIVersion != "" && !strings.HasPrefix(ds.APIVersion, r.group) { - return nil, fmt.Errorf("expecting APIGroup: %s", r.group) - } - info, err := types.ParseNamespace(ds.Namespace) - if err != nil { - return nil, err - } - - cmd := &datasources.AddDataSourceCommand{ - Name: ds.Spec.Title(), - UID: ds.Name, - OrgID: info.OrgID, - Type: r.plugin, - - Access: datasources.DsAccess(ds.Spec.Access()), - URL: ds.Spec.URL(), - Database: ds.Spec.Database(), - User: ds.Spec.User(), - BasicAuth: ds.Spec.BasicAuth(), - BasicAuthUser: ds.Spec.BasicAuthUser(), - WithCredentials: ds.Spec.WithCredentials(), - IsDefault: ds.Spec.IsDefault(), - ReadOnly: ds.Spec.ReadOnly(), - } - - jsonData := ds.Spec.JSONData() - if jsonData != nil { - cmd.JsonData = simplejson.NewFromAny(jsonData) - } - - cmd.SecureJsonData = toSecureJsonData(ds) - return cmd, nil -} - -func (r *converter) toUpdateCommand(ds *datasourceV0.DataSource) (*datasources.UpdateDataSourceCommand, error) { - if r.group != "" && ds.APIVersion != "" && !strings.HasPrefix(ds.APIVersion, r.group) { - return nil, fmt.Errorf("expecting APIGroup: %s", r.group) - } - info, err := types.ParseNamespace(ds.Namespace) - if err != nil { - return nil, err - } - - cmd := &datasources.UpdateDataSourceCommand{ - Name: ds.Spec.Title(), - UID: ds.Name, - OrgID: info.OrgID, - Type: r.plugin, - - Access: datasources.DsAccess(ds.Spec.Access()), - URL: ds.Spec.URL(), - Database: ds.Spec.Database(), - User: ds.Spec.User(), - BasicAuth: ds.Spec.BasicAuth(), - BasicAuthUser: ds.Spec.BasicAuthUser(), - WithCredentials: ds.Spec.WithCredentials(), - IsDefault: ds.Spec.IsDefault(), - ReadOnly: ds.Spec.ReadOnly(), - - // The only field different than add - Version: int(ds.Generation), - } - - jsonData := ds.Spec.JSONData() - if jsonData != nil { - cmd.JsonData = simplejson.NewFromAny(jsonData) - } - cmd.SecureJsonData = toSecureJsonData(ds) - return cmd, err -} - -func toSecureJsonData(ds *datasourceV0.DataSource) map[string]string { - if ds == nil || len(ds.Secure) < 1 { - return nil - } - - secure := map[string]string{} - for k, v := range ds.Secure { - if v.Create != "" { - secure[k] = v.Create.DangerouslyExposeAndConsumeValue() - } - if v.Remove { - secure[k] = "" // Weirdly, this is the best we can do with the legacy API :( - } - } - return secure -} diff --git a/pkg/registry/apis/datasource/converter_test.go b/pkg/registry/apis/datasource/converter_test.go deleted file mode 100644 index e5953928f88..00000000000 --- a/pkg/registry/apis/datasource/converter_test.go +++ /dev/null @@ -1,153 +0,0 @@ -package datasource - -import ( - "encoding/json" - "errors" - "os" - "path/filepath" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/grafana/authlib/types" - "github.com/grafana/grafana/pkg/apis/datasource/v0alpha1" - "github.com/grafana/grafana/pkg/services/datasources" -) - -func TestConverter(t *testing.T) { - t.Run("resource to command", func(t *testing.T) { - converter := converter{ - mapper: types.OrgNamespaceFormatter, - plugin: "grafana-testdata-datasource", - alias: []string{"testdata"}, - group: "testdata.grafana.datasource.app", - } - tests := []struct { - name string - expectedErr string - }{ - {"convert-resource-full", ""}, - {"convert-resource-empty", ""}, - {"convert-resource-invalid", "expecting APIGroup: testdata.grafana.datasource.app"}, - {"convert-resource-invalid2", "invalid stack id"}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - obj := &v0alpha1.DataSource{} - fpath := filepath.Join("testdata", tt.name+".json") - raw, err := os.ReadFile(fpath) // nolint:gosec - require.NoError(t, err) - err = json.Unmarshal(raw, obj) - require.NoError(t, err) - - // The add command - fpath = filepath.Join("testdata", tt.name+"-to-cmd-add.json") - add, err := converter.toAddCommand(obj) - if tt.expectedErr != "" { - require.ErrorContains(t, err, tt.expectedErr) - require.Nil(t, add, "cmd should be nil when error exists") - - update, err := converter.toUpdateCommand(obj) - require.ErrorContains(t, err, tt.expectedErr) - require.Nil(t, update, "cmd should be nil when error exists") - return - } - - require.NoError(t, err) - out, err := json.MarshalIndent(add, "", " ") - require.NoError(t, err) - raw, _ = os.ReadFile(fpath) // nolint:gosec - if !assert.JSONEq(t, string(raw), string(out)) { - _ = os.WriteFile(fpath, out, 0600) - } - - // The update command - fpath = filepath.Join("testdata", tt.name+"-to-cmd-update.json") - update, err := converter.toUpdateCommand(obj) - require.NoError(t, err) - - out, err = json.MarshalIndent(update, "", " ") - require.NoError(t, err) - raw, _ = os.ReadFile(fpath) // nolint:gosec - if !assert.JSONEq(t, string(raw), string(out)) { - _ = os.WriteFile(fpath, out, 0600) - } - - // Round trip the update (NOTE, not all properties will be included) - ds := &datasources.DataSource{} - err = json.Unmarshal(raw, ds) // the add command is also a DataSource - require.NoError(t, err) - - roundtrip, err := converter.asDataSource(ds) - require.NoError(t, err) - - fpath = filepath.Join("testdata", tt.name+"-to-cmd-update-roundtrip.json") - out, err = json.MarshalIndent(roundtrip, "", " ") - require.NoError(t, err) - raw, _ = os.ReadFile(fpath) // nolint:gosec - if !assert.JSONEq(t, string(raw), string(out)) { - _ = os.WriteFile(fpath, out, 0600) - } - }) - } - }) - - t.Run("dto to resource", func(t *testing.T) { - converter := converter{ - mapper: types.OrgNamespaceFormatter, - plugin: "grafana-testdata-datasource", - alias: []string{"testdata"}, - group: "testdata.grafana.datasource.app", - } - tests := []struct { - name string - expectedErr string - }{ - { - name: "convert-dto-testdata", - }, - { - name: "convert-dto-empty", - }, - { - name: "convert-dto-invalid", - expectedErr: "expected datasource type: grafana-testdata-datasource [testdata]", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ds := &datasources.DataSource{} - fpath := filepath.Join("testdata", tt.name+".json") - raw, err := os.ReadFile(fpath) // nolint:gosec - require.NoError(t, err) - err = json.Unmarshal(raw, ds) - require.NoError(t, err) - - obj, err := converter.asDataSource(ds) - if tt.expectedErr != "" { - require.ErrorContains(t, err, tt.expectedErr) - require.Nil(t, obj, "object should be nil when error exists") - } else { - require.NoError(t, err) - } - - // Verify the result - fpath = filepath.Join("testdata", tt.name+"-to-resource.json") - if obj == nil { - _, err := os.Stat(fpath) - require.Error(t, err, "file should not exist") - require.True(t, errors.Is(err, os.ErrNotExist)) - } else { - out, err := json.MarshalIndent(obj, "", " ") - require.NoError(t, err) - raw, _ = os.ReadFile(fpath) // nolint:gosec - if !assert.JSONEq(t, string(raw), string(out)) { - _ = os.WriteFile(fpath, out, 0600) - } - } - }) - } - }) -} diff --git a/pkg/registry/apis/datasource/legacy_store.go b/pkg/registry/apis/datasource/legacy_store.go deleted file mode 100644 index 8786dc8d86e..00000000000 --- a/pkg/registry/apis/datasource/legacy_store.go +++ /dev/null @@ -1,126 +0,0 @@ -package datasource - -import ( - "context" - "fmt" - - "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/pkg/apimachinery/utils" - "github.com/grafana/grafana/pkg/apis/datasource/v0alpha1" -) - -var ( - _ rest.Scoper = (*legacyStorage)(nil) - _ rest.SingularNameProvider = (*legacyStorage)(nil) - _ rest.Getter = (*legacyStorage)(nil) - _ rest.Lister = (*legacyStorage)(nil) - _ rest.Storage = (*legacyStorage)(nil) - _ rest.Creater = (*legacyStorage)(nil) - _ rest.Updater = (*legacyStorage)(nil) - _ rest.GracefulDeleter = (*legacyStorage)(nil) - _ rest.CollectionDeleter = (*legacyStorage)(nil) -) - -type legacyStorage struct { - datasources PluginDatasourceProvider - resourceInfo *utils.ResourceInfo -} - -func (s *legacyStorage) New() runtime.Object { - return s.resourceInfo.NewFunc() -} - -func (s *legacyStorage) Destroy() {} - -func (s *legacyStorage) NamespaceScoped() bool { - return true // namespace == org -} - -func (s *legacyStorage) GetSingularName() string { - return s.resourceInfo.GetSingularName() -} - -func (s *legacyStorage) NewList() runtime.Object { - return s.resourceInfo.NewListFunc() -} - -func (s *legacyStorage) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) { - return s.resourceInfo.TableConverter().ConvertToTable(ctx, object, tableOptions) -} - -func (s *legacyStorage) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) { - return s.datasources.ListDataSources(ctx) -} - -func (s *legacyStorage) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) { - return s.datasources.GetDataSource(ctx, name) -} - -// Create implements rest.Creater. -func (s *legacyStorage) Create(ctx context.Context, obj runtime.Object, createValidation rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) { - ds, ok := obj.(*v0alpha1.DataSource) - if !ok { - return nil, fmt.Errorf("expected a datasource object") - } - return s.datasources.CreateDataSource(ctx, ds) -} - -// Update implements rest.Updater. -func (s *legacyStorage) Update(ctx context.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc, forceAllowCreate bool, options *metav1.UpdateOptions) (runtime.Object, bool, error) { - old, err := s.Get(ctx, name, &metav1.GetOptions{}) - if err != nil { - return nil, false, err - } - - obj, err := objInfo.UpdatedObject(ctx, old) - if err != nil { - return nil, false, err - } - - ds, ok := obj.(*v0alpha1.DataSource) - if !ok { - return nil, false, fmt.Errorf("expected a datasource object") - } - - oldDS, ok := obj.(*v0alpha1.DataSource) - if !ok { - return nil, false, fmt.Errorf("expected a datasource object (old)") - } - - // Keep all the old secure values - if len(oldDS.Secure) > 0 { - for k, v := range oldDS.Secure { - _, found := ds.Secure[k] - if !found { - ds.Secure[k] = v - } - } - } - - ds, err = s.datasources.UpdateDataSource(ctx, ds) - return ds, false, err -} - -// Delete implements rest.GracefulDeleter. -func (s *legacyStorage) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) { - err := s.datasources.DeleteDataSource(ctx, name) - return nil, false, err -} - -// DeleteCollection implements rest.CollectionDeleter. -func (s *legacyStorage) DeleteCollection(ctx context.Context, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions, listOptions *internalversion.ListOptions) (runtime.Object, error) { - dss, err := s.datasources.ListDataSources(ctx) - if err != nil { - return nil, err - } - for _, ds := range dss.Items { - if err = s.datasources.DeleteDataSource(ctx, ds.Name); err != nil { - return nil, err - } - } - return nil, nil -} diff --git a/pkg/registry/apis/datasource/noop.go b/pkg/registry/apis/datasource/noop.go deleted file mode 100644 index 9bff0298db6..00000000000 --- a/pkg/registry/apis/datasource/noop.go +++ /dev/null @@ -1,42 +0,0 @@ -package datasource - -import ( - "context" - - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apiserver/pkg/registry/rest" - - query "github.com/grafana/grafana/pkg/apis/query/v0alpha1" -) - -// Temporary noop storage that lets us map /connections/{name}/query -type noopREST struct{} - -var ( - _ rest.Storage = (*noopREST)(nil) - _ rest.Scoper = (*noopREST)(nil) - _ rest.Getter = (*noopREST)(nil) - _ rest.SingularNameProvider = (*noopREST)(nil) -) - -func (r *noopREST) New() runtime.Object { - return &query.QueryDataResponse{} -} - -func (r *noopREST) Destroy() {} - -func (r *noopREST) NamespaceScoped() bool { - return true -} - -func (r *noopREST) GetSingularName() string { - return "noop" -} - -func (r *noopREST) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) { - return &metav1.Status{ - Status: metav1.StatusSuccess, - Message: "noop", - }, nil -} diff --git a/pkg/registry/apis/datasource/openapi.go b/pkg/registry/apis/datasource/openapi.go deleted file mode 100644 index ab63c29e819..00000000000 --- a/pkg/registry/apis/datasource/openapi.go +++ /dev/null @@ -1,74 +0,0 @@ -package datasource - -import ( - "fmt" - - "k8s.io/kube-openapi/pkg/spec3" - "k8s.io/kube-openapi/pkg/validation/spec" - - "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 - if 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), - }); err != nil { - return nil, err - } - - // Hide the resource routes -- explicit ones will be added if defined below - prefix := root + "namespaces/{namespace}/datasources/{name}/resource" - r := oas.Paths.Paths[prefix] - if r != nil && r.Get != nil { - r.Get.Description = "Get resources in the datasource plugin. NOTE, additional routes may exist, but are not exposed via OpenAPI" - r.Delete = nil - r.Head = nil - r.Patch = nil - r.Post = nil - r.Put = nil - r.Options = nil - } - delete(oas.Paths.Paths, prefix+"/{path}") - - // 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") - - // Mark connections as deprecated - delete(oas.Paths.Paths, root+"namespaces/{namespace}/connections/{name}") - query := oas.Paths.Paths[root+"namespaces/{namespace}/connections/{name}/query"] - for query == nil || query.Post == nil { - return nil, fmt.Errorf("missing temporary connection path") - } - query.Post.Tags = []string{"Connections (deprecated)"} - query.Post.Deprecated = true - query.Post.RequestBody = &spec3.RequestBody{ - RequestBodyProps: spec3.RequestBodyProps{ - Content: map[string]*spec3.MediaType{ - "application/json": { - MediaTypeProps: spec3.MediaTypeProps{ - Schema: spec.MapProperty(nil), - }, - }, - }, - }, - } - - return oas, nil -} diff --git a/pkg/registry/apis/datasource/plugincontext.go b/pkg/registry/apis/datasource/plugincontext.go index a0525030dcb..aa9c01954da 100644 --- a/pkg/registry/apis/datasource/plugincontext.go +++ b/pkg/registry/apis/datasource/plugincontext.go @@ -5,33 +5,27 @@ import ( "fmt" "github.com/grafana/grafana-plugin-sdk-go/backend" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "github.com/grafana/grafana/pkg/apimachinery/identity" - datasourceV0 "github.com/grafana/grafana/pkg/apis/datasource/v0alpha1" + "github.com/grafana/grafana/pkg/apimachinery/utils" + "github.com/grafana/grafana/pkg/apis/datasource/v0alpha1" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" + gapiutil "github.com/grafana/grafana/pkg/services/apiserver/utils" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext" - "github.com/grafana/grafana/pkg/setting" ) // This provides access to settings saved in the database. // Authorization checks will happen within each function, and the user in ctx will // limit which namespace/tenant/org we are talking to type PluginDatasourceProvider interface { - // Get a single data source (any type) - GetDataSource(ctx context.Context, uid string) (*datasourceV0.DataSource, error) + // Get gets a specific datasource (that the user in context can see) + Get(ctx context.Context, uid string) (*v0alpha1.DataSourceConnection, error) - // List all datasources (any type) - ListDataSources(ctx context.Context) (*datasourceV0.DataSourceList, error) - - // Create a data source - CreateDataSource(ctx context.Context, ds *datasourceV0.DataSource) (*datasourceV0.DataSource, error) - - // Update a data source - UpdateDataSource(ctx context.Context, ds *datasourceV0.DataSource) (*datasourceV0.DataSource, error) - - // Delete a data source (any type) - DeleteDataSource(ctx context.Context, uid string) error + // List lists all data sources the user in context can see + List(ctx context.Context) (*v0alpha1.DataSourceConnectionList, error) // Return settings (decrypted!) for a specific plugin // This will require "query" permission for the user in context @@ -50,16 +44,11 @@ type PluginContextWrapper interface { func ProvideDefaultPluginConfigs( dsService datasources.DataSourceService, dsCache datasources.CacheService, - contextProvider *plugincontext.Provider, - cfg *setting.Cfg, -) ScopedPluginDatasourceProvider { + contextProvider *plugincontext.Provider) ScopedPluginDatasourceProvider { return &cachingDatasourceProvider{ dsService: dsService, dsCache: dsCache, contextProvider: contextProvider, - converter: &converter{ - mapper: request.GetNamespaceMapper(cfg), - }, } } @@ -67,22 +56,14 @@ type cachingDatasourceProvider struct { dsService datasources.DataSourceService dsCache datasources.CacheService contextProvider *plugincontext.Provider - converter *converter } func (q *cachingDatasourceProvider) GetDatasourceProvider(pluginJson plugins.JSONData) PluginDatasourceProvider { - group, _ := plugins.GetDatasourceGroupNameFromPluginID(pluginJson.ID) return &scopedDatasourceProvider{ plugin: pluginJson, dsService: q.dsService, dsCache: q.dsCache, contextProvider: q.contextProvider, - converter: &converter{ - mapper: q.converter.mapper, - plugin: pluginJson.ID, - alias: pluginJson.AliasIDs, - group: group, - }, } } @@ -91,7 +72,6 @@ type scopedDatasourceProvider struct { dsService datasources.DataSourceService dsCache datasources.CacheService contextProvider *plugincontext.Provider - converter *converter } var ( @@ -99,62 +79,11 @@ var ( _ ScopedPluginDatasourceProvider = (*cachingDatasourceProvider)(nil) ) -func (q *scopedDatasourceProvider) GetInstanceSettings(ctx context.Context, uid string) (*backend.DataSourceInstanceSettings, error) { - if q.contextProvider == nil { - return nil, fmt.Errorf("missing contextProvider") - } - return q.contextProvider.GetDataSourceInstanceSettings(ctx, uid) -} - -// CreateDataSource implements PluginDatasourceProvider. -func (q *scopedDatasourceProvider) CreateDataSource(ctx context.Context, ds *datasourceV0.DataSource) (*datasourceV0.DataSource, error) { - cmd, err := q.converter.toAddCommand(ds) +func (q *scopedDatasourceProvider) Get(ctx context.Context, uid string) (*v0alpha1.DataSourceConnection, error) { + info, err := request.NamespaceInfoFrom(ctx, true) if err != nil { return nil, err } - out, err := q.dsService.AddDataSource(ctx, cmd) - if err != nil { - return nil, err - } - return q.converter.asDataSource(out) -} - -// UpdateDataSource implements PluginDatasourceProvider. -func (q *scopedDatasourceProvider) UpdateDataSource(ctx context.Context, ds *datasourceV0.DataSource) (*datasourceV0.DataSource, error) { - cmd, err := q.converter.toUpdateCommand(ds) - if err != nil { - return nil, err - } - out, err := q.dsService.UpdateDataSource(ctx, cmd) - if err != nil { - return nil, err - } - return q.converter.asDataSource(out) -} - -// Delete implements PluginDatasourceProvider. -func (q *scopedDatasourceProvider) DeleteDataSource(ctx context.Context, uid string) error { - user, err := identity.GetRequester(ctx) - if err != nil { - return err - } - ds, err := q.dsCache.GetDatasourceByUID(ctx, uid, user, false) - if err != nil { - return err - } - if ds == nil { - return fmt.Errorf("not found") - } - return q.dsService.DeleteDataSource(ctx, &datasources.DeleteDataSourceCommand{ - ID: ds.ID, - UID: ds.UID, - OrgID: ds.OrgID, - Name: ds.Name, - }) -} - -// GetDataSource implements PluginDatasourceProvider. -func (q *scopedDatasourceProvider) GetDataSource(ctx context.Context, uid string) (*datasourceV0.DataSource, error) { user, err := identity.GetRequester(ctx) if err != nil { return nil, err @@ -163,11 +92,10 @@ func (q *scopedDatasourceProvider) GetDataSource(ctx context.Context, uid string if err != nil { return nil, err } - return q.converter.asDataSource(ds) + return asConnection(ds, info.Value) } -// ListDataSource implements PluginDatasourceProvider. -func (q *scopedDatasourceProvider) ListDataSources(ctx context.Context) (*datasourceV0.DataSourceList, error) { +func (q *scopedDatasourceProvider) List(ctx context.Context) (*v0alpha1.DataSourceConnectionList, error) { info, err := request.NamespaceInfoFrom(ctx, true) if err != nil { return nil, err @@ -181,12 +109,37 @@ func (q *scopedDatasourceProvider) ListDataSources(ctx context.Context) (*dataso if err != nil { return nil, err } - result := &datasourceV0.DataSourceList{ - Items: []datasourceV0.DataSource{}, + result := &v0alpha1.DataSourceConnectionList{ + Items: []v0alpha1.DataSourceConnection{}, } for _, ds := range dss { - v, _ := q.converter.asDataSource(ds) + v, _ := asConnection(ds, info.Value) result.Items = append(result.Items, *v) } return result, nil } + +func (q *scopedDatasourceProvider) GetInstanceSettings(ctx context.Context, uid string) (*backend.DataSourceInstanceSettings, error) { + if q.contextProvider == nil { + return nil, fmt.Errorf("missing contextProvider") + } + return q.contextProvider.GetDataSourceInstanceSettings(ctx, uid) +} + +func asConnection(ds *datasources.DataSource, ns string) (*v0alpha1.DataSourceConnection, error) { + v := &v0alpha1.DataSourceConnection{ + ObjectMeta: metav1.ObjectMeta{ + Name: ds.UID, + Namespace: ns, + CreationTimestamp: metav1.NewTime(ds.Created), + ResourceVersion: fmt.Sprintf("%d", ds.Updated.UnixMilli()), + }, + Title: ds.Name, + } + v.UID = gapiutil.CalculateClusterWideUID(v) // indicates if the value changed on the server + meta, err := utils.MetaAccessor(v) + if err != nil { + meta.SetUpdatedTimestamp(&ds.Updated) + } + return v, err +} diff --git a/pkg/registry/apis/datasource/querier.go b/pkg/registry/apis/datasource/querier.go index 5ab7b5e5a5e..c18e3ec1b57 100644 --- a/pkg/registry/apis/datasource/querier.go +++ b/pkg/registry/apis/datasource/querier.go @@ -4,10 +4,13 @@ import ( "context" "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/apimachinery/utils" + "github.com/grafana/grafana/pkg/apis/datasource/v0alpha1" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" "github.com/grafana/grafana/pkg/services/datasources" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) type QuerierFactoryFunc func(ctx context.Context, ri utils.ResourceInfo, pj plugins.JSONData) (Querier, error) @@ -45,6 +48,10 @@ type Querier interface { Health(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) // Resource gets a resource plugin. Resource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error + // Datasource gets all data source plugins (with elevated permissions). + Datasource(ctx context.Context, name string) (*v0alpha1.DataSourceConnection, error) + // Datasources lists all data sources (with elevated permissions). + Datasources(ctx context.Context) (*v0alpha1.DataSourceConnectionList, error) } type DefaultQuerier struct { @@ -94,3 +101,47 @@ func (q *DefaultQuerier) Health(ctx context.Context, query *backend.CheckHealthR } return q.pluginClient.CheckHealth(ctx, query) } + +func (q *DefaultQuerier) Datasource(ctx context.Context, name string) (*v0alpha1.DataSourceConnection, error) { + info, err := request.NamespaceInfoFrom(ctx, true) + if err != nil { + return nil, err + } + user, err := identity.GetRequester(ctx) + if err != nil { + return nil, err + } + ds, err := q.dsCache.GetDatasourceByUID(ctx, name, user, false) + if err != nil { + return nil, err + } + return asConnection(ds, info.Value) +} + +func (q *DefaultQuerier) Datasources(ctx context.Context) (*v0alpha1.DataSourceConnectionList, error) { + info, err := request.NamespaceInfoFrom(ctx, true) + if err != nil { + return nil, err + } + + ds, err := q.dsService.GetDataSourcesByType(ctx, &datasources.GetDataSourcesByTypeQuery{ + OrgID: info.OrgID, + Type: q.pluginJSON.ID, + }) + if err != nil { + return nil, err + } + return asConnectionList(q.connectionResourceInfo.TypeMeta(), ds, info.Value) +} + +func asConnectionList(typeMeta metav1.TypeMeta, dss []*datasources.DataSource, ns string) (*v0alpha1.DataSourceConnectionList, error) { + result := &v0alpha1.DataSourceConnectionList{ + Items: []v0alpha1.DataSourceConnection{}, + } + for _, ds := range dss { + v, _ := asConnection(ds, ns) + result.Items = append(result.Items, *v) + } + + return result, nil +} diff --git a/pkg/registry/apis/datasource/register.go b/pkg/registry/apis/datasource/register.go index dd1b69d971f..dc31d8cbd9e 100644 --- a/pkg/registry/apis/datasource/register.go +++ b/pkg/registry/apis/datasource/register.go @@ -3,7 +3,7 @@ package datasource import ( "context" "encoding/json" - "maps" + "fmt" "github.com/prometheus/client_golang/prometheus" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -13,13 +13,13 @@ 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/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" - grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic" + datasource "github.com/grafana/grafana/pkg/apis/datasource/v0alpha1" + query "github.com/grafana/grafana/pkg/apis/query/v0alpha1" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/promlib/models" @@ -31,22 +31,18 @@ import ( "github.com/grafana/grafana/pkg/tsdb/grafana-testdata-datasource/kinds" ) -var ( - _ builder.APIGroupBuilder = (*DataSourceAPIBuilder)(nil) - // _ builder.APIGroupMutation = (*DataSourceAPIBuilder)(nil) - // _ builder.APIGroupValidation = (*DataSourceAPIBuilder)(nil) -) +var _ builder.APIGroupBuilder = (*DataSourceAPIBuilder)(nil) // DataSourceAPIBuilder is used just so wire has something unique to return type DataSourceAPIBuilder struct { - datasourceResourceInfo utils.ResourceInfo + connectionResourceInfo utils.ResourceInfo pluginJSON plugins.JSONData - client PluginClient // will only ever be called with the same plugin id! + client PluginClient // will only ever be called with the same pluginid! datasources PluginDatasourceProvider contextProvider PluginContextWrapper accessControl accesscontrol.AccessControl - queryTypes *queryV0.QueryTypeDefinitionList + queryTypes *query.QueryTypeDefinitionList log log.Logger } @@ -96,12 +92,6 @@ func RegisterAPIService( if err != nil { return nil, err } - - // TODO: load the schema provider from a static manifest - // if ds.ID == "grafana-testdata-datasource" { - // builder.schemaProvider = hardcoded.TestdataOpenAPIExtension - // } - apiRegistrar.RegisterAPI(builder) } return builder, nil // only used for wire @@ -124,13 +114,13 @@ func NewDataSourceAPIBuilder( accessControl accesscontrol.AccessControl, loadQueryTypes bool, ) (*DataSourceAPIBuilder, error) { - group, err := plugins.GetDatasourceGroupNameFromPluginID(plugin.ID) + ri, err := resourceFromPluginID(plugin.ID) if err != nil { return nil, err } builder := &DataSourceAPIBuilder{ - datasourceResourceInfo: datasourceV0.DataSourceResourceInfo.WithGroupAndShortName(group, plugin.ID), + connectionResourceInfo: ri, pluginJSON: plugin, client: client, datasources: datasources, @@ -140,13 +130,13 @@ func NewDataSourceAPIBuilder( } if loadQueryTypes { // In the future, this will somehow come from the plugin - builder.queryTypes, err = getHardcodedQueryTypes(group) + builder.queryTypes, err = getHardcodedQueryTypes(ri.GroupResource().Group) } return builder, err } // TODO -- somehow get the list from the plugin -- not hardcoded -func getHardcodedQueryTypes(group string) (*queryV0.QueryTypeDefinitionList, error) { +func getHardcodedQueryTypes(group string) (*query.QueryTypeDefinitionList, error) { var err error var raw json.RawMessage switch group { @@ -159,7 +149,7 @@ func getHardcodedQueryTypes(group string) (*queryV0.QueryTypeDefinitionList, err return nil, err } if raw != nil { - types := &queryV0.QueryTypeDefinitionList{} + types := &query.QueryTypeDefinitionList{} err = json.Unmarshal(raw, types) return types, err } @@ -167,27 +157,26 @@ func getHardcodedQueryTypes(group string) (*queryV0.QueryTypeDefinitionList, err } func (b *DataSourceAPIBuilder) GetGroupVersion() schema.GroupVersion { - return b.datasourceResourceInfo.GroupVersion() + return b.connectionResourceInfo.GroupVersion() } func addKnownTypes(scheme *runtime.Scheme, gv schema.GroupVersion) { scheme.AddKnownTypes(gv, - &datasourceV0.DataSource{}, - &datasourceV0.DataSourceList{}, - &datasourceV0.HealthCheckResult{}, + &datasource.DataSourceConnection{}, + &datasource.DataSourceConnectionList{}, + &datasource.HealthCheckResult{}, &unstructured.Unstructured{}, - // Query handler - &queryV0.QueryDataRequest{}, - &queryV0.QueryDataResponse{}, - &queryV0.QueryTypeDefinition{}, - &queryV0.QueryTypeDefinitionList{}, + &query.QueryDataRequest{}, + &query.QueryDataResponse{}, + &query.QueryTypeDefinition{}, + &query.QueryTypeDefinitionList{}, &metav1.Status{}, ) } func (b *DataSourceAPIBuilder) InstallSchema(scheme *runtime.Scheme) error { - gv := b.datasourceResourceInfo.GroupVersion() + gv := b.connectionResourceInfo.GroupVersion() addKnownTypes(scheme, gv) // Link this version to the internal representation. @@ -210,48 +199,43 @@ func (b *DataSourceAPIBuilder) AllowedV0Alpha1Resources() []string { return []string{builder.AllResourcesAllowed} } -func (b *DataSourceAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIGroupInfo, opts builder.APIGroupOptions) error { +func resourceFromPluginID(pluginID string) (utils.ResourceInfo, error) { + group, err := plugins.GetDatasourceGroupNameFromPluginID(pluginID) + if err != nil { + return utils.ResourceInfo{}, err + } + return datasource.GenericConnectionResourceInfo.WithGroupAndShortName(group, pluginID+"-connection"), nil +} + +func (b *DataSourceAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIGroupInfo, _ builder.APIGroupOptions) error { storage := map[string]rest.Storage{} - // Register the raw datasource connection - ds := b.datasourceResourceInfo - legacyStore := &legacyStorage{ - datasources: b.datasources, - resourceInfo: &ds, - } - unified, err := grafanaregistry.NewRegistryStore(opts.Scheme, ds, opts.OptsGetter) - if err != nil { - return err - } - storage[ds.StoragePath()], err = opts.DualWriteBuilder(ds.GroupResource(), legacyStore, unified) - if err != nil { - return err + conn := b.connectionResourceInfo + storage[conn.StoragePath()] = &connectionAccess{ + datasources: b.datasources, + resourceInfo: conn, + tableConverter: conn.TableConverter(), } + storage[conn.StoragePath("query")] = &subQueryREST{builder: b} + storage[conn.StoragePath("health")] = &subHealthREST{builder: b} - storage[ds.StoragePath("query")] = &subQueryREST{builder: b} - storage[ds.StoragePath("health")] = &subHealthREST{builder: b} - storage[ds.StoragePath("resource")] = &subResourceREST{builder: b} - - // FIXME: temporarily register both "datasources" and "connections" query paths - // This lets us deploy both datasources/{uid}/query and connections/{uid}/query - // while we transition requests to the new path - storage["connections"] = &noopREST{} // hidden from openapi - storage["connections/query"] = storage[ds.StoragePath("query")] // deprecated in openapi + // TODO! only setup this endpoint if it is implemented + storage[conn.StoragePath("resource")] = &subResourceREST{builder: b} // Frontend proxy if len(b.pluginJSON.Routes) > 0 { - storage[ds.StoragePath("proxy")] = &subProxyREST{pluginJSON: b.pluginJSON} + storage[conn.StoragePath("proxy")] = &subProxyREST{pluginJSON: b.pluginJSON} } // Register hardcoded query schemas - err = queryschema.RegisterQueryTypes(b.queryTypes, storage) + err := queryschema.RegisterQueryTypes(b.queryTypes, storage) if err != nil { return err } registerQueryConvert(b.client, b.contextProvider, storage) - apiGroupInfo.VersionedResourcesStorageMap[ds.GroupVersion().Version] = storage + apiGroupInfo.VersionedResourcesStorageMap[conn.GroupVersion().Version] = storage return err } @@ -265,8 +249,31 @@ func (b *DataSourceAPIBuilder) getPluginContext(ctx context.Context, uid string) func (b *DataSourceAPIBuilder) GetOpenAPIDefinitions() openapi.GetOpenAPIDefinitions { return func(ref openapi.ReferenceCallback) map[string]openapi.OpenAPIDefinition { - defs := queryV0.GetOpenAPIDefinitions(ref) // required when running standalone - maps.Copy(defs, datasourceV0.GetOpenAPIDefinitions(ref)) + defs := query.GetOpenAPIDefinitions(ref) // required when running standalone + for k, v := range datasource.GetOpenAPIDefinitions(ref) { + defs[k] = v + } 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.connectionResourceInfo.GroupVersion().String() + "/" + + // Add queries to the request properties + // Add queries to the request properties + err := queryschema.AddQueriesToOpenAPI(queryschema.OASQueryOptions{ + Swagger: oas, + PluginJSON: &b.pluginJSON, + QueryTypes: b.queryTypes, + Root: root, + QueryPath: "namespaces/{namespace}/connections/{name}/query", + QueryDescription: fmt.Sprintf("Query the %s datasources", b.pluginJSON.Name), + }) + + return oas, err +} diff --git a/pkg/registry/apis/datasource/sub_query.go b/pkg/registry/apis/datasource/sub_query.go index c7bab42f950..51f8b90d430 100644 --- a/pkg/registry/apis/datasource/sub_query.go +++ b/pkg/registry/apis/datasource/sub_query.go @@ -6,16 +6,18 @@ import ( "fmt" "net/http" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apiserver/pkg/registry/rest" - "github.com/grafana/grafana-plugin-sdk-go/backend" data "github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1" query "github.com/grafana/grafana/pkg/apis/query/v0alpha1" query_headers "github.com/grafana/grafana/pkg/registry/apis/query" "github.com/grafana/grafana/pkg/services/datasources" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apiserver/pkg/registry/rest" "github.com/grafana/grafana/pkg/web" + + k8serrors "k8s.io/apimachinery/pkg/api/errors" ) type subQueryREST struct { @@ -26,7 +28,6 @@ var ( _ rest.Storage = (*subQueryREST)(nil) _ rest.Connecter = (*subQueryREST)(nil) _ rest.StorageMetadata = (*subQueryREST)(nil) - _ rest.Scoper = (*subQueryREST)(nil) ) func (r *subQueryREST) New() runtime.Object { @@ -36,10 +37,6 @@ func (r *subQueryREST) New() runtime.Object { func (r *subQueryREST) Destroy() {} -func (r *subQueryREST) NamespaceScoped() bool { - return true -} - func (r *subQueryREST) ProducesMIMETypes(verb string) []string { return []string{"application/json"} // and parquet! } @@ -61,8 +58,15 @@ func (r *subQueryREST) Connect(ctx context.Context, name string, opts runtime.Ob if err != nil { if errors.Is(err, datasources.ErrDataSourceNotFound) { - return nil, r.builder.datasourceResourceInfo.NewNotFound(name) + return nil, k8serrors.NewNotFound( + schema.GroupResource{ + Group: r.builder.connectionResourceInfo.GroupResource().Group, + Resource: r.builder.connectionResourceInfo.GroupResource().Resource, + }, + name, + ) } + return nil, err } diff --git a/pkg/registry/apis/datasource/sub_query_test.go b/pkg/registry/apis/datasource/sub_query_test.go index 57aa7626aba..6b3dc54b970 100644 --- a/pkg/registry/apis/datasource/sub_query_test.go +++ b/pkg/registry/apis/datasource/sub_query_test.go @@ -8,16 +8,14 @@ import ( "net/http/httptest" "testing" - "github.com/stretchr/testify/require" - k8serrors "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/runtime" - "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana/pkg/apis/datasource/v0alpha1" - queryV0 "github.com/grafana/grafana/pkg/apis/query/v0alpha1" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/ngalert/models" + "github.com/stretchr/testify/require" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" ) func TestSubQueryConnect(t *testing.T) { @@ -117,43 +115,16 @@ func (m mockResponder) Object(statusCode int, obj runtime.Object) { func (m mockResponder) Error(err error) { } -var _ PluginDatasourceProvider = (*mockDatasources)(nil) - type mockDatasources struct { } -// CreateDataSource implements PluginDatasourceProvider. -func (m mockDatasources) CreateDataSource(ctx context.Context, ds *v0alpha1.DataSource) (*v0alpha1.DataSource, error) { - return nil, nil -} - -// UpdateDataSource implements PluginDatasourceProvider. -func (m mockDatasources) UpdateDataSource(ctx context.Context, ds *v0alpha1.DataSource) (*v0alpha1.DataSource, error) { - return nil, nil -} - -// Delete implements PluginDatasourceProvider. -func (m mockDatasources) DeleteDataSource(ctx context.Context, uid string) error { - return nil -} - -// GetDataSource implements PluginDatasourceProvider. -func (m mockDatasources) GetDataSource(ctx context.Context, uid string) (*v0alpha1.DataSource, error) { - return nil, nil -} - -// ListDataSource implements PluginDatasourceProvider. -func (m mockDatasources) ListDataSources(ctx context.Context) (*v0alpha1.DataSourceList, error) { - return nil, nil -} - // Get gets a specific datasource (that the user in context can see) -func (m mockDatasources) GetConnection(ctx context.Context, uid string) (*queryV0.DataSourceConnection, error) { +func (m mockDatasources) Get(ctx context.Context, uid string) (*v0alpha1.DataSourceConnection, error) { return nil, nil } // List lists all data sources the user in context can see -func (m mockDatasources) ListConnections(ctx context.Context) (*queryV0.DataSourceConnectionList, error) { +func (m mockDatasources) List(ctx context.Context) (*v0alpha1.DataSourceConnectionList, error) { return nil, nil } diff --git a/pkg/registry/apis/datasource/sub_resource.go b/pkg/registry/apis/datasource/sub_resource.go index f6d3fc04b96..93027767158 100644 --- a/pkg/registry/apis/datasource/sub_resource.go +++ b/pkg/registry/apis/datasource/sub_resource.go @@ -8,11 +8,11 @@ import ( "net/url" "strings" + "github.com/grafana/grafana-plugin-sdk-go/backend" 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/backend" "github.com/grafana/grafana/pkg/plugins/httpresponsesender" ) diff --git a/pkg/registry/apis/datasource/sub_resource_test.go b/pkg/registry/apis/datasource/sub_resource_test.go index bf66d52b6d2..3900a34a732 100644 --- a/pkg/registry/apis/datasource/sub_resource_test.go +++ b/pkg/registry/apis/datasource/sub_resource_test.go @@ -18,36 +18,36 @@ func TestResourceRequest(t *testing.T) { }{ { desc: "no resource path", - url: "http://localhost:6443/apis/test.datasource.grafana.app/v0alpha1/namespaces/default/datasources/abc", + url: "http://localhost:6443/apis/test.datasource.grafana.app/v0alpha1/namespaces/default/connections/abc", error: true, }, { desc: "root resource path", - url: "http://localhost:6443/apis/test.datasource.grafana.app/v0alpha1/namespaces/default/datasources/abc/resource", + url: "http://localhost:6443/apis/test.datasource.grafana.app/v0alpha1/namespaces/default/connections/abc/resource", expectedPath: "", expectedURL: "", }, { desc: "root resource path", - url: "http://localhost:6443/apis/test.datasource.grafana.app/v0alpha1/namespaces/default/datasources/abc/resource/", + url: "http://localhost:6443/apis/test.datasource.grafana.app/v0alpha1/namespaces/default/connections/abc/resource/", expectedPath: "", expectedURL: "", }, { desc: "resource sub path", - url: "http://localhost:6443/apis/test.datasource.grafana.app/v0alpha1/namespaces/default/datasources/abc/resource/test", + url: "http://localhost:6443/apis/test.datasource.grafana.app/v0alpha1/namespaces/default/connections/abc/resource/test", expectedPath: "test", expectedURL: "test", }, { desc: "resource sub path with colon", - url: "http://localhost:6443/apis/test.datasource.grafana.app/v0alpha1/namespaces/default/datasources/abc/resource/test-*,*:test-*/_mapping", + url: "http://localhost:6443/apis/test.datasource.grafana.app/v0alpha1/namespaces/default/connections/abc/resource/test-*,*:test-*/_mapping", expectedPath: "test-*,*:test-*/_mapping", expectedURL: "./test-%2A,%2A:test-%2A/_mapping", }, { desc: "resource sub path with query params", - url: "http://localhost:6443/apis/test.datasource.grafana.app/v0alpha1/namespaces/default/datasources/abc/resource/test?k1=v1&k2=v2", + url: "http://localhost:6443/apis/test.datasource.grafana.app/v0alpha1/namespaces/default/connections/abc/resource/test?k1=v1&k2=v2", expectedPath: "test", expectedURL: "test?k1=v1&k2=v2", }, diff --git a/pkg/registry/apis/datasource/testdata/convert-dto-empty-to-resource.json b/pkg/registry/apis/datasource/testdata/convert-dto-empty-to-resource.json deleted file mode 100644 index 4b35a3e1da8..00000000000 --- a/pkg/registry/apis/datasource/testdata/convert-dto-empty-to-resource.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "metadata": { - "name": "unique-identifier", - "namespace": "org-0", - "uid": "YpaSG5GQAdxtLZtF6BqQWCeYXOhbVi5C4Cg4oILnJC0X", - "generation": 8, - "creationTimestamp": "2002-03-04T01:00:00Z", - "labels": { - "grafana.app/deprecatedInternalID": "456" - } - }, - "spec": { - "jsonData": null, - "title": "Display name" - } -} \ No newline at end of file diff --git a/pkg/registry/apis/datasource/testdata/convert-dto-empty.json b/pkg/registry/apis/datasource/testdata/convert-dto-empty.json deleted file mode 100644 index f19a05a3f4a..00000000000 --- a/pkg/registry/apis/datasource/testdata/convert-dto-empty.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "id": 456, - "version": 8, - "name": "Display name", - "uid": "unique-identifier", - "type": "grafana-testdata-datasource", - "created": "2002-03-04T01:00:00Z" -} \ No newline at end of file diff --git a/pkg/registry/apis/datasource/testdata/convert-dto-invalid.json b/pkg/registry/apis/datasource/testdata/convert-dto-invalid.json deleted file mode 100644 index 6e5a5ca2ee2..00000000000 --- a/pkg/registry/apis/datasource/testdata/convert-dto-invalid.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "id": 456, - "version": 8, - "name": "Hello", - "uid": "unique-identifier", - "type": "not-valid-plugin", - "created": "2002-03-04T01:00:00Z" -} \ No newline at end of file diff --git a/pkg/registry/apis/datasource/testdata/convert-dto-testdata-to-resource.json b/pkg/registry/apis/datasource/testdata/convert-dto-testdata-to-resource.json deleted file mode 100644 index 0bfdaf4eb02..00000000000 --- a/pkg/registry/apis/datasource/testdata/convert-dto-testdata-to-resource.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "apiVersion": "testdata.grafana.datasource.app/v2alpha1", - "metadata": { - "name": "unique-identifier", - "namespace": "org-0", - "uid": "YpaSG5GQAdxtLZtF6BqQWCeYXOhbVi5C4Cg4oILnJC0X", - "resourceVersion": "1083805200000", - "generation": 2, - "creationTimestamp": "2002-03-04T01:00:00Z", - "labels": { - "grafana.app/deprecatedInternalID": "1234" - }, - "annotations": { - "grafana.app/updatedTimestamp": "2004-05-06T01:00:00Z" - } - }, - "spec": { - "access": "proxy", - "basicAuth": true, - "basicAuthUser": "xxx", - "database": "db", - "isDefault": true, - "jsonData": { - "aaa": "bbb", - "bbb": true, - "ccc": 1.234 - }, - "readOnly": true, - "title": "Hello", - "url": "http://something/", - "user": "A", - "withCredentials": true - }, - "secure": { - "password": { - "name": "ds-d5c1b093af" - } - } -} \ No newline at end of file diff --git a/pkg/registry/apis/datasource/testdata/convert-dto-testdata.json b/pkg/registry/apis/datasource/testdata/convert-dto-testdata.json deleted file mode 100644 index 8dd66e43875..00000000000 --- a/pkg/registry/apis/datasource/testdata/convert-dto-testdata.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "id": 1234, - "version": 2, - "name": "Hello", - "uid": "unique-identifier", - "type": "grafana-testdata-datasource", - "access": "proxy", - "url": "http://something/", - "user": "A", - "database": "db", - "basicAuth": true, - "basicAuthUser": "xxx", - "withCredentials": true, - "isDefault": true, - "jsonData": { - "aaa": "bbb", - "bbb": true, - "ccc": 1.234 - }, - "secureJsonData": { - "password": "XXXX" - }, - "readOnly": true, - "apiVersion": "v2alpha1", - "created": "2002-03-04T01:00:00Z", - "updated": "2004-05-06T01:00:00Z" -} \ No newline at end of file diff --git a/pkg/registry/apis/datasource/testdata/convert-resource-empty-to-cmd-add.json b/pkg/registry/apis/datasource/testdata/convert-resource-empty-to-cmd-add.json deleted file mode 100644 index fc8787b0fb3..00000000000 --- a/pkg/registry/apis/datasource/testdata/convert-resource-empty-to-cmd-add.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "Hello testdata", - "type": "grafana-testdata-datasource", - "access": "", - "url": "", - "user": "", - "database": "", - "basicAuth": false, - "basicAuthUser": "", - "withCredentials": false, - "isDefault": false, - "jsonData": null, - "secureJsonData": null, - "uid": "cejobd88i85j4d" -} \ No newline at end of file diff --git a/pkg/registry/apis/datasource/testdata/convert-resource-empty-to-cmd-update-roundtrip.json b/pkg/registry/apis/datasource/testdata/convert-resource-empty-to-cmd-update-roundtrip.json deleted file mode 100644 index 4b16a1455a2..00000000000 --- a/pkg/registry/apis/datasource/testdata/convert-resource-empty-to-cmd-update-roundtrip.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "metadata": { - "name": "cejobd88i85j4d", - "namespace": "org-0", - "uid": "boDNh7zU3nXj46rOXIJI7r44qaxjs8yy9I9dOj1MyBoX", - "creationTimestamp": null - }, - "spec": { - "jsonData": null, - "title": "Hello testdata" - } -} \ No newline at end of file diff --git a/pkg/registry/apis/datasource/testdata/convert-resource-empty-to-cmd-update.json b/pkg/registry/apis/datasource/testdata/convert-resource-empty-to-cmd-update.json deleted file mode 100644 index 3d5cf24700a..00000000000 --- a/pkg/registry/apis/datasource/testdata/convert-resource-empty-to-cmd-update.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "Hello testdata", - "type": "grafana-testdata-datasource", - "access": "", - "url": "", - "user": "", - "database": "", - "basicAuth": false, - "basicAuthUser": "", - "withCredentials": false, - "isDefault": false, - "jsonData": null, - "secureJsonData": null, - "uid": "cejobd88i85j4d", - "version": 0 -} \ No newline at end of file diff --git a/pkg/registry/apis/datasource/testdata/convert-resource-empty.json b/pkg/registry/apis/datasource/testdata/convert-resource-empty.json deleted file mode 100644 index 55a9d7fb1a6..00000000000 --- a/pkg/registry/apis/datasource/testdata/convert-resource-empty.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "metadata": { - "name": "cejobd88i85j4d" - }, - "spec": { - "title": "Hello testdata" - } -} \ No newline at end of file diff --git a/pkg/registry/apis/datasource/testdata/convert-resource-full-to-cmd-add.json b/pkg/registry/apis/datasource/testdata/convert-resource-full-to-cmd-add.json deleted file mode 100644 index 83e06abc5b2..00000000000 --- a/pkg/registry/apis/datasource/testdata/convert-resource-full-to-cmd-add.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "Hello testdata", - "type": "grafana-testdata-datasource", - "access": "proxy", - "url": "http://something/", - "user": "", - "database": "db", - "basicAuth": true, - "basicAuthUser": "xxx", - "withCredentials": true, - "isDefault": true, - "jsonData": { - "aaa": "bbb", - "bbb": true, - "ccc": 1.234 - }, - "secureJsonData": { - "extra": "", - "password": "XXXX" - }, - "uid": "cejobd88i85j4d" -} \ No newline at end of file diff --git a/pkg/registry/apis/datasource/testdata/convert-resource-full-to-cmd-update-roundtrip.json b/pkg/registry/apis/datasource/testdata/convert-resource-full-to-cmd-update-roundtrip.json deleted file mode 100644 index 85535306cb0..00000000000 --- a/pkg/registry/apis/datasource/testdata/convert-resource-full-to-cmd-update-roundtrip.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "metadata": { - "name": "cejobd88i85j4d", - "namespace": "org-0", - "uid": "boDNh7zU3nXj46rOXIJI7r44qaxjs8yy9I9dOj1MyBoX", - "generation": 2, - "creationTimestamp": null - }, - "spec": { - "access": "proxy", - "basicAuth": true, - "basicAuthUser": "xxx", - "database": "db", - "isDefault": true, - "jsonData": { - "aaa": "bbb", - "bbb": true, - "ccc": 1.234 - }, - "title": "Hello testdata", - "url": "http://something/", - "withCredentials": true - }, - "secure": { - "extra": { - "name": "ds-bb8b5d8b32" - }, - "password": { - "name": "ds-973a1eb29d" - } - } -} \ No newline at end of file diff --git a/pkg/registry/apis/datasource/testdata/convert-resource-full-to-cmd-update.json b/pkg/registry/apis/datasource/testdata/convert-resource-full-to-cmd-update.json deleted file mode 100644 index 13304fc001a..00000000000 --- a/pkg/registry/apis/datasource/testdata/convert-resource-full-to-cmd-update.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "name": "Hello testdata", - "type": "grafana-testdata-datasource", - "access": "proxy", - "url": "http://something/", - "user": "", - "database": "db", - "basicAuth": true, - "basicAuthUser": "xxx", - "withCredentials": true, - "isDefault": true, - "jsonData": { - "aaa": "bbb", - "bbb": true, - "ccc": 1.234 - }, - "secureJsonData": { - "extra": "", - "password": "XXXX" - }, - "uid": "cejobd88i85j4d", - "version": 2 -} \ No newline at end of file diff --git a/pkg/registry/apis/datasource/testdata/convert-resource-full.json b/pkg/registry/apis/datasource/testdata/convert-resource-full.json deleted file mode 100644 index 53c7086435a..00000000000 --- a/pkg/registry/apis/datasource/testdata/convert-resource-full.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "metadata": { - "name": "cejobd88i85j4d", - "namespace": "default", - "uid": "IGIUtEQS21DtLpBG2rSGfuDoUX8cwsGrtb5aXauYeA4X", - "resourceVersion": "1745320815000", - "generation": 2, - "creationTimestamp": "2025-04-22T11:20:11Z", - "labels": { - "grafana.app/deprecatedInternalID": "12345" - } - }, - "spec": { - "title": "Hello testdata", - "access": "proxy", - "isDefault": true, - "readOnly": true, - "url": "http://something/", - "database": "db", - "basicAuth": true, - "basicAuthUser": "xxx", - "withCredentials": true, - "jsonData": { - "aaa": "bbb", - "bbb": true, - "ccc": 1.234 - } - }, - "secure": { - "password": { "create": "XXXX" }, - "extra": { "remove": true } - } -} \ No newline at end of file diff --git a/pkg/registry/apis/datasource/testdata/convert-resource-invalid.json b/pkg/registry/apis/datasource/testdata/convert-resource-invalid.json deleted file mode 100644 index ccd111b6f69..00000000000 --- a/pkg/registry/apis/datasource/testdata/convert-resource-invalid.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "apiVersion": "something/else", - "metadata": { - "name": "cejobd88i85j4d", - "namespace": "default", - "uid": "IGIUtEQS21DtLpBG2rSGfuDoUX8cwsGrtb5aXauYeA4X", - "resourceVersion": "1745320815000", - "generation": 2, - "creationTimestamp": "2025-04-22T11:20:11Z", - "labels": { - "grafana.app/deprecatedInternalID": "12345" - } - }, - "spec": { - "title": "Hello testdata" - } -} \ No newline at end of file diff --git a/pkg/registry/apis/datasource/testdata/convert-resource-invalid2.json b/pkg/registry/apis/datasource/testdata/convert-resource-invalid2.json deleted file mode 100644 index b3f17071243..00000000000 --- a/pkg/registry/apis/datasource/testdata/convert-resource-invalid2.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "metadata": { - "name": "cejobd88i85j4d", - "namespace": "stacks-invalid" - }, - "spec": { - "title": "Hello testdata" - } -} \ No newline at end of file diff --git a/pkg/registry/apis/query/connections.go b/pkg/registry/apis/query/connections.go deleted file mode 100644 index a4a7e385dfc..00000000000 --- a/pkg/registry/apis/query/connections.go +++ /dev/null @@ -1,161 +0,0 @@ -package query - -import ( - "context" - "fmt" - - "k8s.io/apimachinery/pkg/apis/meta/internalversion" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apiserver/pkg/endpoints/request" - "k8s.io/apiserver/pkg/registry/rest" - - authlib "github.com/grafana/authlib/types" - "github.com/grafana/grafana/pkg/apimachinery/utils" - queryV0 "github.com/grafana/grafana/pkg/apis/query/v0alpha1" - gapiutil "github.com/grafana/grafana/pkg/services/apiserver/utils" - "github.com/grafana/grafana/pkg/services/datasources" -) - -var ( - _ rest.Scoper = (*connectionAccess)(nil) - _ rest.SingularNameProvider = (*connectionAccess)(nil) - _ rest.Getter = (*connectionAccess)(nil) - _ rest.Lister = (*connectionAccess)(nil) - _ rest.Storage = (*connectionAccess)(nil) -) - -// Get all datasource connections -- this will be backed by search or duplicated resource in unified storage -type DataSourceConnectionProvider interface { - // Get gets a specific datasource (that the user in context can see) - // The name is {group}:{name}, see /pkg/apis/query/v0alpha1/connection.go#L34 - GetConnection(ctx context.Context, namespace string, name string) (*queryV0.DataSourceConnection, error) - - // List lists all data sources the user in context can see - ListConnections(ctx context.Context, namespace string) (*queryV0.DataSourceConnectionList, error) -} - -type connectionAccess struct { - tableConverter rest.TableConvertor - connections DataSourceConnectionProvider -} - -func (s *connectionAccess) New() runtime.Object { - return queryV0.ConnectionResourceInfo.NewFunc() -} - -func (s *connectionAccess) Destroy() {} - -func (s *connectionAccess) NamespaceScoped() bool { - return true -} - -func (s *connectionAccess) GetSingularName() string { - return queryV0.ConnectionResourceInfo.GetSingularName() -} - -func (s *connectionAccess) ShortNames() []string { - return queryV0.ConnectionResourceInfo.GetShortNames() -} - -func (s *connectionAccess) NewList() runtime.Object { - return queryV0.ConnectionResourceInfo.NewListFunc() -} - -func (s *connectionAccess) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) { - if s.tableConverter == nil { - s.tableConverter = queryV0.ConnectionResourceInfo.TableConverter() - } - return s.tableConverter.ConvertToTable(ctx, object, tableOptions) -} - -func (s *connectionAccess) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) { - return s.connections.GetConnection(ctx, request.NamespaceValue(ctx), name) -} - -func (s *connectionAccess) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) { - return s.connections.ListConnections(ctx, request.NamespaceValue(ctx)) -} - -type connectionsProvider struct { - dsService datasources.DataSourceService - registry queryV0.DataSourceApiServerRegistry -} - -var ( - _ DataSourceConnectionProvider = (*connectionsProvider)(nil) -) - -func (q *connectionsProvider) GetConnection(ctx context.Context, namespace string, name string) (*queryV0.DataSourceConnection, error) { - info, err := authlib.ParseNamespace(namespace) - if err != nil { - return nil, err - } - ds, err := q.dsService.GetDataSource(ctx, &datasources.GetDataSourceQuery{ - UID: name, - OrgID: info.OrgID, - }) - if err != nil { - return nil, err - } - - // TODO... access control? - return q.asConnection(ds, namespace) -} - -func (q *connectionsProvider) ListConnections(ctx context.Context, namespace string) (*queryV0.DataSourceConnectionList, error) { - ns, err := authlib.ParseNamespace(namespace) - if err != nil { - return nil, err - } - - dss, err := q.dsService.GetDataSources(ctx, &datasources.GetDataSourcesQuery{ - OrgID: ns.OrgID, - DataSourceLimit: 10000, - }) - if err != nil { - return nil, err - } - result := &queryV0.DataSourceConnectionList{ - Items: []queryV0.DataSourceConnection{}, - } - for _, ds := range dss { - v, err := q.asConnection(ds, namespace) - if err != nil { - return nil, err - } - result.Items = append(result.Items, *v) - } - return result, nil -} - -func (q *connectionsProvider) asConnection(ds *datasources.DataSource, ns string) (v *queryV0.DataSourceConnection, err error) { - gv, err := q.registry.GetDatasourceGroupVersion(ds.Type) - if err != nil { - return nil, fmt.Errorf("datasource type %q does not map to an apiserver %w", ds.Type, err) - } - - v = &queryV0.DataSourceConnection{ - ObjectMeta: metav1.ObjectMeta{ - Name: queryV0.DataSourceConnectionName(gv.Group, ds.UID), - Namespace: ns, - CreationTimestamp: metav1.NewTime(ds.Created), - ResourceVersion: fmt.Sprintf("%d", ds.Updated.UnixMilli()), - Generation: int64(ds.Version), - }, - Title: ds.Name, - Datasource: queryV0.DataSourceConnectionRef{ - Group: gv.Group, - Version: gv.Version, - Name: ds.UID, - }, - } - v.UID = gapiutil.CalculateClusterWideUID(v) // UID is unique across all groups - if !ds.Updated.IsZero() { - meta, err := utils.MetaAccessor(v) - if err != nil { - meta.SetUpdatedTimestamp(&ds.Updated) - } - } - return v, err -} diff --git a/pkg/registry/apis/query/queryschema/oas_helper.go b/pkg/registry/apis/query/queryschema/oas_helper.go index 5ac97f2577c..628dd56751a 100644 --- a/pkg/registry/apis/query/queryschema/oas_helper.go +++ b/pkg/registry/apis/query/queryschema/oas_helper.go @@ -66,8 +66,21 @@ func AddQueriesToOpenAPI(options OASQueryOptions) error { // Rewrite the query path query := oas.Paths.Paths[root+options.QueryPath] if query != nil && query.Post != nil { - query.Post.Tags = []string{"DataSource"} + 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{ diff --git a/pkg/registry/apis/query/register.go b/pkg/registry/apis/query/register.go index 61a90f2b693..28ed79e060a 100644 --- a/pkg/registry/apis/query/register.go +++ b/pkg/registry/apis/query/register.go @@ -50,7 +50,6 @@ type QueryAPIBuilder struct { converter *expr.ResultConverter queryTypes *query.QueryTypeDefinitionList legacyDatasourceLookup service.LegacyDataSourceLookup - connections DataSourceConnectionProvider } func NewQueryAPIBuilder( @@ -61,7 +60,6 @@ func NewQueryAPIBuilder( registerer prometheus.Registerer, tracer tracing.Tracer, legacyDatasourceLookup service.LegacyDataSourceLookup, - connections DataSourceConnectionProvider, ) (*QueryAPIBuilder, error) { // Include well typed query definitions var queryTypes *query.QueryTypeDefinitionList @@ -88,7 +86,6 @@ func NewQueryAPIBuilder( tracer: tracer, features: features, queryTypes: queryTypes, - connections: connections, converter: &expr.ResultConverter{ Features: features, Tracer: tracer, @@ -130,8 +127,6 @@ func RegisterAPIService( return authorizer.DecisionAllow, "", nil }) - reg := client.NewDataSourceRegistryFromStore(pluginStore, dataSourcesService) - builder, err := NewQueryAPIBuilder( features, client.NewSingleTenantInstanceProvider(cfg, features, pluginClient, pCtxProvider, accessControl), @@ -140,7 +135,6 @@ func RegisterAPIService( registerer, tracer, legacyDatasourceLookup, - &connectionsProvider{dsService: dataSourcesService, registry: reg}, ) apiregistration.RegisterAPI(builder) return builder, err @@ -154,8 +148,6 @@ func addKnownTypes(scheme *runtime.Scheme, gv schema.GroupVersion) { scheme.AddKnownTypes(gv, &query.DataSourceApiServer{}, &query.DataSourceApiServerList{}, - &query.DataSourceConnection{}, - &query.DataSourceConnectionList{}, &query.QueryDataRequest{}, &query.QueryDataResponse{}, &query.QueryTypeDefinition{}, @@ -178,14 +170,6 @@ func (b *QueryAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIG storage := map[string]rest.Storage{} - // Get a list of all datasource instances - if b.features.IsEnabledGlobally(featuremgmt.FlagQueryServiceWithConnections) { - // Eventually this would be backed either by search or reconciler pattern - storage[query.ConnectionResourceInfo.StoragePath()] = &connectionAccess{ - connections: b.connections, - } - } - plugins := newPluginsStorage(b.registry) storage[plugins.resourceInfo.StoragePath()] = plugins if !b.features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) { diff --git a/pkg/server/wire_gen.go b/pkg/server/wire_gen.go index c760b9d8e57..8b9bdbd9720 100644 --- a/pkg/server/wire_gen.go +++ b/pkg/server/wire_gen.go @@ -732,7 +732,7 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api if err != nil { return nil, err } - scopedPluginDatasourceProvider := datasource.ProvideDefaultPluginConfigs(service15, cacheServiceImpl, plugincontextProvider, cfg) + scopedPluginDatasourceProvider := datasource.ProvideDefaultPluginConfigs(service15, cacheServiceImpl, plugincontextProvider) v := builder.ProvideDefaultBuildHandlerChainFuncFromBuilders() aggregatorRunner := aggregatorrunner.ProvideNoopAggregatorConfigurator() playlistAppInstaller, err := playlist.RegisterAppInstaller(playlistService, cfg, featureToggles) @@ -1314,7 +1314,7 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac if err != nil { return nil, err } - scopedPluginDatasourceProvider := datasource.ProvideDefaultPluginConfigs(service15, cacheServiceImpl, plugincontextProvider, cfg) + scopedPluginDatasourceProvider := datasource.ProvideDefaultPluginConfigs(service15, cacheServiceImpl, plugincontextProvider) v := builder.ProvideDefaultBuildHandlerChainFuncFromBuilders() aggregatorRunner := aggregatorrunner.ProvideNoopAggregatorConfigurator() playlistAppInstaller, err := playlist.RegisterAppInstaller(playlistService, cfg, featureToggles) diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 4b0de2ce8d0..07029af9b92 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -499,13 +499,6 @@ var ( Owner: grafanaDatasourcesCoreServicesSquad, RequiresRestart: true, // Adds a route at startup }, - { - Name: "queryServiceWithConnections", - Description: "Adds datasource connections to the query service", - Stage: FeatureStageExperimental, - Owner: grafanaDatasourcesCoreServicesSquad, - RequiresRestart: true, // Adds a route at startup - }, { Name: "queryServiceRewrite", Description: "Rewrite requests targeting /ds/query to the query service", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index c1728533830..fb581e030b4 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -65,7 +65,6 @@ dashboardSchemaValidationLogging,experimental,@grafana/grafana-app-platform-squa scanRowInvalidDashboardParseFallbackEnabled,experimental,@grafana/search-and-storage,false,false,false datasourceQueryTypes,experimental,@grafana/grafana-app-platform-squad,false,true,false queryService,experimental,@grafana/grafana-datasources-core-services,false,true,false -queryServiceWithConnections,experimental,@grafana/grafana-datasources-core-services,false,true,false queryServiceRewrite,experimental,@grafana/grafana-datasources-core-services,false,true,false queryServiceFromUI,experimental,@grafana/grafana-datasources-core-services,false,false,true queryServiceFromExplore,experimental,@grafana/grafana-datasources-core-services,false,false,true diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 8be11c087c5..22383ebb4f9 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -271,10 +271,6 @@ const ( // Register /apis/query.grafana.app/ -- will eventually replace /api/ds/query FlagQueryService = "queryService" - // FlagQueryServiceWithConnections - // Adds datasource connections to the query service - FlagQueryServiceWithConnections = "queryServiceWithConnections" - // FlagQueryServiceRewrite // Rewrite requests targeting /ds/query to the query service FlagQueryServiceRewrite = "queryServiceRewrite" diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 9f98a5a0751..8a3c0b60263 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -2765,19 +2765,6 @@ "requiresRestart": true } }, - { - "metadata": { - "name": "queryServiceWithConnections", - "resourceVersion": "1756367172351", - "creationTimestamp": "2025-08-28T07:46:12Z" - }, - "spec": { - "description": "Adds datasource connections to the query service", - "stage": "experimental", - "codeowner": "@grafana/grafana-datasources-core-services", - "requiresRestart": true - } - }, { "metadata": { "name": "recordedQueriesMulti", diff --git a/pkg/tests/apis/datasource/testdata/testdata-create.yaml b/pkg/tests/apis/datasource/testdata/testdata-create.yaml deleted file mode 100644 index 9bffe6f75ba..00000000000 --- a/pkg/tests/apis/datasource/testdata/testdata-create.yaml +++ /dev/null @@ -1,17 +0,0 @@ -apiVersion: testdata.datasource.grafana.app/v0alpha1 -kind: DataSource -metadata: - name: sample-testdata -spec: - title: Sample datasource - access: proxy - isDefault: true - jsonData: - key: value - hello: 10 - world: false -secure: - sampleA: - create: secret value here # replaced with UID on write - sampleB: - name: XYZ # reference to a existing secret \ No newline at end of file diff --git a/pkg/tests/apis/datasource/testdata_test.go b/pkg/tests/apis/datasource/testdata_test.go index f49807486e4..3ed3b620c16 100644 --- a/pkg/tests/apis/datasource/testdata_test.go +++ b/pkg/tests/apis/datasource/testdata_test.go @@ -2,16 +2,13 @@ package dashboards import ( "context" - "encoding/json" "fmt" "testing" "github.com/stretchr/testify/require" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" - "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/tests/apis" @@ -41,58 +38,97 @@ func TestIntegrationTestDatasource(t *testing.T) { Type: datasources.DS_TESTDATA, UID: "test", OrgID: int64(1), - - // These settings are not actually used, but testing that they get saved - Database: "testdb", - URL: "http://fake.url", - Access: datasources.DS_ACCESS_PROXY, - User: "example", - ReadOnly: true, - JsonData: simplejson.NewFromAny(map[string]any{ - "hello": "world", - }), - SecureJsonData: map[string]string{ - "aaa": "AAA", - "bbb": "BBB", - }, }) require.Equal(t, "test", ds.UID) - t.Run("Admin configs", func(t *testing.T) { - client := helper.Org1.Admin.ResourceClient(t, schema.GroupVersionResource{ - Group: "testdata.datasource.grafana.app", - Version: "v0alpha1", - Resource: "datasources", - }).Namespace("default") - ctx := context.Background() + t.Run("Check discovery client", func(t *testing.T) { + disco := helper.GetGroupVersionInfoJSON("testdata.datasource.grafana.app") + fmt.Printf("%s", disco) - list, err := client.List(ctx, metav1.ListOptions{}) - require.NoError(t, err) - require.Len(t, list.Items, 1, "expected a single connection") - require.Equal(t, "test", list.Items[0].GetName(), "with the test uid") - - spec, _, _ := unstructured.NestedMap(list.Items[0].Object, "spec") - jj, _ := json.MarshalIndent(spec, "", " ") - fmt.Printf("%s\n", string(jj)) - require.JSONEq(t, `{ - "access": "proxy", - "database": "testdb", - "isDefault": true, - "jsonData": { - "hello": "world" - }, - "readOnly": true, - "title": "test", - "url": "http://fake.url", - "user": "example" - }`, string(jj)) + require.JSONEq(t, `[ + { + "freshness": "Current", + "resources": [ + { + "resource": "connections", + "responseKind": { + "group": "", + "kind": "DataSourceConnection", + "version": "" + }, + "scope": "Namespaced", + "shortNames": [ + "grafana-testdata-datasource-connection" + ], + "singularResource": "connection", + "subresources": [ + { + "responseKind": { + "group": "", + "kind": "HealthCheckResult", + "version": "" + }, + "subresource": "health", + "verbs": [ + "get" + ] + }, + { + "responseKind": { + "group": "", + "kind": "QueryDataResponse", + "version": "" + }, + "subresource": "query", + "verbs": [ + "create" + ] + }, + { + "responseKind": { + "group": "", + "kind": "Status", + "version": "" + }, + "subresource": "resource", + "verbs": [ + "create", + "delete", + "get", + "patch", + "update" + ] + } + ], + "verbs": [ + "get", + "list" + ] + }, + { + "resource": "queryconvert", + "responseKind": { + "group": "", + "kind": "QueryDataRequest", + "version": "" + }, + "scope": "Namespaced", + "singularResource": "queryconvert", + "verbs": [ + "create" + ] + } + ], + "version": "v0alpha1" + } + ]`, disco) }) t.Run("Call subresources", func(t *testing.T) { client := helper.Org1.Admin.ResourceClient(t, schema.GroupVersionResource{ Group: "testdata.datasource.grafana.app", Version: "v0alpha1", - Resource: "datasources", + Resource: "connections", }).Namespace("default") ctx := context.Background() @@ -119,7 +155,7 @@ func TestIntegrationTestDatasource(t *testing.T) { raw := apis.DoRequest[any](helper, apis.RequestParams{ User: helper.Org1.Admin, Method: "GET", - Path: "/apis/testdata.datasource.grafana.app/v0alpha1/namespaces/default/datasources/test/resource", + Path: "/apis/testdata.datasource.grafana.app/v0alpha1/namespaces/default/connections/test/resource", }, nil) require.Equal(t, `Hello world from test datasource!`, string(raw.Body)) })