Basic streaming plugin support (#31940)

This pull request migrates testdata to coreplugin streaming capabilities,
this is mostly a working concept of streaming plugins at the moment, 
the work will continue in the following pull requests.
This commit is contained in:
Alexander Emelin
2021-03-23 20:24:08 +03:00
committed by GitHub
parent 1cd8981be4
commit 336bc559a3
31 changed files with 1204 additions and 290 deletions

View File

@ -19,6 +19,7 @@ type corePlugin struct {
backend.CheckHealthHandler
backend.CallResourceHandler
backend.QueryDataHandler
backend.StreamHandler
}
// New returns a new backendplugin.PluginFactoryFunc for creating a core (built-in) backendplugin.Plugin.
@ -30,6 +31,7 @@ func New(opts backend.ServeOpts) backendplugin.PluginFactoryFunc {
CheckHealthHandler: opts.CheckHealthHandler,
CallResourceHandler: opts.CallResourceHandler,
QueryDataHandler: opts.QueryDataHandler,
StreamHandler: opts.StreamHandler,
}, nil
}
}
@ -55,9 +57,7 @@ func (cp *corePlugin) DataQuery(ctx context.Context, dsInfo *models.DataSource,
}
func (cp *corePlugin) Start(ctx context.Context) error {
if cp.QueryDataHandler != nil {
cp.isDataPlugin = true
}
cp.isDataPlugin = cp.QueryDataHandler != nil
return nil
}
@ -92,3 +92,17 @@ func (cp *corePlugin) CallResource(ctx context.Context, req *backend.CallResourc
return backendplugin.ErrMethodNotImplemented
}
func (cp *corePlugin) CanSubscribeToStream(ctx context.Context, req *backend.SubscribeToStreamRequest) (*backend.SubscribeToStreamResponse, error) {
if cp.StreamHandler != nil {
return cp.StreamHandler.CanSubscribeToStream(ctx, req)
}
return nil, backendplugin.ErrMethodNotImplemented
}
func (cp *corePlugin) RunStream(ctx context.Context, req *backend.RunStreamRequest, sender backend.StreamPacketSender) error {
if cp.StreamHandler != nil {
return cp.StreamHandler.RunStream(ctx, req, sender)
}
return backendplugin.ErrMethodNotImplemented
}

View File

