This reverts commit 72eeefabd7.
This commit is contained in:
@@ -25,7 +25,7 @@ func (b *DataSourceAPIBuilder) GetAuthorizer() authorizer.Authorizer {
|
||||
uidScope := datasources.ScopeProvider.GetResourceScopeUID(attr.GetName())
|
||||
|
||||
// Must have query access to see a connection
|
||||
if attr.GetResource() == b.connectionResourceInfo.GroupResource().Resource {
|
||||
if attr.GetResource() == b.datasourceResourceInfo.GroupResource().Resource {
|
||||
scopes := []string{}
|
||||
if attr.GetName() != "" {
|
||||
scopes = []string{uidScope}
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
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,27 +5,33 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||
"github.com/grafana/grafana/pkg/apis/datasource/v0alpha1"
|
||||
datasourceV0 "github.com/grafana/grafana/pkg/apis/datasource/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
|
||||
gapiutil "github.com/grafana/grafana/pkg/services/apiserver/utils"
|
||||
"github.com/grafana/grafana/pkg/services/datasources"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
// This provides access to settings saved in the database.
|
||||
// Authorization checks will happen within each function, and the user in ctx will
|
||||
// limit which namespace/tenant/org we are talking to
|
||||
type PluginDatasourceProvider interface {
|
||||
// Get gets a specific datasource (that the user in context can see)
|
||||
Get(ctx context.Context, uid string) (*v0alpha1.DataSourceConnection, error)
|
||||
// Get a single data source (any type)
|
||||
GetDataSource(ctx context.Context, uid string) (*datasourceV0.DataSource, error)
|
||||
|
||||
// List lists all data sources the user in context can see
|
||||
List(ctx context.Context) (*v0alpha1.DataSourceConnectionList, error)
|
||||
// List all datasources (any type)
|
||||
ListDataSources(ctx context.Context) (*datasourceV0.DataSourceList, error)
|
||||
|
||||
// Create a data source
|
||||
CreateDataSource(ctx context.Context, ds *datasourceV0.DataSource) (*datasourceV0.DataSource, error)
|
||||
|
||||
// Update a data source
|
||||
UpdateDataSource(ctx context.Context, ds *datasourceV0.DataSource) (*datasourceV0.DataSource, error)
|
||||
|
||||
// Delete a data source (any type)
|
||||
DeleteDataSource(ctx context.Context, uid string) error
|
||||
|
||||
// Return settings (decrypted!) for a specific plugin
|
||||
// This will require "query" permission for the user in context
|
||||
@@ -44,11 +50,16 @@ type PluginContextWrapper interface {
|
||||
func ProvideDefaultPluginConfigs(
|
||||
dsService datasources.DataSourceService,
|
||||
dsCache datasources.CacheService,
|
||||
contextProvider *plugincontext.Provider) ScopedPluginDatasourceProvider {
|
||||
contextProvider *plugincontext.Provider,
|
||||
cfg *setting.Cfg,
|
||||
) ScopedPluginDatasourceProvider {
|
||||
return &cachingDatasourceProvider{
|
||||
dsService: dsService,
|
||||
dsCache: dsCache,
|
||||
contextProvider: contextProvider,
|
||||
converter: &converter{
|
||||
mapper: request.GetNamespaceMapper(cfg),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,14 +67,22 @@ type cachingDatasourceProvider struct {
|
||||
dsService datasources.DataSourceService
|
||||
dsCache datasources.CacheService
|
||||
contextProvider *plugincontext.Provider
|
||||
converter *converter
|
||||
}
|
||||
|
||||
func (q *cachingDatasourceProvider) GetDatasourceProvider(pluginJson plugins.JSONData) PluginDatasourceProvider {
|
||||
group, _ := plugins.GetDatasourceGroupNameFromPluginID(pluginJson.ID)
|
||||
return &scopedDatasourceProvider{
|
||||
plugin: pluginJson,
|
||||
dsService: q.dsService,
|
||||
dsCache: q.dsCache,
|
||||
contextProvider: q.contextProvider,
|
||||
converter: &converter{
|
||||
mapper: q.converter.mapper,
|
||||
plugin: pluginJson.ID,
|
||||
alias: pluginJson.AliasIDs,
|
||||
group: group,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,6 +91,7 @@ type scopedDatasourceProvider struct {
|
||||
dsService datasources.DataSourceService
|
||||
dsCache datasources.CacheService
|
||||
contextProvider *plugincontext.Provider
|
||||
converter *converter
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -79,11 +99,62 @@ var (
|
||||
_ ScopedPluginDatasourceProvider = (*cachingDatasourceProvider)(nil)
|
||||
)
|
||||
|
||||
func (q *scopedDatasourceProvider) Get(ctx context.Context, uid string) (*v0alpha1.DataSourceConnection, error) {
|
||||
info, err := request.NamespaceInfoFrom(ctx, true)
|
||||
func (q *scopedDatasourceProvider) GetInstanceSettings(ctx context.Context, uid string) (*backend.DataSourceInstanceSettings, error) {
|
||||
if q.contextProvider == nil {
|
||||
return nil, fmt.Errorf("missing contextProvider")
|
||||
}
|
||||
return q.contextProvider.GetDataSourceInstanceSettings(ctx, uid)
|
||||
}
|
||||
|
||||
// CreateDataSource implements PluginDatasourceProvider.
|
||||
func (q *scopedDatasourceProvider) CreateDataSource(ctx context.Context, ds *datasourceV0.DataSource) (*datasourceV0.DataSource, error) {
|
||||
cmd, err := q.converter.toAddCommand(ds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out, err := q.dsService.AddDataSource(ctx, cmd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return q.converter.asDataSource(out)
|
||||
}
|
||||
|
||||
// UpdateDataSource implements PluginDatasourceProvider.
|
||||
func (q *scopedDatasourceProvider) UpdateDataSource(ctx context.Context, ds *datasourceV0.DataSource) (*datasourceV0.DataSource, error) {
|
||||
cmd, err := q.converter.toUpdateCommand(ds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out, err := q.dsService.UpdateDataSource(ctx, cmd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return q.converter.asDataSource(out)
|
||||
}
|
||||
|
||||
// Delete implements PluginDatasourceProvider.
|
||||
func (q *scopedDatasourceProvider) DeleteDataSource(ctx context.Context, uid string) error {
|
||||
user, err := identity.GetRequester(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ds, err := q.dsCache.GetDatasourceByUID(ctx, uid, user, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if ds == nil {
|
||||
return fmt.Errorf("not found")
|
||||
}
|
||||
return q.dsService.DeleteDataSource(ctx, &datasources.DeleteDataSourceCommand{
|
||||
ID: ds.ID,
|
||||
UID: ds.UID,
|
||||
OrgID: ds.OrgID,
|
||||
Name: ds.Name,
|
||||
})
|
||||
}
|
||||
|
||||
// GetDataSource implements PluginDatasourceProvider.
|
||||
func (q *scopedDatasourceProvider) GetDataSource(ctx context.Context, uid string) (*datasourceV0.DataSource, error) {
|
||||
user, err := identity.GetRequester(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -92,10 +163,11 @@ func (q *scopedDatasourceProvider) Get(ctx context.Context, uid string) (*v0alph
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return asConnection(ds, info.Value)
|
||||
return q.converter.asDataSource(ds)
|
||||
}
|
||||
|
||||
func (q *scopedDatasourceProvider) List(ctx context.Context) (*v0alpha1.DataSourceConnectionList, error) {
|
||||
// ListDataSource implements PluginDatasourceProvider.
|
||||
func (q *scopedDatasourceProvider) ListDataSources(ctx context.Context) (*datasourceV0.DataSourceList, error) {
|
||||
info, err := request.NamespaceInfoFrom(ctx, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -109,37 +181,12 @@ func (q *scopedDatasourceProvider) List(ctx context.Context) (*v0alpha1.DataSour
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := &v0alpha1.DataSourceConnectionList{
|
||||
Items: []v0alpha1.DataSourceConnection{},
|
||||
result := &datasourceV0.DataSourceList{
|
||||
Items: []datasourceV0.DataSource{},
|
||||
}
|
||||
for _, ds := range dss {
|
||||
v, _ := asConnection(ds, info.Value)
|
||||
v, _ := q.converter.asDataSource(ds)
|
||||
result.Items = append(result.Items, *v)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (q *scopedDatasourceProvider) GetInstanceSettings(ctx context.Context, uid string) (*backend.DataSourceInstanceSettings, error) {
|
||||
if q.contextProvider == nil {
|
||||
return nil, fmt.Errorf("missing contextProvider")
|
||||
}
|
||||
return q.contextProvider.GetDataSourceInstanceSettings(ctx, uid)
|
||||
}
|
||||
|
||||
func asConnection(ds *datasources.DataSource, ns string) (*v0alpha1.DataSourceConnection, error) {
|
||||
v := &v0alpha1.DataSourceConnection{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: ds.UID,
|
||||
Namespace: ns,
|
||||
CreationTimestamp: metav1.NewTime(ds.Created),
|
||||
ResourceVersion: fmt.Sprintf("%d", ds.Updated.UnixMilli()),
|
||||
},
|
||||
Title: ds.Name,
|
||||
}
|
||||
v.UID = gapiutil.CalculateClusterWideUID(v) // indicates if the value changed on the server
|
||||
meta, err := utils.MetaAccessor(v)
|
||||
if err != nil {
|
||||
meta.SetUpdatedTimestamp(&ds.Updated)
|
||||
}
|
||||
return v, err
|
||||
}
|
||||
|
||||
@@ -4,13 +4,10 @@ import (
|
||||
"context"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||
"github.com/grafana/grafana/pkg/apis/datasource/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
|
||||
"github.com/grafana/grafana/pkg/services/datasources"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
type QuerierFactoryFunc func(ctx context.Context, ri utils.ResourceInfo, pj plugins.JSONData) (Querier, error)
|
||||
@@ -48,10 +45,6 @@ type Querier interface {
|
||||
Health(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error)
|
||||
// Resource gets a resource plugin.
|
||||
Resource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error
|
||||
// Datasource gets all data source plugins (with elevated permissions).
|
||||
Datasource(ctx context.Context, name string) (*v0alpha1.DataSourceConnection, error)
|
||||
// Datasources lists all data sources (with elevated permissions).
|
||||
Datasources(ctx context.Context) (*v0alpha1.DataSourceConnectionList, error)
|
||||
}
|
||||
|
||||
type DefaultQuerier struct {
|
||||
@@ -101,47 +94,3 @@ func (q *DefaultQuerier) Health(ctx context.Context, query *backend.CheckHealthR
|
||||
}
|
||||
return q.pluginClient.CheckHealth(ctx, query)
|
||||
}
|
||||
|
||||
func (q *DefaultQuerier) Datasource(ctx context.Context, name string) (*v0alpha1.DataSourceConnection, error) {
|
||||
info, err := request.NamespaceInfoFrom(ctx, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
user, err := identity.GetRequester(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ds, err := q.dsCache.GetDatasourceByUID(ctx, name, user, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return asConnection(ds, info.Value)
|
||||
}
|
||||
|
||||
func (q *DefaultQuerier) Datasources(ctx context.Context) (*v0alpha1.DataSourceConnectionList, error) {
|
||||
info, err := request.NamespaceInfoFrom(ctx, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ds, err := q.dsService.GetDataSourcesByType(ctx, &datasources.GetDataSourcesByTypeQuery{
|
||||
OrgID: info.OrgID,
|
||||
Type: q.pluginJSON.ID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return asConnectionList(q.connectionResourceInfo.TypeMeta(), ds, info.Value)
|
||||
}
|
||||
|
||||
func asConnectionList(typeMeta metav1.TypeMeta, dss []*datasources.DataSource, ns string) (*v0alpha1.DataSourceConnectionList, error) {
|
||||
result := &v0alpha1.DataSourceConnectionList{
|
||||
Items: []v0alpha1.DataSourceConnection{},
|
||||
}
|
||||
for _, ds := range dss {
|
||||
v, _ := asConnection(ds, ns)
|
||||
result.Items = append(result.Items, *v)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
@@ -3,9 +3,8 @@ package datasource
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"maps"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
@@ -15,38 +14,38 @@ import (
|
||||
"k8s.io/apiserver/pkg/registry/rest"
|
||||
genericapiserver "k8s.io/apiserver/pkg/server"
|
||||
openapi "k8s.io/kube-openapi/pkg/common"
|
||||
"k8s.io/kube-openapi/pkg/spec3"
|
||||
"k8s.io/utils/strings/slices"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||
datasource "github.com/grafana/grafana/pkg/apis/datasource/v0alpha1"
|
||||
query "github.com/grafana/grafana/pkg/apis/query/v0alpha1"
|
||||
datasourceV0 "github.com/grafana/grafana/pkg/apis/datasource/v0alpha1"
|
||||
queryV0 "github.com/grafana/grafana/pkg/apis/query/v0alpha1"
|
||||
grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic"
|
||||
"github.com/grafana/grafana/pkg/configprovider"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/sources"
|
||||
"github.com/grafana/grafana/pkg/promlib/models"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/query/queryschema"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/builder"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/tsdb/grafana-testdata-datasource/kinds"
|
||||
)
|
||||
|
||||
var _ builder.APIGroupBuilder = (*DataSourceAPIBuilder)(nil)
|
||||
var (
|
||||
_ builder.APIGroupBuilder = (*DataSourceAPIBuilder)(nil)
|
||||
)
|
||||
|
||||
// DataSourceAPIBuilder is used just so wire has something unique to return
|
||||
type DataSourceAPIBuilder struct {
|
||||
connectionResourceInfo utils.ResourceInfo
|
||||
datasourceResourceInfo utils.ResourceInfo
|
||||
|
||||
pluginJSON plugins.JSONData
|
||||
client PluginClient // will only ever be called with the same pluginid!
|
||||
client PluginClient // will only ever be called with the same plugin id!
|
||||
datasources PluginDatasourceProvider
|
||||
contextProvider PluginContextWrapper
|
||||
accessControl accesscontrol.AccessControl
|
||||
queryTypes *query.QueryTypeDefinitionList
|
||||
queryTypes *queryV0.QueryTypeDefinitionList
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
@@ -114,6 +113,12 @@ func RegisterAPIService(
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// TODO: load the schema provider from a static manifest
|
||||
// if ds.ID == "grafana-testdata-datasource" {
|
||||
// builder.schemaProvider = hardcoded.TestdataOpenAPIExtension
|
||||
// }
|
||||
|
||||
apiRegistrar.RegisterAPI(builder)
|
||||
}
|
||||
return builder, nil // only used for wire
|
||||
@@ -136,13 +141,13 @@ func NewDataSourceAPIBuilder(
|
||||
accessControl accesscontrol.AccessControl,
|
||||
loadQueryTypes bool,
|
||||
) (*DataSourceAPIBuilder, error) {
|
||||
ri, err := resourceFromPluginID(plugin.ID)
|
||||
group, err := plugins.GetDatasourceGroupNameFromPluginID(plugin.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
builder := &DataSourceAPIBuilder{
|
||||
connectionResourceInfo: ri,
|
||||
datasourceResourceInfo: datasourceV0.DataSourceResourceInfo.WithGroupAndShortName(group, plugin.ID),
|
||||
pluginJSON: plugin,
|
||||
client: client,
|
||||
datasources: datasources,
|
||||
@@ -152,13 +157,13 @@ func NewDataSourceAPIBuilder(
|
||||
}
|
||||
if loadQueryTypes {
|
||||
// In the future, this will somehow come from the plugin
|
||||
builder.queryTypes, err = getHardcodedQueryTypes(ri.GroupResource().Group)
|
||||
builder.queryTypes, err = getHardcodedQueryTypes(group)
|
||||
}
|
||||
return builder, err
|
||||
}
|
||||
|
||||
// TODO -- somehow get the list from the plugin -- not hardcoded
|
||||
func getHardcodedQueryTypes(group string) (*query.QueryTypeDefinitionList, error) {
|
||||
func getHardcodedQueryTypes(group string) (*queryV0.QueryTypeDefinitionList, error) {
|
||||
var err error
|
||||
var raw json.RawMessage
|
||||
switch group {
|
||||
@@ -171,7 +176,7 @@ func getHardcodedQueryTypes(group string) (*query.QueryTypeDefinitionList, error
|
||||
return nil, err
|
||||
}
|
||||
if raw != nil {
|
||||
types := &query.QueryTypeDefinitionList{}
|
||||
types := &queryV0.QueryTypeDefinitionList{}
|
||||
err = json.Unmarshal(raw, types)
|
||||
return types, err
|
||||
}
|
||||
@@ -179,26 +184,27 @@ func getHardcodedQueryTypes(group string) (*query.QueryTypeDefinitionList, error
|
||||
}
|
||||
|
||||
func (b *DataSourceAPIBuilder) GetGroupVersion() schema.GroupVersion {
|
||||
return b.connectionResourceInfo.GroupVersion()
|
||||
return b.datasourceResourceInfo.GroupVersion()
|
||||
}
|
||||
|
||||
func addKnownTypes(scheme *runtime.Scheme, gv schema.GroupVersion) {
|
||||
scheme.AddKnownTypes(gv,
|
||||
&datasource.DataSourceConnection{},
|
||||
&datasource.DataSourceConnectionList{},
|
||||
&datasource.HealthCheckResult{},
|
||||
&datasourceV0.DataSource{},
|
||||
&datasourceV0.DataSourceList{},
|
||||
&datasourceV0.HealthCheckResult{},
|
||||
&unstructured.Unstructured{},
|
||||
|
||||
// Query handler
|
||||
&query.QueryDataRequest{},
|
||||
&query.QueryDataResponse{},
|
||||
&query.QueryTypeDefinition{},
|
||||
&query.QueryTypeDefinitionList{},
|
||||
&queryV0.QueryDataRequest{},
|
||||
&queryV0.QueryDataResponse{},
|
||||
&queryV0.QueryTypeDefinition{},
|
||||
&queryV0.QueryTypeDefinitionList{},
|
||||
&metav1.Status{},
|
||||
)
|
||||
}
|
||||
|
||||
func (b *DataSourceAPIBuilder) InstallSchema(scheme *runtime.Scheme) error {
|
||||
gv := b.connectionResourceInfo.GroupVersion()
|
||||
gv := b.datasourceResourceInfo.GroupVersion()
|
||||
addKnownTypes(scheme, gv)
|
||||
|
||||
// Link this version to the internal representation.
|
||||
@@ -221,43 +227,48 @@ func (b *DataSourceAPIBuilder) AllowedV0Alpha1Resources() []string {
|
||||
return []string{builder.AllResourcesAllowed}
|
||||
}
|
||||
|
||||
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 {
|
||||
func (b *DataSourceAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIGroupInfo, opts builder.APIGroupOptions) error {
|
||||
storage := map[string]rest.Storage{}
|
||||
|
||||
conn := b.connectionResourceInfo
|
||||
storage[conn.StoragePath()] = &connectionAccess{
|
||||
datasources: b.datasources,
|
||||
resourceInfo: conn,
|
||||
tableConverter: conn.TableConverter(),
|
||||
// Register the raw datasource connection
|
||||
ds := b.datasourceResourceInfo
|
||||
legacyStore := &legacyStorage{
|
||||
datasources: b.datasources,
|
||||
resourceInfo: &ds,
|
||||
}
|
||||
unified, err := grafanaregistry.NewRegistryStore(opts.Scheme, ds, opts.OptsGetter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
storage[ds.StoragePath()], err = opts.DualWriteBuilder(ds.GroupResource(), legacyStore, unified)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
storage[conn.StoragePath("query")] = &subQueryREST{builder: b}
|
||||
storage[conn.StoragePath("health")] = &subHealthREST{builder: b}
|
||||
|
||||
// TODO! only setup this endpoint if it is implemented
|
||||
storage[conn.StoragePath("resource")] = &subResourceREST{builder: b}
|
||||
storage[ds.StoragePath("query")] = &subQueryREST{builder: b}
|
||||
storage[ds.StoragePath("health")] = &subHealthREST{builder: b}
|
||||
storage[ds.StoragePath("resource")] = &subResourceREST{builder: b}
|
||||
|
||||
// FIXME: temporarily register both "datasources" and "connections" query paths
|
||||
// This lets us deploy both datasources/{uid}/query and connections/{uid}/query
|
||||
// while we transition requests to the new path
|
||||
storage["connections"] = &noopREST{} // hidden from openapi
|
||||
storage["connections/query"] = storage[ds.StoragePath("query")] // deprecated in openapi
|
||||
|
||||
// Frontend proxy
|
||||
if len(b.pluginJSON.Routes) > 0 {
|
||||
storage[conn.StoragePath("proxy")] = &subProxyREST{pluginJSON: b.pluginJSON}
|
||||
storage[ds.StoragePath("proxy")] = &subProxyREST{pluginJSON: b.pluginJSON}
|
||||
}
|
||||
|
||||
// Register hardcoded query schemas
|
||||
err := queryschema.RegisterQueryTypes(b.queryTypes, storage)
|
||||
err = queryschema.RegisterQueryTypes(b.queryTypes, storage)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
registerQueryConvert(b.client, b.contextProvider, storage)
|
||||
|
||||
apiGroupInfo.VersionedResourcesStorageMap[conn.GroupVersion().Version] = storage
|
||||
apiGroupInfo.VersionedResourcesStorageMap[ds.GroupVersion().Version] = storage
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -271,50 +282,8 @@ func (b *DataSourceAPIBuilder) getPluginContext(ctx context.Context, uid string)
|
||||
|
||||
func (b *DataSourceAPIBuilder) GetOpenAPIDefinitions() openapi.GetOpenAPIDefinitions {
|
||||
return func(ref openapi.ReferenceCallback) map[string]openapi.OpenAPIDefinition {
|
||||
defs := query.GetOpenAPIDefinitions(ref) // required when running standalone
|
||||
for k, v := range datasource.GetOpenAPIDefinitions(ref) {
|
||||
defs[k] = v
|
||||
}
|
||||
defs := queryV0.GetOpenAPIDefinitions(ref) // required when running standalone
|
||||
maps.Copy(defs, datasourceV0.GetOpenAPIDefinitions(ref))
|
||||
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
|
||||
}
|
||||
|
||||
func getCorePlugins(cfg *setting.Cfg) ([]plugins.JSONData, error) {
|
||||
coreDataSourcesPath := filepath.Join(cfg.StaticRootPath, "app", "plugins", "datasource")
|
||||
coreDataSourcesSrc := sources.NewLocalSource(
|
||||
plugins.ClassCore,
|
||||
[]string{coreDataSourcesPath},
|
||||
)
|
||||
|
||||
res, err := coreDataSourcesSrc.Discover(context.Background())
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to load core data source plugins")
|
||||
}
|
||||
|
||||
pluginJSONs := make([]plugins.JSONData, 0, len(res))
|
||||
for _, p := range res {
|
||||
pluginJSONs = append(pluginJSONs, p.Primary.JSONData)
|
||||
}
|
||||
return pluginJSONs, nil
|
||||
}
|
||||
|
||||
@@ -6,19 +6,17 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apiserver/pkg/registry/rest"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
data "github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/errutil"
|
||||
query "github.com/grafana/grafana/pkg/apis/query/v0alpha1"
|
||||
query_headers "github.com/grafana/grafana/pkg/registry/apis/query"
|
||||
"github.com/grafana/grafana/pkg/services/datasources"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apiserver/pkg/registry/rest"
|
||||
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
|
||||
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
)
|
||||
|
||||
type subQueryREST struct {
|
||||
@@ -29,6 +27,7 @@ var (
|
||||
_ rest.Storage = (*subQueryREST)(nil)
|
||||
_ rest.Connecter = (*subQueryREST)(nil)
|
||||
_ rest.StorageMetadata = (*subQueryREST)(nil)
|
||||
_ rest.Scoper = (*subQueryREST)(nil)
|
||||
)
|
||||
|
||||
func (r *subQueryREST) New() runtime.Object {
|
||||
@@ -38,6 +37,10 @@ func (r *subQueryREST) New() runtime.Object {
|
||||
|
||||
func (r *subQueryREST) Destroy() {}
|
||||
|
||||
func (r *subQueryREST) NamespaceScoped() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (r *subQueryREST) ProducesMIMETypes(verb string) []string {
|
||||
return []string{"application/json"} // and parquet!
|
||||
}
|
||||
@@ -59,15 +62,8 @@ func (r *subQueryREST) Connect(ctx context.Context, name string, opts runtime.Ob
|
||||
|
||||
if err != nil {
|
||||
if errors.Is(err, datasources.ErrDataSourceNotFound) {
|
||||
return nil, k8serrors.NewNotFound(
|
||||
schema.GroupResource{
|
||||
Group: r.builder.connectionResourceInfo.GroupResource().Group,
|
||||
Resource: r.builder.connectionResourceInfo.GroupResource().Resource,
|
||||
},
|
||||
name,
|
||||
)
|
||||
return nil, r.builder.datasourceResourceInfo.NewNotFound(name)
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
@@ -8,14 +8,16 @@ import (
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana/pkg/apis/datasource/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/services/datasources"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||
"github.com/stretchr/testify/require"
|
||||
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana/pkg/apis/datasource/v0alpha1"
|
||||
queryV0 "github.com/grafana/grafana/pkg/apis/query/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/services/datasources"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||
)
|
||||
|
||||
func TestSubQueryConnect(t *testing.T) {
|
||||
@@ -115,16 +117,43 @@ func (m mockResponder) Object(statusCode int, obj runtime.Object) {
|
||||
func (m mockResponder) Error(err error) {
|
||||
}
|
||||
|
||||
var _ PluginDatasourceProvider = (*mockDatasources)(nil)
|
||||
|
||||
type mockDatasources struct {
|
||||
}
|
||||
|
||||
// CreateDataSource implements PluginDatasourceProvider.
|
||||
func (m mockDatasources) CreateDataSource(ctx context.Context, ds *v0alpha1.DataSource) (*v0alpha1.DataSource, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// UpdateDataSource implements PluginDatasourceProvider.
|
||||
func (m mockDatasources) UpdateDataSource(ctx context.Context, ds *v0alpha1.DataSource) (*v0alpha1.DataSource, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Delete implements PluginDatasourceProvider.
|
||||
func (m mockDatasources) DeleteDataSource(ctx context.Context, uid string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetDataSource implements PluginDatasourceProvider.
|
||||
func (m mockDatasources) GetDataSource(ctx context.Context, uid string) (*v0alpha1.DataSource, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// ListDataSource implements PluginDatasourceProvider.
|
||||
func (m mockDatasources) ListDataSources(ctx context.Context) (*v0alpha1.DataSourceList, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Get gets a specific datasource (that the user in context can see)
|
||||
func (m mockDatasources) Get(ctx context.Context, uid string) (*v0alpha1.DataSourceConnection, error) {
|
||||
func (m mockDatasources) GetConnection(ctx context.Context, uid string) (*queryV0.DataSourceConnection, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// List lists all data sources the user in context can see
|
||||
func (m mockDatasources) List(ctx context.Context) (*v0alpha1.DataSourceConnectionList, error) {
|
||||
func (m mockDatasources) ListConnections(ctx context.Context) (*queryV0.DataSourceConnectionList, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -8,11 +8,11 @@ import (
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apiserver/pkg/registry/rest"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana/pkg/plugins/httpresponsesender"
|
||||
)
|
||||
|
||||
|
||||
@@ -18,36 +18,36 @@ func TestResourceRequest(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
desc: "no resource path",
|
||||
url: "http://localhost:6443/apis/test.datasource.grafana.app/v0alpha1/namespaces/default/connections/abc",
|
||||
url: "http://localhost:6443/apis/test.datasource.grafana.app/v0alpha1/namespaces/default/datasources/abc",
|
||||
error: true,
|
||||
},
|
||||
{
|
||||
desc: "root resource path",
|
||||
url: "http://localhost:6443/apis/test.datasource.grafana.app/v0alpha1/namespaces/default/connections/abc/resource",
|
||||
url: "http://localhost:6443/apis/test.datasource.grafana.app/v0alpha1/namespaces/default/datasources/abc/resource",
|
||||
expectedPath: "",
|
||||
expectedURL: "",
|
||||
},
|
||||
{
|
||||
desc: "root resource path",
|
||||
url: "http://localhost:6443/apis/test.datasource.grafana.app/v0alpha1/namespaces/default/connections/abc/resource/",
|
||||
url: "http://localhost:6443/apis/test.datasource.grafana.app/v0alpha1/namespaces/default/datasources/abc/resource/",
|
||||
expectedPath: "",
|
||||
expectedURL: "",
|
||||
},
|
||||
{
|
||||
desc: "resource sub path",
|
||||
url: "http://localhost:6443/apis/test.datasource.grafana.app/v0alpha1/namespaces/default/connections/abc/resource/test",
|
||||
url: "http://localhost:6443/apis/test.datasource.grafana.app/v0alpha1/namespaces/default/datasources/abc/resource/test",
|
||||
expectedPath: "test",
|
||||
expectedURL: "test",
|
||||
},
|
||||
{
|
||||
desc: "resource sub path with colon",
|
||||
url: "http://localhost:6443/apis/test.datasource.grafana.app/v0alpha1/namespaces/default/connections/abc/resource/test-*,*:test-*/_mapping",
|
||||
url: "http://localhost:6443/apis/test.datasource.grafana.app/v0alpha1/namespaces/default/datasources/abc/resource/test-*,*:test-*/_mapping",
|
||||
expectedPath: "test-*,*:test-*/_mapping",
|
||||
expectedURL: "./test-%2A,%2A:test-%2A/_mapping",
|
||||
},
|
||||
{
|
||||
desc: "resource sub path with query params",
|
||||
url: "http://localhost:6443/apis/test.datasource.grafana.app/v0alpha1/namespaces/default/connections/abc/resource/test?k1=v1&k2=v2",
|
||||
url: "http://localhost:6443/apis/test.datasource.grafana.app/v0alpha1/namespaces/default/datasources/abc/resource/test?k1=v1&k2=v2",
|
||||
expectedPath: "test",
|
||||
expectedURL: "test?k1=v1&k2=v2",
|
||||
},
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"id": 456,
|
||||
"version": 8,
|
||||
"name": "Display name",
|
||||
"uid": "unique-identifier",
|
||||
"type": "grafana-testdata-datasource",
|
||||
"created": "2002-03-04T01:00:00Z"
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"id": 456,
|
||||
"version": 8,
|
||||
"name": "Hello",
|
||||
"uid": "unique-identifier",
|
||||
"type": "not-valid-plugin",
|
||||
"created": "2002-03-04T01:00:00Z"
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "Hello testdata",
|
||||
"type": "grafana-testdata-datasource",
|
||||
"access": "",
|
||||
"url": "",
|
||||
"user": "",
|
||||
"database": "",
|
||||
"basicAuth": false,
|
||||
"basicAuthUser": "",
|
||||
"withCredentials": false,
|
||||
"isDefault": false,
|
||||
"jsonData": null,
|
||||
"secureJsonData": null,
|
||||
"uid": "cejobd88i85j4d"
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"metadata": {
|
||||
"name": "cejobd88i85j4d",
|
||||
"namespace": "org-0",
|
||||
"uid": "boDNh7zU3nXj46rOXIJI7r44qaxjs8yy9I9dOj1MyBoX",
|
||||
"creationTimestamp": null
|
||||
},
|
||||
"spec": {
|
||||
"jsonData": null,
|
||||
"title": "Hello testdata"
|
||||
}
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"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
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"metadata": {
|
||||
"name": "cejobd88i85j4d"
|
||||
},
|
||||
"spec": {
|
||||
"title": "Hello testdata"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"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
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"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 }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"metadata": {
|
||||
"name": "cejobd88i85j4d",
|
||||
"namespace": "stacks-invalid"
|
||||
},
|
||||
"spec": {
|
||||
"title": "Hello testdata"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user