Backend plugins: Refactor to allow shared contract between core and external backend plugins (#25472)

Refactor to allow shared contract between core and external backend plugins 
allowing core backend data sources in Grafana to be implemented in same 
way as an external backend plugin.
Use v0.67.0 of sdk.
Add tests for verifying plugin is restarted when process is killed.
Enable strict linting for backendplugin packages
This commit is contained in:
Marcus Efraimsson
2020-06-11 16:14:05 +02:00
committed by GitHub
parent 40b3473a10
commit c0f3b2929c
29 changed files with 1495 additions and 612 deletions
@@ -0,0 +1,81 @@
package coreplugin
import (
"context"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins/backendplugin"
"github.com/grafana/grafana/pkg/tsdb"
)
type corePlugin struct {
pluginID string
logger log.Logger
backend.CheckHealthHandler
backend.CallResourceHandler
backend.QueryDataHandler
}
// New returns a new backendplugin.PluginFactoryFunc for creating a core (built-in) backendplugin.Plugin.
func New(opts backend.ServeOpts) backendplugin.PluginFactoryFunc {
return backendplugin.PluginFactoryFunc(func(pluginID string, logger log.Logger, env []string) (backendplugin.Plugin, error) {
return &corePlugin{
pluginID: pluginID,
logger: logger,
CheckHealthHandler: opts.CheckHealthHandler,
CallResourceHandler: opts.CallResourceHandler,
QueryDataHandler: opts.QueryDataHandler,
}, nil
})
}
func (cp *corePlugin) PluginID() string {
return cp.pluginID
}
func (cp *corePlugin) Logger() log.Logger {
return cp.logger
}
func (cp *corePlugin) Start(ctx context.Context) error {
if cp.QueryDataHandler != nil {
tsdb.RegisterTsdbQueryEndpoint(cp.pluginID, func(dsInfo *models.DataSource) (tsdb.TsdbQueryEndpoint, error) {
return newQueryEndpointAdapter(cp.pluginID, cp.logger, backendplugin.InstrumentQueryDataHandler(cp.QueryDataHandler)), nil
})
}
return nil
}
func (cp *corePlugin) Stop(ctx context.Context) error {
return nil
}
func (cp *corePlugin) IsManaged() bool {
return false
}
func (cp *corePlugin) Exited() bool {
return false
}
func (cp *corePlugin) CollectMetrics(ctx context.Context) (*backend.CollectMetricsResult, error) {
return nil, backendplugin.ErrMethodNotImplemented
}
func (cp *corePlugin) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) {
if cp.CheckHealthHandler != nil {
return cp.CheckHealthHandler.CheckHealth(ctx, req)
}
return nil, backendplugin.ErrMethodNotImplemented
}
func (cp *corePlugin) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error {
if cp.CallResourceHandler != nil {
return cp.CallResourceHandler.CallResource(ctx, req, sender)
}
return backendplugin.ErrMethodNotImplemented
}
@@ -0,0 +1,67 @@
package coreplugin_test
import (
"context"
"testing"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/plugins/backendplugin"
"github.com/grafana/grafana/pkg/plugins/backendplugin/coreplugin"
"github.com/stretchr/testify/require"
)
func TestCorePlugin(t *testing.T) {
t.Run("New core plugin with empty opts should return expected values", func(t *testing.T) {
factory := coreplugin.New(backend.ServeOpts{})
p, err := factory("plugin", log.New("test"), nil)
require.NoError(t, err)
require.NotNil(t, p)
require.NoError(t, p.Start(context.Background()))
require.NoError(t, p.Stop(context.Background()))
require.False(t, p.IsManaged())
require.False(t, p.Exited())
_, err = p.CollectMetrics(context.Background())
require.Equal(t, backendplugin.ErrMethodNotImplemented, err)
_, err = p.CheckHealth(context.Background(), nil)
require.Equal(t, backendplugin.ErrMethodNotImplemented, err)
err = p.CallResource(context.Background(), nil, nil)
require.Equal(t, backendplugin.ErrMethodNotImplemented, err)
})
t.Run("New core plugin with handlers set in opts should return expected values", func(t *testing.T) {
checkHealthCalled := false
callResourceCalled := false
factory := coreplugin.New(backend.ServeOpts{
CheckHealthHandler: backend.CheckHealthHandlerFunc(func(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) {
checkHealthCalled = true
return nil, nil
}),
CallResourceHandler: backend.CallResourceHandlerFunc(func(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error {
callResourceCalled = true
return nil
}),
})
p, err := factory("plugin", log.New("test"), nil)
require.NoError(t, err)
require.NotNil(t, p)
require.NoError(t, p.Start(context.Background()))
require.NoError(t, p.Stop(context.Background()))
require.False(t, p.IsManaged())
require.False(t, p.Exited())
_, err = p.CollectMetrics(context.Background())
require.Equal(t, backendplugin.ErrMethodNotImplemented, err)
_, err = p.CheckHealth(context.Background(), &backend.CheckHealthRequest{})
require.NoError(t, err)
require.True(t, checkHealthCalled)
err = p.CallResource(context.Background(), &backend.CallResourceRequest{}, nil)
require.NoError(t, err)
require.True(t, callResourceCalled)
})
}
@@ -0,0 +1,112 @@
package coreplugin
import (
"context"
"time"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins/datasource/wrapper"
"github.com/grafana/grafana/pkg/tsdb"
)
func newQueryEndpointAdapter(pluginID string, logger log.Logger, handler backend.QueryDataHandler) tsdb.TsdbQueryEndpoint {
return &queryEndpointAdapter{
pluginID: pluginID,
logger: logger,
handler: handler,
}
}
type queryEndpointAdapter struct {
pluginID string
logger log.Logger
handler backend.QueryDataHandler
}
func modelToInstanceSettings(ds *models.DataSource) (*backend.DataSourceInstanceSettings, error) {
jsonDataBytes, err := ds.JsonData.MarshalJSON()
if err != nil {
return nil, err
}
return &backend.DataSourceInstanceSettings{
ID: ds.Id,
Name: ds.Name,
URL: ds.Url,
Database: ds.Database,
User: ds.User,
BasicAuthEnabled: ds.BasicAuth,
BasicAuthUser: ds.BasicAuthUser,
JSONData: jsonDataBytes,
DecryptedSecureJSONData: ds.DecryptedValues(),
Updated: ds.Updated,
}, nil
}
func (a *queryEndpointAdapter) Query(ctx context.Context, ds *models.DataSource, query *tsdb.TsdbQuery) (*tsdb.Response, error) {
instanceSettings, err := modelToInstanceSettings(ds)
if err != nil {
return nil, err
}
req := &backend.QueryDataRequest{
PluginContext: backend.PluginContext{
OrgID: ds.OrgId,
PluginID: a.pluginID,
User: wrapper.BackendUserFromSignedInUser(query.User),
DataSourceInstanceSettings: instanceSettings,
},
Queries: []backend.DataQuery{},
}
for _, q := range query.Queries {
modelJSON, err := q.Model.MarshalJSON()
if err != nil {
return nil, err
}
req.Queries = append(req.Queries, backend.DataQuery{
RefID: q.RefId,
Interval: time.Duration(q.IntervalMs) * time.Millisecond,
MaxDataPoints: q.MaxDataPoints,
TimeRange: backend.TimeRange{
From: query.TimeRange.GetFromAsTimeUTC(),
To: query.TimeRange.GetToAsTimeUTC(),
},
QueryType: q.QueryType,
JSON: modelJSON,
})
}
resp, err := a.handler.QueryData(ctx, req)
if err != nil {
return nil, err
}
tR := &tsdb.Response{
Results: make(map[string]*tsdb.QueryResult, len(resp.Responses)),
}
for refID, r := range resp.Responses {
qr := &tsdb.QueryResult{
RefId: refID,
}
for _, f := range r.Frames {
if f.RefID == "" {
f.RefID = refID
}
}
qr.Dataframes = tsdb.NewDecodedDataFrames(r.Frames)
if r.Error != nil {
qr.Error = r.Error
}
tR.Results[refID] = qr
}
return tR, nil
}