mirror of https://github.com/grafana/grafana.git
Revert "DataSource: Support config CRUD from apiservers (#106996)"
This reverts commit eda94a6434
.
This commit is contained in:
parent
3a3ba483b1
commit
72eeefabd7
|
@ -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/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 h1:yOuPqEq4ovnhEjpHmfFwsqBXDYbQeT6Nb0bwD6XnD5o=
|
||||||
github.com/uber-go/atomic v1.4.0/go.mod h1:/Ct5t2lcmbJ4OSe/waGBoaVvVqtO0bmtfVNex1PFV8g=
|
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 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
|
||||||
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
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=
|
github.com/vertica/vertica-sql-go v1.3.3 h1:fL+FKEAEy5ONmsvya2WH5T8bhkvY27y/Ik3ReR2T+Qw=
|
||||||
|
|
|
@ -301,10 +301,6 @@ export interface FeatureToggles {
|
||||||
*/
|
*/
|
||||||
queryService?: boolean;
|
queryService?: boolean;
|
||||||
/**
|
/**
|
||||||
* Adds datasource connections to the query service
|
|
||||||
*/
|
|
||||||
queryServiceWithConnections?: boolean;
|
|
||||||
/**
|
|
||||||
* Rewrite requests targeting /ds/query to the query service
|
* Rewrite requests targeting /ds/query to the query service
|
||||||
*/
|
*/
|
||||||
queryServiceRewrite?: boolean;
|
queryServiceRewrite?: boolean;
|
||||||
|
|
|
@ -5,9 +5,9 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"path"
|
"path"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||||
"k8s.io/apimachinery/pkg/runtime/serializer"
|
"k8s.io/apimachinery/pkg/runtime/serializer"
|
||||||
|
|
||||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
|
||||||
aggregationv0alpha1 "github.com/grafana/grafana/pkg/aggregator/apis/aggregation/v0alpha1"
|
aggregationv0alpha1 "github.com/grafana/grafana/pkg/aggregator/apis/aggregation/v0alpha1"
|
||||||
"github.com/grafana/grafana/pkg/aggregator/apiserver/plugin/admission"
|
"github.com/grafana/grafana/pkg/aggregator/apiserver/plugin/admission"
|
||||||
)
|
)
|
||||||
|
@ -64,7 +64,7 @@ func (h *PluginHandler) registerRoutes() {
|
||||||
case aggregationv0alpha1.DataSourceProxyServiceType:
|
case aggregationv0alpha1.DataSourceProxyServiceType:
|
||||||
// TODO: implement in future PR
|
// TODO: implement in future PR
|
||||||
case aggregationv0alpha1.QueryServiceType:
|
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:
|
case aggregationv0alpha1.RouteServiceType:
|
||||||
// TODO: implement in future PR
|
// TODO: implement in future PR
|
||||||
case aggregationv0alpha1.StreamServiceType:
|
case aggregationv0alpha1.StreamServiceType:
|
||||||
|
|
|
@ -6,15 +6,15 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"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"
|
semconv "go.opentelemetry.io/otel/semconv/v1.21.0"
|
||||||
"k8s.io/component-base/tracing"
|
"k8s.io/component-base/tracing"
|
||||||
"k8s.io/klog/v2"
|
"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"
|
aggregationv0alpha1 "github.com/grafana/grafana/pkg/aggregator/apis/aggregation/v0alpha1"
|
||||||
"github.com/grafana/grafana/pkg/aggregator/apiserver/util"
|
"github.com/grafana/grafana/pkg/aggregator/apiserver/util"
|
||||||
grafanasemconv "github.com/grafana/grafana/pkg/semconv"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (h *PluginHandler) QueryDataHandler() http.HandlerFunc {
|
func (h *PluginHandler) QueryDataHandler() http.HandlerFunc {
|
||||||
|
|
|
@ -10,14 +10,13 @@ import (
|
||||||
|
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
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/backend"
|
||||||
"github.com/grafana/grafana-plugin-sdk-go/data"
|
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||||
datav0alpha1 "github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1"
|
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/apis/aggregation/v0alpha1"
|
||||||
"github.com/grafana/grafana/pkg/aggregator/apiserver/plugin/fakes"
|
"github.com/grafana/grafana/pkg/aggregator/apiserver/plugin/fakes"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestQueryDataHandler(t *testing.T) {
|
func TestQueryDataHandler(t *testing.T) {
|
||||||
|
@ -88,7 +87,7 @@ func TestQueryDataHandler(t *testing.T) {
|
||||||
buf := bytes.NewBuffer(nil)
|
buf := bytes.NewBuffer(nil)
|
||||||
assert.NoError(t, json.NewEncoder(buf).Encode(qdr))
|
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)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
rr := httptest.NewRecorder()
|
rr := httptest.NewRecorder()
|
||||||
|
@ -114,7 +113,7 @@ func TestQueryDataHandler(t *testing.T) {
|
||||||
buf := bytes.NewBuffer(nil)
|
buf := bytes.NewBuffer(nil)
|
||||||
assert.NoError(t, json.NewEncoder(buf).Encode(qdr))
|
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)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
rr := httptest.NewRecorder()
|
rr := httptest.NewRecorder()
|
||||||
|
@ -142,7 +141,7 @@ func TestQueryDataHandler(t *testing.T) {
|
||||||
buf := bytes.NewBuffer(nil)
|
buf := bytes.NewBuffer(nil)
|
||||||
assert.NoError(t, json.NewEncoder(buf).Encode(qdr))
|
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)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
rr := httptest.NewRecorder()
|
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) {
|
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)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
rr := httptest.NewRecorder()
|
rr := httptest.NewRecorder()
|
||||||
|
|
|
@ -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"`
|
|
||||||
}
|
|
|
@ -1,3 +1,6 @@
|
||||||
|
// +k8s:deepcopy-gen=package
|
||||||
|
// +k8s:openapi-gen=true
|
||||||
|
// +k8s:defaulter-gen=TypeMeta
|
||||||
// +groupName=datasource.grafana.com
|
// +groupName=datasource.grafana.com
|
||||||
|
|
||||||
package v0alpha1
|
package v0alpha1
|
||||||
|
|
|
@ -4,10 +4,9 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -15,24 +14,26 @@ const (
|
||||||
VERSION = "v0alpha1"
|
VERSION = "v0alpha1"
|
||||||
)
|
)
|
||||||
|
|
||||||
var DataSourceResourceInfo = utils.NewResourceInfo(GROUP, VERSION,
|
var GenericConnectionResourceInfo = utils.NewResourceInfo(GROUP, VERSION,
|
||||||
"datasources", "datasource", "DataSource",
|
"connections", "connection", "DataSourceConnection",
|
||||||
func() runtime.Object { return &DataSource{} },
|
func() runtime.Object { return &DataSourceConnection{} },
|
||||||
func() runtime.Object { return &DataSourceList{} },
|
func() runtime.Object { return &DataSourceConnectionList{} },
|
||||||
utils.TableColumns{
|
utils.TableColumns{
|
||||||
Definition: []metav1.TableColumnDefinition{
|
Definition: []metav1.TableColumnDefinition{
|
||||||
{Name: "Name", Type: "string", Format: "name"},
|
{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"},
|
{Name: "Created At", Type: "date"},
|
||||||
},
|
},
|
||||||
Reader: func(obj any) ([]any, error) {
|
Reader: func(obj any) ([]interface{}, error) {
|
||||||
m, ok := obj.(*DataSource)
|
m, ok := obj.(*DataSourceConnection)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("expected connection")
|
return nil, fmt.Errorf("expected connection")
|
||||||
}
|
}
|
||||||
return []any{
|
return []interface{}{
|
||||||
m.Name,
|
m.Name,
|
||||||
m.Spec.Object["title"],
|
m.Title,
|
||||||
|
m.APIVersion,
|
||||||
m.CreationTimestamp.UTC().Format(time.RFC3339),
|
m.CreationTimestamp.UTC().Format(time.RFC3339),
|
||||||
}, nil
|
}, nil
|
||||||
},
|
},
|
||||||
|
|
|
@ -6,8 +6,26 @@ import (
|
||||||
common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
|
common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
|
||||||
)
|
)
|
||||||
|
|
||||||
// +k8s:deepcopy-gen=true
|
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||||
// +k8s:openapi-gen=true
|
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
|
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||||
type HealthCheckResult struct {
|
type HealthCheckResult struct {
|
||||||
metav1.TypeMeta `json:",inline"`
|
metav1.TypeMeta `json:",inline"`
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -8,38 +8,29 @@
|
||||||
package v0alpha1
|
package v0alpha1
|
||||||
|
|
||||||
import (
|
import (
|
||||||
commonv0alpha1 "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
|
|
||||||
runtime "k8s.io/apimachinery/pkg/runtime"
|
runtime "k8s.io/apimachinery/pkg/runtime"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
// 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 = *in
|
||||||
out.TypeMeta = in.TypeMeta
|
out.TypeMeta = in.TypeMeta
|
||||||
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DataSource.
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DataSourceConnection.
|
||||||
func (in *DataSource) DeepCopy() *DataSource {
|
func (in *DataSourceConnection) DeepCopy() *DataSourceConnection {
|
||||||
if in == nil {
|
if in == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
out := new(DataSource)
|
out := new(DataSourceConnection)
|
||||||
in.DeepCopyInto(out)
|
in.DeepCopyInto(out)
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
// 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 {
|
if c := in.DeepCopy(); c != nil {
|
||||||
return c
|
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.
|
// 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 = *in
|
||||||
out.TypeMeta = in.TypeMeta
|
out.TypeMeta = in.TypeMeta
|
||||||
in.ListMeta.DeepCopyInto(&out.ListMeta)
|
in.ListMeta.DeepCopyInto(&out.ListMeta)
|
||||||
if in.Items != nil {
|
if in.Items != nil {
|
||||||
in, out := &in.Items, &out.Items
|
in, out := &in.Items, &out.Items
|
||||||
*out = make([]DataSource, len(*in))
|
*out = make([]DataSourceConnection, len(*in))
|
||||||
for i := range *in {
|
for i := range *in {
|
||||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||||
}
|
}
|
||||||
|
@ -61,18 +52,18 @@ func (in *DataSourceList) DeepCopyInto(out *DataSourceList) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DataSourceList.
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DataSourceConnectionList.
|
||||||
func (in *DataSourceList) DeepCopy() *DataSourceList {
|
func (in *DataSourceConnectionList) DeepCopy() *DataSourceConnectionList {
|
||||||
if in == nil {
|
if in == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
out := new(DataSourceList)
|
out := new(DataSourceConnectionList)
|
||||||
in.DeepCopyInto(out)
|
in.DeepCopyInto(out)
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
// 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 {
|
if c := in.DeepCopy(); c != nil {
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -14,15 +14,13 @@ import (
|
||||||
|
|
||||||
func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition {
|
func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition {
|
||||||
return 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.DataSourceConnection": schema_pkg_apis_datasource_v0alpha1_DataSourceConnection(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.DataSourceConnectionList": schema_pkg_apis_datasource_v0alpha1_DataSourceConnectionList(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.HealthCheckResult": schema_pkg_apis_datasource_v0alpha1_HealthCheckResult(ref),
|
||||||
"github.com/grafana/grafana/pkg/apis/datasource/v0alpha1.UnstructuredSpec": UnstructuredSpec{}.OpenAPIDefinition(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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{
|
return common.OpenAPIDefinition{
|
||||||
Schema: spec.Schema{
|
Schema: spec.Schema{
|
||||||
SchemaProps: spec.SchemaProps{
|
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"),
|
Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"spec": {
|
"title": {
|
||||||
SchemaProps: spec.SchemaProps{
|
SchemaProps: spec.SchemaProps{
|
||||||
Description: "DataSource configuration -- these properties are all visible to anyone able to query the data source from their browser",
|
Description: "The display name",
|
||||||
Ref: ref("github.com/grafana/grafana/pkg/apis/datasource/v0alpha1.UnstructuredSpec"),
|
Default: "",
|
||||||
|
Type: []string{"string"},
|
||||||
|
Format: "",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"secure": {
|
"description": {
|
||||||
SchemaProps: spec.SchemaProps{
|
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",
|
Description: "Optional description for the data source (does not exist yet)",
|
||||||
Type: []string{"object"},
|
Type: []string{"string"},
|
||||||
AdditionalProperties: &spec.SchemaOrBool{
|
Format: "",
|
||||||
Allows: true,
|
|
||||||
Schema: &spec.Schema{
|
|
||||||
SchemaProps: spec.SchemaProps{
|
|
||||||
Default: map[string]interface{}{},
|
|
||||||
Ref: ref("github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.InlineSecureValue"),
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
Required: []string{"title"},
|
||||||
},
|
|
||||||
},
|
|
||||||
Required: []string{"metadata", "spec"},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Dependencies: []string{
|
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{
|
return common.OpenAPIDefinition{
|
||||||
Schema: spec.Schema{
|
Schema: spec.Schema{
|
||||||
SchemaProps: spec.SchemaProps{
|
SchemaProps: spec.SchemaProps{
|
||||||
|
@ -111,104 +103,18 @@ func schema_pkg_apis_datasource_v0alpha1_DataSourceList(ref common.ReferenceCall
|
||||||
Schema: &spec.Schema{
|
Schema: &spec.Schema{
|
||||||
SchemaProps: spec.SchemaProps{
|
SchemaProps: spec.SchemaProps{
|
||||||
Default: map[string]interface{}{},
|
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{
|
Dependencies: []string{
|
||||||
"github.com/grafana/grafana/pkg/apis/datasource/v0alpha1.DataSource", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"},
|
"github.com/grafana/grafana/pkg/apis/datasource/v0alpha1.DataSourceConnection", "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"},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
|
@ -7,40 +7,6 @@ import (
|
||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
"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 {
|
type DataSourceApiServerRegistry interface {
|
||||||
// Get the group and preferred version for a plugin
|
// Get the group and preferred version for a plugin
|
||||||
GetDatasourceGroupVersion(pluginId string) (schema.GroupVersion, error)
|
GetDatasourceGroupVersion(pluginId string) (schema.GroupVersion, error)
|
||||||
|
@ -58,7 +24,7 @@ type DataSourceApiServerRegistry interface {
|
||||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||||
type DataSourceApiServer struct {
|
type DataSourceApiServer struct {
|
||||||
metav1.TypeMeta `json:",inline"`
|
metav1.TypeMeta `json:",inline"`
|
||||||
metav1.ObjectMeta `json:"metadata,omitzero,omitempty"`
|
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||||
|
|
||||||
// The display name
|
// The display name
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
|
@ -77,7 +43,7 @@ type DataSourceApiServer struct {
|
||||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||||
type DataSourceApiServerList struct {
|
type DataSourceApiServerList struct {
|
||||||
metav1.TypeMeta `json:",inline"`
|
metav1.TypeMeta `json:",inline"`
|
||||||
metav1.ListMeta `json:"metadata,omitzero,omitempty"`
|
metav1.ListMeta `json:"metadata,omitempty"`
|
||||||
|
|
||||||
Items []DataSourceApiServer `json:"items"`
|
Items []DataSourceApiServer `json:"items"`
|
||||||
}
|
}
|
|
@ -1,10 +1,6 @@
|
||||||
package v0alpha1
|
package v0alpha1
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
|
||||||
|
@ -17,32 +13,6 @@ const (
|
||||||
APIVERSION = GROUP + "/" + VERSION
|
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,
|
var DataSourceApiServerResourceInfo = utils.NewResourceInfo(GROUP, VERSION,
|
||||||
"datasourceapiservers", "datasourceapiserver", "DataSourceApiServer",
|
"datasourceapiservers", "datasourceapiserver", "DataSourceApiServer",
|
||||||
func() runtime.Object { return &DataSourceApiServer{} },
|
func() runtime.Object { return &DataSourceApiServer{} },
|
||||||
|
|
|
@ -75,82 +75,6 @@ func (in *DataSourceApiServerList) DeepCopyObject() runtime.Object {
|
||||||
return nil
|
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.
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
func (in *QueryDataRequest) DeepCopyInto(out *QueryDataRequest) {
|
func (in *QueryDataRequest) DeepCopyInto(out *QueryDataRequest) {
|
||||||
*out = *in
|
*out = *in
|
||||||
|
|
|
@ -16,9 +16,6 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA
|
||||||
return 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.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.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.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.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.QueryTypeDefinition": schema_pkg_apis_query_v0alpha1_QueryTypeDefinition(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 {
|
func schema_pkg_apis_query_v0alpha1_QueryDataRequest(ref common.ReferenceCallback) common.OpenAPIDefinition {
|
||||||
return common.OpenAPIDefinition{
|
return common.OpenAPIDefinition{
|
||||||
Schema: spec.Schema{
|
Schema: spec.Schema{
|
||||||
|
|
|
@ -1,3 +1 @@
|
||||||
API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/query/v0alpha1,DataSourceApiServer,AliasIDs
|
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
|
|
||||||
|
|
|
@ -25,7 +25,7 @@ func (b *DataSourceAPIBuilder) GetAuthorizer() authorizer.Authorizer {
|
||||||
uidScope := datasources.ScopeProvider.GetResourceScopeUID(attr.GetName())
|
uidScope := datasources.ScopeProvider.GetResourceScopeUID(attr.GetName())
|
||||||
|
|
||||||
// Must have query access to see a connection
|
// Must have query access to see a connection
|
||||||
if attr.GetResource() == b.datasourceResourceInfo.GroupResource().Resource {
|
if attr.GetResource() == b.connectionResourceInfo.GroupResource().Resource {
|
||||||
scopes := []string{}
|
scopes := []string{}
|
||||||
if attr.GetName() != "" {
|
if attr.GetName() != "" {
|
||||||
scopes = []string{uidScope}
|
scopes = []string{uidScope}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -5,33 +5,27 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
"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/plugins"
|
||||||
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
|
"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/datasources"
|
||||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext"
|
"github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// This provides access to settings saved in the database.
|
// This provides access to settings saved in the database.
|
||||||
// Authorization checks will happen within each function, and the user in ctx will
|
// Authorization checks will happen within each function, and the user in ctx will
|
||||||
// limit which namespace/tenant/org we are talking to
|
// limit which namespace/tenant/org we are talking to
|
||||||
type PluginDatasourceProvider interface {
|
type PluginDatasourceProvider interface {
|
||||||
// Get a single data source (any type)
|
// Get gets a specific datasource (that the user in context can see)
|
||||||
GetDataSource(ctx context.Context, uid string) (*datasourceV0.DataSource, error)
|
Get(ctx context.Context, uid string) (*v0alpha1.DataSourceConnection, error)
|
||||||
|
|
||||||
// List all datasources (any type)
|
// List lists all data sources the user in context can see
|
||||||
ListDataSources(ctx context.Context) (*datasourceV0.DataSourceList, error)
|
List(ctx context.Context) (*v0alpha1.DataSourceConnectionList, 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
|
|
||||||
|
|
||||||
// Return settings (decrypted!) for a specific plugin
|
// Return settings (decrypted!) for a specific plugin
|
||||||
// This will require "query" permission for the user in context
|
// This will require "query" permission for the user in context
|
||||||
|
@ -50,16 +44,11 @@ type PluginContextWrapper interface {
|
||||||
func ProvideDefaultPluginConfigs(
|
func ProvideDefaultPluginConfigs(
|
||||||
dsService datasources.DataSourceService,
|
dsService datasources.DataSourceService,
|
||||||
dsCache datasources.CacheService,
|
dsCache datasources.CacheService,
|
||||||
contextProvider *plugincontext.Provider,
|
contextProvider *plugincontext.Provider) ScopedPluginDatasourceProvider {
|
||||||
cfg *setting.Cfg,
|
|
||||||
) ScopedPluginDatasourceProvider {
|
|
||||||
return &cachingDatasourceProvider{
|
return &cachingDatasourceProvider{
|
||||||
dsService: dsService,
|
dsService: dsService,
|
||||||
dsCache: dsCache,
|
dsCache: dsCache,
|
||||||
contextProvider: contextProvider,
|
contextProvider: contextProvider,
|
||||||
converter: &converter{
|
|
||||||
mapper: request.GetNamespaceMapper(cfg),
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,22 +56,14 @@ type cachingDatasourceProvider struct {
|
||||||
dsService datasources.DataSourceService
|
dsService datasources.DataSourceService
|
||||||
dsCache datasources.CacheService
|
dsCache datasources.CacheService
|
||||||
contextProvider *plugincontext.Provider
|
contextProvider *plugincontext.Provider
|
||||||
converter *converter
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *cachingDatasourceProvider) GetDatasourceProvider(pluginJson plugins.JSONData) PluginDatasourceProvider {
|
func (q *cachingDatasourceProvider) GetDatasourceProvider(pluginJson plugins.JSONData) PluginDatasourceProvider {
|
||||||
group, _ := plugins.GetDatasourceGroupNameFromPluginID(pluginJson.ID)
|
|
||||||
return &scopedDatasourceProvider{
|
return &scopedDatasourceProvider{
|
||||||
plugin: pluginJson,
|
plugin: pluginJson,
|
||||||
dsService: q.dsService,
|
dsService: q.dsService,
|
||||||
dsCache: q.dsCache,
|
dsCache: q.dsCache,
|
||||||
contextProvider: q.contextProvider,
|
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
|
dsService datasources.DataSourceService
|
||||||
dsCache datasources.CacheService
|
dsCache datasources.CacheService
|
||||||
contextProvider *plugincontext.Provider
|
contextProvider *plugincontext.Provider
|
||||||
converter *converter
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -99,62 +79,11 @@ var (
|
||||||
_ ScopedPluginDatasourceProvider = (*cachingDatasourceProvider)(nil)
|
_ ScopedPluginDatasourceProvider = (*cachingDatasourceProvider)(nil)
|
||||||
)
|
)
|
||||||
|
|
||||||
func (q *scopedDatasourceProvider) GetInstanceSettings(ctx context.Context, uid string) (*backend.DataSourceInstanceSettings, error) {
|
func (q *scopedDatasourceProvider) Get(ctx context.Context, uid string) (*v0alpha1.DataSourceConnection, error) {
|
||||||
if q.contextProvider == nil {
|
info, err := request.NamespaceInfoFrom(ctx, true)
|
||||||
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)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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)
|
user, err := identity.GetRequester(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -163,11 +92,10 @@ func (q *scopedDatasourceProvider) GetDataSource(ctx context.Context, uid string
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return q.converter.asDataSource(ds)
|
return asConnection(ds, info.Value)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListDataSource implements PluginDatasourceProvider.
|
func (q *scopedDatasourceProvider) List(ctx context.Context) (*v0alpha1.DataSourceConnectionList, error) {
|
||||||
func (q *scopedDatasourceProvider) ListDataSources(ctx context.Context) (*datasourceV0.DataSourceList, error) {
|
|
||||||
info, err := request.NamespaceInfoFrom(ctx, true)
|
info, err := request.NamespaceInfoFrom(ctx, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -181,12 +109,37 @@ func (q *scopedDatasourceProvider) ListDataSources(ctx context.Context) (*dataso
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
result := &datasourceV0.DataSourceList{
|
result := &v0alpha1.DataSourceConnectionList{
|
||||||
Items: []datasourceV0.DataSource{},
|
Items: []v0alpha1.DataSourceConnection{},
|
||||||
}
|
}
|
||||||
for _, ds := range dss {
|
for _, ds := range dss {
|
||||||
v, _ := q.converter.asDataSource(ds)
|
v, _ := asConnection(ds, info.Value)
|
||||||
result.Items = append(result.Items, *v)
|
result.Items = append(result.Items, *v)
|
||||||
}
|
}
|
||||||
return result, nil
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -4,10 +4,13 @@ import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
"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/apimachinery/utils"
|
||||||
|
"github.com/grafana/grafana/pkg/apis/datasource/v0alpha1"
|
||||||
"github.com/grafana/grafana/pkg/plugins"
|
"github.com/grafana/grafana/pkg/plugins"
|
||||||
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
|
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
|
||||||
"github.com/grafana/grafana/pkg/services/datasources"
|
"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)
|
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)
|
Health(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error)
|
||||||
// Resource gets a resource plugin.
|
// Resource gets a resource plugin.
|
||||||
Resource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error
|
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 {
|
type DefaultQuerier struct {
|
||||||
|
@ -94,3 +101,47 @@ func (q *DefaultQuerier) Health(ctx context.Context, query *backend.CheckHealthR
|
||||||
}
|
}
|
||||||
return q.pluginClient.CheckHealth(ctx, query)
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ package datasource
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"maps"
|
"fmt"
|
||||||
|
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
@ -13,13 +13,13 @@ import (
|
||||||
"k8s.io/apiserver/pkg/registry/rest"
|
"k8s.io/apiserver/pkg/registry/rest"
|
||||||
genericapiserver "k8s.io/apiserver/pkg/server"
|
genericapiserver "k8s.io/apiserver/pkg/server"
|
||||||
openapi "k8s.io/kube-openapi/pkg/common"
|
openapi "k8s.io/kube-openapi/pkg/common"
|
||||||
|
"k8s.io/kube-openapi/pkg/spec3"
|
||||||
"k8s.io/utils/strings/slices"
|
"k8s.io/utils/strings/slices"
|
||||||
|
|
||||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||||
datasourceV0 "github.com/grafana/grafana/pkg/apis/datasource/v0alpha1"
|
datasource "github.com/grafana/grafana/pkg/apis/datasource/v0alpha1"
|
||||||
queryV0 "github.com/grafana/grafana/pkg/apis/query/v0alpha1"
|
query "github.com/grafana/grafana/pkg/apis/query/v0alpha1"
|
||||||
grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic"
|
|
||||||
"github.com/grafana/grafana/pkg/infra/log"
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
"github.com/grafana/grafana/pkg/plugins"
|
"github.com/grafana/grafana/pkg/plugins"
|
||||||
"github.com/grafana/grafana/pkg/promlib/models"
|
"github.com/grafana/grafana/pkg/promlib/models"
|
||||||
|
@ -31,22 +31,18 @@ import (
|
||||||
"github.com/grafana/grafana/pkg/tsdb/grafana-testdata-datasource/kinds"
|
"github.com/grafana/grafana/pkg/tsdb/grafana-testdata-datasource/kinds"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var _ builder.APIGroupBuilder = (*DataSourceAPIBuilder)(nil)
|
||||||
_ builder.APIGroupBuilder = (*DataSourceAPIBuilder)(nil)
|
|
||||||
// _ builder.APIGroupMutation = (*DataSourceAPIBuilder)(nil)
|
|
||||||
// _ builder.APIGroupValidation = (*DataSourceAPIBuilder)(nil)
|
|
||||||
)
|
|
||||||
|
|
||||||
// DataSourceAPIBuilder is used just so wire has something unique to return
|
// DataSourceAPIBuilder is used just so wire has something unique to return
|
||||||
type DataSourceAPIBuilder struct {
|
type DataSourceAPIBuilder struct {
|
||||||
datasourceResourceInfo utils.ResourceInfo
|
connectionResourceInfo utils.ResourceInfo
|
||||||
|
|
||||||
pluginJSON plugins.JSONData
|
pluginJSON plugins.JSONData
|
||||||
client PluginClient // will only ever be called with the same pluginid!
|
client PluginClient // will only ever be called with the same pluginid!
|
||||||
datasources PluginDatasourceProvider
|
datasources PluginDatasourceProvider
|
||||||
contextProvider PluginContextWrapper
|
contextProvider PluginContextWrapper
|
||||||
accessControl accesscontrol.AccessControl
|
accessControl accesscontrol.AccessControl
|
||||||
queryTypes *queryV0.QueryTypeDefinitionList
|
queryTypes *query.QueryTypeDefinitionList
|
||||||
log log.Logger
|
log log.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -96,12 +92,6 @@ func RegisterAPIService(
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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)
|
apiRegistrar.RegisterAPI(builder)
|
||||||
}
|
}
|
||||||
return builder, nil // only used for wire
|
return builder, nil // only used for wire
|
||||||
|
@ -124,13 +114,13 @@ func NewDataSourceAPIBuilder(
|
||||||
accessControl accesscontrol.AccessControl,
|
accessControl accesscontrol.AccessControl,
|
||||||
loadQueryTypes bool,
|
loadQueryTypes bool,
|
||||||
) (*DataSourceAPIBuilder, error) {
|
) (*DataSourceAPIBuilder, error) {
|
||||||
group, err := plugins.GetDatasourceGroupNameFromPluginID(plugin.ID)
|
ri, err := resourceFromPluginID(plugin.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
builder := &DataSourceAPIBuilder{
|
builder := &DataSourceAPIBuilder{
|
||||||
datasourceResourceInfo: datasourceV0.DataSourceResourceInfo.WithGroupAndShortName(group, plugin.ID),
|
connectionResourceInfo: ri,
|
||||||
pluginJSON: plugin,
|
pluginJSON: plugin,
|
||||||
client: client,
|
client: client,
|
||||||
datasources: datasources,
|
datasources: datasources,
|
||||||
|
@ -140,13 +130,13 @@ func NewDataSourceAPIBuilder(
|
||||||
}
|
}
|
||||||
if loadQueryTypes {
|
if loadQueryTypes {
|
||||||
// In the future, this will somehow come from the plugin
|
// 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
|
return builder, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO -- somehow get the list from the plugin -- not hardcoded
|
// 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 err error
|
||||||
var raw json.RawMessage
|
var raw json.RawMessage
|
||||||
switch group {
|
switch group {
|
||||||
|
@ -159,7 +149,7 @@ func getHardcodedQueryTypes(group string) (*queryV0.QueryTypeDefinitionList, err
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if raw != nil {
|
if raw != nil {
|
||||||
types := &queryV0.QueryTypeDefinitionList{}
|
types := &query.QueryTypeDefinitionList{}
|
||||||
err = json.Unmarshal(raw, types)
|
err = json.Unmarshal(raw, types)
|
||||||
return types, err
|
return types, err
|
||||||
}
|
}
|
||||||
|
@ -167,27 +157,26 @@ func getHardcodedQueryTypes(group string) (*queryV0.QueryTypeDefinitionList, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *DataSourceAPIBuilder) GetGroupVersion() schema.GroupVersion {
|
func (b *DataSourceAPIBuilder) GetGroupVersion() schema.GroupVersion {
|
||||||
return b.datasourceResourceInfo.GroupVersion()
|
return b.connectionResourceInfo.GroupVersion()
|
||||||
}
|
}
|
||||||
|
|
||||||
func addKnownTypes(scheme *runtime.Scheme, gv schema.GroupVersion) {
|
func addKnownTypes(scheme *runtime.Scheme, gv schema.GroupVersion) {
|
||||||
scheme.AddKnownTypes(gv,
|
scheme.AddKnownTypes(gv,
|
||||||
&datasourceV0.DataSource{},
|
&datasource.DataSourceConnection{},
|
||||||
&datasourceV0.DataSourceList{},
|
&datasource.DataSourceConnectionList{},
|
||||||
&datasourceV0.HealthCheckResult{},
|
&datasource.HealthCheckResult{},
|
||||||
&unstructured.Unstructured{},
|
&unstructured.Unstructured{},
|
||||||
|
|
||||||
// Query handler
|
// Query handler
|
||||||
&queryV0.QueryDataRequest{},
|
&query.QueryDataRequest{},
|
||||||
&queryV0.QueryDataResponse{},
|
&query.QueryDataResponse{},
|
||||||
&queryV0.QueryTypeDefinition{},
|
&query.QueryTypeDefinition{},
|
||||||
&queryV0.QueryTypeDefinitionList{},
|
&query.QueryTypeDefinitionList{},
|
||||||
&metav1.Status{},
|
&metav1.Status{},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *DataSourceAPIBuilder) InstallSchema(scheme *runtime.Scheme) error {
|
func (b *DataSourceAPIBuilder) InstallSchema(scheme *runtime.Scheme) error {
|
||||||
gv := b.datasourceResourceInfo.GroupVersion()
|
gv := b.connectionResourceInfo.GroupVersion()
|
||||||
addKnownTypes(scheme, gv)
|
addKnownTypes(scheme, gv)
|
||||||
|
|
||||||
// Link this version to the internal representation.
|
// Link this version to the internal representation.
|
||||||
|
@ -210,48 +199,43 @@ func (b *DataSourceAPIBuilder) AllowedV0Alpha1Resources() []string {
|
||||||
return []string{builder.AllResourcesAllowed}
|
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{}
|
storage := map[string]rest.Storage{}
|
||||||
|
|
||||||
// Register the raw datasource connection
|
conn := b.connectionResourceInfo
|
||||||
ds := b.datasourceResourceInfo
|
storage[conn.StoragePath()] = &connectionAccess{
|
||||||
legacyStore := &legacyStorage{
|
|
||||||
datasources: b.datasources,
|
datasources: b.datasources,
|
||||||
resourceInfo: &ds,
|
resourceInfo: conn,
|
||||||
}
|
tableConverter: conn.TableConverter(),
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
storage[conn.StoragePath("query")] = &subQueryREST{builder: b}
|
||||||
|
storage[conn.StoragePath("health")] = &subHealthREST{builder: b}
|
||||||
|
|
||||||
storage[ds.StoragePath("query")] = &subQueryREST{builder: b}
|
// TODO! only setup this endpoint if it is implemented
|
||||||
storage[ds.StoragePath("health")] = &subHealthREST{builder: b}
|
storage[conn.StoragePath("resource")] = &subResourceREST{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
|
|
||||||
|
|
||||||
// Frontend proxy
|
// Frontend proxy
|
||||||
if len(b.pluginJSON.Routes) > 0 {
|
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
|
// Register hardcoded query schemas
|
||||||
err = queryschema.RegisterQueryTypes(b.queryTypes, storage)
|
err := queryschema.RegisterQueryTypes(b.queryTypes, storage)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
registerQueryConvert(b.client, b.contextProvider, storage)
|
registerQueryConvert(b.client, b.contextProvider, storage)
|
||||||
|
|
||||||
apiGroupInfo.VersionedResourcesStorageMap[ds.GroupVersion().Version] = storage
|
apiGroupInfo.VersionedResourcesStorageMap[conn.GroupVersion().Version] = storage
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -265,8 +249,31 @@ func (b *DataSourceAPIBuilder) getPluginContext(ctx context.Context, uid string)
|
||||||
|
|
||||||
func (b *DataSourceAPIBuilder) GetOpenAPIDefinitions() openapi.GetOpenAPIDefinitions {
|
func (b *DataSourceAPIBuilder) GetOpenAPIDefinitions() openapi.GetOpenAPIDefinitions {
|
||||||
return func(ref openapi.ReferenceCallback) map[string]openapi.OpenAPIDefinition {
|
return func(ref openapi.ReferenceCallback) map[string]openapi.OpenAPIDefinition {
|
||||||
defs := queryV0.GetOpenAPIDefinitions(ref) // required when running standalone
|
defs := query.GetOpenAPIDefinitions(ref) // required when running standalone
|
||||||
maps.Copy(defs, datasourceV0.GetOpenAPIDefinitions(ref))
|
for k, v := range datasource.GetOpenAPIDefinitions(ref) {
|
||||||
|
defs[k] = v
|
||||||
|
}
|
||||||
return defs
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -6,16 +6,18 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
|
||||||
"k8s.io/apiserver/pkg/registry/rest"
|
|
||||||
|
|
||||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||||
data "github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1"
|
data "github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1"
|
||||||
query "github.com/grafana/grafana/pkg/apis/query/v0alpha1"
|
query "github.com/grafana/grafana/pkg/apis/query/v0alpha1"
|
||||||
query_headers "github.com/grafana/grafana/pkg/registry/apis/query"
|
query_headers "github.com/grafana/grafana/pkg/registry/apis/query"
|
||||||
"github.com/grafana/grafana/pkg/services/datasources"
|
"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"
|
"github.com/grafana/grafana/pkg/web"
|
||||||
|
|
||||||
|
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
type subQueryREST struct {
|
type subQueryREST struct {
|
||||||
|
@ -26,7 +28,6 @@ var (
|
||||||
_ rest.Storage = (*subQueryREST)(nil)
|
_ rest.Storage = (*subQueryREST)(nil)
|
||||||
_ rest.Connecter = (*subQueryREST)(nil)
|
_ rest.Connecter = (*subQueryREST)(nil)
|
||||||
_ rest.StorageMetadata = (*subQueryREST)(nil)
|
_ rest.StorageMetadata = (*subQueryREST)(nil)
|
||||||
_ rest.Scoper = (*subQueryREST)(nil)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (r *subQueryREST) New() runtime.Object {
|
func (r *subQueryREST) New() runtime.Object {
|
||||||
|
@ -36,10 +37,6 @@ func (r *subQueryREST) New() runtime.Object {
|
||||||
|
|
||||||
func (r *subQueryREST) Destroy() {}
|
func (r *subQueryREST) Destroy() {}
|
||||||
|
|
||||||
func (r *subQueryREST) NamespaceScoped() bool {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *subQueryREST) ProducesMIMETypes(verb string) []string {
|
func (r *subQueryREST) ProducesMIMETypes(verb string) []string {
|
||||||
return []string{"application/json"} // and parquet!
|
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 err != nil {
|
||||||
if errors.Is(err, datasources.ErrDataSourceNotFound) {
|
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
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,16 +8,14 @@ import (
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"testing"
|
"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-plugin-sdk-go/backend"
|
||||||
"github.com/grafana/grafana/pkg/apis/datasource/v0alpha1"
|
"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/infra/log"
|
||||||
"github.com/grafana/grafana/pkg/services/datasources"
|
"github.com/grafana/grafana/pkg/services/datasources"
|
||||||
"github.com/grafana/grafana/pkg/services/ngalert/models"
|
"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) {
|
func TestSubQueryConnect(t *testing.T) {
|
||||||
|
@ -117,43 +115,16 @@ func (m mockResponder) Object(statusCode int, obj runtime.Object) {
|
||||||
func (m mockResponder) Error(err error) {
|
func (m mockResponder) Error(err error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ PluginDatasourceProvider = (*mockDatasources)(nil)
|
|
||||||
|
|
||||||
type mockDatasources struct {
|
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)
|
// 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
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// List lists all data sources the user in context can see
|
// 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
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,11 +8,11 @@ import (
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
"k8s.io/apiserver/pkg/registry/rest"
|
"k8s.io/apiserver/pkg/registry/rest"
|
||||||
|
|
||||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
|
||||||
"github.com/grafana/grafana/pkg/plugins/httpresponsesender"
|
"github.com/grafana/grafana/pkg/plugins/httpresponsesender"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -18,36 +18,36 @@ func TestResourceRequest(t *testing.T) {
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
desc: "no resource path",
|
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,
|
error: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
desc: "root resource path",
|
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: "",
|
expectedPath: "",
|
||||||
expectedURL: "",
|
expectedURL: "",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
desc: "root resource path",
|
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: "",
|
expectedPath: "",
|
||||||
expectedURL: "",
|
expectedURL: "",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
desc: "resource sub path",
|
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",
|
expectedPath: "test",
|
||||||
expectedURL: "test",
|
expectedURL: "test",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
desc: "resource sub path with colon",
|
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",
|
expectedPath: "test-*,*:test-*/_mapping",
|
||||||
expectedURL: "./test-%2A,%2A:test-%2A/_mapping",
|
expectedURL: "./test-%2A,%2A:test-%2A/_mapping",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
desc: "resource sub path with query params",
|
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",
|
expectedPath: "test",
|
||||||
expectedURL: "test?k1=v1&k2=v2",
|
expectedURL: "test?k1=v1&k2=v2",
|
||||||
},
|
},
|
||||||
|
|
|
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
{
|
|
||||||
"id": 456,
|
|
||||||
"version": 8,
|
|
||||||
"name": "Display name",
|
|
||||||
"uid": "unique-identifier",
|
|
||||||
"type": "grafana-testdata-datasource",
|
|
||||||
"created": "2002-03-04T01:00:00Z"
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
{
|
|
||||||
"id": 456,
|
|
||||||
"version": 8,
|
|
||||||
"name": "Hello",
|
|
||||||
"uid": "unique-identifier",
|
|
||||||
"type": "not-valid-plugin",
|
|
||||||
"created": "2002-03-04T01:00:00Z"
|
|
||||||
}
|
|
|
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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"
|
|
||||||
}
|
|
|
@ -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"
|
|
||||||
}
|
|
|
@ -1,12 +0,0 @@
|
||||||
{
|
|
||||||
"metadata": {
|
|
||||||
"name": "cejobd88i85j4d",
|
|
||||||
"namespace": "org-0",
|
|
||||||
"uid": "boDNh7zU3nXj46rOXIJI7r44qaxjs8yy9I9dOj1MyBoX",
|
|
||||||
"creationTimestamp": null
|
|
||||||
},
|
|
||||||
"spec": {
|
|
||||||
"jsonData": null,
|
|
||||||
"title": "Hello testdata"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
{
|
|
||||||
"metadata": {
|
|
||||||
"name": "cejobd88i85j4d"
|
|
||||||
},
|
|
||||||
"spec": {
|
|
||||||
"title": "Hello testdata"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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"
|
|
||||||
}
|
|
|
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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 }
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
{
|
|
||||||
"metadata": {
|
|
||||||
"name": "cejobd88i85j4d",
|
|
||||||
"namespace": "stacks-invalid"
|
|
||||||
},
|
|
||||||
"spec": {
|
|
||||||
"title": "Hello testdata"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -66,8 +66,21 @@ func AddQueriesToOpenAPI(options OASQueryOptions) error {
|
||||||
// Rewrite the query path
|
// Rewrite the query path
|
||||||
query := oas.Paths.Paths[root+options.QueryPath]
|
query := oas.Paths.Paths[root+options.QueryPath]
|
||||||
if query != nil && query.Post != nil {
|
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.Description = options.QueryDescription
|
||||||
|
query.Post.Parameters = nil //
|
||||||
query.Post.RequestBody = &spec3.RequestBody{
|
query.Post.RequestBody = &spec3.RequestBody{
|
||||||
RequestBodyProps: spec3.RequestBodyProps{
|
RequestBodyProps: spec3.RequestBodyProps{
|
||||||
Content: map[string]*spec3.MediaType{
|
Content: map[string]*spec3.MediaType{
|
||||||
|
|
|
@ -50,7 +50,6 @@ type QueryAPIBuilder struct {
|
||||||
converter *expr.ResultConverter
|
converter *expr.ResultConverter
|
||||||
queryTypes *query.QueryTypeDefinitionList
|
queryTypes *query.QueryTypeDefinitionList
|
||||||
legacyDatasourceLookup service.LegacyDataSourceLookup
|
legacyDatasourceLookup service.LegacyDataSourceLookup
|
||||||
connections DataSourceConnectionProvider
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewQueryAPIBuilder(
|
func NewQueryAPIBuilder(
|
||||||
|
@ -61,7 +60,6 @@ func NewQueryAPIBuilder(
|
||||||
registerer prometheus.Registerer,
|
registerer prometheus.Registerer,
|
||||||
tracer tracing.Tracer,
|
tracer tracing.Tracer,
|
||||||
legacyDatasourceLookup service.LegacyDataSourceLookup,
|
legacyDatasourceLookup service.LegacyDataSourceLookup,
|
||||||
connections DataSourceConnectionProvider,
|
|
||||||
) (*QueryAPIBuilder, error) {
|
) (*QueryAPIBuilder, error) {
|
||||||
// Include well typed query definitions
|
// Include well typed query definitions
|
||||||
var queryTypes *query.QueryTypeDefinitionList
|
var queryTypes *query.QueryTypeDefinitionList
|
||||||
|
@ -88,7 +86,6 @@ func NewQueryAPIBuilder(
|
||||||
tracer: tracer,
|
tracer: tracer,
|
||||||
features: features,
|
features: features,
|
||||||
queryTypes: queryTypes,
|
queryTypes: queryTypes,
|
||||||
connections: connections,
|
|
||||||
converter: &expr.ResultConverter{
|
converter: &expr.ResultConverter{
|
||||||
Features: features,
|
Features: features,
|
||||||
Tracer: tracer,
|
Tracer: tracer,
|
||||||
|
@ -130,8 +127,6 @@ func RegisterAPIService(
|
||||||
return authorizer.DecisionAllow, "", nil
|
return authorizer.DecisionAllow, "", nil
|
||||||
})
|
})
|
||||||
|
|
||||||
reg := client.NewDataSourceRegistryFromStore(pluginStore, dataSourcesService)
|
|
||||||
|
|
||||||
builder, err := NewQueryAPIBuilder(
|
builder, err := NewQueryAPIBuilder(
|
||||||
features,
|
features,
|
||||||
client.NewSingleTenantInstanceProvider(cfg, features, pluginClient, pCtxProvider, accessControl),
|
client.NewSingleTenantInstanceProvider(cfg, features, pluginClient, pCtxProvider, accessControl),
|
||||||
|
@ -140,7 +135,6 @@ func RegisterAPIService(
|
||||||
registerer,
|
registerer,
|
||||||
tracer,
|
tracer,
|
||||||
legacyDatasourceLookup,
|
legacyDatasourceLookup,
|
||||||
&connectionsProvider{dsService: dataSourcesService, registry: reg},
|
|
||||||
)
|
)
|
||||||
apiregistration.RegisterAPI(builder)
|
apiregistration.RegisterAPI(builder)
|
||||||
return builder, err
|
return builder, err
|
||||||
|
@ -154,8 +148,6 @@ func addKnownTypes(scheme *runtime.Scheme, gv schema.GroupVersion) {
|
||||||
scheme.AddKnownTypes(gv,
|
scheme.AddKnownTypes(gv,
|
||||||
&query.DataSourceApiServer{},
|
&query.DataSourceApiServer{},
|
||||||
&query.DataSourceApiServerList{},
|
&query.DataSourceApiServerList{},
|
||||||
&query.DataSourceConnection{},
|
|
||||||
&query.DataSourceConnectionList{},
|
|
||||||
&query.QueryDataRequest{},
|
&query.QueryDataRequest{},
|
||||||
&query.QueryDataResponse{},
|
&query.QueryDataResponse{},
|
||||||
&query.QueryTypeDefinition{},
|
&query.QueryTypeDefinition{},
|
||||||
|
@ -178,14 +170,6 @@ func (b *QueryAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIG
|
||||||
|
|
||||||
storage := map[string]rest.Storage{}
|
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)
|
plugins := newPluginsStorage(b.registry)
|
||||||
storage[plugins.resourceInfo.StoragePath()] = plugins
|
storage[plugins.resourceInfo.StoragePath()] = plugins
|
||||||
if !b.features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) {
|
if !b.features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) {
|
||||||
|
|
|
@ -732,7 +732,7 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
scopedPluginDatasourceProvider := datasource.ProvideDefaultPluginConfigs(service15, cacheServiceImpl, plugincontextProvider, cfg)
|
scopedPluginDatasourceProvider := datasource.ProvideDefaultPluginConfigs(service15, cacheServiceImpl, plugincontextProvider)
|
||||||
v := builder.ProvideDefaultBuildHandlerChainFuncFromBuilders()
|
v := builder.ProvideDefaultBuildHandlerChainFuncFromBuilders()
|
||||||
aggregatorRunner := aggregatorrunner.ProvideNoopAggregatorConfigurator()
|
aggregatorRunner := aggregatorrunner.ProvideNoopAggregatorConfigurator()
|
||||||
playlistAppInstaller, err := playlist.RegisterAppInstaller(playlistService, cfg, featureToggles)
|
playlistAppInstaller, err := playlist.RegisterAppInstaller(playlistService, cfg, featureToggles)
|
||||||
|
@ -1314,7 +1314,7 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
scopedPluginDatasourceProvider := datasource.ProvideDefaultPluginConfigs(service15, cacheServiceImpl, plugincontextProvider, cfg)
|
scopedPluginDatasourceProvider := datasource.ProvideDefaultPluginConfigs(service15, cacheServiceImpl, plugincontextProvider)
|
||||||
v := builder.ProvideDefaultBuildHandlerChainFuncFromBuilders()
|
v := builder.ProvideDefaultBuildHandlerChainFuncFromBuilders()
|
||||||
aggregatorRunner := aggregatorrunner.ProvideNoopAggregatorConfigurator()
|
aggregatorRunner := aggregatorrunner.ProvideNoopAggregatorConfigurator()
|
||||||
playlistAppInstaller, err := playlist.RegisterAppInstaller(playlistService, cfg, featureToggles)
|
playlistAppInstaller, err := playlist.RegisterAppInstaller(playlistService, cfg, featureToggles)
|
||||||
|
|
|
@ -499,13 +499,6 @@ var (
|
||||||
Owner: grafanaDatasourcesCoreServicesSquad,
|
Owner: grafanaDatasourcesCoreServicesSquad,
|
||||||
RequiresRestart: true, // Adds a route at startup
|
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",
|
Name: "queryServiceRewrite",
|
||||||
Description: "Rewrite requests targeting /ds/query to the query service",
|
Description: "Rewrite requests targeting /ds/query to the query service",
|
||||||
|
|
|
@ -65,7 +65,6 @@ dashboardSchemaValidationLogging,experimental,@grafana/grafana-app-platform-squa
|
||||||
scanRowInvalidDashboardParseFallbackEnabled,experimental,@grafana/search-and-storage,false,false,false
|
scanRowInvalidDashboardParseFallbackEnabled,experimental,@grafana/search-and-storage,false,false,false
|
||||||
datasourceQueryTypes,experimental,@grafana/grafana-app-platform-squad,false,true,false
|
datasourceQueryTypes,experimental,@grafana/grafana-app-platform-squad,false,true,false
|
||||||
queryService,experimental,@grafana/grafana-datasources-core-services,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
|
queryServiceRewrite,experimental,@grafana/grafana-datasources-core-services,false,true,false
|
||||||
queryServiceFromUI,experimental,@grafana/grafana-datasources-core-services,false,false,true
|
queryServiceFromUI,experimental,@grafana/grafana-datasources-core-services,false,false,true
|
||||||
queryServiceFromExplore,experimental,@grafana/grafana-datasources-core-services,false,false,true
|
queryServiceFromExplore,experimental,@grafana/grafana-datasources-core-services,false,false,true
|
||||||
|
|
|
|
@ -271,10 +271,6 @@ const (
|
||||||
// Register /apis/query.grafana.app/ -- will eventually replace /api/ds/query
|
// Register /apis/query.grafana.app/ -- will eventually replace /api/ds/query
|
||||||
FlagQueryService = "queryService"
|
FlagQueryService = "queryService"
|
||||||
|
|
||||||
// FlagQueryServiceWithConnections
|
|
||||||
// Adds datasource connections to the query service
|
|
||||||
FlagQueryServiceWithConnections = "queryServiceWithConnections"
|
|
||||||
|
|
||||||
// FlagQueryServiceRewrite
|
// FlagQueryServiceRewrite
|
||||||
// Rewrite requests targeting /ds/query to the query service
|
// Rewrite requests targeting /ds/query to the query service
|
||||||
FlagQueryServiceRewrite = "queryServiceRewrite"
|
FlagQueryServiceRewrite = "queryServiceRewrite"
|
||||||
|
|
|
@ -2765,19 +2765,6 @@
|
||||||
"requiresRestart": true
|
"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": {
|
"metadata": {
|
||||||
"name": "recordedQueriesMulti",
|
"name": "recordedQueriesMulti",
|
||||||
|
|
|
@ -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
|
|
|
@ -2,16 +2,13 @@ package dashboards
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
|
||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
"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/datasources"
|
||||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/tests/apis"
|
"github.com/grafana/grafana/pkg/tests/apis"
|
||||||
|
@ -41,58 +38,97 @@ func TestIntegrationTestDatasource(t *testing.T) {
|
||||||
Type: datasources.DS_TESTDATA,
|
Type: datasources.DS_TESTDATA,
|
||||||
UID: "test",
|
UID: "test",
|
||||||
OrgID: int64(1),
|
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)
|
require.Equal(t, "test", ds.UID)
|
||||||
|
|
||||||
t.Run("Admin configs", func(t *testing.T) {
|
t.Run("Check discovery client", func(t *testing.T) {
|
||||||
client := helper.Org1.Admin.ResourceClient(t, schema.GroupVersionResource{
|
disco := helper.GetGroupVersionInfoJSON("testdata.datasource.grafana.app")
|
||||||
Group: "testdata.datasource.grafana.app",
|
fmt.Printf("%s", disco)
|
||||||
Version: "v0alpha1",
|
|
||||||
Resource: "datasources",
|
|
||||||
}).Namespace("default")
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
list, err := client.List(ctx, metav1.ListOptions{})
|
require.JSONEq(t, `[
|
||||||
require.NoError(t, err)
|
{
|
||||||
require.Len(t, list.Items, 1, "expected a single connection")
|
"freshness": "Current",
|
||||||
require.Equal(t, "test", list.Items[0].GetName(), "with the test uid")
|
"resources": [
|
||||||
|
{
|
||||||
spec, _, _ := unstructured.NestedMap(list.Items[0].Object, "spec")
|
"resource": "connections",
|
||||||
jj, _ := json.MarshalIndent(spec, "", " ")
|
"responseKind": {
|
||||||
fmt.Printf("%s\n", string(jj))
|
"group": "",
|
||||||
require.JSONEq(t, `{
|
"kind": "DataSourceConnection",
|
||||||
"access": "proxy",
|
"version": ""
|
||||||
"database": "testdb",
|
|
||||||
"isDefault": true,
|
|
||||||
"jsonData": {
|
|
||||||
"hello": "world"
|
|
||||||
},
|
},
|
||||||
"readOnly": true,
|
"scope": "Namespaced",
|
||||||
"title": "test",
|
"shortNames": [
|
||||||
"url": "http://fake.url",
|
"grafana-testdata-datasource-connection"
|
||||||
"user": "example"
|
],
|
||||||
}`, string(jj))
|
"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) {
|
t.Run("Call subresources", func(t *testing.T) {
|
||||||
client := helper.Org1.Admin.ResourceClient(t, schema.GroupVersionResource{
|
client := helper.Org1.Admin.ResourceClient(t, schema.GroupVersionResource{
|
||||||
Group: "testdata.datasource.grafana.app",
|
Group: "testdata.datasource.grafana.app",
|
||||||
Version: "v0alpha1",
|
Version: "v0alpha1",
|
||||||
Resource: "datasources",
|
Resource: "connections",
|
||||||
}).Namespace("default")
|
}).Namespace("default")
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
|
@ -119,7 +155,7 @@ func TestIntegrationTestDatasource(t *testing.T) {
|
||||||
raw := apis.DoRequest[any](helper, apis.RequestParams{
|
raw := apis.DoRequest[any](helper, apis.RequestParams{
|
||||||
User: helper.Org1.Admin,
|
User: helper.Org1.Admin,
|
||||||
Method: "GET",
|
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)
|
}, nil)
|
||||||
require.Equal(t, `Hello world from test datasource!`, string(raw.Body))
|
require.Equal(t, `Hello world from test datasource!`, string(raw.Body))
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in New Issue