provide an interface for the datasourceConnection
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -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(
|
||||
|
||||
Generated
+6
-3
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
|
||||
}
|
||||
Reference in New Issue
Block a user