mirror of
https://github.com/grafana/grafana.git
synced 2025-08-03 02:02:12 +08:00
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:
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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) {
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
118
pkg/plugins/plugincontext/plugincontext.go
Normal file
118
pkg/plugins/plugincontext/plugincontext.go
Normal 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(®istry.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
|
||||
}
|
Reference in New Issue
Block a user