Plugins: Support Admission validation hooks (#87718)

This commit is contained in:
Ryan McKinley 2024-05-24 18:45:16 +03:00 committed by GitHub
parent b1eb4b7dad
commit ffc2702552
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
43 changed files with 1091 additions and 117 deletions

2
go.mod
View File

@ -99,7 +99,7 @@ require (
github.com/grafana/grafana-azure-sdk-go/v2 v2.0.3 // @grafana/partner-datasources
github.com/grafana/grafana-google-sdk-go v0.1.0 // @grafana/partner-datasources
github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79 // @grafana/grafana-backend-group
github.com/grafana/grafana-plugin-sdk-go v0.231.0 // @grafana/plugins-platform-backend
github.com/grafana/grafana-plugin-sdk-go v0.232.0 // @grafana/plugins-platform-backend
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240226124929-648abdbd0ea4 // @grafana/grafana-app-platform-squad
github.com/grafana/grafana/pkg/apiserver v0.0.0-20240226124929-648abdbd0ea4 // @grafana/grafana-app-platform-squad
// This needs to be here for other projects that import grafana/grafana

4
go.sum
View File

@ -2179,8 +2179,8 @@ github.com/grafana/grafana-google-sdk-go v0.1.0/go.mod h1:Vo2TKWfDVmNTELBUM+3lkr
github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79 h1:r+mU5bGMzcXCRVAuOrTn54S80qbfVkvTdUJZfSfTNbs=
github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79/go.mod h1:wc6Hbh3K2TgCUSfBC/BOzabItujtHMESZeFk5ZhdxhQ=
github.com/grafana/grafana-plugin-sdk-go v0.114.0/go.mod h1:D7x3ah+1d4phNXpbnOaxa/osSaZlwh9/ZUnGGzegRbk=
github.com/grafana/grafana-plugin-sdk-go v0.231.0 h1:Qt4PBDR8b4MTUxL48EaZw1fHI1rXUNNhvTU/Nf0Ex2g=
github.com/grafana/grafana-plugin-sdk-go v0.231.0/go.mod h1:8fJk+5J1hMkpqY/7vrXHKgAsqELWNkQvLQ5A5xCVZHk=
github.com/grafana/grafana-plugin-sdk-go v0.232.0 h1:RnaQwhAOxYdp9wwy0Yz5cJUGY5tpIXPxoFWmEKflfww=
github.com/grafana/grafana-plugin-sdk-go v0.232.0/go.mod h1:bNgmNmub1I7Mc8dzIncgNqHC5jTgSZPPHlZ3aG8HKJQ=
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240226124929-648abdbd0ea4 h1:hpyusz8c3yRFoJPlA0o34rWnsLbaOOBZleqRhFBi5Lg=
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240226124929-648abdbd0ea4/go.mod h1:vrRQJuNprTWqwm6JPxHf3BoTJhvO15QMEjQ7Q/YUOnI=
github.com/grafana/grafana/pkg/apiserver v0.0.0-20240226124929-648abdbd0ea4 h1:tIbI5zgos92vwJ8lV3zwHwuxkV03GR3FGLkFW9V5LxY=

View File

@ -661,6 +661,7 @@ github.com/grafana/grafana-plugin-sdk-go v0.227.1-0.20240430073540-ce4d126ae8b8/
github.com/grafana/grafana-plugin-sdk-go v0.228.0/go.mod h1:u4K9vVN6eU86loO68977eTXGypC4brUCnk4sfDzutZU=
github.com/grafana/grafana-plugin-sdk-go v0.229.0/go.mod h1:6V6ikT4ryva8MrAp7Bdz5fTJx3/ztzKvpMJFfpzr4CI=
github.com/grafana/grafana-plugin-sdk-go v0.230.0/go.mod h1:6V6ikT4ryva8MrAp7Bdz5fTJx3/ztzKvpMJFfpzr4CI=
github.com/grafana/grafana-plugin-sdk-go v0.231.1-0.20240523124942-62dae9836284/go.mod h1:bNgmNmub1I7Mc8dzIncgNqHC5jTgSZPPHlZ3aG8HKJQ=
github.com/grafana/grafana/pkg/promlib v0.0.3/go.mod h1:3El4NlsfALz8QQCbEGHGFvJUG+538QLMuALRhZ3pcoo=
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 h1:pdN6V1QBWetyv/0+wjACpqVH+eVULgEjkurDLq3goeM=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.1/go.mod h1:YvJ2f6MplWDhfxiUC3KpyTy76kYUZA4W3pTv/wdKQ9Y=

View File

@ -27,6 +27,7 @@ import (
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/plugins"
pluginfakes "github.com/grafana/grafana/pkg/plugins/manager/fakes"
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
"github.com/grafana/grafana/pkg/services/accesscontrol/actest"
"github.com/grafana/grafana/pkg/services/auth/identity"
@ -827,7 +828,8 @@ func getDatasourceProxiedRequest(t *testing.T, ctx *contextmodel.ReqContext, cfg
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
features := featuremgmt.WithFeatures()
quotaService := quotatest.New(false, nil)
dsService, err := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, features, acimpl.ProvideAccessControl(features), &actest.FakePermissionsService{}, quotaService, &pluginstore.FakePluginStore{})
dsService, err := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, features, acimpl.ProvideAccessControl(features),
&actest.FakePermissionsService{}, quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{})
require.NoError(t, err)
proxy, err := NewDataSourceProxy(ds, routes, ctx, "", cfg, httpclient.NewProvider(), &oauthtoken.Service{}, dsService, tracer, features)
require.NoError(t, err)
@ -947,7 +949,8 @@ func runDatasourceAuthTest(t *testing.T, secretsService secrets.Service, secrets
var routes []*plugins.Route
features := featuremgmt.WithFeatures()
quotaService := quotatest.New(false, nil)
dsService, err := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, features, acimpl.ProvideAccessControl(features), &actest.FakePermissionsService{}, quotaService, &pluginstore.FakePluginStore{})
dsService, err := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, features, acimpl.ProvideAccessControl(features),
&actest.FakePermissionsService{}, quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{})
require.NoError(t, err)
proxy, err := NewDataSourceProxy(test.datasource, routes, ctx, "", &setting.Cfg{}, httpclient.NewProvider(), &oauthtoken.Service{}, dsService, tracer, features)
require.NoError(t, err)
@ -1001,7 +1004,8 @@ func setupDSProxyTest(t *testing.T, ctx *contextmodel.ReqContext, ds *datasource
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
secretsStore := secretskvs.NewSQLSecretsKVStore(dbtest.NewFakeDB(), secretsService, log.NewNopLogger())
features := featuremgmt.WithFeatures()
dsService, err := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, features, acimpl.ProvideAccessControl(features), &actest.FakePermissionsService{}, quotatest.New(false, nil), &pluginstore.FakePluginStore{})
dsService, err := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, features, acimpl.ProvideAccessControl(features),
&actest.FakePermissionsService{}, quotatest.New(false, nil), &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{})
require.NoError(t, err)
tracer := tracing.InitializeTracerForTest()

View File

@ -5,7 +5,7 @@ go 1.21.10
require (
github.com/bwmarrin/snowflake v0.3.0
github.com/gorilla/mux v1.8.1
github.com/grafana/grafana-plugin-sdk-go v0.231.0
github.com/grafana/grafana-plugin-sdk-go v0.232.0
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240409140820-518d3341d58f
github.com/prometheus/client_golang v1.19.0
github.com/stretchr/testify v1.9.0

View File

@ -124,7 +124,7 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grafana/grafana-plugin-sdk-go v0.231.0 h1:Qt4PBDR8b4MTUxL48EaZw1fHI1rXUNNhvTU/Nf0Ex2g=
github.com/grafana/grafana-plugin-sdk-go v0.232.0 h1:RnaQwhAOxYdp9wwy0Yz5cJUGY5tpIXPxoFWmEKflfww=
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240409140820-518d3341d58f h1:+CK3tH3XrAAqx5urmVqpgSxMrL2MlpTOnLVSU4w4IjY=
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240409140820-518d3341d58f/go.mod h1:ZxIaCOlDmFupiL55aLU+Qp7O1dgwkDMBAQBK7wnEVBg=
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI=

View File

@ -25,13 +25,17 @@
"type"
],
"properties": {
"apiVersion": {
"description": "The apiserver version",
"type": "string"
},
"type": {
"description": "The datasource plugin type",
"type": "string",
"pattern": "^__expr__$"
},
"uid": {
"description": "Datasource UID",
"description": "Datasource UID (NOTE: name in k8s)",
"type": "string"
}
},
@ -150,13 +154,17 @@
"type"
],
"properties": {
"apiVersion": {
"description": "The apiserver version",
"type": "string"
},
"type": {
"description": "The datasource plugin type",
"type": "string",
"pattern": "^__expr__$"
},
"uid": {
"description": "Datasource UID",
"description": "Datasource UID (NOTE: name in k8s)",
"type": "string"
}
},
@ -316,13 +324,17 @@
"type"
],
"properties": {
"apiVersion": {
"description": "The apiserver version",
"type": "string"
},
"type": {
"description": "The datasource plugin type",
"type": "string",
"pattern": "^__expr__$"
},
"uid": {
"description": "Datasource UID",
"description": "Datasource UID (NOTE: name in k8s)",
"type": "string"
}
},
@ -556,13 +568,17 @@
"type"
],
"properties": {
"apiVersion": {
"description": "The apiserver version",
"type": "string"
},
"type": {
"description": "The datasource plugin type",
"type": "string",
"pattern": "^__expr__$"
},
"uid": {
"description": "Datasource UID",
"description": "Datasource UID (NOTE: name in k8s)",
"type": "string"
}
},
@ -744,13 +760,17 @@
"type"
],
"properties": {
"apiVersion": {
"description": "The apiserver version",
"type": "string"
},
"type": {
"description": "The datasource plugin type",
"type": "string",
"pattern": "^__expr__$"
},
"uid": {
"description": "Datasource UID",
"description": "Datasource UID (NOTE: name in k8s)",
"type": "string"
}
},
@ -868,13 +888,17 @@
"type"
],
"properties": {
"apiVersion": {
"description": "The apiserver version",
"type": "string"
},
"type": {
"description": "The datasource plugin type",
"type": "string",
"pattern": "^__expr__$"
},
"uid": {
"description": "Datasource UID",
"description": "Datasource UID (NOTE: name in k8s)",
"type": "string"
}
},