@ -74,6 +74,7 @@ func getV2PluginSet() goplugin.PluginSet {
"diagnostics": &grpcplugin.DiagnosticsGRPCPlugin{},
"resource": &grpcplugin.ResourceGRPCPlugin{},
"data": &grpcplugin.DataGRPCPlugin{},
"stream": &grpcplugin.StreamGRPCPlugin{},
"renderer": &pluginextensionv2.RendererGRPCPlugin{},
}
}
@ -120,4 +121,5 @@ type LegacyClient struct {
type Client struct {
DataPlugin grpcplugin.DataClient
RendererPlugin pluginextensionv2.RendererPlugin
StreamClient grpcplugin.StreamClient
}

View File

@ -63,6 +63,14 @@ func (c *clientV1) CallResource(ctx context.Context, req *backend.CallResourceRe
return backendplugin.ErrMethodNotImplemented
}
func (c *clientV1) CanSubscribeToStream(ctx context.Context, request *backend.SubscribeToStreamRequest) (*backend.SubscribeToStreamResponse, error) {
return nil, backendplugin.ErrMethodNotImplemented
}
func (c *clientV1) RunStream(ctx context.Context, request *backend.RunStreamRequest, sender backend.StreamPacketSender) error {
return backendplugin.ErrMethodNotImplemented
}
type datasourceV1QueryFunc func(ctx context.Context, req *datasourceV1.DatasourceRequest) (*datasourceV1.DatasourceResponse, error)
func (fn datasourceV1QueryFunc) Query(ctx context.Context, req *datasourceV1.DatasourceRequest) (*datasourceV1.DatasourceResponse, error) {

View File

@ -23,6 +23,7 @@ type clientV2 struct {
grpcplugin.DiagnosticsClient
grpcplugin.ResourceClient
grpcplugin.DataClient
grpcplugin.StreamClient
pluginextensionv2.RendererPlugin
}
@ -42,6 +43,11 @@ func newClientV2(descriptor PluginDescriptor, logger log.Logger, rpcClient plugi
return nil, err
}
rawStream, err := rpcClient.Dispense("stream")
if err != nil {
return nil, err
}
rawRenderer, err := rpcClient.Dispense("renderer")
if err != nil {
return nil, err
@ -66,6 +72,12 @@ func newClientV2(descriptor PluginDescriptor, logger log.Logger, rpcClient plugi
}
}
if rawStream != nil {
if plugin, ok := rawStream.(grpcplugin.StreamClient); ok {
c.StreamClient = plugin
}
}
if rawRenderer != nil {
if plugin, ok := rawRenderer.(pluginextensionv2.RendererPlugin); ok {
c.RendererPlugin = plugin
@ -76,6 +88,7 @@ func newClientV2(descriptor PluginDescriptor, logger log.Logger, rpcClient plugi
client := &Client{
DataPlugin: c.DataClient,
RendererPlugin: c.RendererPlugin,
StreamClient: c.StreamClient,
}
if err := descriptor.startFns.OnStart(descriptor.pluginID, client, logger); err != nil {
return nil, err
@ -158,6 +171,48 @@ func (c *clientV2) CallResource(ctx context.Context, req *backend.CallResourceRe
}
}
func (c *clientV2) CanSubscribeToStream(ctx context.Context, req *backend.SubscribeToStreamRequest) (*backend.SubscribeToStreamResponse, error) {
if c.StreamClient == nil {
return nil, backendplugin.ErrMethodNotImplemented
}
protoResp, err := c.StreamClient.CanSubscribeToStream(ctx, backend.ToProto().SubscribeToStreamRequest(req))
if err != nil {
return nil, err
}
return backend.FromProto().SubscribeToStreamResponse(protoResp), nil
}
func (c *clientV2) RunStream(ctx context.Context, req *backend.RunStreamRequest, sender backend.StreamPacketSender) error {
if c.StreamClient == nil {
return backendplugin.ErrMethodNotImplemented
}
protoReq := backend.ToProto().RunStreamRequest(req)
protoStream, err := c.StreamClient.RunStream(ctx, protoReq)
if err != nil {
if status.Code(err) == codes.Unimplemented {
return backendplugin.ErrMethodNotImplemented
}
return errutil.Wrap("Failed to call resource", err)
}
for {
protoResp, err := protoStream.Recv()
if err != nil {
if status.Code(err) == codes.Unimplemented {
return backendplugin.ErrMethodNotImplemented
}
if errors.Is(err, io.EOF) {
return nil
}
return errutil.Wrap("failed to receive call resource response", err)
}
if err := sender.Send(backend.FromProto().StreamPacket(protoResp)); err != nil {
return err
}
}
}
type dataClientQueryDataFunc func(ctx context.Context, req *pluginv2.QueryDataRequest, opts ...grpc.CallOption) (*pluginv2.QueryDataResponse, error)
func (fn dataClientQueryDataFunc) QueryData(ctx context.Context, req *pluginv2.QueryDataRequest, opts ...grpc.CallOption) (*pluginv2.QueryDataResponse, error) {

View File

@ -15,6 +15,7 @@ type pluginClient interface {
backend.CollectMetricsHandler
backend.CheckHealthHandler
backend.CallResourceHandler
backend.StreamHandler
}
type grpcPlugin struct {
@ -138,3 +139,27 @@ func (p *grpcPlugin) CallResource(ctx context.Context, req *backend.CallResource
return pluginClient.CallResource(ctx, req, sender)
}
func (p *grpcPlugin) CanSubscribeToStream(ctx context.Context, request *backend.SubscribeToStreamRequest) (*backend.SubscribeToStreamResponse, error) {
p.mutex.RLock()
if p.client == nil || p.client.Exited() || p.pluginClient == nil {
p.mutex.RUnlock()
return nil, backendplugin.ErrPluginUnavailable
}
pluginClient := p.pluginClient
p.mutex.RUnlock()
return pluginClient.CanSubscribeToStream(ctx, request)
}
func (p *grpcPlugin) RunStream(ctx context.Context, req *backend.RunStreamRequest, sender backend.StreamPacketSender) error {
p.mutex.RLock()
if p.client == nil || p.client.Exited() || p.pluginClient == nil {
p.mutex.RUnlock()
return backendplugin.ErrPluginUnavailable
}
pluginClient := p.pluginClient
p.mutex.RUnlock()
return pluginClient.RunStream(ctx, req, sender)
}

View File

@ -20,6 +20,8 @@ type Manager interface {
CheckHealth(ctx context.Context, pCtx backend.PluginContext) (*backend.CheckHealthResult, error)
// CallResource calls a plugin resource.
CallResource(pluginConfig backend.PluginContext, ctx *models.ReqContext, path string)
// Get plugin by its ID.
Get(pluginID string) (Plugin, bool)
// GetDataPlugin gets a DataPlugin with a certain ID or nil if it doesn't exist.
// TODO: interface{} is the return type in order to break a dependency cycle. Should be plugins.DataPlugin.
GetDataPlugin(pluginID string) interface{}
@ -37,4 +39,5 @@ type Plugin interface {
backend.CollectMetricsHandler
backend.CheckHealthHandler
backend.CallResourceHandler
backend.StreamHandler
}

View File

@ -96,6 +96,11 @@ func (m *manager) Register(pluginID string, factory backendplugin.PluginFactoryF
return nil
}
func (m *manager) Get(pluginID string) (backendplugin.Plugin, bool) {
p, ok := m.plugins[pluginID]
return p, ok
}
func (m *manager) getAWSEnvironmentVariables() []string {
variables := []string{}
if m.Cfg.AWSAssumeRoleEnabled {

View File

@ -396,6 +396,14 @@ func (tp *testPlugin) CallResource(ctx context.Context, req *backend.CallResourc
return backendplugin.ErrMethodNotImplemented
}
func (tp *testPlugin) CanSubscribeToStream(ctx context.Context, request *backend.SubscribeToStreamRequest) (*backend.SubscribeToStreamResponse, error) {
return nil, backendplugin.ErrMethodNotImplemented
}
func (tp *testPlugin) RunStream(ctx context.Context, request *backend.RunStreamRequest, sender backend.StreamPacketSender) error {
return backendplugin.ErrMethodNotImplemented
}
type testLicensingService struct {
edition string
hasLicense bool

View File

@ -14,8 +14,8 @@ import (
"github.com/grafana/grafana/pkg/services/oauthtoken"
)
func newDataSourcePluginWrapperV2(log log.Logger, pluginId, pluginType string, client grpcplugin.DataClient) *DatasourcePluginWrapperV2 {
return &DatasourcePluginWrapperV2{DataClient: client, logger: log, pluginId: pluginId, pluginType: pluginType}
func newDataSourcePluginWrapperV2(log log.Logger, pluginId, pluginType string, dataClient grpcplugin.DataClient) *DatasourcePluginWrapperV2 {
return &DatasourcePluginWrapperV2{DataClient: dataClient, logger: log, pluginId: pluginId, pluginType: pluginType}
}
type DatasourcePluginWrapperV2 struct {

View File

@ -0,0 +1,118 @@
package plugincontext
import (
"encoding/json"
"errors"
"time"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/infra/localcache"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/adapters"
"github.com/grafana/grafana/pkg/registry"
"github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/util/errutil"
)
func init() {
registry.Register(&registry.Descriptor{
Name: "PluginContextProvider",
Instance: newProvider(),
})
}
func newProvider() *Provider {
return &Provider{}
}
type Provider struct {
Bus bus.Bus `inject:""`
CacheService *localcache.CacheService `inject:""`
PluginManager plugins.Manager `inject:""`
DatasourceCache datasources.CacheService `inject:""`
}
func (p *Provider) Init() error {
return nil
}
// Get allows getting plugin context by its id. If datasourceUID is not empty string
// then PluginContext.DataSourceInstanceSettings will be resolved and appended to
// returned context.
func (p *Provider) Get(pluginID string, datasourceUID string, user *models.SignedInUser) (backend.PluginContext, bool, error) {
pc := backend.PluginContext{}
plugin := p.PluginManager.GetPlugin(pluginID)
if plugin == nil {
return pc, false, nil
}
jsonData := json.RawMessage{}
decryptedSecureJSONData := map[string]string{}
var updated time.Time
ps, err := p.getCachedPluginSettings(pluginID, user)
if err != nil {
// models.ErrPluginSettingNotFound is expected if there's no row found for plugin setting in database (if non-app plugin).
// If it's not this expected error something is wrong with cache or database and we return the error to the client.
if !errors.Is(err, models.ErrPluginSettingNotFound) {
return pc, false, errutil.Wrap("Failed to get plugin settings", err)
}
} else {
jsonData, err = json.Marshal(ps.JsonData)
if err != nil {
return pc, false, errutil.Wrap("Failed to unmarshal plugin json data", err)
}
decryptedSecureJSONData = ps.DecryptedValues()
updated = ps.Updated
}
pCtx := backend.PluginContext{
OrgID: user.OrgId,
PluginID: plugin.Id,
User: adapters.BackendUserFromSignedInUser(user),
AppInstanceSettings: &backend.AppInstanceSettings{
JSONData: jsonData,
DecryptedSecureJSONData: decryptedSecureJSONData,
Updated: updated,
},
}
if datasourceUID != "" {
ds, err := p.DatasourceCache.GetDatasourceByUID(datasourceUID, user, false)
if err != nil {
return pc, false, errutil.Wrap("Failed to get datasource", err)
}
datasourceSettings, err := adapters.ModelToInstanceSettings(ds)
if err != nil {
return pc, false, errutil.Wrap("Failed to convert datasource", err)
}
pCtx.DataSourceInstanceSettings = datasourceSettings
}
return pCtx, true, nil
}
const pluginSettingsCacheTTL = 5 * time.Second
const pluginSettingsCachePrefix = "plugin-setting-"
func (p *Provider) getCachedPluginSettings(pluginID string, user *models.SignedInUser) (*models.PluginSetting, error) {
cacheKey := pluginSettingsCachePrefix + pluginID
if cached, found := p.CacheService.Get(cacheKey); found {
ps := cached.(*models.PluginSetting)
if ps.OrgId == user.OrgId {
return ps, nil
}
}
query := models.GetPluginSettingByIdQuery{PluginId: pluginID, OrgId: user.OrgId}
if err := p.Bus.Dispatch(&query); err != nil {
return nil, err
}
p.CacheService.Set(cacheKey, query.Result, pluginSettingsCacheTTL)
return query.Result, nil
}