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:
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user