View File

@ -35,13 +35,17 @@
"type"
],
"properties": {
"apiVersion": {
"description": "The apiserver version",
"type": "string"
},
"type": {
"description": "The datasource plugin type",
"type": "string",
"pattern": "^__expr__$"
},
"uid": {
"description": "Datasource UID",
"description": "Datasource UID (NOTE: name in k8s)",
"type": "string"
}
},
@ -168,13 +172,17 @@
"type"
],
"properties": {
"apiVersion": {
"description": "The apiserver version",
"type": "string"
},
"type": {
"description": "The datasource plugin type",
"type": "string",
"pattern": "^__expr__$"
},
"uid": {
"description": "Datasource UID",
"description": "Datasource UID (NOTE: name in k8s)",
"type": "string"
}
},
@ -342,13 +350,17 @@
"type"
],
"properties": {
"apiVersion": {
"description": "The apiserver version",
"type": "string"
},
"type": {
"description": "The datasource plugin type",
"type": "string",
"pattern": "^__expr__$"
},
"uid": {
"description": "Datasource UID",
"description": "Datasource UID (NOTE: name in k8s)",
"type": "string"
}
},
@ -590,13 +602,17 @@
"type"
],
"properties": {
"apiVersion": {
"description": "The apiserver version",
"type": "string"
},
"type": {
"description": "The datasource plugin type",
"type": "string",
"pattern": "^__expr__$"
},
"uid": {
"description": "Datasource UID",
"description": "Datasource UID (NOTE: name in k8s)",
"type": "string"
}
},
@ -786,13 +802,17 @@
"type"
],
"properties": {
"apiVersion": {
"description": "The apiserver version",
"type": "string"
},
"type": {
"description": "The datasource plugin type",
"type": "string",
"pattern": "^__expr__$"
},
"uid": {
"description": "Datasource UID",
"description": "Datasource UID (NOTE: name in k8s)",
"type": "string"
}
},
@ -918,13 +938,17 @@
"type"
],
"properties": {
"apiVersion": {
"description": "The apiserver version",
"type": "string"
},
"type": {
"description": "The datasource plugin type",
"type": "string",
"pattern": "^__expr__$"
},
"uid": {
"description": "Datasource UID",
"description": "Datasource UID (NOTE: name in k8s)",
"type": "string"
}
},

View File

@ -18,6 +18,7 @@ type corePlugin struct {
backend.CallResourceHandler
backend.QueryDataHandler
backend.StreamHandler
backend.AdmissionHandler
}
// New returns a new backendplugin.PluginFactoryFunc for creating a core (built-in) backendplugin.Plugin.
@ -29,6 +30,7 @@ func New(opts backend.ServeOpts) backendplugin.PluginFactoryFunc {
CheckHealthHandler: opts.CheckHealthHandler,
CallResourceHandler: opts.CallResourceHandler,
QueryDataHandler: opts.QueryDataHandler,
AdmissionHandler: opts.AdmissionHandler,
StreamHandler: opts.StreamHandler,
}, nil
}
@ -124,3 +126,27 @@ func (cp *corePlugin) RunStream(ctx context.Context, req *backend.RunStreamReque
}
return plugins.ErrMethodNotImplemented
}
func (cp *corePlugin) MutateAdmission(ctx context.Context, req *backend.AdmissionRequest) (*backend.MutationResponse, error) {
if cp.AdmissionHandler != nil {
ctx = backend.WithGrafanaConfig(ctx, req.PluginContext.GrafanaConfig)
return cp.AdmissionHandler.MutateAdmission(ctx, req)
}
return nil, plugins.ErrMethodNotImplemented
}
func (cp *corePlugin) ValidateAdmission(ctx context.Context, req *backend.AdmissionRequest) (*backend.ValidationResponse, error) {
if cp.AdmissionHandler != nil {
ctx = backend.WithGrafanaConfig(ctx, req.PluginContext.GrafanaConfig)
return cp.AdmissionHandler.ValidateAdmission(ctx, req)
}
return nil, plugins.ErrMethodNotImplemented
}
func (cp *corePlugin) ConvertObject(ctx context.Context, req *backend.ConversionRequest) (*backend.ConversionResponse, error) {
if cp.AdmissionHandler != nil {
ctx = backend.WithGrafanaConfig(ctx, req.PluginContext.GrafanaConfig)
return cp.AdmissionHandler.ConvertObject(ctx, req)
}
return nil, plugins.ErrMethodNotImplemented
}

View File

