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
+142 -98
View File
@@ -6,28 +6,31 @@ import (
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"sync"
"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/registry"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util/errutil"
"github.com/grafana/grafana/pkg/util/proxyutil"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/registry"
plugin "github.com/hashicorp/go-plugin"
"golang.org/x/xerrors"
)
var (
// ErrPluginNotRegistered error returned when plugin not registered.
ErrPluginNotRegistered = errors.New("Plugin not registered")
// ErrDiagnosticsNotSupported error returned when plugin doesn't support diagnostics.
ErrDiagnosticsNotSupported = errors.New("Plugin diagnostics not supported")
// ErrHealthCheckFailed error returned when health check failed.
ErrHealthCheckFailed = errors.New("Health check failed")
// ErrPluginUnavailable error returned when plugin is unavailable.
ErrPluginUnavailable = errors.New("Plugin unavailable")
// ErrMethodNotImplemented error returned when plugin method not implemented.
ErrMethodNotImplemented = errors.New("method not implemented")
)
func init() {
@@ -41,13 +44,13 @@ func init() {
// Manager manages backend plugins.
type Manager interface {
// Register registers a backend plugin
Register(descriptor PluginDescriptor) error
Register(pluginID string, factory PluginFactoryFunc) error
// StartPlugin starts a non-managed backend plugin
StartPlugin(ctx context.Context, pluginID string) error
// CollectMetrics collects metrics from a registered backend plugin.
CollectMetrics(ctx context.Context, pluginID string) (*CollectMetricsResult, error)
CollectMetrics(ctx context.Context, pluginID string) (*backend.CollectMetricsResult, error)
// CheckHealth checks the health of a registered backend plugin.
CheckHealth(ctx context.Context, pCtx backend.PluginContext) (*CheckHealthResult, error)
CheckHealth(ctx context.Context, pCtx backend.PluginContext) (*backend.CheckHealthResult, error)
// CallResource calls a plugin resource.
CallResource(pluginConfig backend.PluginContext, ctx *models.ReqContext, path string)
}
@@ -56,13 +59,13 @@ type manager struct {
Cfg *setting.Cfg `inject:""`
License models.Licensing `inject:""`
pluginsMu sync.RWMutex
plugins map[string]*BackendPlugin
plugins map[string]Plugin
logger log.Logger
pluginSettings map[string]pluginSettings
}
func (m *manager) Init() error {
m.plugins = make(map[string]*BackendPlugin)
m.plugins = make(map[string]Plugin)
m.logger = log.New("plugins.backend")
m.pluginSettings = extractPluginSettings(m.Cfg)
@@ -72,27 +75,27 @@ func (m *manager) Init() error {
func (m *manager) Run(ctx context.Context) error {
m.start(ctx)
<-ctx.Done()
m.stop()
m.stop(ctx)
return ctx.Err()
}
// Register registers a backend plugin
func (m *manager) Register(descriptor PluginDescriptor) error {
m.logger.Debug("Registering backend plugin", "pluginId", descriptor.pluginID, "executablePath", descriptor.executablePath)
func (m *manager) Register(pluginID string, factory PluginFactoryFunc) error {
m.logger.Debug("Registering backend plugin", "pluginId", pluginID)
m.pluginsMu.Lock()
defer m.pluginsMu.Unlock()
if _, exists := m.plugins[descriptor.pluginID]; exists {
if _, exists := m.plugins[pluginID]; exists {
return errors.New("Backend plugin already registered")
}
pluginSettings := pluginSettings{}
if ps, exists := m.pluginSettings[descriptor.pluginID]; exists {
if ps, exists := m.pluginSettings[pluginID]; exists {
pluginSettings = ps
}
hostEnv := []string{
fmt.Sprintf("GF_VERSION=%s", setting.BuildVersion),
fmt.Sprintf("GF_VERSION=%s", m.Cfg.BuildVersion),
fmt.Sprintf("GF_EDITION=%s", m.License.Edition()),
}
@@ -102,20 +105,14 @@ func (m *manager) Register(descriptor PluginDescriptor) error {
env := pluginSettings.ToEnv("GF_PLUGIN", hostEnv)
pluginLogger := m.logger.New("pluginId", descriptor.pluginID)
plugin := &BackendPlugin{
id: descriptor.pluginID,
executablePath: descriptor.executablePath,
managed: descriptor.managed,
clientFactory: func() *plugin.Client {
return plugin.NewClient(newClientConfig(descriptor.executablePath, env, pluginLogger, descriptor.versionedPlugins))
},
startFns: descriptor.startFns,
logger: pluginLogger,
pluginLogger := m.logger.New("pluginId", pluginID)
plugin, err := factory(pluginID, pluginLogger, env)
if err != nil {
return err
}
m.plugins[descriptor.pluginID] = plugin
m.logger.Debug("Backend plugin registered", "pluginId", descriptor.pluginID, "executablePath", descriptor.executablePath)
m.plugins[pluginID] = plugin
m.logger.Debug("Backend plugin registered", "pluginId", pluginID)
return nil
}
@@ -124,12 +121,12 @@ func (m *manager) start(ctx context.Context) {
m.pluginsMu.RLock()
defer m.pluginsMu.RUnlock()
for _, p := range m.plugins {
if !p.managed {
if !p.IsManaged() {
continue
}
if err := startPluginAndRestartKilledProcesses(ctx, p); err != nil {
p.logger.Error("Failed to start plugin", "error", err)
p.Logger().Error("Failed to start plugin", "error", err)
continue
}
}
@@ -141,10 +138,10 @@ func (m *manager) StartPlugin(ctx context.Context, pluginID string) error {
p, registered := m.plugins[pluginID]
m.pluginsMu.RUnlock()
if !registered {
return errors.New("Backend plugin not registered")
return ErrPluginNotRegistered
}
if p.managed {
if p.IsManaged() {
return errors.New("Backend plugin is managed and cannot be manually started")
}
@@ -152,22 +149,26 @@ func (m *manager) StartPlugin(ctx context.Context, pluginID string) error {
}
// stop stops all managed backend plugins
func (m *manager) stop() {
func (m *manager) stop(ctx context.Context) {
m.pluginsMu.RLock()
defer m.pluginsMu.RUnlock()
var wg sync.WaitGroup
for _, p := range m.plugins {
go func(p *BackendPlugin) {
p.logger.Debug("Stopping plugin")
if err := p.stop(); err != nil {
p.logger.Error("Failed to stop plugin", "error", err)
wg.Add(1)
go func(p Plugin, ctx context.Context) {
defer wg.Done()
p.Logger().Debug("Stopping plugin")
if err := p.Stop(ctx); err != nil {
p.Logger().Error("Failed to stop plugin", "error", err)
}
p.logger.Debug("Plugin stopped")
}(p)
p.Logger().Debug("Plugin stopped")
}(p, ctx)
}
wg.Wait()
}
// CollectMetrics collects metrics from a registered backend plugin.
func (m *manager) CollectMetrics(ctx context.Context, pluginID string) (*CollectMetricsResult, error) {
func (m *manager) CollectMetrics(ctx context.Context, pluginID string) (*backend.CollectMetricsResult, error) {
m.pluginsMu.RLock()
p, registered := m.plugins[pluginID]
m.pluginsMu.RUnlock()
@@ -176,98 +177,139 @@ func (m *manager) CollectMetrics(ctx context.Context, pluginID string) (*Collect
return nil, ErrPluginNotRegistered
}
if !p.supportsDiagnostics() {
return nil, ErrDiagnosticsNotSupported
}
res, err := p.CollectMetrics(ctx)
var resp *backend.CollectMetricsResult
err := instrumentCollectMetrics(p.PluginID(), func() (innerErr error) {
resp, innerErr = p.CollectMetrics(ctx)
return
})
if err != nil {
return nil, err
}
return collectMetricsResultFromProto(res), nil
return resp, nil
}
// CheckHealth checks the health of a registered backend plugin.
func (m *manager) CheckHealth(ctx context.Context, pluginConfig backend.PluginContext) (*CheckHealthResult, error) {
func (m *manager) CheckHealth(ctx context.Context, pluginContext backend.PluginContext) (*backend.CheckHealthResult, error) {
m.pluginsMu.RLock()
p, registered := m.plugins[pluginConfig.PluginID]
p, registered := m.plugins[pluginContext.PluginID]
m.pluginsMu.RUnlock()
if !registered {
return nil, ErrPluginNotRegistered
}
if !p.supportsDiagnostics() {
return nil, ErrDiagnosticsNotSupported
}
var resp *backend.CheckHealthResult
err := instrumentCheckHealthRequest(p.PluginID(), func() (innerErr error) {
resp, innerErr = p.CheckHealth(ctx, &backend.CheckHealthRequest{PluginContext: pluginContext})
return
})
res, err := p.checkHealth(ctx, pluginConfig)
if err != nil {
p.logger.Error("Failed to check plugin health", "error", err)
return nil, ErrHealthCheckFailed
if errors.Is(err, ErrMethodNotImplemented) {
return nil, err
}
return nil, errutil.Wrap("Failed to check plugin health", ErrHealthCheckFailed)
}
return checkHealthResultFromProto(res), nil
return resp, nil
}
type keepCookiesJSONModel struct {
KeepCookies []string `json:"keepCookies"`
}
// CallResource calls a plugin resource.
func (m *manager) CallResource(pCtx backend.PluginContext, reqCtx *models.ReqContext, path string) {
func (m *manager) callResourceInternal(w http.ResponseWriter, req *http.Request, pCtx backend.PluginContext) error {
m.pluginsMu.RLock()
p, registered := m.plugins[pCtx.PluginID]
m.pluginsMu.RUnlock()
if !registered {
reqCtx.JsonApiErr(404, "Plugin not registered", nil)
return
return ErrPluginNotRegistered
}
clonedReq := reqCtx.Req.Clone(reqCtx.Req.Context())
keepCookieModel := keepCookiesJSONModel{}
if dis := pCtx.DataSourceInstanceSettings; dis != nil {
err := json.Unmarshal(dis.JSONData, &keepCookieModel)
if err != nil {
p.logger.Error("Failed to to unpack JSONData in datasource instance settings", "error", err)
p.Logger().Error("Failed to to unpack JSONData in datasource instance settings", "error", err)
}
}
proxyutil.ClearCookieHeader(clonedReq, keepCookieModel.KeepCookies)
proxyutil.PrepareProxyRequest(clonedReq)
proxyutil.ClearCookieHeader(req, keepCookieModel.KeepCookies)
proxyutil.PrepareProxyRequest(req)
body, err := reqCtx.Req.Body().Bytes()
body, err := ioutil.ReadAll(req.Body)
if err != nil {
reqCtx.JsonApiErr(500, "Failed to read request body", err)
return
return errors.New("Failed to read request body")
}
req := &backend.CallResourceRequest{
crReq := &backend.CallResourceRequest{
PluginContext: pCtx,
Path: path,
Method: clonedReq.Method,
URL: clonedReq.URL.String(),
Headers: clonedReq.Header,
Path: req.URL.Path,
Method: req.Method,
URL: req.URL.String(),
Headers: req.Header,
Body: body,
}
err = InstrumentPluginRequest(p.id, "resource", func() error {
stream, err := p.callResource(clonedReq.Context(), req)
if err != nil {
return errutil.Wrap("Failed to call resource", err)
return instrumentCallResourceRequest(p.PluginID(), func() error {
childCtx, cancel := context.WithCancel(req.Context())
defer cancel()
stream := newCallResourceResponseStream(childCtx)
var wg sync.WaitGroup
wg.Add(1)
var flushStreamErr error
go func() {
flushStreamErr = flushStream(p, stream, w)
wg.Done()
}()
innerErr := p.CallResource(req.Context(), crReq, stream)
stream.Close()
if innerErr != nil {
return innerErr
}
return flushStream(p, stream, reqCtx)
wg.Wait()
return flushStreamErr
})
}
// CallResource calls a plugin resource.
func (m *manager) CallResource(pCtx backend.PluginContext, reqCtx *models.ReqContext, path string) {
clonedReq := reqCtx.Req.Clone(reqCtx.Req.Context())
rawURL := path
if clonedReq.URL.RawQuery != "" {
rawURL += "?" + clonedReq.URL.RawQuery
}
urlPath, err := url.Parse(rawURL)
if err != nil {
reqCtx.JsonApiErr(500, "Failed to ", err)
handleCallResourceError(err, reqCtx)
return
}
clonedReq.URL = urlPath
err = m.callResourceInternal(reqCtx.Resp, clonedReq, pCtx)
if err != nil {
handleCallResourceError(err, reqCtx)
}
}
func flushStream(plugin *BackendPlugin, stream callResourceResultStream, reqCtx *models.ReqContext) error {
func handleCallResourceError(err error, reqCtx *models.ReqContext) {
if errors.Is(err, ErrPluginUnavailable) {
reqCtx.JsonApiErr(503, "Plugin unavailable", err)
return
}
if errors.Is(err, ErrMethodNotImplemented) {
reqCtx.JsonApiErr(404, "Not found", err)
return
}
reqCtx.JsonApiErr(500, "Failed to call resource", err)
}
func flushStream(plugin Plugin, stream CallResourceClientResponseStream, w http.ResponseWriter) error {
processedStreams := 0
for {
@@ -283,12 +325,12 @@ func flushStream(plugin *BackendPlugin, stream callResourceResultStream, reqCtx
return errutil.Wrap("Failed to receive response from resource call", err)
}
plugin.logger.Error("Failed to receive response from resource call", "error", err)
return nil
plugin.Logger().Error("Failed to receive response from resource call", "error", err)
return stream.Close()
}
// Expected that headers and status are only part of first stream
if processedStreams == 0 {
if processedStreams == 0 && resp.Headers != nil {
// Make sure a content type always is returned in response
if _, exists := resp.Headers["Content-Type"]; !exists {
resp.Headers["Content-Type"] = []string{"application/json"}
@@ -302,37 +344,39 @@ func flushStream(plugin *BackendPlugin, stream callResourceResultStream, reqCtx
}
for _, v := range values {
reqCtx.Resp.Header().Add(k, v)
w.Header().Add(k, v)
}
}
reqCtx.WriteHeader(resp.Status)
w.WriteHeader(resp.Status)
}
if _, err := reqCtx.Write(resp.Body); err != nil {
plugin.logger.Error("Failed to write resource response", "error", err)
if _, err := w.Write(resp.Body); err != nil {
plugin.Logger().Error("Failed to write resource response", "error", err)
}
reqCtx.Resp.Flush()
if flusher, ok := w.(http.Flusher); ok {
flusher.Flush()
}
processedStreams++
}
}
func startPluginAndRestartKilledProcesses(ctx context.Context, p *BackendPlugin) error {
if err := p.start(ctx); err != nil {
func startPluginAndRestartKilledProcesses(ctx context.Context, p Plugin) error {
if err := p.Start(ctx); err != nil {
return err
}
go func(ctx context.Context, p *BackendPlugin) {
go func(ctx context.Context, p Plugin) {
if err := restartKilledProcess(ctx, p); err != nil {
p.logger.Error("Attempt to restart killed plugin process failed", "error", err)
p.Logger().Error("Attempt to restart killed plugin process failed", "error", err)
}
}(ctx, p)
return nil
}
func restartKilledProcess(ctx context.Context, p *BackendPlugin) error {
func restartKilledProcess(ctx context.Context, p Plugin) error {
ticker := time.NewTicker(time.Second * 1)
for {
@@ -343,16 +387,16 @@ func restartKilledProcess(ctx context.Context, p *BackendPlugin) error {
}
return nil
case <-ticker.C:
if !p.client.Exited() {
if !p.Exited() {
continue
}
p.logger.Debug("Restarting plugin")
if err := p.start(ctx); err != nil {
p.logger.Error("Failed to restart plugin", "error", err)
p.Logger().Debug("Restarting plugin")
if err := p.Start(ctx); err != nil {
p.Logger().Error("Failed to restart plugin", "error", err)
continue
}
p.logger.Debug("Plugin restarted")
p.Logger().Debug("Plugin restarted")
}
}
}