mirror of
https://github.com/grafana/grafana.git
synced 2026-02-03 20:49:50 -05:00
Fetch datasource config in the Grafana UI via app platform endpoints
When `config.featureToggles.queryServiceWithConnections` is true, calls to `getDataSourceByUid` are routed through a new frontend function, `getDataSourceFromK8sAPI`, which exercises the new app platform kubernetes style APIs.
This commit is contained in:
parent
8f8e076700
commit
089176bf23
9 changed files with 508 additions and 161 deletions
|
|
@ -4,6 +4,7 @@ import (
|
|||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
)
|
||||
|
||||
// +k8s:deepcopy-gen=true
|
||||
|
|
@ -24,3 +25,11 @@ type HealthCheckResult struct {
|
|||
// Spec depends on the plugin
|
||||
Details *common.Unstructured `json:"details,omitempty"`
|
||||
}
|
||||
|
||||
// +k8s:deepcopy-gen=true
|
||||
// +k8s:openapi-gen=true
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
type DatasourceAccessInfo struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
Permissions accesscontrol.Metadata
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ package v0alpha1
|
|||
|
||||
import (
|
||||
commonv0alpha1 "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
|
||||
accesscontrol "github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
runtime "k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
|
|
@ -79,6 +80,38 @@ func (in *DataSourceList) DeepCopyObject() runtime.Object {
|
|||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *DatasourceAccessInfo) DeepCopyInto(out *DatasourceAccessInfo) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
if in.Permissions != nil {
|
||||
in, out := &in.Permissions, &out.Permissions
|
||||
*out = make(accesscontrol.Metadata, len(*in))
|
||||
for key, val := range *in {
|
||||
(*out)[key] = val
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DatasourceAccessInfo.
|
||||
func (in *DatasourceAccessInfo) DeepCopy() *DatasourceAccessInfo {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(DatasourceAccessInfo)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *DatasourceAccessInfo) 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 *HealthCheckResult) DeepCopyInto(out *HealthCheckResult) {
|
||||
*out = *in
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA
|
|||
return map[string]common.OpenAPIDefinition{
|
||||
"github.com/grafana/grafana/pkg/apis/datasource/v0alpha1.DataSource": schema_pkg_apis_datasource_v0alpha1_DataSource(ref),
|
||||
"github.com/grafana/grafana/pkg/apis/datasource/v0alpha1.DataSourceList": schema_pkg_apis_datasource_v0alpha1_DataSourceList(ref),
|
||||
"github.com/grafana/grafana/pkg/apis/datasource/v0alpha1.DatasourceAccessInfo": schema_pkg_apis_datasource_v0alpha1_DatasourceAccessInfo(ref),
|
||||
"github.com/grafana/grafana/pkg/apis/datasource/v0alpha1.GenericDataSourceSpec": schema_pkg_apis_datasource_v0alpha1_GenericDataSourceSpec(ref),
|
||||
"github.com/grafana/grafana/pkg/apis/datasource/v0alpha1.HealthCheckResult": schema_pkg_apis_datasource_v0alpha1_HealthCheckResult(ref),
|
||||
"github.com/grafana/grafana/pkg/apis/datasource/v0alpha1.UnstructuredSpec": UnstructuredSpec{}.OpenAPIDefinition(),
|
||||
|
|
@ -126,6 +127,48 @@ func schema_pkg_apis_datasource_v0alpha1_DataSourceList(ref common.ReferenceCall
|
|||
}
|
||||
}
|
||||
|
||||
func schema_pkg_apis_datasource_v0alpha1_DatasourceAccessInfo(ref common.ReferenceCallback) common.OpenAPIDefinition {
|
||||
return common.OpenAPIDefinition{
|
||||
Schema: spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
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: "",
|
||||
},
|
||||
},
|
||||
"Permissions": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"object"},
|
||||
AdditionalProperties: &spec.SchemaOrBool{
|
||||
Allows: true,
|
||||
Schema: &spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: false,
|
||||
Type: []string{"boolean"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Required: []string{"Permissions"},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func schema_pkg_apis_datasource_v0alpha1_GenericDataSourceSpec(ref common.ReferenceCallback) common.OpenAPIDefinition {
|
||||
return common.OpenAPIDefinition{
|
||||
Schema: spec.Schema{
|
||||
|
|
|
|||
|
|
@ -1,2 +1,3 @@
|
|||
API rule violation: names_match,github.com/grafana/grafana/pkg/apis/datasource/v0alpha1,DatasourceAccessInfo,Permissions
|
||||
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
|
||||
|
|
|
|||
|
|
@ -191,6 +191,7 @@ func addKnownTypes(scheme *runtime.Scheme, gv schema.GroupVersion) {
|
|||
&datasourceV0.DataSourceList{},
|
||||
&datasourceV0.HealthCheckResult{},
|
||||
&unstructured.Unstructured{},
|
||||
&datasourceV0.DatasourceAccessInfo{},
|
||||
|
||||
// Query handler
|
||||
&queryV0.QueryDataRequest{},
|
||||
|
|
@ -263,6 +264,10 @@ func (b *DataSourceAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver
|
|||
return err
|
||||
}
|
||||
storage[ds.StoragePath()], err = opts.DualWriteBuilder(ds.GroupResource(), legacyStore, unified)
|
||||
storage[ds.StoragePath("access")] = &subAccessREST{
|
||||
builder: b,
|
||||
getter: legacyStore,
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
57
pkg/registry/apis/datasource/sub_access.go
Normal file
57
pkg/registry/apis/datasource/sub_access.go
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
package datasource
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apiserver/pkg/registry/rest"
|
||||
|
||||
datasourceV0alpha1 "github.com/grafana/grafana/pkg/apis/datasource/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/contexthandler"
|
||||
"github.com/grafana/grafana/pkg/services/datasources"
|
||||
)
|
||||
|
||||
type subAccessREST struct {
|
||||
builder *DataSourceAPIBuilder
|
||||
getter rest.Getter
|
||||
}
|
||||
|
||||
var _ = rest.Connecter(&subAccessREST{})
|
||||
|
||||
func (r *subAccessREST) New() runtime.Object {
|
||||
return &datasourceV0alpha1.DatasourceAccessInfo{}
|
||||
}
|
||||
|
||||
func (r *subAccessREST) Destroy() {
|
||||
// no-op implemenation needed for rest.Storage interface.
|
||||
}
|
||||
|
||||
func (r *subAccessREST) ConnectMethods() []string {
|
||||
return []string{"GET"}
|
||||
}
|
||||
|
||||
func (r *subAccessREST) NewConnectOptions() (runtime.Object, bool, string) {
|
||||
return nil, false, "" // true means you can use the trailing path as a variable
|
||||
}
|
||||
|
||||
func (r *subAccessREST) Connect(ctx context.Context, name string, opts runtime.Object, responder rest.Responder) (http.Handler, error) {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
access, err := r.getAccessInfo(ctx, name)
|
||||
if err != nil {
|
||||
responder.Error(err)
|
||||
} else {
|
||||
responder.Object(200, access)
|
||||
}
|
||||
}), nil
|
||||
}
|
||||
|
||||
func (r *subAccessREST) getAccessInfo(ctx context.Context, name string) (*datasourceV0alpha1.DatasourceAccessInfo, error) {
|
||||
reqContext := contexthandler.FromContext(ctx)
|
||||
resourceIDs := map[string]bool{datasources.ScopePrefix: true}
|
||||
access := accesscontrol.GetResourcesMetadata(reqContext.Req.Context(), reqContext.GetPermissions(), datasources.ScopePrefix, resourceIDs)
|
||||
return &datasourceV0alpha1.DatasourceAccessInfo{
|
||||
Permissions: access[datasources.ScopePrefix],
|
||||
}, nil
|
||||
}
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
package dashboards
|
||||
package datasource
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"maps"
|
||||
"net/http"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
|
|
@ -16,7 +16,9 @@ import (
|
|||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
|
||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||
datasourceV0alpha1 "github.com/grafana/grafana/pkg/apis/datasource/v0alpha1"
|
||||
grafanarest "github.com/grafana/grafana/pkg/apiserver/rest"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/datasources"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
|
|
@ -34,122 +36,115 @@ func TestIntegrationTestDatasource(t *testing.T) {
|
|||
testutil.SkipIntegrationTestInShortMode(t)
|
||||
expectedAPIVersion := "grafana-testdata-datasource.datasource.grafana.app/v0alpha1"
|
||||
|
||||
for _, mode := range []grafanarest.DualWriterMode{
|
||||
grafanarest.Mode0, // Legacy only
|
||||
grafanarest.Mode2, // write both, read legacy
|
||||
grafanarest.Mode3, // write both, read unified
|
||||
grafanarest.Mode5, // Unified only
|
||||
} {
|
||||
t.Run(fmt.Sprintf("testdata (mode:%d)", mode), func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
|
||||
DisableAnonymous: true,
|
||||
EnableFeatureToggles: []string{
|
||||
featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs, // Required to start the datasource api servers
|
||||
featuremgmt.FlagQueryServiceWithConnections, // enables CRUD endpoints
|
||||
ctx := context.Background()
|
||||
helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
|
||||
DisableAnonymous: true,
|
||||
EnableFeatureToggles: []string{
|
||||
featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs, // Required to start the datasource api servers
|
||||
featuremgmt.FlagQueryServiceWithConnections, // enables CRUD endpoints
|
||||
},
|
||||
UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{
|
||||
"datasources.grafana-testdata-datasource.datasource.grafana.app": {
|
||||
DualWriterMode: grafanarest.Mode0,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
client := helper.Org1.Admin.ResourceClient(t, schema.GroupVersionResource{
|
||||
Group: "grafana-testdata-datasource.datasource.grafana.app",
|
||||
Version: "v0alpha1",
|
||||
Resource: "datasources",
|
||||
}).Namespace("default")
|
||||
|
||||
t.Run("create", func(t *testing.T) {
|
||||
out, err := client.Create(ctx, &unstructured.Unstructured{
|
||||
Object: map[string]any{
|
||||
"apiVersion": "grafana-testdata-datasource.datasource.grafana.app/v0alpha1",
|
||||
"kind": "DataSource",
|
||||
"metadata": map[string]any{
|
||||
"name": "test",
|
||||
},
|
||||
UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{
|
||||
"datasources.grafana-testdata-datasource.datasource.grafana.app": {
|
||||
DualWriterMode: mode,
|
||||
"spec": map[string]any{
|
||||
"title": "test",
|
||||
},
|
||||
"secure": map[string]any{
|
||||
"aaa": map[string]any{
|
||||
"create": "AAA",
|
||||
},
|
||||
"bbb": map[string]any{
|
||||
"create": "BBB",
|
||||
},
|
||||
},
|
||||
})
|
||||
},
|
||||
}, metav1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "test", out.GetName())
|
||||
require.Equal(t, expectedAPIVersion, out.GetAPIVersion())
|
||||
|
||||
client := helper.Org1.Admin.ResourceClient(t, schema.GroupVersionResource{
|
||||
Group: "grafana-testdata-datasource.datasource.grafana.app",
|
||||
Version: "v0alpha1",
|
||||
Resource: "datasources",
|
||||
}).Namespace("default")
|
||||
obj, err := utils.MetaAccessor(out)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("create", func(t *testing.T) {
|
||||
out, err := client.Create(ctx, &unstructured.Unstructured{
|
||||
Object: map[string]any{
|
||||
"apiVersion": "grafana-testdata-datasource.datasource.grafana.app/v0alpha1",
|
||||
"kind": "DataSource",
|
||||
"metadata": map[string]any{
|
||||
"name": "test",
|
||||
},
|
||||
"spec": map[string]any{
|
||||
"title": "test",
|
||||
},
|
||||
"secure": map[string]any{
|
||||
"aaa": map[string]any{
|
||||
"create": "AAA",
|
||||
},
|
||||
"bbb": map[string]any{
|
||||
"create": "BBB",
|
||||
},
|
||||
},
|
||||
secure, err := obj.GetSecureValues()
|
||||
require.NoError(t, err)
|
||||
|
||||
keys := slices.Collect(maps.Keys(secure))
|
||||
require.ElementsMatch(t, []string{"aaa", "bbb"}, keys)
|
||||
})
|
||||
|
||||
t.Run("update", func(t *testing.T) {
|
||||
out, err := client.Update(ctx, &unstructured.Unstructured{
|
||||
Object: map[string]any{
|
||||
"apiVersion": "grafana-testdata-datasource.datasource.grafana.app/v0alpha1",
|
||||
"metadata": map[string]any{
|
||||
"name": "test",
|
||||
},
|
||||
"spec": map[string]any{
|
||||
"title": "test",
|
||||
"database": "testdb",
|
||||
"url": "http://fake.url",
|
||||
"access": datasources.DS_ACCESS_PROXY,
|
||||
"user": "example",
|
||||
"isDefault": true,
|
||||
"readOnly": true,
|
||||
"jsonData": map[string]any{
|
||||
"hello": "world",
|
||||
},
|
||||
}, metav1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "test", out.GetName())
|
||||
require.Equal(t, expectedAPIVersion, out.GetAPIVersion())
|
||||
|
||||
obj, err := utils.MetaAccessor(out)
|
||||
require.NoError(t, err)
|
||||
|
||||
secure, err := obj.GetSecureValues()
|
||||
require.NoError(t, err)
|
||||
|
||||
keys := slices.Collect(maps.Keys(secure))
|
||||
require.ElementsMatch(t, []string{"aaa", "bbb"}, keys)
|
||||
})
|
||||
|
||||
t.Run("update", func(t *testing.T) {
|
||||
out, err := client.Update(ctx, &unstructured.Unstructured{
|
||||
Object: map[string]any{
|
||||
"apiVersion": "grafana-testdata-datasource.datasource.grafana.app/v0alpha1",
|
||||
"metadata": map[string]any{
|
||||
"name": "test",
|
||||
},
|
||||
"spec": map[string]any{
|
||||
"title": "test",
|
||||
"database": "testdb",
|
||||
"url": "http://fake.url",
|
||||
"access": datasources.DS_ACCESS_PROXY,
|
||||
"user": "example",
|
||||
"isDefault": true,
|
||||
"readOnly": true,
|
||||
"jsonData": map[string]any{
|
||||
"hello": "world",
|
||||
},
|
||||
},
|
||||
"secure": map[string]any{
|
||||
// "aaa": map[string]any{
|
||||
// "remove": true, // remove does not really remove in legacy!
|
||||
// },
|
||||
"ccc": map[string]any{
|
||||
"create": "CCC", // add a third value
|
||||
},
|
||||
},
|
||||
},
|
||||
"secure": map[string]any{
|
||||
// "aaa": map[string]any{
|
||||
// "remove": true, // remove does not really remove in legacy!
|
||||
// },
|
||||
"ccc": map[string]any{
|
||||
"create": "CCC", // add a third value
|
||||
},
|
||||
}, metav1.UpdateOptions{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "test", out.GetName())
|
||||
require.Equal(t, expectedAPIVersion, out.GetAPIVersion())
|
||||
},
|
||||
},
|
||||
}, metav1.UpdateOptions{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "test", out.GetName())
|
||||
require.Equal(t, expectedAPIVersion, out.GetAPIVersion())
|
||||
|
||||
obj, err := utils.MetaAccessor(out)
|
||||
require.NoError(t, err)
|
||||
obj, err := utils.MetaAccessor(out)
|
||||
require.NoError(t, err)
|
||||
|
||||
secure, err := obj.GetSecureValues()
|
||||
require.NoError(t, err)
|
||||
secure, err := obj.GetSecureValues()
|
||||
require.NoError(t, err)
|
||||
|
||||
keys := slices.Collect(maps.Keys(secure))
|
||||
require.ElementsMatch(t, []string{"aaa", "bbb", "ccc"}, keys)
|
||||
})
|
||||
keys := slices.Collect(maps.Keys(secure))
|
||||
require.ElementsMatch(t, []string{"aaa", "bbb", "ccc"}, keys)
|
||||
})
|
||||
|
||||
t.Run("list", func(t *testing.T) {
|
||||
list, err := client.List(ctx, metav1.ListOptions{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedAPIVersion, list.GetAPIVersion())
|
||||
require.Len(t, list.Items, 1, "expected a single datasource")
|
||||
require.Equal(t, "test", list.Items[0].GetName(), "with the test uid")
|
||||
t.Run("list", func(t *testing.T) {
|
||||
list, err := client.List(ctx, metav1.ListOptions{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedAPIVersion, list.GetAPIVersion())
|
||||
require.Len(t, list.Items, 1, "expected a single datasource")
|
||||
require.Equal(t, "test", list.Items[0].GetName(), "with the test uid")
|
||||
|
||||
spec, _, _ := unstructured.NestedMap(list.Items[0].Object, "spec")
|
||||
jj, _ := json.MarshalIndent(spec, "", " ")
|
||||
// fmt.Printf("%s\n", string(jj))
|
||||
require.JSONEq(t, `{
|
||||
spec, _, _ := unstructured.NestedMap(list.Items[0].Object, "spec")
|
||||
jj, _ := json.MarshalIndent(spec, "", " ")
|
||||
// fmt.Printf("%s\n", string(jj))
|
||||
require.JSONEq(t, `{
|
||||
"access": "proxy",
|
||||
"database": "testdb",
|
||||
"isDefault": true,
|
||||
|
|
@ -161,63 +156,109 @@ func TestIntegrationTestDatasource(t *testing.T) {
|
|||
"url": "http://fake.url",
|
||||
"user": "example"
|
||||
}`, string(jj))
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("execute", func(t *testing.T) {
|
||||
client := helper.Org1.Admin.ResourceClient(t, schema.GroupVersionResource{
|
||||
Group: "grafana-testdata-datasource.datasource.grafana.app",
|
||||
Version: "v0alpha1",
|
||||
Resource: "datasources",
|
||||
}).Namespace("default")
|
||||
ctx := context.Background()
|
||||
t.Run("execute", func(t *testing.T) {
|
||||
client := helper.Org1.Admin.ResourceClient(t, schema.GroupVersionResource{
|
||||
Group: "grafana-testdata-datasource.datasource.grafana.app",
|
||||
Version: "v0alpha1",
|
||||
Resource: "datasources",
|
||||
}).Namespace("default")
|
||||
ctx := context.Background()
|
||||
|
||||
list, err := client.List(ctx, metav1.ListOptions{})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, list.Items, 1, "expected a single connection")
|
||||
require.Equal(t, "test", list.Items[0].GetName(), "with the test uid")
|
||||
list, err := client.List(ctx, metav1.ListOptions{})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, list.Items, 1, "expected a single connection")
|
||||
require.Equal(t, "test", list.Items[0].GetName(), "with the test uid")
|
||||
|
||||
_, err = client.Get(ctx, "test", metav1.GetOptions{}, "health")
|
||||
// endpoint is disabled currently because it has not been
|
||||
// sufficiently tested.
|
||||
// for more info see pkg/registry/apis/datasource/sub_health.go
|
||||
require.Error(t, err)
|
||||
var statusErr *apierrors.StatusError
|
||||
require.True(t, errors.As(err, &statusErr))
|
||||
require.Equal(t, int32(501), statusErr.ErrStatus.Code)
|
||||
// require.NoError(t, err)
|
||||
// body, err := rsp.MarshalJSON()
|
||||
// require.NoError(t, err)
|
||||
// //fmt.Printf("GOT: %v\n", string(body))
|
||||
// require.JSONEq(t, `{
|
||||
// "apiVersion": "grafana-testdata-datasource.datasource.grafana.app/v0alpha1",
|
||||
// "code": 1,
|
||||
// "kind": "HealthCheckResult",
|
||||
// "message": "Data source is working",
|
||||
// "status": "OK"
|
||||
// }
|
||||
// `, string(body))
|
||||
_, err = client.Get(ctx, "test", metav1.GetOptions{}, "health")
|
||||
// endpoint is disabled currently because it has not been
|
||||
// sufficiently tested.
|
||||
// for more info see pkg/registry/apis/datasource/sub_health.go
|
||||
require.Error(t, err)
|
||||
var statusErr *apierrors.StatusError
|
||||
require.True(t, errors.As(err, &statusErr))
|
||||
require.Equal(t, int32(501), statusErr.ErrStatus.Code)
|
||||
// require.NoError(t, err)
|
||||
// body, err := rsp.MarshalJSON()
|
||||
// require.NoError(t, err)
|
||||
// //fmt.Printf("GOT: %v\n", string(body))
|
||||
// require.JSONEq(t, `{
|
||||
// "apiVersion": "grafana-testdata-datasource.datasource.grafana.app/v0alpha1",
|
||||
// "code": 1,
|
||||
// "kind": "HealthCheckResult",
|
||||
// "message": "Data source is working",
|
||||
// "status": "OK"
|
||||
// }
|
||||
// `, string(body))
|
||||
|
||||
// Test connecting to non-JSON marshaled data
|
||||
raw := apis.DoRequest[any](helper, apis.RequestParams{
|
||||
User: helper.Org1.Admin,
|
||||
Method: "GET",
|
||||
Path: "/apis/grafana-testdata-datasource.datasource.grafana.app/v0alpha1/namespaces/default/datasources/test/resource",
|
||||
}, nil)
|
||||
// endpoint is disabled currently because it has not been
|
||||
// sufficiently tested.
|
||||
// for more info see pkg/registry/apis/datasource/sub_resource.go
|
||||
require.Equal(t, int32(501), raw.Status.Code)
|
||||
// require.Equal(t, `Hello world from test datasource!`, string(raw.Body))
|
||||
})
|
||||
// Test connecting to non-JSON marshaled data
|
||||
raw := apis.DoRequest[any](helper, apis.RequestParams{
|
||||
User: helper.Org1.Admin,
|
||||
Method: "GET",
|
||||
Path: "/apis/grafana-testdata-datasource.datasource.grafana.app/v0alpha1/namespaces/default/datasources/test/resource",
|
||||
}, nil)
|
||||
// endpoint is disabled currently because it has not been
|
||||
// sufficiently tested.
|
||||
// for more info see pkg/registry/apis/datasource/sub_resource.go
|
||||
require.Equal(t, int32(501), raw.Status.Code)
|
||||
// require.Equal(t, `Hello world from test datasource!`, string(raw.Body))
|
||||
})
|
||||
|
||||
t.Run("delete", func(t *testing.T) {
|
||||
err := client.Delete(ctx, "test", metav1.DeleteOptions{})
|
||||
require.NoError(t, err)
|
||||
t.Run("delete", func(t *testing.T) {
|
||||
err := client.Delete(ctx, "test", metav1.DeleteOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
list, err := client.List(ctx, metav1.ListOptions{})
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, list.Items)
|
||||
})
|
||||
})
|
||||
}
|
||||
list, err := client.List(ctx, metav1.ListOptions{})
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, list.Items)
|
||||
})
|
||||
}
|
||||
|
||||
func TestIntegrationTestDatasourceAccess(t *testing.T) {
|
||||
testutil.SkipIntegrationTestInShortMode(t)
|
||||
|
||||
t.Run("get DatasourceAccessInfo", func(t *testing.T) {
|
||||
helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
|
||||
DisableAnonymous: true,
|
||||
EnableFeatureToggles: []string{
|
||||
featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs, // Required to start the datasource api servers
|
||||
featuremgmt.FlagQueryServiceWithConnections, // enables CRUD endpoints
|
||||
},
|
||||
UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{
|
||||
"datasources.grafana-testdata-datasource.datasource.grafana.app": {
|
||||
DualWriterMode: grafanarest.Mode0,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
var datasourceAccessInfo datasourceV0alpha1.DatasourceAccessInfo
|
||||
raw := apis.DoRequest(helper, apis.RequestParams{
|
||||
User: helper.Org1.Admin,
|
||||
Method: "GET",
|
||||
Path: "/apis/grafana-testdata-datasource.datasource.grafana.app/v0alpha1/namespaces/default/datasources/test/access",
|
||||
}, &datasourceAccessInfo)
|
||||
require.Equal(t, http.StatusOK, raw.Response.StatusCode)
|
||||
|
||||
expectedDatasourceAccessInfo := datasourceV0alpha1.DatasourceAccessInfo{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "DatasourceAccessInfo",
|
||||
APIVersion: "grafana-testdata-datasource.datasource.grafana.app/v0alpha1",
|
||||
},
|
||||
Permissions: accesscontrol.Metadata{
|
||||
"alert.instances.external:read": true,
|
||||
"alert.instances.external:write": true,
|
||||
"alert.notifications.external:read": true,
|
||||
"alert.notifications.external:write": true,
|
||||
"alert.rules.external:read": true,
|
||||
"alert.rules.external:write": true,
|
||||
"datasources.id:read": true,
|
||||
"datasources:delete": true,
|
||||
"datasources:query": true,
|
||||
"datasources:read": true,
|
||||
"datasources:write": true,
|
||||
},
|
||||
}
|
||||
require.Equal(t, datasourceAccessInfo, expectedDatasourceAccessInfo)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -849,6 +849,55 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"/apis/grafana-testdata-datasource.datasource.grafana.app/v0alpha1/namespaces/{namespace}/datasources/{name}/access": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"DataSource"
|
||||
],
|
||||
"description": "connect GET requests to access of DataSource",
|
||||
"operationId": "getDataSourceAccess",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"*/*": {
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"x-kubernetes-action": "connect",
|
||||
"x-kubernetes-group-version-kind": {
|
||||
"group": "grafana-testdata-datasource.datasource.grafana.app",
|
||||
"kind": "DatasourceAccessInfo",
|
||||
"version": "v0alpha1"
|
||||
}
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"name": "name",
|
||||
"in": "path",
|
||||
"description": "name of the DatasourceAccessInfo",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"uniqueItems": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "namespace",
|
||||
"in": "path",
|
||||
"description": "object name and auth scope, such as for teams and projects",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"uniqueItems": true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"/apis/grafana-testdata-datasource.datasource.grafana.app/v0alpha1/namespaces/{namespace}/datasources/{name}/health": {
|
||||
"get": {
|
||||
"tags": [
|
||||
|
|
@ -1876,4 +1925,4 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import { lastValueFrom } from 'rxjs';
|
||||
|
||||
import { DataSourceSettings } from '@grafana/data';
|
||||
import { DataSourceSettings, DataSourceJsonData } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { getBackendSrv } from 'app/core/services/backend_srv';
|
||||
import { accessControlQueryParam } from 'app/core/utils/accessControl';
|
||||
|
||||
|
|
@ -8,7 +9,115 @@ export const getDataSources = async (): Promise<DataSourceSettings[]> => {
|
|||
return await getBackendSrv().get('/api/datasources');
|
||||
};
|
||||
|
||||
export interface K8sMetadata {
|
||||
name: string;
|
||||
namespace: string;
|
||||
uid: string;
|
||||
resourceVersion: string;
|
||||
generation: number;
|
||||
creationTimestamp: string;
|
||||
labels: Map<string, string>;
|
||||
annotations: Map<string, string>;
|
||||
}
|
||||
|
||||
export interface DatasourceInstanceK8sSpec {
|
||||
access: string;
|
||||
jsonData: DataSourceJsonData;
|
||||
title: string;
|
||||
url: string;
|
||||
basicAuth: boolean;
|
||||
basicAuthUser: string;
|
||||
}
|
||||
|
||||
export interface DatasourceAccessK8s {
|
||||
kind: string;
|
||||
apiVersion: string;
|
||||
Permissions: Record<string, boolean>;
|
||||
}
|
||||
|
||||
export interface DataSourceSettingsK8s {
|
||||
kind: string;
|
||||
apiVersion: string;
|
||||
metadata: K8sMetadata;
|
||||
spec: DatasourceInstanceK8sSpec;
|
||||
}
|
||||
|
||||
export const getDataSourceK8sGroup = (uid: string): string => {
|
||||
for (const [key, ds] of Object.entries(config.datasources)) {
|
||||
if (key.startsWith('--')) {
|
||||
continue;
|
||||
}
|
||||
if (config.datasources[key].uid === uid) {
|
||||
return ds.type + '.datasource.grafana.app';
|
||||
}
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
export const getDataSourceFromK8sAPI = async (k8sName: string, stackId: string) => {
|
||||
// TODO: read this from backend.
|
||||
let k8sVersion = 'v0alpha1';
|
||||
let k8sGroup = getDataSourceK8sGroup(k8sName);
|
||||
if (k8sGroup === '') {
|
||||
throw Error(`Could not find data source group with uid: "${k8sName}"`);
|
||||
}
|
||||
|
||||
const response = await lastValueFrom(
|
||||
getBackendSrv().fetch<DataSourceSettingsK8s>({
|
||||
method: 'GET',
|
||||
url: `/apis/${k8sGroup}/${k8sVersion}/namespaces/${stackId}/datasources/${k8sName}`,
|
||||
showErrorAlert: false,
|
||||
})
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw Error(`Could not find data source by group-version-name: "${k8sGroup}" "${k8sVersion}" "${k8sName}"`);
|
||||
}
|
||||
|
||||
let dsK8sSettings = response.data;
|
||||
let labels = new Map(Object.entries(dsK8sSettings.metadata.labels));
|
||||
let id = parseInt(labels.get('grafana.app/deprecatedInternalID') || '', 10);
|
||||
let dsSettings: DataSourceSettings = {
|
||||
id: id,
|
||||
uid: dsK8sSettings.metadata.name,
|
||||
orgId: 1,
|
||||
name: dsK8sSettings.spec.title,
|
||||
typeLogoUrl: '',
|
||||
type: dsK8sSettings.apiVersion.replace(/\.datasource\.grafana\.app\/[a-z0-9]+$/, ''),
|
||||
typeName: '',
|
||||
access: dsK8sSettings.spec.access,
|
||||
url: dsK8sSettings.spec.url,
|
||||
user: '',
|
||||
database: '',
|
||||
basicAuth: dsK8sSettings.spec.basicAuth,
|
||||
basicAuthUser: dsK8sSettings.spec.basicAuthUser,
|
||||
isDefault: false,
|
||||
jsonData: dsK8sSettings.spec.jsonData,
|
||||
secureJsonFields: {},
|
||||
readOnly: false,
|
||||
withCredentials: false,
|
||||
};
|
||||
|
||||
const accessResponse = await lastValueFrom(
|
||||
getBackendSrv().fetch<DatasourceAccessK8s>({
|
||||
method: 'GET',
|
||||
url: `/apis/${k8sGroup}/${k8sVersion}/namespaces/${stackId}/datasources/${k8sName}/access`,
|
||||
showErrorAlert: false,
|
||||
})
|
||||
);
|
||||
if (!accessResponse.ok) {
|
||||
throw Error(
|
||||
`Could not find data source access information by group-version-name: "${k8sGroup}" "${k8sVersion}" "${k8sName}"`
|
||||
);
|
||||
}
|
||||
dsSettings.accessControl = accessResponse.data.Permissions;
|
||||
return dsSettings;
|
||||
};
|
||||
|
||||
export const getDataSourceByUid = async (uid: string) => {
|
||||
if (config.featureToggles.queryServiceWithConnections) {
|
||||
return getDataSourceFromK8sAPI(uid, config.namespace);
|
||||
}
|
||||
|
||||
const response = await lastValueFrom(
|
||||
getBackendSrv().fetch<DataSourceSettings>({
|
||||
method: 'GET',
|
||||
|
|
|
|||
Loading…
Reference in a new issue