@ -10,6 +10,7 @@ import (
sdktracing "github.com/grafana/grafana-plugin-sdk-go/backend/tracing"
"github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/backendplugin"
@ -145,6 +146,9 @@ func asBackendPlugin(svc any) backendplugin.PluginFactoryFunc {
if healthHandler, ok := svc.(backend.CheckHealthHandler); ok {
opts.CheckHealthHandler = healthHandler
}
if storageHandler, ok := svc.(backend.AdmissionHandler); ok {
opts.AdmissionHandler = storageHandler
}
if opts.QueryDataHandler != nil || opts.CallResourceHandler != nil ||
opts.CheckHealthHandler != nil || opts.StreamHandler != nil {

View File

@ -33,6 +33,7 @@ var pluginSet = map[int]goplugin.PluginSet{
"resource": &grpcplugin.ResourceGRPCPlugin{},
"data": &grpcplugin.DataGRPCPlugin{},
"stream": &grpcplugin.StreamGRPCPlugin{},
"admission": &grpcplugin.AdmissionGRPCPlugin{},
"renderer": &pluginextensionv2.RendererGRPCPlugin{},
"secretsmanager": &secretsmanagerplugin.SecretsManagerGRPCPlugin{},
},

View File

@ -24,6 +24,7 @@ type ClientV2 struct {
grpcplugin.ResourceClient
grpcplugin.DataClient
grpcplugin.StreamClient
grpcplugin.AdmissionClient
pluginextensionv2.RendererPlugin
secretsmanagerplugin.SecretsManagerPlugin
}
@ -44,6 +45,11 @@ func newClientV2(descriptor PluginDescriptor, logger log.Logger, rpcClient plugi
return nil, err
}
rawAdmission, err := rpcClient.Dispense("admission")
if err != nil {
return nil, err
}
rawStream, err := rpcClient.Dispense("stream")
if err != nil {
return nil, err
@ -78,6 +84,12 @@ func newClientV2(descriptor PluginDescriptor, logger log.Logger, rpcClient plugi
}
}
if rawAdmission != nil {
if admissionClient, ok := rawAdmission.(grpcplugin.AdmissionClient); ok {
c.AdmissionClient = admissionClient
}
}
if rawStream != nil {
if streamClient, ok := rawStream.(grpcplugin.StreamClient); ok {
c.StreamClient = streamClient
@ -257,3 +269,60 @@ func (c *ClientV2) RunStream(ctx context.Context, req *backend.RunStreamRequest,
}
}
}
func (c *ClientV2) ValidateAdmission(ctx context.Context, req *backend.AdmissionRequest) (*backend.ValidationResponse, error) {
if c.AdmissionClient == nil {
return nil, plugins.ErrMethodNotImplemented
}
protoReq := backend.ToProto().AdmissionRequest(req)
protoResp, err := c.AdmissionClient.ValidateAdmission(ctx, protoReq)
if err != nil {
if status.Code(err) == codes.Unimplemented {
return nil, plugins.ErrMethodNotImplemented
}
return nil, fmt.Errorf("%v: %w", "Failed to ValidateAdmission", err)
}
return backend.FromProto().ValidationResponse(protoResp), nil
}
func (c *ClientV2) MutateAdmission(ctx context.Context, req *backend.AdmissionRequest) (*backend.MutationResponse, error) {
if c.AdmissionClient == nil {
return nil, plugins.ErrMethodNotImplemented
}
protoReq := backend.ToProto().AdmissionRequest(req)
protoResp, err := c.AdmissionClient.MutateAdmission(ctx, protoReq)
if err != nil {
if status.Code(err) == codes.Unimplemented {
return nil, plugins.ErrMethodNotImplemented
}
return nil, fmt.Errorf("%v: %w", "Failed to MutateAdmission", err)
}
return backend.FromProto().MutationResponse(protoResp), nil
}
func (c *ClientV2) ConvertObject(ctx context.Context, req *backend.ConversionRequest) (*backend.ConversionResponse, error) {
if c.AdmissionClient == nil {
return nil, plugins.ErrMethodNotImplemented
}
protoReq := backend.ToProto().ConversionRequest(req)
protoResp, err := c.AdmissionClient.ConvertObject(ctx, protoReq)
if err != nil {
if status.Code(err) == codes.Unimplemented {
return nil, plugins.ErrMethodNotImplemented
}
return nil, fmt.Errorf("%v: %w", "Failed to ConvertObject", err)
}
return backend.FromProto().ConversionResponse(protoResp), nil
}

View File

@ -19,6 +19,7 @@ type pluginClient interface {
backend.CheckHealthHandler
backend.QueryDataHandler
backend.CallResourceHandler
backend.AdmissionHandler
backend.StreamHandler
}
@ -199,3 +200,27 @@ func (p *grpcPlugin) RunStream(ctx context.Context, req *backend.RunStreamReques
}
return pluginClient.RunStream(ctx, req, sender)
}
func (p *grpcPlugin) ValidateAdmission(ctx context.Context, request *backend.AdmissionRequest) (*backend.ValidationResponse, error) {
pluginClient, ok := p.getPluginClient()
if !ok {
return nil, plugins.ErrPluginUnavailable
}
return pluginClient.ValidateAdmission(ctx, request)
}
func (p *grpcPlugin) MutateAdmission(ctx context.Context, request *backend.AdmissionRequest) (*backend.MutationResponse, error) {
pluginClient, ok := p.getPluginClient()
if !ok {
return nil, plugins.ErrPluginUnavailable
}
return pluginClient.MutateAdmission(ctx, request)
}
func (p *grpcPlugin) ConvertObject(ctx context.Context, request *backend.ConversionRequest) (*backend.ConversionResponse, error) {
pluginClient, ok := p.getPluginClient()
if !ok {
return nil, plugins.ErrPluginUnavailable
}
return pluginClient.ConvertObject(ctx, request)
}

View File

@ -23,6 +23,7 @@ type Plugin interface {
backend.CheckHealthHandler
backend.QueryDataHandler
backend.CallResourceHandler
backend.AdmissionHandler
backend.StreamHandler
}

View File

@ -90,6 +90,7 @@ type Client interface {
backend.QueryDataHandler
backend.CheckHealthHandler
backend.StreamHandler
backend.AdmissionHandler
backend.CallResourceHandler
backend.CollectMetricsHandler
}

View File

@ -217,6 +217,48 @@ func (s *Service) RunStream(ctx context.Context, req *backend.RunStreamRequest,
return plugin.RunStream(ctx, req, sender)
}
// ConvertObject implements plugins.Client.
func (s *Service) ConvertObject(ctx context.Context, req *backend.ConversionRequest) (*backend.ConversionResponse, error) {
if req == nil {
return nil, errNilRequest
}
plugin, exists := s.plugin(ctx, req.PluginContext.PluginID, req.PluginContext.PluginVersion)
if !exists {
return nil, plugins.ErrPluginNotRegistered
}
return plugin.ConvertObject(ctx, req)
}
// MutateAdmission implements plugins.Client.
func (s *Service) MutateAdmission(ctx context.Context, req *backend.AdmissionRequest) (*backend.MutationResponse, error) {
if req == nil {
return nil, errNilRequest
}
plugin, exists := s.plugin(ctx, req.PluginContext.PluginID, req.PluginContext.PluginVersion)
if !exists {
return nil, plugins.ErrPluginNotRegistered
}
return plugin.MutateAdmission(ctx, req)
}
// ValidateAdmission implements plugins.Client.
func (s *Service) ValidateAdmission(ctx context.Context, req *backend.AdmissionRequest) (*backend.ValidationResponse, error) {
if req == nil {
return nil, errNilRequest
}
plugin, exists := s.plugin(ctx, req.PluginContext.PluginID, req.PluginContext.PluginVersion)
if !exists {
return nil, plugins.ErrPluginNotRegistered
}
return plugin.ValidateAdmission(ctx, req)
}
// plugin finds a plugin with `pluginID` from the registry that is not decommissioned
func (s *Service) plugin(ctx context.Context, pluginID, pluginVersion string) (*plugins.Plugin, bool) {
p, exists := s.pluginRegistry.Plugin(ctx, pluginID, pluginVersion)

View File

@ -20,13 +20,16 @@ import (
type TestClient struct {
plugins.Client
QueryDataFunc backend.QueryDataHandlerFunc
CallResourceFunc backend.CallResourceHandlerFunc
CheckHealthFunc backend.CheckHealthHandlerFunc
CollectMetricsFunc backend.CollectMetricsHandlerFunc
SubscribeStreamFunc func(ctx context.Context, req *backend.SubscribeStreamRequest) (*backend.SubscribeStreamResponse, error)
PublishStreamFunc func(ctx context.Context, req *backend.PublishStreamRequest) (*backend.PublishStreamResponse, error)
RunStreamFunc func(ctx context.Context, req *backend.RunStreamRequest, sender *backend.StreamSender) error
QueryDataFunc backend.QueryDataHandlerFunc
CallResourceFunc backend.CallResourceHandlerFunc
CheckHealthFunc backend.CheckHealthHandlerFunc
CollectMetricsFunc backend.CollectMetricsHandlerFunc
SubscribeStreamFunc func(ctx context.Context, req *backend.SubscribeStreamRequest) (*backend.SubscribeStreamResponse, error)
PublishStreamFunc func(ctx context.Context, req *backend.PublishStreamRequest) (*backend.PublishStreamResponse, error)
RunStreamFunc func(ctx context.Context, req *backend.RunStreamRequest, sender *backend.StreamSender) error
ValidateAdmissionFunc backend.ValidateAdmissionFunc
MutateAdmissionFunc backend.MutateAdmissionFunc
ConvertObjectFunc backend.ConvertObjectFunc
}
func (c *TestClient) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
@ -84,14 +87,39 @@ func (c *TestClient) RunStream(ctx context.Context, req *backend.RunStreamReques
return nil
}
func (c *TestClient) ValidateAdmission(ctx context.Context, req *backend.AdmissionRequest) (*backend.ValidationResponse, error) {
if c.ValidateAdmissionFunc != nil {
return c.ValidateAdmissionFunc(ctx, req)
}
return nil, nil
}
func (c *TestClient) MutateAdmission(ctx context.Context, req *backend.AdmissionRequest) (*backend.MutationResponse, error) {
if c.MutateAdmissionFunc != nil {
return c.MutateAdmissionFunc(ctx, req)
}
return nil, nil
}
func (c *TestClient) ConvertObject(ctx context.Context, req *backend.ConversionRequest) (*backend.ConversionResponse, error) {
if c.ConvertObjectFunc != nil {
return c.ConvertObjectFunc(ctx, req)
}
return nil, nil
}
type MiddlewareScenarioContext struct {
QueryDataCallChain []string
CallResourceCallChain []string
CollectMetricsCallChain []string
CheckHealthCallChain []string
SubscribeStreamCallChain []string
PublishStreamCallChain []string
RunStreamCallChain []string
QueryDataCallChain []string
CallResourceCallChain []string
CollectMetricsCallChain []string
CheckHealthCallChain []string
SubscribeStreamCallChain []string
PublishStreamCallChain []string
RunStreamCallChain []string
InstanceSettingsCallChain []string
ValidateAdmissionCallChain []string
MutateAdmissionCallChain []string
ConvertObjectCallChain []string
}
func (ctx *MiddlewareScenarioContext) NewMiddleware(name string) plugins.ClientMiddleware {
@ -131,6 +159,27 @@ func (m *TestMiddleware) CollectMetrics(ctx context.Context, req *backend.Collec
return res, err
}
func (m *TestMiddleware) ValidateAdmission(ctx context.Context, req *backend.AdmissionRequest) (*backend.ValidationResponse, error) {
m.sCtx.ValidateAdmissionCallChain = append(m.sCtx.ValidateAdmissionCallChain, fmt.Sprintf("before %s", m.Name))
res, err := m.next.ValidateAdmission(ctx, req)
m.sCtx.ValidateAdmissionCallChain = append(m.sCtx.ValidateAdmissionCallChain, fmt.Sprintf("after %s", m.Name))
return res, err
}
func (m *TestMiddleware) MutateAdmission(ctx context.Context, req *backend.AdmissionRequest) (*backend.MutationResponse, error) {
m.sCtx.MutateAdmissionCallChain = append(m.sCtx.MutateAdmissionCallChain, fmt.Sprintf("before %s", m.Name))
res, err := m.next.MutateAdmission(ctx, req)
m.sCtx.MutateAdmissionCallChain = append(m.sCtx.MutateAdmissionCallChain, fmt.Sprintf("after %s", m.Name))
return res, err
}
func (m *TestMiddleware) ConvertObject(ctx context.Context, req *backend.ConversionRequest) (*backend.ConversionResponse, error) {
m.sCtx.ConvertObjectCallChain = append(m.sCtx.ConvertObjectCallChain, fmt.Sprintf("before %s", m.Name))
res, err := m.next.ConvertObject(ctx, req)
m.sCtx.ConvertObjectCallChain = append(m.sCtx.ConvertObjectCallChain, fmt.Sprintf("after %s", m.Name))
return res, err
}
func (m *TestMiddleware) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) {
m.sCtx.CheckHealthCallChain = append(m.sCtx.CheckHealthCallChain, fmt.Sprintf("before %s", m.Name))
res, err := m.next.CheckHealth(ctx, req)

View File

@ -5,6 +5,7 @@ import (
"errors"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana/pkg/plugins"
)
@ -14,6 +15,10 @@ type Decorator struct {
middlewares []plugins.ClientMiddleware
}
var (
_ = plugins.Client(&Decorator{})
)
// NewDecorator creates a new plugins.client decorator.
func NewDecorator(client plugins.Client, middlewares ...plugins.ClientMiddleware) (*Decorator, error) {
if client == nil {
@ -98,6 +103,33 @@ func (d *Decorator) RunStream(ctx context.Context, req *backend.RunStreamRequest
return client.RunStream(ctx, req, sender)
}
func (d *Decorator) ValidateAdmission(ctx context.Context, req *backend.AdmissionRequest) (*backend.ValidationResponse, error) {
if req == nil {
return nil, errNilRequest
}
client := clientFromMiddlewares(d.middlewares, d.client)
return client.ValidateAdmission(ctx, req)
}
func (d *Decorator) MutateAdmission(ctx context.Context, req *backend.AdmissionRequest) (*backend.MutationResponse, error) {
if req == nil {
return nil, errNilRequest
}
client := clientFromMiddlewares(d.middlewares, d.client)
return client.MutateAdmission(ctx, req)
}
func (d *Decorator) ConvertObject(ctx context.Context, req *backend.ConversionRequest) (*backend.ConversionResponse, error) {
if req == nil {
return nil, errNilRequest
}
client := clientFromMiddlewares(d.middlewares, d.client)
return client.ConvertObject(ctx, req)
}
func clientFromMiddlewares(middlewares []plugins.ClientMiddleware, finalClient plugins.Client) plugins.Client {
if len(middlewares) == 0 {
return finalClient
@ -123,5 +155,3 @@ func reverseMiddlewares(middlewares []plugins.ClientMiddleware) []plugins.Client
return reversed
}
var _ plugins.Client = &Decorator{}

View File

@ -6,8 +6,9 @@ import (
"testing"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana/pkg/plugins"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/plugins"
)
func TestDecorator(t *testing.T) {
@ -146,13 +147,17 @@ func (c *TestClient) CheckHealth(ctx context.Context, req *backend.CheckHealthRe
}
type MiddlewareScenarioContext struct {
QueryDataCallChain []string
CallResourceCallChain []string
CollectMetricsCallChain []string
CheckHealthCallChain []string
SubscribeStreamCallChain []string
PublishStreamCallChain []string
RunStreamCallChain []string
QueryDataCallChain []string
CallResourceCallChain []string
CollectMetricsCallChain []string
CheckHealthCallChain []string
SubscribeStreamCallChain []string
PublishStreamCallChain []string
RunStreamCallChain []string
InstanceSettingsCallChain []string
ValidateAdmissionCallChain []string
MutateAdmissionCallChain []string
ConvertObjectCallChain []string
}
func (ctx *MiddlewareScenarioContext) NewMiddleware(name string) plugins.ClientMiddleware {
@ -220,4 +225,25 @@ func (m *TestMiddleware) RunStream(ctx context.Context, req *backend.RunStreamRe
return err
}
func (m *TestMiddleware) ValidateAdmission(ctx context.Context, req *backend.AdmissionRequest) (*backend.ValidationResponse, error) {
m.sCtx.ValidateAdmissionCallChain = append(m.sCtx.ValidateAdmissionCallChain, fmt.Sprintf("before %s", m.Name))
res, err := m.next.ValidateAdmission(ctx, req)
m.sCtx.ValidateAdmissionCallChain = append(m.sCtx.ValidateAdmissionCallChain, fmt.Sprintf("after %s", m.Name))
return res, err
}
func (m *TestMiddleware) MutateAdmission(ctx context.Context, req *backend.AdmissionRequest) (*backend.MutationResponse, error) {
m.sCtx.MutateAdmissionCallChain = append(m.sCtx.MutateAdmissionCallChain, fmt.Sprintf("before %s", m.Name))
res, err := m.next.MutateAdmission(ctx, req)
m.sCtx.MutateAdmissionCallChain = append(m.sCtx.MutateAdmissionCallChain, fmt.Sprintf("after %s", m.Name))
return res, err
}
func (m *TestMiddleware) ConvertObject(ctx context.Context, req *backend.ConversionRequest) (*backend.ConversionResponse, error) {
m.sCtx.ConvertObjectCallChain = append(m.sCtx.ConvertObjectCallChain, fmt.Sprintf("before %s", m.Name))
res, err := m.next.ConvertObject(ctx, req)
m.sCtx.ConvertObjectCallChain = append(m.sCtx.ConvertObjectCallChain, fmt.Sprintf("after %s", m.Name))
return res, err
}
var _ plugins.Client = &TestClient{}

View File

@ -70,6 +70,9 @@ type FakePluginClient struct {
backend.CheckHealthHandlerFunc
backend.QueryDataHandlerFunc
backend.CallResourceHandlerFunc
backend.MutateAdmissionFunc
backend.ValidateAdmissionFunc
backend.ConvertObjectFunc
mutex sync.RWMutex
backendplugin.Plugin
@ -166,6 +169,30 @@ func (pc *FakePluginClient) RunStream(_ context.Context, _ *backend.RunStreamReq
return plugins.ErrMethodNotImplemented
}
func (pc *FakePluginClient) ValidateAdmission(ctx context.Context, req *backend.AdmissionRequest) (*backend.ValidationResponse, error) {
if pc.ValidateAdmissionFunc != nil {
return pc.ValidateAdmissionFunc(ctx, req)
}
return nil, plugins.ErrMethodNotImplemented
}
func (pc *FakePluginClient) MutateAdmission(ctx context.Context, req *backend.AdmissionRequest) (*backend.MutationResponse, error) {
if pc.MutateAdmissionFunc != nil {
return pc.MutateAdmissionFunc(ctx, req)
}
return nil, plugins.ErrMethodNotImplemented
}
func (pc *FakePluginClient) ConvertObject(ctx context.Context, req *backend.ConversionRequest) (*backend.ConversionResponse, error) {
if pc.ConvertObjectFunc != nil {
return pc.ConvertObjectFunc(ctx, req)
}
return nil, plugins.ErrMethodNotImplemented
}
type FakePluginRegistry struct {
Store map[string]*plugins.Plugin
}

View File

@ -69,6 +69,15 @@ type Plugin struct {
mu sync.Mutex
}
var (
_ = backend.CollectMetricsHandler(&Plugin{})
_ = backend.CheckHealthHandler(&Plugin{})
_ = backend.QueryDataHandler(&Plugin{})
_ = backend.CallResourceHandler(&Plugin{})
_ = backend.StreamHandler(&Plugin{})
_ = backend.AdmissionHandler(&Plugin{})
)
type AngularMeta struct {
Detected bool `json:"detected"`
HideDeprecation bool `json:"hideDeprecation"`
@ -360,6 +369,33 @@ func (p *Plugin) RunStream(ctx context.Context, req *backend.RunStreamRequest, s
return pluginClient.RunStream(ctx, req, sender)
}
// ValidateAdmission implements backend.AdmissionHandler.
func (p *Plugin) ValidateAdmission(ctx context.Context, req *backend.AdmissionRequest) (*backend.ValidationResponse, error) {
pluginClient, ok := p.Client()
if !ok {
return nil, ErrPluginUnavailable
}
return pluginClient.ValidateAdmission(ctx, req)
}
// MutateAdmission implements backend.AdmissionHandler.
func (p *Plugin) MutateAdmission(ctx context.Context, req *backend.AdmissionRequest) (*backend.MutationResponse, error) {
pluginClient, ok := p.Client()
if !ok {
return nil, ErrPluginUnavailable
}
return pluginClient.MutateAdmission(ctx, req)
}
// ConvertObject implements backend.AdmissionHandler.
func (p *Plugin) ConvertObject(ctx context.Context, req *backend.ConversionRequest) (*backend.ConversionResponse, error) {
pluginClient, ok := p.Client()
if !ok {
return nil, ErrPluginUnavailable
}
return pluginClient.ConvertObject(ctx, req)
}
func (p *Plugin) File(name string) (fs.File, error) {
cleanPath, err := util.CleanRelativePath(name)
if err != nil {
@ -418,6 +454,7 @@ type PluginClient interface {
backend.CollectMetricsHandler
backend.CheckHealthHandler
backend.CallResourceHandler
backend.AdmissionHandler
backend.StreamHandler
}

View File

@ -3,7 +3,7 @@ module github.com/grafana/grafana/pkg/promlib
go 1.21.10
require (
github.com/grafana/grafana-plugin-sdk-go v0.231.0
github.com/grafana/grafana-plugin-sdk-go v0.232.0
github.com/json-iterator/go v1.1.12
github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/prometheus/client_golang v1.19.0

View File

@ -86,7 +86,7 @@ github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e h1:JKmoR8x90Iww1
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/grafana/grafana-plugin-sdk-go v0.231.0 h1:Qt4PBDR8b4MTUxL48EaZw1fHI1rXUNNhvTU/Nf0Ex2g=
github.com/grafana/grafana-plugin-sdk-go v0.232.0 h1:RnaQwhAOxYdp9wwy0Yz5cJUGY5tpIXPxoFWmEKflfww=
github.com/grafana/regexp v0.0.0-20221123153739-15dc172cd2db h1:7aN5cccjIqCLTzedH7MZzRZt5/lsAHch6Z3L2ZGn5FA=
github.com/grafana/regexp v0.0.0-20221123153739-15dc172cd2db/go.mod h1:M5qHK+eWfAv8VR/265dIuEpL3fNfeC21tXXp9itM24A=
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1 h1:qnpSQwGEnkcRpTqNOIR6bJbR0gAorgP9CSALpRcKoAA=

View File

@ -46,13 +46,17 @@
"type"
],
"properties": {
"apiVersion": {
"description": "The apiserver version",
"type": "string"
},
"type": {
"description": "The datasource plugin type",
"type": "string",
"pattern": "^prometheus$"
},
"uid": {
"description": "Datasource UID",
"description": "Datasource UID (NOTE: name in k8s)",
"type": "string"
}
},

View File

@ -56,13 +56,17 @@
"type"
],
"properties": {
"apiVersion": {
"description": "The apiserver version",
"type": "string"
},
"type": {
"description": "The datasource plugin type",
"type": "string",
"pattern": "^prometheus$"
},
"uid": {
"description": "Datasource UID",
"description": "Datasource UID (NOTE: name in k8s)",
"type": "string"
}
},

View File

@ -11,6 +11,7 @@ import (
"sync"
"time"
"github.com/grafana/grafana-plugin-sdk-go/backend"
sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
sdkproxy "github.com/grafana/grafana-plugin-sdk-go/backend/proxy"
@ -18,6 +19,7 @@ import (
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/httpclient"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/featuremgmt"
@ -26,6 +28,7 @@ import (
"github.com/grafana/grafana/pkg/services/secrets"
"github.com/grafana/grafana/pkg/services/secrets/kvstore"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util/errutil"
)
const (
@ -44,6 +47,7 @@ type Service struct {
logger log.Logger
db db.DB
pluginStore pluginstore.Store
pluginClient plugins.Client // access to everything
ptc proxyTransportCache
}
@ -61,7 +65,7 @@ type cachedRoundTripper struct {
func ProvideService(
db db.DB, secretsService secrets.Service, secretsStore kvstore.SecretsKVStore, cfg *setting.Cfg,
features featuremgmt.FeatureToggles, ac accesscontrol.AccessControl, datasourcePermissionsService accesscontrol.DatasourcePermissionsService,
quotaService quota.Service, pluginStore pluginstore.Store,
quotaService quota.Service, pluginStore pluginstore.Store, pluginClient plugins.Client,
) (*Service, error) {
dslogger := log.New("datasources")
store := &SqlStore{db: db, logger: dslogger, features: features}
@ -79,6 +83,7 @@ func ProvideService(
logger: dslogger,
db: db,
pluginStore: pluginStore,
pluginClient: pluginClient,
}
ac.RegisterScopeAttributeResolver(NewNameScopeResolver(store))
@ -205,10 +210,48 @@ func (s *Service) AddDataSource(ctx context.Context, cmd *datasources.AddDataSou
cmd.Name = getAvailableName(cmd.Type, dataSources)
}
if err := s.validateFields(ctx, cmd.Name, cmd.URL, cmd.Type, cmd.APIVersion); err != nil {
// Validate the command
jd, err := cmd.JsonData.ToDB()
if err != nil {
return nil, fmt.Errorf("invalid jsonData")
}
settings, err := s.prepareInstanceSettings(ctx, backend.PluginContext{
OrgID: cmd.OrgID,
PluginID: cmd.Type,
}, &backend.DataSourceInstanceSettings{
UID: cmd.UID,
Name: cmd.Name,
URL: cmd.URL,
Database: cmd.Database,
JSONData: jd,
DecryptedSecureJSONData: cmd.SecureJsonData,
Type: cmd.Type,
User: cmd.User,
BasicAuthEnabled: cmd.BasicAuth,
BasicAuthUser: cmd.BasicAuthUser,
APIVersion: cmd.APIVersion,
})
if err != nil {
return nil, err
}
// The mutable properties
cmd.URL = settings.URL
cmd.User = settings.User
cmd.BasicAuth = settings.BasicAuthEnabled
cmd.BasicAuthUser = settings.BasicAuthUser
cmd.Database = settings.Database
cmd.SecureJsonData = settings.DecryptedSecureJSONData
cmd.JsonData = nil
if settings.JSONData != nil {
cmd.JsonData = simplejson.New()
err := cmd.JsonData.FromDB(settings.JSONData)
if err != nil {
return nil, err
}
}
var dataSource *datasources.DataSource
return dataSource, s.db.InTransaction(ctx, func(ctx context.Context) error {
var err error
@ -252,6 +295,122 @@ func (s *Service) AddDataSource(ctx context.Context, cmd *datasources.AddDataSou
})
}
// This will valid validate the instance settings return a version that is safe to be saved
func (s *Service) prepareInstanceSettings(ctx context.Context, pluginContext backend.PluginContext, settings *backend.DataSourceInstanceSettings) (*backend.DataSourceInstanceSettings, error) {
operation := backend.AdmissionRequestCreate
// First apply global validation rules -- these are required regardless which plugin we are talking to
if len(settings.Name) > maxDatasourceNameLen {
return nil, datasources.ErrDataSourceNameInvalid.Errorf("max length is %d", maxDatasourceNameLen)
}
if len(settings.URL) > maxDatasourceUrlLen {
return nil, datasources.ErrDataSourceURLInvalid.Errorf("max length is %d", maxDatasourceUrlLen)
}
if settings.Type == "" {
return settings, nil // NOOP used in tests
}
// Make sure it is a known plugin type
p, found := s.pluginStore.Plugin(ctx, settings.Type)
if !found {
return nil, errutil.BadRequest("datasource.unknownPlugin",
errutil.WithPublicMessage(fmt.Sprintf("plugin '%s' not found", settings.Type)))
}
// When the APIVersion is set, the client must also implement AdmissionHandler
if p.APIVersion == "" {
if settings.APIVersion != "" {
return nil, fmt.Errorf("invalid request apiVersion (datasource does not have one configured)")
}
return settings, nil // NOOP
}
pb, err := backend.DataSourceInstanceSettingsToProtoBytes(settings)
if err != nil {
return nil, err
}
req := &backend.AdmissionRequest{
Operation: backend.AdmissionRequestCreate,
PluginContext: pluginContext,
Kind: settings.GVK(),
ObjectBytes: pb,
}
// Set the old bytes and change the operation
if pluginContext.DataSourceInstanceSettings != nil {
req.Operation = backend.AdmissionRequestUpdate
req.OldObjectBytes, err = backend.DataSourceInstanceSettingsToProtoBytes(settings)
if err != nil {
return nil, err
}
}
{ // As an example, this will first call validate (then mutate)
// Implementations may vary, but typically validation is
// more strict because it does not have the option to fix anything
// that has reasonable fixes.
rsp, err := s.pluginClient.ValidateAdmission(ctx, req)
if err != nil {
if errors.Is(err, plugins.ErrMethodNotImplemented) {
return nil, errutil.Internal("plugin.unimplemented").
Errorf("plugin (%s) with apiVersion=%s must implement ValidateAdmission", p.ID, p.APIVersion)
}
return nil, err
}
if rsp == nil {
return nil, fmt.Errorf("expected response (%v)", operation)
}
if !rsp.Allowed {
if rsp.Result != nil {
return nil, toError(rsp.Result)
}
return nil, fmt.Errorf("not allowed")
}
// payload is OK, but now lets do the mutate version...
}
// Next calling mutation -- this will try to get the input into an acceptable form
rsp, err := s.pluginClient.MutateAdmission(ctx, req)
if err != nil {
if errors.Is(err, plugins.ErrMethodNotImplemented) {
return nil, errutil.Internal("plugin.unimplemented").
Errorf("plugin (%s) with apiVersion=%s must implement MutateAdmission", p.ID, p.APIVersion)
}
return nil, err
}
if rsp == nil {
return nil, fmt.Errorf("expected response (%v)", operation)
}
if !rsp.Allowed {
if rsp.Result != nil {
return nil, toError(rsp.Result)
}
return nil, fmt.Errorf("not allowed")
}
if rsp.ObjectBytes == nil {
return nil, fmt.Errorf("mutation response is missing value")
}
return backend.DataSourceInstanceSettingsFromProto(rsp.ObjectBytes, pluginContext.PluginID)
}
func toError(status *backend.StatusResult) error {
if status == nil {
return fmt.Errorf("error converting status")
}
// hymm -- no way to pass the raw http status along!!
// Looks like it must be based on the reason string
return errutil.Error{
Reason: errutil.CoreStatus(status.Reason),
MessageID: "datasource.config.mutate",
LogMessage: status.Message,
PublicMessage: status.Message,
Source: errutil.SourceDownstream,
}
}
// getAvailableName finds the first available name for a datasource of the given type.
func getAvailableName(dsType string, dataSources []*datasources.DataSource) string {
dsNames := make(map[string]bool)
@ -284,13 +443,40 @@ func (s *Service) DeleteDataSource(ctx context.Context, cmd *datasources.DeleteD
})
}
func (s *Service) getPluginContext(ctx context.Context, orgID int64, pluginID string, ds *datasources.DataSource) (backend.PluginContext, error) {
var err error
if ds == nil {
return backend.PluginContext{
OrgID: orgID,
PluginID: pluginID,
}, err
}
pctx := backend.PluginContext{
OrgID: orgID,
PluginID: pluginID,
DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{
UID: ds.UID,
Type: pluginID,
Name: ds.Name,
URL: ds.URL,
Database: ds.Database,
BasicAuthEnabled: ds.BasicAuth,
BasicAuthUser: ds.BasicAuthUser,
Updated: ds.Updated,
APIVersion: ds.APIVersion,
User: ds.User,
},
}
pctx.DataSourceInstanceSettings.JSONData, err = ds.JsonData.ToDB()
if err == nil && len(ds.SecureJsonData) > 0 {
pctx.DataSourceInstanceSettings.DecryptedSecureJSONData, err = s.DecryptedValues(ctx, ds)
}
return pctx, err
}
func (s *Service) UpdateDataSource(ctx context.Context, cmd *datasources.UpdateDataSourceCommand) (*datasources.DataSource, error) {
var dataSource *datasources.DataSource
if err := s.validateFields(ctx, cmd.Name, cmd.URL, cmd.Type, cmd.APIVersion); err != nil {
return dataSource, err
}
return dataSource, s.db.InTransaction(ctx, func(ctx context.Context) error {
var err error
@ -303,6 +489,54 @@ func (s *Service) UpdateDataSource(ctx context.Context, cmd *datasources.UpdateD
return err
}
// Validate the command
jd, err := cmd.JsonData.ToDB()
if err != nil {
return fmt.Errorf("invalid jsonData")
}
pctx, err := s.getPluginContext(ctx, cmd.OrgID, cmd.Type, dataSource)
if err != nil {
return err
}
settings, err := s.prepareInstanceSettings(ctx, pctx,
&backend.DataSourceInstanceSettings{
UID: cmd.UID,
Name: cmd.Name,
URL: cmd.URL,
Database: cmd.Database,
JSONData: jd,
DecryptedSecureJSONData: cmd.SecureJsonData,
Type: cmd.Type,
User: cmd.User,
BasicAuthEnabled: cmd.BasicAuth,
BasicAuthUser: cmd.BasicAuthUser,
APIVersion: cmd.APIVersion,
Updated: time.Now(),
})
if err != nil {
return err
}
if settings == nil {
return fmt.Errorf("settings or an error is required")
}
// The mutable properties
cmd.URL = settings.URL
cmd.User = settings.User
cmd.BasicAuth = settings.BasicAuthEnabled
cmd.BasicAuthUser = settings.BasicAuthUser
cmd.Database = settings.Database
cmd.SecureJsonData = settings.DecryptedSecureJSONData
cmd.JsonData = nil
if settings.JSONData != nil {
cmd.JsonData = simplejson.New()
err := cmd.JsonData.FromDB(settings.JSONData)
if err != nil {
return err
}
}
if cmd.Name != "" && cmd.Name != dataSource.Name {
query := &datasources.GetDataSourceQuery{
Name: cmd.Name,
@ -716,32 +950,6 @@ func (s *Service) fillWithSecureJSONData(ctx context.Context, cmd *datasources.U
return nil
}
func (s *Service) validateFields(ctx context.Context, name, url, pluginID, apiVersion string) error {
if len(name) > maxDatasourceNameLen {
return datasources.ErrDataSourceNameInvalid.Errorf("max length is %d", maxDatasourceNameLen)
}
if len(url) > maxDatasourceUrlLen {
return datasources.ErrDataSourceURLInvalid.Errorf("max length is %d", maxDatasourceUrlLen)
}
if apiVersion == "" {
return nil
}
p, found := s.pluginStore.Plugin(context.Background(), pluginID)
if !found {
// Plugin not installed, ignore apiVersion check
return nil
}
if p.APIVersion != "" && p.APIVersion != apiVersion {
return datasources.ErrDataSourceAPIVersionInvalid.Errorf("expected %s, got %s", p.APIVersion, apiVersion)
}
return nil
}
func readQuotaConfig(cfg *setting.Cfg) (*quota.Map, error) {
limits := &quota.Map{}

View File

@ -3,6 +3,7 @@ package service
import (
"context"
"encoding/json"
"errors"
"io"
"net/http"
"net/http/httptest"
@ -20,6 +21,7 @@ import (
"github.com/grafana/grafana/pkg/infra/httpclient"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/plugins"
pluginfakes "github.com/grafana/grafana/pkg/plugins/manager/fakes"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/actest"
acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
@ -33,6 +35,8 @@ import (
secretsmng "github.com/grafana/grafana/pkg/services/secrets/manager"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/tests/testsuite"
testdatasource "github.com/grafana/grafana/pkg/tsdb/grafana-testdata-datasource"
"github.com/grafana/grafana/pkg/util/errutil"
)
func TestMain(m *testing.M) {
@ -59,6 +63,7 @@ func TestService_AddDataSource(t *testing.T) {
cfg := &setting.Cfg{}
t.Run("should return validation error if command validation failed", func(t *testing.T) {
dsplugin := &testdatasource.Service{}
sqlStore := db.InitTestDB(t)
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
@ -67,10 +72,16 @@ func TestService_AddDataSource(t *testing.T) {
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), actest.FakeAccessControl{}, mockPermission, quotaService, &pluginstore.FakePluginStore{
PluginList: []pluginstore.Plugin{{
JSONData: plugins.JSONData{
ID: "test",
Type: plugins.TypeDataSource,
Name: "test",
APIVersion: "v0alpha1",
APIVersion: "v0alpha1", // When a value exists in plugin.json, the callback will be executed
},
}},
}, &pluginfakes.FakePluginClient{
ValidateAdmissionFunc: dsplugin.ValidateAdmission,
MutateAdmissionFunc: dsplugin.MutateAdmission,
ConvertObjectFunc: dsplugin.ConvertObject,
})
require.NoError(t, err)
@ -92,12 +103,17 @@ func TestService_AddDataSource(t *testing.T) {
cmd = &datasources.AddDataSourceCommand{
OrgID: 1,
Type: "test", // required to validate apiserver
Name: "test",
APIVersion: "v0alpha2",
APIVersion: "v123", // invalid apiVersion
}
_, err = dsService.AddDataSource(context.Background(), cmd)
require.EqualError(t, err, "[datasource.apiVersionInvalid] expected v0alpha1, got v0alpha2")
var gErr errutil.Error
require.True(t, errors.As(err, &gErr))
require.Equal(t, "expected apiVersion: v0alpha1, found: v123", gErr.PublicMessage)
require.Equal(t, 400, gErr.Public().StatusCode)
})
}
@ -189,7 +205,7 @@ func TestService_UpdateDataSource(t *testing.T) {
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
quotaService := quotatest.New(false, nil)
mockPermission := acmock.NewMockedPermissionsService()
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), actest.FakeAccessControl{}, mockPermission, quotaService, &pluginstore.FakePluginStore{})
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), actest.FakeAccessControl{}, mockPermission, quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{})
require.NoError(t, err)
cmd := &datasources.UpdateDataSourceCommand{
@ -208,11 +224,31 @@ func TestService_UpdateDataSource(t *testing.T) {
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
quotaService := quotatest.New(false, nil)
mockPermission := acmock.NewMockedPermissionsService()
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), actest.FakeAccessControl{}, mockPermission, quotaService, &pluginstore.FakePluginStore{})
mockPermission.On("SetPermissions", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return([]accesscontrol.ResourcePermission{}, nil)
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), actest.FakeAccessControl{}, mockPermission, quotaService,
&pluginstore.FakePluginStore{
PluginList: []pluginstore.Plugin{
{
JSONData: plugins.JSONData{
ID: "test",
},
},
},
}, &pluginfakes.FakePluginClient{})
require.NoError(t, err)
// First add the datasource
ds, err := dsService.AddDataSource(context.Background(), &datasources.AddDataSourceCommand{
OrgID: 1,
Name: "test",
Type: "test",
UserID: 0,
})
require.NoError(t, err)
cmd := &datasources.UpdateDataSourceCommand{
ID: 1,
ID: ds.ID,
UID: ds.UID,
OrgID: 1,
Name: string(make([]byte, 256)),
}
@ -221,7 +257,8 @@ func TestService_UpdateDataSource(t *testing.T) {
require.EqualError(t, err, "[datasource.nameInvalid] max length is 190")
cmd = &datasources.UpdateDataSourceCommand{
ID: 1,
ID: ds.ID,
UID: ds.UID,
OrgID: 1,
URL: string(make([]byte, 256)),
}
@ -236,7 +273,7 @@ func TestService_UpdateDataSource(t *testing.T) {
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
quotaService := quotatest.New(false, nil)
mockPermission := acmock.NewMockedPermissionsService()
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), actest.FakeAccessControl{}, mockPermission, quotaService, &pluginstore.FakePluginStore{})
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), actest.FakeAccessControl{}, mockPermission, quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{})
require.NoError(t, err)
mockPermission.On("SetPermissions", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return([]accesscontrol.ResourcePermission{}, nil)
@ -263,7 +300,7 @@ func TestService_UpdateDataSource(t *testing.T) {
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
quotaService := quotatest.New(false, nil)
mockPermission := acmock.NewMockedPermissionsService()
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), actest.FakeAccessControl{}, mockPermission, quotaService, &pluginstore.FakePluginStore{})
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), actest.FakeAccessControl{}, mockPermission, quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{})
require.NoError(t, err)
mockPermission.On("SetPermissions", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return([]accesscontrol.ResourcePermission{}, nil)
@ -296,7 +333,7 @@ func TestService_UpdateDataSource(t *testing.T) {
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
quotaService := quotatest.New(false, nil)
mockPermission := acmock.NewMockedPermissionsService()
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), actest.FakeAccessControl{}, mockPermission, quotaService, &pluginstore.FakePluginStore{})
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), actest.FakeAccessControl{}, mockPermission, quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{})
require.NoError(t, err)
mockPermission.On("SetPermissions", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return([]accesscontrol.ResourcePermission{}, nil)
@ -340,7 +377,8 @@ func TestService_UpdateDataSource(t *testing.T) {
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
quotaService := quotatest.New(false, nil)
mockPermission := acmock.NewMockedPermissionsService()
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), actest.FakeAccessControl{}, mockPermission, quotaService, &pluginstore.FakePluginStore{})
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), actest.FakeAccessControl{},
mockPermission, quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{})
require.NoError(t, err)
mockPermission.On("SetPermissions", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return([]accesscontrol.ResourcePermission{}, nil)
@ -602,7 +640,7 @@ func TestService_GetHttpTransport(t *testing.T) {
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
quotaService := quotatest.New(false, nil)
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService, &pluginstore.FakePluginStore{})
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{})
require.NoError(t, err)
rt1, err := dsService.GetHTTPTransport(context.Background(), &ds, provider)
@ -639,7 +677,7 @@ func TestService_GetHttpTransport(t *testing.T) {
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
quotaService := quotatest.New(false, nil)
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService, &pluginstore.FakePluginStore{})
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{})
require.NoError(t, err)
ds := datasources.DataSource{
@ -690,7 +728,7 @@ func TestService_GetHttpTransport(t *testing.T) {
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
quotaService := quotatest.New(false, nil)
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService, &pluginstore.FakePluginStore{})
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{})
require.NoError(t, err)
ds := datasources.DataSource{
@ -738,7 +776,7 @@ func TestService_GetHttpTransport(t *testing.T) {
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
quotaService := quotatest.New(false, nil)
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService, &pluginstore.FakePluginStore{})
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{})
require.NoError(t, err)
ds := datasources.DataSource{
@ -794,7 +832,7 @@ func TestService_GetHttpTransport(t *testing.T) {
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
quotaService := quotatest.New(false, nil)
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService, &pluginstore.FakePluginStore{})
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{})
require.NoError(t, err)
ds := datasources.DataSource{
@ -829,7 +867,7 @@ func TestService_GetHttpTransport(t *testing.T) {
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
quotaService := quotatest.New(false, nil)
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService, &pluginstore.FakePluginStore{})
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{})
require.NoError(t, err)
ds := datasources.DataSource{
@ -898,7 +936,7 @@ func TestService_GetHttpTransport(t *testing.T) {
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
quotaService := quotatest.New(false, nil)
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService, &pluginstore.FakePluginStore{})
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{})
require.NoError(t, err)
ds := datasources.DataSource{
@ -977,7 +1015,7 @@ func TestService_GetHttpTransport(t *testing.T) {
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
quotaService := quotatest.New(false, nil)
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService, &pluginstore.FakePluginStore{})
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{})
require.NoError(t, err)
ds := datasources.DataSource{
@ -998,7 +1036,7 @@ func TestService_getProxySettings(t *testing.T) {
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
quotaService := quotatest.New(false, nil)
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, &setting.Cfg{}, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService, &pluginstore.FakePluginStore{})
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, &setting.Cfg{}, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{})
require.NoError(t, err)
t.Run("Should default to disabled", func(t *testing.T) {
@ -1094,7 +1132,7 @@ func TestService_getTimeout(t *testing.T) {
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
quotaService := quotatest.New(false, nil)
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService, &pluginstore.FakePluginStore{})
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{})
require.NoError(t, err)
for _, tc := range testCases {
@ -1117,7 +1155,7 @@ func TestService_GetDecryptedValues(t *testing.T) {
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
quotaService := quotatest.New(false, nil)
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, nil, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService, &pluginstore.FakePluginStore{})
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, nil, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{})
require.NoError(t, err)
jsonData := map[string]string{
@ -1145,7 +1183,7 @@ func TestService_GetDecryptedValues(t *testing.T) {
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
quotaService := quotatest.New(false, nil)
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, nil, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService, &pluginstore.FakePluginStore{})
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, nil, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{})
require.NoError(t, err)
jsonData := map[string]string{
@ -1169,7 +1207,7 @@ func TestDataSource_CustomHeaders(t *testing.T) {
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
quotaService := quotatest.New(false, nil)
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, nil, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService, &pluginstore.FakePluginStore{})
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, nil, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{})
require.NoError(t, err)
dsService.cfg = setting.NewCfg()

View File

@ -43,3 +43,18 @@ func (m *baseMiddleware) PublishStream(ctx context.Context, req *backend.Publish
func (m *baseMiddleware) RunStream(ctx context.Context, req *backend.RunStreamRequest, sender *backend.StreamSender) error {
return m.next.RunStream(ctx, req, sender)
}
// ValidateAdmission implements backend.AdmissionHandler.
func (m *baseMiddleware) ValidateAdmission(ctx context.Context, req *backend.AdmissionRequest) (*backend.ValidationResponse, error) {
return m.next.ValidateAdmission(ctx, req)
}
// MutateAdmission implements backend.AdmissionHandler.
func (m *baseMiddleware) MutateAdmission(ctx context.Context, req *backend.AdmissionRequest) (*backend.MutationResponse, error) {
return m.next.MutateAdmission(ctx, req)
}
// ConvertObject implements backend.AdmissionHandler.
func (m *baseMiddleware) ConvertObject(ctx context.Context, req *backend.ConversionRequest) (*backend.ConversionResponse, error) {
return m.next.ConvertObject(ctx, req)
}

View File

@ -70,3 +70,21 @@ func (m *ContextualLoggerMiddleware) RunStream(ctx context.Context, req *backend
ctx = instrumentContext(ctx, endpointRunStream, req.PluginContext)
return m.next.RunStream(ctx, req, sender)
}
// ValidateAdmission implements backend.AdmissionHandler.
func (m *ContextualLoggerMiddleware) ValidateAdmission(ctx context.Context, req *backend.AdmissionRequest) (*backend.ValidationResponse, error) {
ctx = instrumentContext(ctx, endpointValidateAdmission, req.PluginContext)
return m.next.ValidateAdmission(ctx, req)
}
// MutateAdmission implements backend.AdmissionHandler.
func (m *ContextualLoggerMiddleware) MutateAdmission(ctx context.Context, req *backend.AdmissionRequest) (*backend.MutationResponse, error) {
ctx = instrumentContext(ctx, endpointMutateAdmission, req.PluginContext)
return m.next.MutateAdmission(ctx, req)
}
// ConvertObject implements backend.AdmissionHandler.
func (m *ContextualLoggerMiddleware) ConvertObject(ctx context.Context, req *backend.ConversionRequest) (*backend.ConversionResponse, error) {
ctx = instrumentContext(ctx, endpointConvertObject, req.PluginContext)
return m.next.ConvertObject(ctx, req)
}

View File

@ -185,3 +185,33 @@ func (m *MetricsMiddleware) CollectMetrics(ctx context.Context, req *backend.Col
})
return result, err
}
func (m *MetricsMiddleware) ValidateAdmission(ctx context.Context, req *backend.AdmissionRequest) (*backend.ValidationResponse, error) {
var result *backend.ValidationResponse
err := m.instrumentPluginRequest(ctx, req.PluginContext, endpointMutateAdmission, func(ctx context.Context) (status requestStatus, innerErr error) {
result, innerErr = m.next.ValidateAdmission(ctx, req)
return requestStatusFromError(innerErr), innerErr
})
return result, err
}
func (m *MetricsMiddleware) MutateAdmission(ctx context.Context, req *backend.AdmissionRequest) (*backend.MutationResponse, error) {
var result *backend.MutationResponse
err := m.instrumentPluginRequest(ctx, req.PluginContext, endpointMutateAdmission, func(ctx context.Context) (status requestStatus, innerErr error) {
result, innerErr = m.next.MutateAdmission(ctx, req)
return requestStatusFromError(innerErr), innerErr
})
return result, err
}
func (m *MetricsMiddleware) ConvertObject(ctx context.Context, req *backend.ConversionRequest) (*backend.ConversionResponse, error) {
var result *backend.ConversionResponse
err := m.instrumentPluginRequest(ctx, req.PluginContext, endpointMutateAdmission, func(ctx context.Context) (status requestStatus, innerErr error) {
result, innerErr = m.next.ConvertObject(ctx, req)
return requestStatusFromError(innerErr), innerErr
})
return result, err
}

View File

@ -67,3 +67,21 @@ func (m *PluginRequestMetaMiddleware) RunStream(ctx context.Context, req *backen
ctx = m.withDefaultPluginRequestMeta(ctx)
return m.next.RunStream(ctx, req, sender)
}
// ValidateAdmission implements backend.AdmissionHandler.
func (m *PluginRequestMetaMiddleware) ValidateAdmission(ctx context.Context, req *backend.AdmissionRequest) (*backend.ValidationResponse, error) {
ctx = m.withDefaultPluginRequestMeta(ctx)
return m.next.ValidateAdmission(ctx, req)
}
// MutateAdmission implements backend.AdmissionHandler.
func (m *PluginRequestMetaMiddleware) MutateAdmission(ctx context.Context, req *backend.AdmissionRequest) (*backend.MutationResponse, error) {
ctx = m.withDefaultPluginRequestMeta(ctx)
return m.next.MutateAdmission(ctx, req)
}
// ConvertObject implements backend.AdmissionHandler.
func (m *PluginRequestMetaMiddleware) ConvertObject(ctx context.Context, req *backend.ConversionRequest) (*backend.ConversionResponse, error) {
ctx = m.withDefaultPluginRequestMeta(ctx)
return m.next.ConvertObject(ctx, req)
}

View File

@ -135,3 +135,30 @@ func (m *TracingMiddleware) RunStream(ctx context.Context, req *backend.RunStrea
err = m.next.RunStream(ctx, req, sender)
return err
}
// ValidateAdmission implements backend.AdmissionHandler.
func (m *TracingMiddleware) ValidateAdmission(ctx context.Context, req *backend.AdmissionRequest) (*backend.ValidationResponse, error) {
var err error
ctx, end := m.traceWrap(ctx, req.PluginContext, endpointValidateAdmission)
defer func() { end(err) }()
resp, err := m.next.ValidateAdmission(ctx, req)
return resp, err
}
// MutateAdmission implements backend.AdmissionHandler.
func (m *TracingMiddleware) MutateAdmission(ctx context.Context, req *backend.AdmissionRequest) (*backend.MutationResponse, error) {
var err error
ctx, end := m.traceWrap(ctx, req.PluginContext, endpointMutateAdmission)
defer func() { end(err) }()
resp, err := m.next.MutateAdmission(ctx, req)
return resp, err
}
// ConvertObject implements backend.AdmissionHandler.
func (m *TracingMiddleware) ConvertObject(ctx context.Context, req *backend.ConversionRequest) (*backend.ConversionResponse, error) {
var err error
ctx, end := m.traceWrap(ctx, req.PluginContext, endpointConvertObject)
defer func() { end(err) }()
resp, err := m.next.ConvertObject(ctx, req)
return resp, err
}

View File

@ -387,6 +387,21 @@ func (m *alwaysErrorFuncMiddleware) RunStream(ctx context.Context, req *backend.
return m.f()
}
// ValidateAdmission implements backend.AdmissionHandler.
func (m *alwaysErrorFuncMiddleware) ValidateAdmission(ctx context.Context, req *backend.AdmissionRequest) (*backend.ValidationResponse, error) {
return nil, m.f()
}
// MutateAdmission implements backend.AdmissionHandler.
func (m *alwaysErrorFuncMiddleware) MutateAdmission(ctx context.Context, req *backend.AdmissionRequest) (*backend.MutationResponse, error) {
return nil, m.f()
}
// ConvertObject implements backend.AdmissionHandler.
func (m *alwaysErrorFuncMiddleware) ConvertObject(ctx context.Context, req *backend.ConversionRequest) (*backend.ConversionResponse, error) {
return nil, m.f()
}
// newAlwaysErrorMiddleware returns a new middleware that always returns the specified error.
func newAlwaysErrorMiddleware(err error) plugins.ClientMiddleware {
return plugins.ClientMiddlewareFunc(func(next plugins.Client) plugins.Client {
@ -401,7 +416,6 @@ func newAlwaysPanicMiddleware(message string) plugins.ClientMiddleware {
return plugins.ClientMiddlewareFunc(func(next plugins.Client) plugins.Client {
return &alwaysErrorFuncMiddleware{func() error {
panic(message)
return nil // nolint:govet
}}
})
}

View File

@ -25,13 +25,16 @@ func (status requestStatus) String() string {
}
const (
endpointCallResource = "callResource"
endpointCheckHealth = "checkHealth"
endpointCollectMetrics = "collectMetrics"
endpointQueryData = "queryData"
endpointSubscribeStream = "subscribeStream"
endpointPublishStream = "publishStream"
endpointRunStream = "runStream"
endpointCallResource = "callResource"
endpointCheckHealth = "checkHealth"
endpointCollectMetrics = "collectMetrics"
endpointQueryData = "queryData"
endpointSubscribeStream = "subscribeStream"
endpointPublishStream = "publishStream"
endpointRunStream = "runStream"
endpointValidateAdmission = "validateAdmission"
endpointMutateAdmission = "mutateAdmission"
endpointConvertObject = "convertObject"
)
type callResourceResponseSenderFunc func(res *backend.CallResourceResponse) error

View File

@ -13,6 +13,7 @@ import (
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/tracing"
pluginfakes "github.com/grafana/grafana/pkg/plugins/manager/fakes"
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
"github.com/grafana/grafana/pkg/services/annotations/annotationstest"
@ -484,7 +485,8 @@ func setupEnv(t *testing.T, sqlStore db.DB, cfg *setting.Cfg, b bus.Bus, quotaSe
require.NoError(t, err)
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
_, err = dsservice.ProvideService(sqlStore, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService, &pluginstore.FakePluginStore{})
_, err = dsservice.ProvideService(sqlStore, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(),
quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{})
require.NoError(t, err)
m := metrics.NewNGAlert(prometheus.NewRegistry())

View File

@ -10,6 +10,7 @@ import (
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/kvstore"
"github.com/grafana/grafana/pkg/infra/log"
pluginfakes "github.com/grafana/grafana/pkg/plugins/manager/fakes"
acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
"github.com/grafana/grafana/pkg/services/datasources"
dsservice "github.com/grafana/grafana/pkg/services/datasources/service"
@ -36,7 +37,8 @@ func SetupTestDataSourceSecretMigrationService(t *testing.T, sqlStore db.DB, kvS
}
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
quotaService := quotatest.New(false, nil)
dsService, err := dsservice.ProvideService(sqlStore, secretsService, secretsStore, cfg, features, acmock.New(), acmock.NewMockedPermissionsService(), quotaService, &pluginstore.FakePluginStore{})
dsService, err := dsservice.ProvideService(sqlStore, secretsService, secretsStore, cfg, features, acmock.New(),
acmock.NewMockedPermissionsService(), quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{})
require.NoError(t, err)
migService := ProvideDataSourceMigrationService(dsService, kvStore, features)
return migService

View File

@ -846,6 +846,7 @@ type testPlugin struct {
backend.CallResourceHandler
backend.QueryDataHandler
backend.StreamHandler
backend.AdmissionHandler
}
func (tp *testPlugin) PluginID() string {
@ -933,6 +934,33 @@ func (tp *testPlugin) RunStream(ctx context.Context, req *backend.RunStreamReque
return plugins.ErrMethodNotImplemented
}
// ValidateAdmission implements backend.AdmissionHandler.
func (tp *testPlugin) ValidateAdmission(ctx context.Context, req *backend.AdmissionRequest) (*backend.ValidationResponse, error) {
if tp.AdmissionHandler != nil {
return tp.AdmissionHandler.ValidateAdmission(ctx, req)
}
return nil, plugins.ErrMethodNotImplemented
}
// MutateAdmission implements backend.AdmissionHandler.
func (tp *testPlugin) MutateAdmission(ctx context.Context, req *backend.AdmissionRequest) (*backend.MutationResponse, error) {
if tp.AdmissionHandler != nil {
return tp.AdmissionHandler.MutateAdmission(ctx, req)
}
return nil, plugins.ErrMethodNotImplemented
}
// ConvertObject implements backend.AdmissionHandler.
func (tp *testPlugin) ConvertObject(ctx context.Context, req *backend.ConversionRequest) (*backend.ConversionResponse, error) {
if tp.AdmissionHandler != nil {
return tp.AdmissionHandler.ConvertObject(ctx, req)
}
return nil, plugins.ErrMethodNotImplemented
}
func metricRequestWithQueries(t *testing.T, rawQueries ...string) dtos.MetricRequest {
t.Helper()
queries := make([]*simplejson.Json, 0)

View File

@ -0,0 +1,87 @@
package testdatasource
import (
"context"
"encoding/json"
"fmt"
"net/http"
"github.com/grafana/grafana-plugin-sdk-go/backend"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// ValidateAdmission implements backend.AdmissionHandler.
func (s *Service) ValidateAdmission(ctx context.Context, req *backend.AdmissionRequest) (*backend.ValidationResponse, error) {
rsp, err := s.MutateAdmission(ctx, req)
if rsp != nil {
return &backend.ValidationResponse{
Allowed: rsp.Allowed,
Result: rsp.Result,
Warnings: rsp.Warnings,
}, err
}
return nil, err
}
// MutateAdmission implements backend.AdmissionHandler.
func (s *Service) MutateAdmission(ctx context.Context, req *backend.AdmissionRequest) (*backend.MutationResponse, error) {
expected := (&backend.DataSourceInstanceSettings{}).GVK()
if req.Kind.Kind != expected.Kind && req.Kind.Group != expected.Group {
return getBadRequest("expected DataSourceInstanceSettings protobuf payload"), nil
}
// Convert the payload from protobuf to an SDK struct
settings, err := backend.DataSourceInstanceSettingsFromProto(req.ObjectBytes, "")
if err != nil {
return nil, err
}
if settings == nil {
return getBadRequest("missing datasource settings"), nil
}
switch settings.APIVersion {
case "", "v0alpha1":
// OK!
default:
return getBadRequest(fmt.Sprintf("expected apiVersion: v0alpha1, found: %s", settings.APIVersion)), nil
}
if settings.JSONData != nil {
anything := map[string]any{}
err := json.Unmarshal(settings.JSONData, &anything)
if err != nil || len(anything) > 0 {
return getBadRequest("Expected empty jsonData settings"), nil
}
}
if len(settings.DecryptedSecureJSONData) > 0 {
return getBadRequest("found unsupported secure json fields"), nil
}
if settings.URL != "" {
return getBadRequest("unsupported URL value"), nil
}
if settings.User != "" {
return getBadRequest("unsupported User value"), nil
}
pb, err := backend.DataSourceInstanceSettingsToProtoBytes(settings)
return &backend.MutationResponse{
Allowed: true,
ObjectBytes: pb,
}, err
}
// ConvertObject implements backend.AdmissionHandler.
func (s *Service) ConvertObject(ctx context.Context, req *backend.ConversionRequest) (*backend.ConversionResponse, error) {
return nil, fmt.Errorf("not implemented")
}
func getBadRequest(msg string) *backend.MutationResponse {
return &backend.MutationResponse{
Allowed: false,
Result: &backend.StatusResult{
Status: "Failure",
Message: msg,
Reason: string(metav1.StatusReasonBadRequest),
Code: http.StatusBadRequest,
},
}
}

View File

@ -0,0 +1,68 @@
package testdatasource
import (
"context"
"encoding/json"
"testing"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/stretchr/testify/require"
)
func TestSettingsHandler(t *testing.T) {
svc := &Service{}
require.NotNil(t, svc)
// Check missing datasource
s, err := svc.MutateAdmission(context.Background(), &backend.AdmissionRequest{
PluginContext: backend.PluginContext{},
})
require.NoError(t, err)
require.False(t, s.Allowed)
require.Equal(t, int32(400), s.Result.Code)
// Empty is OK
s, _ = svc.MutateAdmission(context.Background(),
asAdmissionRequest(&backend.DataSourceInstanceSettings{
APIVersion: "v0alpha1",
}))
require.True(t, s.Allowed)
// Any values should be an error
s, err = svc.MutateAdmission(context.Background(),
asAdmissionRequest(&backend.DataSourceInstanceSettings{
JSONData: json.RawMessage(`{"hello": "world"}`), // Settings must be empty
}))
require.NoError(t, err)
require.False(t, s.Allowed)
require.Equal(t, int32(400), s.Result.Code)
// Any values should be an error
s, err = svc.MutateAdmission(context.Background(),
asAdmissionRequest(&backend.DataSourceInstanceSettings{
DecryptedSecureJSONData: map[string]string{
"A": "Value",
},
}))
require.NoError(t, err)
require.False(t, s.Allowed)
require.Equal(t, int32(400), s.Result.Code)
// Invalid API Version
s, err = svc.MutateAdmission(context.Background(),
asAdmissionRequest(&backend.DataSourceInstanceSettings{
APIVersion: "v1234",
}))
require.NoError(t, err)
require.False(t, s.Allowed)
require.Equal(t, int32(400), s.Result.Code)
}
func asAdmissionRequest(settings *backend.DataSourceInstanceSettings) *backend.AdmissionRequest {
req := &backend.AdmissionRequest{}
if settings != nil {
req.Kind = settings.GVK()
req.ObjectBytes, _ = backend.DataSourceInstanceSettingsToProtoBytes(settings)
}
return req
}

View File

@ -53,13 +53,17 @@
"type"
],
"properties": {
"apiVersion": {
"description": "The apiserver version",
"type": "string"
},
"type": {
"description": "The datasource plugin type",
"type": "string",
"pattern": "^grafana-testdata-datasource$|^testdata$"
},
"uid": {
"description": "Datasource UID",
"description": "Datasource UID (NOTE: name in k8s)",
"type": "string"
}
},

View File

@ -63,13 +63,17 @@
"type"
],
"properties": {
"apiVersion": {
"description": "The apiserver version",
"type": "string"
},
"type": {
"description": "The datasource plugin type",
"type": "string",
"pattern": "^grafana-testdata-datasource$|^testdata$"
},
"uid": {
"description": "Datasource UID",
"description": "Datasource UID (NOTE: name in k8s)",
"type": "string"
}
},

View File

@ -47,6 +47,13 @@ func ProvideService() *Service {
return s
}
var (
_ backend.QueryDataHandler = (*Service)(nil)
_ backend.CallResourceHandler = (*Service)(nil)
_ backend.AdmissionHandler = (*Service)(nil)
_ backend.CollectMetricsHandler = (*Service)(nil)
)
type Service struct {
logger log.Logger
scenarios map[kinds.TestDataQueryType]*Scenario

View File

@ -12,6 +12,7 @@ import (
"github.com/grafana/grafana/pkg/infra/localcache"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/plugins"
pluginfakes "github.com/grafana/grafana/pkg/plugins/manager/fakes"
acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
"github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/datasources/guardian"
@ -48,7 +49,8 @@ func TestHandleRequest(t *testing.T) {
datasourcePermissions := acmock.NewMockedPermissionsService()
quotaService := quotatest.New(false, nil)
dsCache := datasourceservice.ProvideCacheService(localcache.ProvideService(), sqlStore, guardian.ProvideGuardian())
dsService, err := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), datasourcePermissions, quotaService, &pluginstore.FakePluginStore{})
dsService, err := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(),
acmock.New(), datasourcePermissions, quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{})
require.NoError(t, err)
pCtxProvider := plugincontext.ProvideService(cfg, localcache.ProvideService(), &pluginstore.FakePluginStore{