Files
grafana/pkg/plugins/backendplugin/backend_plugin.go
Marcus Efraimsson 871ad73414 Backend plugins: Renderer v2 plugin (#23625)
grafana-plugin-model is legacy and is replaced by new backend 
plugins SDK and architecture. Renderer is not part of SDK and 
we want to keep it that way for now since it's highly unlikely there 
will be more than one kind of renderer plugin.
So this PR adds support for renderer plugin v2.
Also adds support sending a Device Scale Factor parameter to the 
plugin v2 remote rendering service and by that replaces #22474.
Adds support sending a Headers parameter to the plugin v2 and
remote rendering service which for now only include 
Accect-Language header (the user locale in browser when using 
Grafana), ref grafana/grafana-image-renderer#45.
Fixes health check json details response.
Adds image renderer plugin configuration settings in defaults.ini 
and sample.ini.

Co-Authored-By: Arve Knudsen <arve.knudsen@gmail.com>
2020-04-21 16:16:41 +02:00

320 lines
8.6 KiB
Go

package backendplugin
import (
"context"
"errors"
"net/http"
"time"
datasourceV1 "github.com/grafana/grafana-plugin-model/go/datasource"
rendererV1 "github.com/grafana/grafana-plugin-model/go/renderer"
"github.com/grafana/grafana-plugin-sdk-go/genproto/pluginv2"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/plugins/backendplugin/pluginextensionv2"
"github.com/grafana/grafana/pkg/util/errutil"
plugin "github.com/hashicorp/go-plugin"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
// BackendPlugin a registered backend plugin.
type BackendPlugin struct {
id string
executablePath string
managed bool
clientFactory func() *plugin.Client
client *plugin.Client
logger log.Logger
startFns PluginStartFuncs
diagnostics DiagnosticsPlugin
resource ResourcePlugin
}
func (p *BackendPlugin) start(ctx context.Context) error {
p.client = p.clientFactory()
rpcClient, err := p.client.Client()
if err != nil {
return err
}
var legacyClient *LegacyClient
var client *Client
if p.client.NegotiatedVersion() > 1 {
rawDiagnostics, err := rpcClient.Dispense("diagnostics")
if err != nil {
return err
}
rawResource, err := rpcClient.Dispense("resource")
if err != nil {
return err
}
rawData, err := rpcClient.Dispense("data")
if err != nil {
return err
}
rawTransform, err := rpcClient.Dispense("transform")
if err != nil {
return err
}
rawRenderer, err := rpcClient.Dispense("renderer")
if err != nil {
return err
}
if rawDiagnostics != nil {
if plugin, ok := rawDiagnostics.(DiagnosticsPlugin); ok {
p.diagnostics = plugin
}
}
client = &Client{}
if rawResource != nil {
if plugin, ok := rawResource.(ResourcePlugin); ok {
p.resource = plugin
client.ResourcePlugin = plugin
}
}
if rawData != nil {
if plugin, ok := rawData.(DataPlugin); ok {
client.DataPlugin = plugin
}
}
if rawTransform != nil {
if plugin, ok := rawTransform.(TransformPlugin); ok {
client.TransformPlugin = plugin
}
}
if rawRenderer != nil {
if plugin, ok := rawRenderer.(pluginextensionv2.RendererPlugin); ok {
client.RendererPlugin = plugin
}
}
} else {
raw, err := rpcClient.Dispense(p.id)
if err != nil {
return err
}
legacyClient = &LegacyClient{}
if plugin, ok := raw.(datasourceV1.DatasourcePlugin); ok {
legacyClient.DatasourcePlugin = plugin
}
if plugin, ok := raw.(rendererV1.RendererPlugin); ok {
legacyClient.RendererPlugin = plugin
}
}
if legacyClient == nil && client == nil {
return errors.New("no compatible plugin implementation found")
}
if legacyClient != nil && p.startFns.OnLegacyStart != nil {
if err := p.startFns.OnLegacyStart(p.id, legacyClient, p.logger); err != nil {
return err
}
}
if client != nil && p.startFns.OnStart != nil {
if err := p.startFns.OnStart(p.id, client, p.logger); err != nil {
return err
}
}
return nil
}
func (p *BackendPlugin) stop() error {
if p.client != nil {
p.client.Kill()
}
return nil
}
// supportsDiagnostics return whether backend plugin supports diagnostics like metrics and health check.
func (p *BackendPlugin) supportsDiagnostics() bool {
return p.diagnostics != nil
}
// CollectMetrics implements the collector.Collector interface.
func (p *BackendPlugin) CollectMetrics(ctx context.Context) (*pluginv2.CollectMetricsResponse, error) {
if p.diagnostics == nil || p.client == nil || p.client.Exited() {
return &pluginv2.CollectMetricsResponse{
Metrics: &pluginv2.CollectMetricsResponse_Payload{},
}, nil
}
var res *pluginv2.CollectMetricsResponse
err := InstrumentPluginRequest(p.id, "metrics", func() error {
var innerErr error
res, innerErr = p.diagnostics.CollectMetrics(ctx, &pluginv2.CollectMetricsRequest{})
return innerErr
})
if err != nil {
if st, ok := status.FromError(err); ok {
if st.Code() == codes.Unimplemented {
return &pluginv2.CollectMetricsResponse{
Metrics: &pluginv2.CollectMetricsResponse_Payload{},
}, nil
}
}
return nil, err
}
return res, nil
}
func (p *BackendPlugin) checkHealth(ctx context.Context, config *PluginConfig) (*pluginv2.CheckHealthResponse, error) {
if p.diagnostics == nil || p.client == nil || p.client.Exited() {
return &pluginv2.CheckHealthResponse{
Status: pluginv2.CheckHealthResponse_UNKNOWN,
}, nil
}
jsonDataBytes, err := config.JSONData.ToDB()
if err != nil {
return nil, err
}
pconfig := &pluginv2.PluginConfig{
OrgId: config.OrgID,
PluginId: config.PluginID,
JsonData: jsonDataBytes,
DecryptedSecureJsonData: config.DecryptedSecureJSONData,
LastUpdatedMS: config.Updated.UnixNano() / int64(time.Millisecond),
}
if config.DataSourceConfig != nil {
datasourceJSONData, err := config.DataSourceConfig.JSONData.ToDB()
if err != nil {
return nil, err
}
pconfig.DatasourceConfig = &pluginv2.DataSourceConfig{
Id: config.DataSourceConfig.ID,
Name: config.DataSourceConfig.Name,
Url: config.DataSourceConfig.URL,
User: config.DataSourceConfig.User,
Database: config.DataSourceConfig.Database,
BasicAuthEnabled: config.DataSourceConfig.BasicAuthEnabled,
BasicAuthUser: config.DataSourceConfig.BasicAuthUser,
JsonData: datasourceJSONData,
DecryptedSecureJsonData: config.DataSourceConfig.DecryptedSecureJSONData,
LastUpdatedMS: config.DataSourceConfig.Updated.Unix() / int64(time.Millisecond),
}
}
var res *pluginv2.CheckHealthResponse
err = InstrumentPluginRequest(p.id, "checkhealth", func() error {
var innerErr error
res, innerErr = p.diagnostics.CheckHealth(ctx, &pluginv2.CheckHealthRequest{Config: pconfig})
return innerErr
})
if err != nil {
if st, ok := status.FromError(err); ok {
if st.Code() == codes.Unimplemented {
return &pluginv2.CheckHealthResponse{
Status: pluginv2.CheckHealthResponse_UNKNOWN,
Message: "Health check not implemented",
}, nil
}
}
return nil, err
}
return res, nil
}
func (p *BackendPlugin) callResource(ctx context.Context, req CallResourceRequest) (callResourceResultStream, error) {
p.logger.Debug("Calling resource", "path", req.Path, "method", req.Method)
if p.resource == nil || p.client == nil || p.client.Exited() {
return nil, errors.New("plugin not running, cannot call resource")
}
reqHeaders := map[string]*pluginv2.StringList{}
for k, v := range req.Headers {
reqHeaders[k] = &pluginv2.StringList{Values: v}
}
jsonDataBytes, err := req.Config.JSONData.ToDB()
if err != nil {
return nil, err
}
protoReq := &pluginv2.CallResourceRequest{
Config: &pluginv2.PluginConfig{
OrgId: req.Config.OrgID,
PluginId: req.Config.PluginID,
JsonData: jsonDataBytes,
DecryptedSecureJsonData: req.Config.DecryptedSecureJSONData,
LastUpdatedMS: req.Config.Updated.UnixNano() / int64(time.Millisecond),
},
Path: req.Path,
Method: req.Method,
Url: req.URL,
Headers: reqHeaders,
Body: req.Body,
}
if req.User != nil {
protoReq.User = &pluginv2.User{
Name: req.User.Name,
Login: req.User.Login,
Email: req.User.Email,
Role: string(req.User.OrgRole),
}
}
if req.Config.DataSourceConfig != nil {
datasourceJSONData, err := req.Config.DataSourceConfig.JSONData.ToDB()
if err != nil {
return nil, err
}
protoReq.Config.DatasourceConfig = &pluginv2.DataSourceConfig{
Id: req.Config.DataSourceConfig.ID,
Name: req.Config.DataSourceConfig.Name,
Url: req.Config.DataSourceConfig.URL,
Database: req.Config.DataSourceConfig.Database,
User: req.Config.DataSourceConfig.User,
BasicAuthEnabled: req.Config.DataSourceConfig.BasicAuthEnabled,
BasicAuthUser: req.Config.DataSourceConfig.BasicAuthUser,
JsonData: datasourceJSONData,
DecryptedSecureJsonData: req.Config.DataSourceConfig.DecryptedSecureJSONData,
LastUpdatedMS: req.Config.DataSourceConfig.Updated.UnixNano() / int64(time.Millisecond),
}
}
protoStream, err := p.resource.CallResource(ctx, protoReq)
if err != nil {
if st, ok := status.FromError(err); ok {
if st.Code() == codes.Unimplemented {
return &singleCallResourceResult{
result: &CallResourceResult{
Status: http.StatusNotImplemented,
},
}, nil
}
}
return nil, errutil.Wrap("Failed to call resource", err)
}
return &callResourceResultStreamImpl{
stream: protoStream,
}, nil
}