provide an interface for the datasourceConnection

This commit is contained in:
Dafydd
2025-12-04 16:34:22 +00:00
parent 90c4ab9d96
commit 5499ad8023
8 changed files with 265 additions and 116 deletions
+2
View File
@@ -181,6 +181,8 @@ import (
//go:generate mockery --name InterfaceName --structname MockImplementationName --inpackage --filename my_implementation_mock.go
```
The current `go:generate` command format used in this repository is only compatible with mockery v2.
## Globals
As a general rule of thumb, avoid using global variables, since they make the code difficult to maintain and reason
@@ -3,23 +3,21 @@ package collections
import (
"context"
"fmt"
"net/http"
collections "github.com/grafana/grafana/apps/collections/pkg/apis/collections/v1alpha1"
"github.com/grafana/grafana/pkg/services/apiserver"
"github.com/grafana/grafana/pkg/services/apiserver/builder"
"github.com/grafana/grafana/pkg/services/datasources/service/client"
"k8s.io/apiserver/pkg/admission"
"k8s.io/client-go/kubernetes"
)
var _ builder.APIGroupValidation = (*DatasourceStacksValidator)(nil)
type DatasourceStacksValidator struct {
restConfigProvider apiserver.RestConfigProvider
dsClient client.DataSourceConnectionClient
}
func GetDatasourceStacksValidator(restConfigProvider apiserver.RestConfigProvider) builder.APIGroupValidation {
return &DatasourceStacksValidator{restConfigProvider: restConfigProvider}
func GetDatasourceStacksValidator(dsClient client.DataSourceConnectionClient) builder.APIGroupValidation {
return &DatasourceStacksValidator{dsClient: dsClient}
}
func (v *DatasourceStacksValidator) Validate(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces) (err error) {
@@ -66,13 +64,9 @@ func (v *DatasourceStacksValidator) Validate(ctx context.Context, a admission.At
}
exists, err := v.checkDatasourceExists(ctx, template[key].Group, item.DataSourceRef)
if err != nil {
return fmt.Errorf("error fetching: datasource '%s' does not exist (%s %s): %w", item.DataSourceRef, a.GetName(), a.GetKind().GroupVersion().String(), err)
if err != nil || !exists {
return fmt.Errorf("datasource '%s' in group '%s' does not exist (%s %s): %w", item.DataSourceRef, template[key].Group, a.GetName(), a.GetKind().GroupVersion().String(), err)
}
if !exists {
return fmt.Errorf("datasource '%s' does not exist (%s %s)", item.DataSourceRef, a.GetName(), a.GetKind().GroupVersion().String())
}
}
}
@@ -80,33 +74,15 @@ func (v *DatasourceStacksValidator) Validate(ctx context.Context, a admission.At
}
func (v *DatasourceStacksValidator) checkDatasourceExists(ctx context.Context, group, name string) (bool, error) {
cfg, err := v.restConfigProvider.GetRestConfig(ctx)
dsConn, err := v.dsClient.Get(ctx, group, "", name)
if err != nil {
return false, err
}
client, err := kubernetes.NewForConfig(cfg)
if err != nil {
return false, err
}
result := client.RESTClient().Get().
Prefix("apis", group, "v0alpha1").
Namespace("default").
Resource("datasources").
Name(name).
Do(ctx)
if err = result.Error(); err != nil {
return false, err
}
var statusCode int
result = result.StatusCode(&statusCode)
if statusCode == http.StatusNotFound {
if dsConn == nil {
return false, nil
}
return true, nil
}
@@ -5,23 +5,29 @@ import (
"testing"
collectionsv1alpha1 "github.com/grafana/grafana/apps/collections/pkg/apis/collections/v1alpha1"
queryv0alpha1 "github.com/grafana/grafana/pkg/apis/query/v0alpha1"
"github.com/grafana/grafana/pkg/registry/apis/collections"
datasourcesclient "github.com/grafana/grafana/pkg/services/datasources/service/client"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/admission"
)
func TestDataSourceValidator_Validate(t *testing.T) {
validator := &collections.DatasourceStacksValidator{}
ctx := context.Background()
tests := []struct {
name string
operation admission.Operation
object runtime.Object
expectError bool
errorMsg string
name string
operation admission.Operation
object runtime.Object
needMockDSClient bool // only set to true if you expect to make a call to the datasource client
dsClientReturnValue *queryv0alpha1.DataSourceConnection
dsClientReturnError error
expectError bool
errorMsg string
}{
{
name: "should return no error for invalid kind",
@@ -94,6 +100,61 @@ func TestDataSourceValidator_Validate(t *testing.T) {
expectError: true,
errorMsg: "key 'notintemplate' is not in the DataSourceStack template (test-datasourcestack collections.grafana.app/v1alpha1)",
},
{
name: "error if data source does not exist",
operation: admission.Create,
object: &collectionsv1alpha1.DataSourceStack{
Spec: collectionsv1alpha1.DataSourceStackSpec{
Template: collectionsv1alpha1.DataSourceStackTemplateSpec{
"key1": collectionsv1alpha1.DataSourceStackDataSourceStackTemplateItem{
Name: "foo",
Group: "foo.grafana",
},
},
Modes: []collectionsv1alpha1.DataSourceStackModeSpec{
{
Name: "prod",
Definition: collectionsv1alpha1.DataSourceStackMode{
"key1": collectionsv1alpha1.DataSourceStackModeItem{
DataSourceRef: "ref",
},
},
},
},
},
},
needMockDSClient: true,
dsClientReturnValue: nil, // no result - this is the default anyway
expectError: true,
errorMsg: "datasource 'ref' in group 'foo.grafana' does not exist (test-datasourcestack collections.grafana.app/v1alpha1)",
},
{
name: "valid request",
operation: admission.Create,
object: &collectionsv1alpha1.DataSourceStack{
Spec: collectionsv1alpha1.DataSourceStackSpec{
Template: collectionsv1alpha1.DataSourceStackTemplateSpec{
"key1": collectionsv1alpha1.DataSourceStackDataSourceStackTemplateItem{
Name: "foo",
Group: "foo.grafana",
},
},
Modes: []collectionsv1alpha1.DataSourceStackModeSpec{
{
Name: "prod",
Definition: collectionsv1alpha1.DataSourceStackMode{
"key1": collectionsv1alpha1.DataSourceStackModeItem{
DataSourceRef: "ref",
},
},
},
},
},
},
needMockDSClient: true,
dsClientReturnValue: &queryv0alpha1.DataSourceConnection{}, // returning any non-nil value will pass validation
expectError: false,
},
}
for _, tt := range tests {
@@ -105,6 +166,13 @@ func TestDataSourceValidator_Validate(t *testing.T) {
Kind: schema.GroupVersionKind{Group: "collections.grafana.app", Version: "v1alpha1", Kind: "DataSourceStack"},
}
var client *datasourcesclient.MockDataSourceConnectionClient
if tt.needMockDSClient {
client = datasourcesclient.NewMockDataSourceConnectionClient(t)
client.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(tt.dsClientReturnValue, tt.dsClientReturnError)
}
validator := collections.GetDatasourceStacksValidator(client)
err := validator.Validate(ctx, attrs, nil)
if tt.expectError {
+5 -1
View File
@@ -25,6 +25,7 @@ import (
"github.com/grafana/grafana/pkg/services/apiserver"
"github.com/grafana/grafana/pkg/services/apiserver/builder"
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
datasourcesClient "github.com/grafana/grafana/pkg/services/datasources/service/client"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/star"
"github.com/grafana/grafana/pkg/services/user"
@@ -51,6 +52,7 @@ func RegisterAPIService(
stars star.Service,
users user.Service,
apiregistration builder.APIRegistrar,
dsConnClientFactory datasourcesClient.DataSourceConnectionClientFactory,
restConfigProvider apiserver.RestConfigProvider,
) *APIBuilder {
// Requires development settings and clearly experimental
@@ -59,9 +61,11 @@ func RegisterAPIService(
return nil
}
dsConnClient := dsConnClientFactory(restConfigProvider)
sql := legacy.NewLegacySQL(legacysql.NewDatabaseProvider(db))
builder := &APIBuilder{
datasourceStacksValidator: GetDatasourceStacksValidator(restConfigProvider),
datasourceStacksValidator: GetDatasourceStacksValidator(dsConnClient),
authorizer: &utils.AuthorizeFromName{
Resource: map[string][]utils.ResourceOwner{
"stars": {utils.UserResourceOwner},
+2
View File
@@ -88,6 +88,7 @@ import (
"github.com/grafana/grafana/pkg/services/datasourceproxy"
"github.com/grafana/grafana/pkg/services/datasources"
datasourceservice "github.com/grafana/grafana/pkg/services/datasources/service"
datasourcesclient "github.com/grafana/grafana/pkg/services/datasources/service/client"
"github.com/grafana/grafana/pkg/services/dsquerierclient"
"github.com/grafana/grafana/pkg/services/encryption"
encryptionservice "github.com/grafana/grafana/pkg/services/encryption/service"
@@ -476,6 +477,7 @@ var wireBasicSet = wire.NewSet(
appregistry.WireSet,
// Dashboard Kubernetes helpers
dashboardclient.ProvideK8sClientWithFallback,
datasourcesclient.ProvideDataSourceConnectionClientFactory,
)
var wireSet = wire.NewSet(
+6 -3
View File
File diff suppressed because one or more lines are too long
@@ -1,90 +1,86 @@
package client
// import (
// "context"
// "encoding/json"
import (
"context"
"errors"
"net/http"
// "github.com/grafana/grafana/pkg/services/apiserver"
// "github.com/grafana/grafana/pkg/services/apiserver/client"
// v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
// "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
// "k8s.io/apimachinery/pkg/runtime/schema"
// "k8s.io/client-go/kubernetes"
// )
datasourcev0alpha1 "github.com/grafana/grafana/pkg/apis/datasource/v0alpha1"
queryv0alpha1 "github.com/grafana/grafana/pkg/apis/query/v0alpha1"
"github.com/grafana/grafana/pkg/services/apiserver"
"k8s.io/client-go/kubernetes"
)
// // K8sClientFactory creates a K8sClient for the given group
// type K8sClientFactory func(ctx context.Context, group string, version string) client.K8sHandler
// DataSourceConnectionClient can get information about data source connections.
//
//go:generate mockery --name DataSourceConnectionClient --structname MockDataSourceConnectionClient --inpackage --filename=client_mock.go --with-expecter
type DataSourceConnectionClient interface {
Get(ctx context.Context, group, version, name string) (*queryv0alpha1.DataSourceConnection, error)
}
// type K8sHandler struct {
// client.K8sHandler
// restConfigProvider apiserver.RestConfigProvider
// gvr schema.GroupVersionResource
// }
func ProvideDataSourceConnectionClientFactory(
restConfigProvider apiserver.RestConfigProvider,
) DataSourceConnectionClientFactory {
return func(configProvider apiserver.RestConfigProvider) DataSourceConnectionClient {
return &dataSourceConnectionClient{
configProvider: configProvider,
}
}
}
// type K8sClient struct {
// client.K8sHandler
// newClientFunc K8sClientFactory
// }
type DataSourceConnectionClientFactory func(configProvider apiserver.RestConfigProvider) DataSourceConnectionClient
// func ProvideK8sClient(
// restConfigProvider apiserver.RestConfigProvider,
// ) K8sHandler {
// return NewK8sClient(restConfigProvider)
// }
type dataSourceConnectionClient struct {
configProvider apiserver.RestConfigProvider
}
// func NewK8sClient(restConfigProvider apiserver.RestConfigProvider) *K8sClient {
// newClientFunc := newK8sClientFactory(restConfigProvider)
// return &K8sClient{
// K8sHandler: newClientFunc(context.Background(),),
// }
// }
func (dc *dataSourceConnectionClient) Get(ctx context.Context, group, version, name string) (*queryv0alpha1.DataSourceConnection, error) {
cfg, err := dc.configProvider.GetRestConfig(ctx)
if err != nil {
return nil, err
}
// func (c K8sHandler) Get(ctx context.Context, name string, orgID int64, options v1.GetOptions, subresource ...string) (*unstructured.Unstructured, error) {
// cfg, err := c.restConfigProvider.GetRestConfig(ctx)
// if err != nil {
// return nil, err
// }
// client, err := kubernetes.NewForConfig(cfg)
// if err != nil {
// return nil, err
// }
client, err := kubernetes.NewForConfig(cfg)
if err != nil {
return nil, err
}
// result := client.RESTClient().Get().
// Prefix("apis", c.gvr.Group, c.gvr.Version).
// Namespace(string(orgID)).
// Resource(c.gvr.Resource).
// Name(name).
// Do(ctx)
if version == "" {
version = "v0alpha1"
}
// if err = result.Error(); err != nil {
// return nil, err
// }
result := client.RESTClient().Get().
Prefix("apis", group, version).
Namespace("default"). // TODO do something about namespace
Resource("datasources").
Name(name).
Do(ctx)
// body, err := result.Raw()
// if err != nil {
// return nil, err
// }
if err = result.Error(); err != nil {
return nil, err
}
// value := &unstructured.Unstructured{}
// if err = json.Unmarshal(body, value); err != nil {
// return nil, err
// }
var statusCode int
// return value, nil
// }
result = result.StatusCode(&statusCode)
if statusCode == http.StatusNotFound {
return nil, errors.New("not found")
}
// func newK8sClientFactory(restConfigProvider apiserver.RestConfigProvider) K8sClientFactory {
// return func(ctx context.Context, group string, version string) client.K8sHandler {
// gvr := schema.GroupVersionResource{
// Group: group,
// Version: version,
// Resource: "datasources",
// }
fullDS := datasourcev0alpha1.DataSource{}
err = result.Into(&fullDS)
if err != nil {
return nil, err
}
// return K8sHandler{
// restConfigProvider: restConfigProvider,
// gvr: gvr,
// }
dsConnection := &queryv0alpha1.DataSourceConnection{
Title: fullDS.Spec.Title(),
Datasource: queryv0alpha1.DataSourceConnectionRef{
Group: fullDS.GroupVersionKind().Group,
Name: fullDS.ObjectMeta.Name,
Version: fullDS.GroupVersionKind().Version,
},
}
// }
// }
return dsConnection, nil
}
@@ -0,0 +1,98 @@
// Code generated by mockery v2.53.3. DO NOT EDIT.
package client
import (
context "context"
v0alpha1 "github.com/grafana/grafana/pkg/apis/query/v0alpha1"
mock "github.com/stretchr/testify/mock"
)
// MockDataSourceConnectionClient is an autogenerated mock type for the DataSourceConnectionClient type
type MockDataSourceConnectionClient struct {
mock.Mock
}
type MockDataSourceConnectionClient_Expecter struct {
mock *mock.Mock
}
func (_m *MockDataSourceConnectionClient) EXPECT() *MockDataSourceConnectionClient_Expecter {
return &MockDataSourceConnectionClient_Expecter{mock: &_m.Mock}
}
// Get provides a mock function with given fields: ctx, group, version, name
func (_m *MockDataSourceConnectionClient) Get(ctx context.Context, group string, version string, name string) (*v0alpha1.DataSourceConnection, error) {
ret := _m.Called(ctx, group, version, name)
if len(ret) == 0 {
panic("no return value specified for Get")
}
var r0 *v0alpha1.DataSourceConnection
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, string, string, string) (*v0alpha1.DataSourceConnection, error)); ok {
return rf(ctx, group, version, name)
}
if rf, ok := ret.Get(0).(func(context.Context, string, string, string) *v0alpha1.DataSourceConnection); ok {
r0 = rf(ctx, group, version, name)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*v0alpha1.DataSourceConnection)
}
}
if rf, ok := ret.Get(1).(func(context.Context, string, string, string) error); ok {
r1 = rf(ctx, group, version, name)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MockDataSourceConnectionClient_Get_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Get'
type MockDataSourceConnectionClient_Get_Call struct {
*mock.Call
}
// Get is a helper method to define mock.On call
// - ctx context.Context
// - group string
// - version string
// - name string
func (_e *MockDataSourceConnectionClient_Expecter) Get(ctx interface{}, group interface{}, version interface{}, name interface{}) *MockDataSourceConnectionClient_Get_Call {
return &MockDataSourceConnectionClient_Get_Call{Call: _e.mock.On("Get", ctx, group, version, name)}
}
func (_c *MockDataSourceConnectionClient_Get_Call) Run(run func(ctx context.Context, group string, version string, name string)) *MockDataSourceConnectionClient_Get_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(string), args[2].(string), args[3].(string))
})
return _c
}
func (_c *MockDataSourceConnectionClient_Get_Call) Return(_a0 *v0alpha1.DataSourceConnection, _a1 error) *MockDataSourceConnectionClient_Get_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *MockDataSourceConnectionClient_Get_Call) RunAndReturn(run func(context.Context, string, string, string) (*v0alpha1.DataSourceConnection, error)) *MockDataSourceConnectionClient_Get_Call {
_c.Call.Return(run)
return _c
}
// NewMockDataSourceConnectionClient creates a new instance of MockDataSourceConnectionClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewMockDataSourceConnectionClient(t interface {
mock.TestingT
Cleanup(func())
}) *MockDataSourceConnectionClient {
mock := &MockDataSourceConnectionClient{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}