Introduce TSDB service (#31520)

* Introduce TSDB service

Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com>

Co-authored-by: Erik Sundell <erik.sundell87@gmail.com>
Co-authored-by: Will Browne <will.browne@grafana.com>
Co-authored-by: Torkel Ödegaard <torkel@grafana.org>
Co-authored-by: Will Browne <wbrowne@users.noreply.github.com>
Co-authored-by: Zoltán Bedi <zoltan.bedi@gmail.com>
This commit is contained in:
Arve Knudsen
2021-03-08 07:02:49 +01:00
committed by GitHub
co-authored by Erik Sundell Will Browne Torkel Ödegaard Will Browne Zoltán Bedi
parent c899bf3592
commit b79e61656a
203 changed files with 5269 additions and 4776 deletions
@@ -0,0 +1,441 @@
package manager
import (
"context"
"encoding/json"
"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/plugins"
"github.com/grafana/grafana/pkg/plugins/backendplugin"
"github.com/grafana/grafana/pkg/plugins/backendplugin/instrumentation"
"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"
)
func init() {
registry.RegisterServiceWithPriority(&manager{}, registry.MediumHigh)
}
type manager struct {
Cfg *setting.Cfg `inject:""`
License models.Licensing `inject:""`
PluginRequestValidator models.PluginRequestValidator `inject:""`
pluginsMu sync.RWMutex
plugins map[string]backendplugin.Plugin
logger log.Logger
pluginSettings map[string]pluginSettings
}
func (m *manager) Init() error {
m.plugins = make(map[string]backendplugin.Plugin)
m.logger = log.New("plugins.backend")
m.pluginSettings = extractPluginSettings(m.Cfg)
return nil
}
func (m *manager) Run(ctx context.Context) error {
m.start(ctx)
<-ctx.Done()
m.stop(ctx)
return ctx.Err()
}
// Register registers a backend plugin
func (m *manager) Register(pluginID string, factory backendplugin.PluginFactoryFunc) error {
m.logger.Debug("Registering backend plugin", "pluginId", pluginID)
m.pluginsMu.Lock()
defer m.pluginsMu.Unlock()
if _, exists := m.plugins[pluginID]; exists {
return fmt.Errorf("backend plugin %s already registered", pluginID)
}
pluginSettings := pluginSettings{}
if ps, exists := m.pluginSettings[pluginID]; exists {
pluginSettings = ps
}
hostEnv := []string{
fmt.Sprintf("GF_VERSION=%s", m.Cfg.BuildVersion),
fmt.Sprintf("GF_EDITION=%s", m.License.Edition()),
}
if m.License.HasLicense() {
hostEnv = append(
hostEnv,
fmt.Sprintf("GF_ENTERPRISE_LICENSE_PATH=%s", m.Cfg.EnterpriseLicensePath),
)
if envProvider, ok := m.License.(models.LicenseEnvironment); ok {
for k, v := range envProvider.Environment() {
hostEnv = append(hostEnv, fmt.Sprintf("%s=%s", k, v))
}
}
}
env := pluginSettings.ToEnv("GF_PLUGIN", hostEnv)
pluginLogger := m.logger.New("pluginId", pluginID)
plugin, err := factory(pluginID, pluginLogger, env)
if err != nil {
return err
}
m.plugins[pluginID] = plugin
m.logger.Debug("Backend plugin registered", "pluginId", pluginID)
return nil
}
func (m *manager) GetDataPlugin(pluginID string) interface{} {
plugin := m.plugins[pluginID]
if plugin == nil || !plugin.CanHandleDataQueries() {
return nil
}
if dataPlugin, ok := plugin.(plugins.DataPlugin); ok {
return dataPlugin
}
return nil
}
// start starts all managed backend plugins
func (m *manager) start(ctx context.Context) {
m.pluginsMu.RLock()
defer m.pluginsMu.RUnlock()
for _, p := range m.plugins {
if !p.IsManaged() {
continue
}
if err := startPluginAndRestartKilledProcesses(ctx, p); err != nil {
p.Logger().Error("Failed to start plugin", "error", err)
continue
}
}
}
// StartPlugin starts a non-managed backend plugin
func (m *manager) StartPlugin(ctx context.Context, pluginID string) error {
m.pluginsMu.RLock()
p, registered := m.plugins[pluginID]
m.pluginsMu.RUnlock()
if !registered {
return backendplugin.ErrPluginNotRegistered
}
if p.IsManaged() {
return errors.New("backend plugin is managed and cannot be manually started")
}
return startPluginAndRestartKilledProcesses(ctx, p)
}
// stop stops all managed backend plugins
func (m *manager) stop(ctx context.Context) {
m.pluginsMu.RLock()
defer m.pluginsMu.RUnlock()
var wg sync.WaitGroup
for _, p := range m.plugins {
wg.Add(1)
go func(p backendplugin.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, ctx)
}
wg.Wait()
}
// CollectMetrics collects metrics from a registered backend plugin.
func (m *manager) CollectMetrics(ctx context.Context, pluginID string) (*backend.CollectMetricsResult, error) {
m.pluginsMu.RLock()
p, registered := m.plugins[pluginID]
m.pluginsMu.RUnlock()
if !registered {
return nil, backendplugin.ErrPluginNotRegistered
}
var resp *backend.CollectMetricsResult
err := instrumentation.InstrumentCollectMetrics(p.PluginID(), func() (innerErr error) {
resp, innerErr = p.CollectMetrics(ctx)
return
})
if err != nil {
return nil, err
}
return resp, nil
}
// CheckHealth checks the health of a registered backend plugin.
func (m *manager) CheckHealth(ctx context.Context, pluginContext backend.PluginContext) (*backend.CheckHealthResult, error) {
var dsURL string
if pluginContext.DataSourceInstanceSettings != nil {
dsURL = pluginContext.DataSourceInstanceSettings.URL
}
err := m.PluginRequestValidator.Validate(dsURL, nil)
if err != nil {
return &backend.CheckHealthResult{
Status: http.StatusForbidden,
Message: "Access denied",
}, nil
}
m.pluginsMu.RLock()
p, registered := m.plugins[pluginContext.PluginID]
m.pluginsMu.RUnlock()
if !registered {
return nil, backendplugin.ErrPluginNotRegistered
}
var resp *backend.CheckHealthResult
err = instrumentation.InstrumentCheckHealthRequest(p.PluginID(), func() (innerErr error) {
resp, innerErr = p.CheckHealth(ctx, &backend.CheckHealthRequest{PluginContext: pluginContext})
return
})
if err != nil {
if errors.Is(err, backendplugin.ErrMethodNotImplemented) {
return nil, err
}
if errors.Is(err, backendplugin.ErrPluginUnavailable) {
return nil, err
}
return nil, errutil.Wrap("failed to check plugin health", backendplugin.ErrHealthCheckFailed)
}
return resp, nil
}
type keepCookiesJSONModel struct {
KeepCookies []string `json:"keepCookies"`
}
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 {
return backendplugin.ErrPluginNotRegistered
}
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)
}
}
proxyutil.ClearCookieHeader(req, keepCookieModel.KeepCookies)
proxyutil.PrepareProxyRequest(req)
body, err := ioutil.ReadAll(req.Body)
if err != nil {
return fmt.Errorf("failed to read request body: %w", err)
}
crReq := &backend.CallResourceRequest{
PluginContext: pCtx,
Path: req.URL.Path,
Method: req.Method,
URL: req.URL.String(),
Headers: req.Header,
Body: body,
}
return instrumentation.InstrumentCallResourceRequest(p.PluginID(), func() error {
childCtx, cancel := context.WithCancel(req.Context())
defer cancel()
stream := newCallResourceResponseStream(childCtx)
var wg sync.WaitGroup
wg.Add(1)
defer func() {
if err := stream.Close(); err != nil {
m.logger.Warn("Failed to close stream", "err", err)
}
wg.Wait()
}()
var flushStreamErr error
go func() {
flushStreamErr = flushStream(p, stream, w)
wg.Done()
}()
if err := p.CallResource(req.Context(), crReq, stream); err != nil {
return err
}
return flushStreamErr
})
}
// CallResource calls a plugin resource.
func (m *manager) CallResource(pCtx backend.PluginContext, reqCtx *models.ReqContext, path string) {
var dsURL string
if pCtx.DataSourceInstanceSettings != nil {
dsURL = pCtx.DataSourceInstanceSettings.URL
}
err := m.PluginRequestValidator.Validate(dsURL, reqCtx.Req.Request)
if err != nil {
reqCtx.JsonApiErr(http.StatusForbidden, "Access denied", err)
return
}
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 {
handleCallResourceError(err, reqCtx)
return
}
clonedReq.URL = urlPath
err = m.callResourceInternal(reqCtx.Resp, clonedReq, pCtx)
if err != nil {
handleCallResourceError(err, reqCtx)
}
}
func handleCallResourceError(err error, reqCtx *models.ReqContext) {
if errors.Is(err, backendplugin.ErrPluginUnavailable) {
reqCtx.JsonApiErr(503, "Plugin unavailable", err)
return
}
if errors.Is(err, backendplugin.ErrMethodNotImplemented) {
reqCtx.JsonApiErr(404, "Not found", err)
return
}
reqCtx.JsonApiErr(500, "Failed to call resource", err)
}
func flushStream(plugin backendplugin.Plugin, stream callResourceClientResponseStream, w http.ResponseWriter) error {
processedStreams := 0
for {
resp, err := stream.Recv()
if errors.Is(err, io.EOF) {
if processedStreams == 0 {
return errors.New("received empty resource response")
}
return nil
}
if err != nil {
if processedStreams == 0 {
return errutil.Wrap("failed to receive response from resource call", err)
}
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 && 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"}
}
for k, values := range resp.Headers {
// Due to security reasons we don't want to forward
// cookies from a backend plugin to clients/browsers.
if k == "Set-Cookie" {
continue
}
for _, v := range values {
// TODO: Figure out if we should use Set here instead
// nolint:gocritic
w.Header().Add(k, v)
}
}
w.WriteHeader(resp.Status)
}
if _, err := w.Write(resp.Body); err != nil {
plugin.Logger().Error("Failed to write resource response", "error", err)
}
if flusher, ok := w.(http.Flusher); ok {
flusher.Flush()
}
processedStreams++
}
}
func startPluginAndRestartKilledProcesses(ctx context.Context, p backendplugin.Plugin) error {
if err := p.Start(ctx); err != nil {
return err
}
go func(ctx context.Context, p backendplugin.Plugin) {
if err := restartKilledProcess(ctx, p); err != nil {
p.Logger().Error("Attempt to restart killed plugin process failed", "error", err)
}
}(ctx, p)
return nil
}
func restartKilledProcess(ctx context.Context, p backendplugin.Plugin) error {
ticker := time.NewTicker(time.Second * 1)
for {
select {
case <-ctx.Done():
if err := ctx.Err(); err != nil && !errors.Is(err, context.Canceled) {
return err
}
return nil
case <-ticker.C:
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)
continue
}
p.Logger().Debug("Plugin restarted")
}
}
}
// callResourceClientResponseStream is used for receiving resource call responses.
type callResourceClientResponseStream interface {
Recv() (*backend.CallResourceResponse, error)
Close() error
}
@@ -0,0 +1,434 @@
package manager
import (
"bytes"
"context"
"net/http"
"net/http/httptest"
"sync"
"testing"
"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/backendplugin"
"github.com/grafana/grafana/pkg/setting"
"github.com/stretchr/testify/require"
)
const testPluginID = "test-plugin"
func TestManager(t *testing.T) {
newManagerScenario(t, false, func(t *testing.T, ctx *managerScenarioCtx) {
t.Run("Unregistered plugin scenario", func(t *testing.T) {
err := ctx.manager.StartPlugin(context.Background(), testPluginID)
require.Equal(t, backendplugin.ErrPluginNotRegistered, err)
_, err = ctx.manager.CollectMetrics(context.Background(), testPluginID)
require.Equal(t, backendplugin.ErrPluginNotRegistered, err)
_, err = ctx.manager.CheckHealth(context.Background(), backend.PluginContext{PluginID: testPluginID})
require.Equal(t, backendplugin.ErrPluginNotRegistered, err)
req, err := http.NewRequest(http.MethodGet, "/test", nil)
require.NoError(t, err)
w := httptest.NewRecorder()
err = ctx.manager.callResourceInternal(w, req, backend.PluginContext{PluginID: testPluginID})
require.Equal(t, backendplugin.ErrPluginNotRegistered, err)
})
})
newManagerScenario(t, true, func(t *testing.T, ctx *managerScenarioCtx) {
t.Run("Managed plugin scenario", func(t *testing.T) {
ctx.license.edition = "Open Source"
ctx.license.hasLicense = false
ctx.cfg.BuildVersion = "7.0.0"
t.Run("Should be able to register plugin", func(t *testing.T) {
err := ctx.manager.Register(testPluginID, ctx.factory)
require.NoError(t, err)
require.NotNil(t, ctx.plugin)
require.Equal(t, testPluginID, ctx.plugin.pluginID)
require.NotNil(t, ctx.plugin.logger)
t.Run("Should not be able to register an already registered plugin", func(t *testing.T) {
err := ctx.manager.Register(testPluginID, ctx.factory)
require.Error(t, err)
})
t.Run("Should provide expected host environment variables", func(t *testing.T) {
require.Len(t, ctx.env, 2)
require.EqualValues(t, []string{"GF_VERSION=7.0.0", "GF_EDITION=Open Source"}, ctx.env)
})
t.Run("When manager runs should start and stop plugin", func(t *testing.T) {
pCtx := context.Background()
cCtx, cancel := context.WithCancel(pCtx)
var wg sync.WaitGroup
wg.Add(1)
var runErr error
go func() {
runErr = ctx.manager.Run(cCtx)
wg.Done()
}()
time.Sleep(time.Millisecond)
cancel()
wg.Wait()
require.Equal(t, context.Canceled, runErr)
require.Equal(t, 1, ctx.plugin.startCount)
require.Equal(t, 1, ctx.plugin.stopCount)
})
t.Run("When manager runs should restart plugin process when killed", func(t *testing.T) {
ctx.plugin.stopCount = 0
ctx.plugin.startCount = 0
pCtx := context.Background()
cCtx, cancel := context.WithCancel(pCtx)
var wgRun sync.WaitGroup
wgRun.Add(1)
var runErr error
go func() {
runErr = ctx.manager.Run(cCtx)
wgRun.Done()
}()
time.Sleep(time.Millisecond)
var wgKill sync.WaitGroup
wgKill.Add(1)
go func() {
ctx.plugin.kill()
for {
if !ctx.plugin.Exited() {
break
}
}
cancel()
wgKill.Done()
}()
wgKill.Wait()
wgRun.Wait()
require.Equal(t, context.Canceled, runErr)
require.Equal(t, 1, ctx.plugin.stopCount)
require.Equal(t, 2, ctx.plugin.startCount)
})
t.Run("Shouldn't be able to start managed plugin", func(t *testing.T) {
err := ctx.manager.StartPlugin(context.Background(), testPluginID)
require.NotNil(t, err)
})
t.Run("Unimplemented handlers", func(t *testing.T) {
t.Run("Collect metrics should return method not implemented error", func(t *testing.T) {
_, err = ctx.manager.CollectMetrics(context.Background(), testPluginID)
require.Equal(t, backendplugin.ErrMethodNotImplemented, err)
})
t.Run("Check health should return method not implemented error", func(t *testing.T) {
_, err = ctx.manager.CheckHealth(context.Background(), backend.PluginContext{PluginID: testPluginID})
require.Equal(t, backendplugin.ErrMethodNotImplemented, err)
})
t.Run("Call resource should return method not implemented error", func(t *testing.T) {
req, err := http.NewRequest(http.MethodGet, "/test", bytes.NewReader([]byte{}))
require.NoError(t, err)
w := httptest.NewRecorder()
err = ctx.manager.callResourceInternal(w, req, backend.PluginContext{PluginID: testPluginID})
require.Equal(t, backendplugin.ErrMethodNotImplemented, err)
})
})
t.Run("Implemented handlers", func(t *testing.T) {
t.Run("Collect metrics should return expected result", func(t *testing.T) {
ctx.plugin.CollectMetricsHandlerFunc = func(ctx context.Context) (*backend.CollectMetricsResult, error) {
return &backend.CollectMetricsResult{
PrometheusMetrics: []byte("hello"),
}, nil
}
res, err := ctx.manager.CollectMetrics(context.Background(), testPluginID)
require.NoError(t, err)
require.NotNil(t, res)
require.Equal(t, "hello", string(res.PrometheusMetrics))
})
t.Run("Check health should return expected result", func(t *testing.T) {
json := []byte(`{
"key": "value"
}`)
ctx.plugin.CheckHealthHandlerFunc = func(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) {
return &backend.CheckHealthResult{
Status: backend.HealthStatusOk,
Message: "All good",
JSONDetails: json,
}, nil
}
res, err := ctx.manager.CheckHealth(context.Background(), backend.PluginContext{PluginID: testPluginID})
require.NoError(t, err)
require.NotNil(t, res)
require.Equal(t, backend.HealthStatusOk, res.Status)
require.Equal(t, "All good", res.Message)
require.Equal(t, json, res.JSONDetails)
})
t.Run("Call resource should return expected response", func(t *testing.T) {
ctx.plugin.CallResourceHandlerFunc = func(ctx context.Context,
req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error {
return sender.Send(&backend.CallResourceResponse{
Status: http.StatusOK,
})
}
req, err := http.NewRequest(http.MethodGet, "/test", bytes.NewReader([]byte{}))
require.NoError(t, err)
w := httptest.NewRecorder()
err = ctx.manager.callResourceInternal(w, req, backend.PluginContext{PluginID: testPluginID})
require.NoError(t, err)
require.Equal(t, http.StatusOK, w.Code)
})
})
})
})
})
newManagerScenario(t, false, func(t *testing.T, ctx *managerScenarioCtx) {
t.Run("Unmanaged plugin scenario", func(t *testing.T) {
ctx.license.edition = "Open Source"
ctx.license.hasLicense = false
ctx.cfg.BuildVersion = "7.0.0"
t.Run("Should be able to register plugin", func(t *testing.T) {
err := ctx.manager.Register(testPluginID, ctx.factory)
require.NoError(t, err)
require.False(t, ctx.plugin.managed)
t.Run("When manager runs should not start plugin", func(t *testing.T) {
pCtx := context.Background()
cCtx, cancel := context.WithCancel(pCtx)
var wg sync.WaitGroup
wg.Add(1)
var runErr error
go func() {
runErr = ctx.manager.Run(cCtx)
wg.Done()
}()
go func() {
cancel()
}()
wg.Wait()
require.Equal(t, context.Canceled, runErr)
require.Equal(t, 0, ctx.plugin.startCount)
require.Equal(t, 1, ctx.plugin.stopCount)
})
t.Run("Should be able to start unmanaged plugin and be restarted when process is killed", func(t *testing.T) {
pCtx := context.Background()
cCtx, cancel := context.WithCancel(pCtx)
defer cancel()
err := ctx.manager.StartPlugin(cCtx, testPluginID)
require.Nil(t, err)
require.Equal(t, 1, ctx.plugin.startCount)
var wg sync.WaitGroup
wg.Add(1)
go func() {
ctx.plugin.kill()
for {
if !ctx.plugin.Exited() {
break
}
}
wg.Done()
}()
wg.Wait()
require.Equal(t, 2, ctx.plugin.startCount)
})
})
})
})
newManagerScenario(t, true, func(t *testing.T, ctx *managerScenarioCtx) {
t.Run("Plugin registration scenario when Grafana is licensed", func(t *testing.T) {
ctx.license.edition = "Enterprise"
ctx.license.hasLicense = true
ctx.license.tokenRaw = "testtoken"
ctx.cfg.BuildVersion = "7.0.0"
ctx.cfg.EnterpriseLicensePath = "/license.txt"
err := ctx.manager.Register(testPluginID, ctx.factory)
require.NoError(t, err)
t.Run("Should provide expected host environment variables", func(t *testing.T) {
require.Len(t, ctx.env, 4)
require.EqualValues(t, []string{"GF_VERSION=7.0.0", "GF_EDITION=Enterprise", "GF_ENTERPRISE_LICENSE_PATH=/license.txt", "GF_ENTERPRISE_LICENSE_TEXT=testtoken"}, ctx.env)
})
})
})
}
type managerScenarioCtx struct {
cfg *setting.Cfg
license *testLicensingService
manager *manager
factory backendplugin.PluginFactoryFunc
plugin *testPlugin
env []string
}
func newManagerScenario(t *testing.T, managed bool, fn func(t *testing.T, ctx *managerScenarioCtx)) {
t.Helper()
cfg := setting.NewCfg()
license := &testLicensingService{}
validator := &testPluginRequestValidator{}
ctx := &managerScenarioCtx{
cfg: cfg,
license: license,
manager: &manager{
Cfg: cfg,
License: license,
PluginRequestValidator: validator,
},
}
err := ctx.manager.Init()
require.NoError(t, err)
ctx.factory = func(pluginID string, logger log.Logger, env []string) (backendplugin.Plugin, error) {
ctx.plugin = &testPlugin{
pluginID: pluginID,
logger: logger,
managed: managed,
}
ctx.env = env
return ctx.plugin, nil
}
fn(t, ctx)
}
type testPlugin struct {
pluginID string
logger log.Logger
startCount int
stopCount int
managed bool
exited bool
backend.CollectMetricsHandlerFunc
backend.CheckHealthHandlerFunc
backend.CallResourceHandlerFunc
mutex sync.RWMutex
}
func (tp *testPlugin) PluginID() string {
return tp.pluginID
}
func (tp *testPlugin) Logger() log.Logger {
return tp.logger
}
func (tp *testPlugin) CanHandleDataQueries() bool {
return false
}
func (tp *testPlugin) Start(ctx context.Context) error {
tp.mutex.Lock()
defer tp.mutex.Unlock()
tp.exited = false
tp.startCount++
return nil
}
func (tp *testPlugin) Stop(ctx context.Context) error {
tp.mutex.Lock()
defer tp.mutex.Unlock()
tp.stopCount++
return nil
}
func (tp *testPlugin) IsManaged() bool {
return tp.managed
}
func (tp *testPlugin) Exited() bool {
tp.mutex.RLock()
defer tp.mutex.RUnlock()
return tp.exited
}
func (tp *testPlugin) kill() {
tp.mutex.Lock()
defer tp.mutex.Unlock()
tp.exited = true
}
func (tp *testPlugin) CollectMetrics(ctx context.Context) (*backend.CollectMetricsResult, error) {
if tp.CollectMetricsHandlerFunc != nil {
return tp.CollectMetricsHandlerFunc(ctx)
}
return nil, backendplugin.ErrMethodNotImplemented
}
func (tp *testPlugin) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) {
if tp.CheckHealthHandlerFunc != nil {
return tp.CheckHealthHandlerFunc(ctx, req)
}
return nil, backendplugin.ErrMethodNotImplemented
}
func (tp *testPlugin) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error {
if tp.CallResourceHandlerFunc != nil {
return tp.CallResourceHandlerFunc(ctx, req, sender)
}
return backendplugin.ErrMethodNotImplemented
}
type testLicensingService struct {
edition string
hasLicense bool
tokenRaw string
}
func (t *testLicensingService) HasLicense() bool {
return t.hasLicense
}
func (t *testLicensingService) Expiry() int64 {
return 0
}
func (t *testLicensingService) Edition() string {
return t.edition
}
func (t *testLicensingService) StateInfo() string {
return ""
}
func (t *testLicensingService) ContentDeliveryPrefix() string {
return ""
}
func (t *testLicensingService) LicenseURL(user *models.SignedInUser) string {
return ""
}
func (t *testLicensingService) HasValidLicense() bool {
return false
}
func (t *testLicensingService) Environment() map[string]string {
return map[string]string{"GF_ENTERPRISE_LICENSE_TEXT": t.tokenRaw}
}
type testPluginRequestValidator struct{}
func (t *testPluginRequestValidator) Validate(string, *http.Request) error {
return nil
}
@@ -0,0 +1,39 @@
package manager
import (
"fmt"
"strings"
"github.com/grafana/grafana/pkg/setting"
)
type pluginSettings map[string]string
func (ps pluginSettings) ToEnv(prefix string, hostEnv []string) []string {
env := []string{}
for k, v := range ps {
env = append(env, fmt.Sprintf("%s_%s=%s", prefix, strings.ToUpper(k), v))
}
env = append(env, hostEnv...)
return env
}
func extractPluginSettings(cfg *setting.Cfg) map[string]pluginSettings {
psMap := map[string]pluginSettings{}
for pluginID, settings := range cfg.PluginSettings {
ps := pluginSettings{}
for k, v := range settings {
if k == "path" || strings.ToLower(k) == "id" {
continue
}
ps[k] = v
}
psMap[pluginID] = ps
}
return psMap
}
@@ -0,0 +1,46 @@
package manager
import (
"sort"
"testing"
"github.com/grafana/grafana/pkg/setting"
"github.com/stretchr/testify/require"
)
func TestPluginSettings(t *testing.T) {
t.Run("Should only extract from sections beginning with 'plugin.' in config", func(t *testing.T) {
cfg := &setting.Cfg{
PluginSettings: setting.PluginSettings{
"plugin": map[string]string{
"key1": "value1",
"key2": "value2",
},
},
}
ps := extractPluginSettings(cfg)
require.Len(t, ps, 1)
require.Len(t, ps["plugin"], 2)
t.Run("Should skip path setting", func(t *testing.T) {
cfg.PluginSettings["plugin"]["path"] = "value"
ps := extractPluginSettings(cfg)
require.Len(t, ps["plugin"], 2)
})
t.Run("Should skip id setting", func(t *testing.T) {
cfg.PluginSettings["plugin"]["id"] = "value"
ps := extractPluginSettings(cfg)
require.Len(t, ps["plugin"], 2)
})
t.Run("Should return expected environment variables from plugin settings ", func(t *testing.T) {
ps := extractPluginSettings(cfg)
env := ps["plugin"].ToEnv("GF_PLUGIN", []string{"GF_VERSION=6.7.0"})
sort.Strings(env)
require.Len(t, env, 3)
require.EqualValues(t, []string{"GF_PLUGIN_KEY1=value1", "GF_PLUGIN_KEY2=value2", "GF_VERSION=6.7.0"}, env)
})
})
}
@@ -0,0 +1,57 @@
package manager
import (
"context"
"errors"
"io"
"github.com/grafana/grafana-plugin-sdk-go/backend"
)
func newCallResourceResponseStream(ctx context.Context) *callResourceResponseStream {
return &callResourceResponseStream{
ctx: ctx,
stream: make(chan *backend.CallResourceResponse),
}
}
type callResourceResponseStream struct {
ctx context.Context
stream chan *backend.CallResourceResponse
closed bool
}
func (s *callResourceResponseStream) Send(res *backend.CallResourceResponse) error {
if s.closed {
return errors.New("cannot send to a closed stream")
}
select {
case <-s.ctx.Done():
return errors.New("cancelled")
case s.stream <- res:
return nil
}
}
func (s *callResourceResponseStream) Recv() (*backend.CallResourceResponse, error) {
select {
case <-s.ctx.Done():
return nil, s.ctx.Err()
case res, ok := <-s.stream:
if !ok {
return nil, io.EOF
}
return res, nil
}
}
func (s *callResourceResponseStream) Close() error {
if s.closed {
return errors.New("cannot close a closed stream")
}
close(s.stream)
s.closed = true
return nil
}