mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Backend Plugins: Plugin configuration using Grafana config (#23451)
Enables adding a section `plugin.<plugin id>` and key/value to Grafana configuration file which will be converted and sent as environment variables to the backend plugin. Also sends some additional environment variables, Grafana version (GF_VERSION), Grafana edition (GF_EDITION) and enterprise license path (GF_ENTERPRISE_LICENSE_PATH). Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com> Fixes #21515,
This commit is contained in:
co-authored by
Arve Knudsen
parent
941cd59894
commit
34266cd369
@@ -30,9 +30,12 @@ var handshake = goplugin.HandshakeConfig{
|
|||||||
MagicCookieValue: grpcplugin.MagicCookieValue,
|
MagicCookieValue: grpcplugin.MagicCookieValue,
|
||||||
}
|
}
|
||||||
|
|
||||||
func newClientConfig(executablePath string, logger log.Logger, versionedPlugins map[int]goplugin.PluginSet) *goplugin.ClientConfig {
|
func newClientConfig(executablePath string, env []string, logger log.Logger, versionedPlugins map[int]goplugin.PluginSet) *goplugin.ClientConfig {
|
||||||
|
cmd := exec.Command(executablePath)
|
||||||
|
cmd.Env = env
|
||||||
|
|
||||||
return &goplugin.ClientConfig{
|
return &goplugin.ClientConfig{
|
||||||
Cmd: exec.Command(executablePath),
|
Cmd: cmd,
|
||||||
HandshakeConfig: handshake,
|
HandshakeConfig: handshake,
|
||||||
VersionedPlugins: versionedPlugins,
|
VersionedPlugins: versionedPlugins,
|
||||||
Logger: logWrapper{Logger: logger},
|
Logger: logWrapper{Logger: logger},
|
||||||
|
|||||||
@@ -3,11 +3,13 @@ package backendplugin
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/models"
|
"github.com/grafana/grafana/pkg/models"
|
||||||
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
"github.com/grafana/grafana/pkg/util/errutil"
|
"github.com/grafana/grafana/pkg/util/errutil"
|
||||||
"github.com/grafana/grafana/pkg/util/proxyutil"
|
"github.com/grafana/grafana/pkg/util/proxyutil"
|
||||||
|
|
||||||
@@ -49,14 +51,18 @@ type Manager interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type manager struct {
|
type manager struct {
|
||||||
pluginsMu sync.RWMutex
|
Cfg *setting.Cfg `inject:""`
|
||||||
plugins map[string]*BackendPlugin
|
License models.Licensing `inject:""`
|
||||||
logger log.Logger
|
pluginsMu sync.RWMutex
|
||||||
|
plugins map[string]*BackendPlugin
|
||||||
|
logger log.Logger
|
||||||
|
pluginSettings map[string]pluginSettings
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *manager) Init() error {
|
func (m *manager) Init() error {
|
||||||
m.plugins = make(map[string]*BackendPlugin)
|
m.plugins = make(map[string]*BackendPlugin)
|
||||||
m.logger = log.New("plugins.backend")
|
m.logger = log.New("plugins.backend")
|
||||||
|
m.pluginSettings = extractPluginSettings(m.Cfg)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -78,13 +84,29 @@ func (m *manager) Register(descriptor PluginDescriptor) error {
|
|||||||
return errors.New("Backend plugin already registered")
|
return errors.New("Backend plugin already registered")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pluginSettings := pluginSettings{}
|
||||||
|
if ps, exists := m.pluginSettings[descriptor.pluginID]; exists {
|
||||||
|
pluginSettings = ps
|
||||||
|
}
|
||||||
|
|
||||||
|
hostEnv := []string{
|
||||||
|
fmt.Sprintf("GF_VERSION=%s", setting.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))
|
||||||
|
}
|
||||||
|
|
||||||
|
env := pluginSettings.ToEnv("GF_PLUGIN", hostEnv)
|
||||||
|
|
||||||
pluginLogger := m.logger.New("pluginId", descriptor.pluginID)
|
pluginLogger := m.logger.New("pluginId", descriptor.pluginID)
|
||||||
plugin := &BackendPlugin{
|
plugin := &BackendPlugin{
|
||||||
id: descriptor.pluginID,
|
id: descriptor.pluginID,
|
||||||
executablePath: descriptor.executablePath,
|
executablePath: descriptor.executablePath,
|
||||||
managed: descriptor.managed,
|
managed: descriptor.managed,
|
||||||
clientFactory: func() *plugin.Client {
|
clientFactory: func() *plugin.Client {
|
||||||
return plugin.NewClient(newClientConfig(descriptor.executablePath, pluginLogger, descriptor.versionedPlugins))
|
return plugin.NewClient(newClientConfig(descriptor.executablePath, env, pluginLogger, descriptor.versionedPlugins))
|
||||||
},
|
},
|
||||||
startFns: descriptor.startFns,
|
startFns: descriptor.startFns,
|
||||||
logger: pluginLogger,
|
logger: pluginLogger,
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package backendplugin
|
||||||
|
|
||||||
|
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 backendplugin
|
||||||
|
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -9,7 +9,6 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
. "github.com/smartystreets/goconvey/convey"
|
. "github.com/smartystreets/goconvey/convey"
|
||||||
"gopkg.in/ini.v1"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestDashboardImport(t *testing.T) {
|
func TestDashboardImport(t *testing.T) {
|
||||||
@@ -87,17 +86,17 @@ func TestDashboardImport(t *testing.T) {
|
|||||||
|
|
||||||
func pluginScenario(desc string, t *testing.T, fn func()) {
|
func pluginScenario(desc string, t *testing.T, fn func()) {
|
||||||
Convey("Given a plugin", t, func() {
|
Convey("Given a plugin", t, func() {
|
||||||
setting.Raw = ini.Empty()
|
|
||||||
sec, _ := setting.Raw.NewSection("plugin.test-app")
|
|
||||||
_, err := sec.NewKey("path", "testdata/test-app")
|
|
||||||
So(err, ShouldBeNil)
|
|
||||||
|
|
||||||
pm := &PluginManager{
|
pm := &PluginManager{
|
||||||
Cfg: &setting.Cfg{
|
Cfg: &setting.Cfg{
|
||||||
FeatureToggles: map[string]bool{},
|
FeatureToggles: map[string]bool{},
|
||||||
|
PluginSettings: setting.PluginSettings{
|
||||||
|
"test-app": map[string]string{
|
||||||
|
"path": "testdata/test-app",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
err = pm.Init()
|
err := pm.Init()
|
||||||
So(err, ShouldBeNil)
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
Convey(desc, fn)
|
Convey(desc, fn)
|
||||||
|
|||||||
@@ -8,22 +8,21 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/models"
|
"github.com/grafana/grafana/pkg/models"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
. "github.com/smartystreets/goconvey/convey"
|
. "github.com/smartystreets/goconvey/convey"
|
||||||
"gopkg.in/ini.v1"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestPluginDashboards(t *testing.T) {
|
func TestPluginDashboards(t *testing.T) {
|
||||||
Convey("When asking plugin dashboard info", t, func() {
|
Convey("When asking plugin dashboard info", t, func() {
|
||||||
setting.Raw = ini.Empty()
|
|
||||||
sec, _ := setting.Raw.NewSection("plugin.test-app")
|
|
||||||
_, err := sec.NewKey("path", "testdata/test-app")
|
|
||||||
So(err, ShouldBeNil)
|
|
||||||
|
|
||||||
pm := &PluginManager{
|
pm := &PluginManager{
|
||||||
Cfg: &setting.Cfg{
|
Cfg: &setting.Cfg{
|
||||||
FeatureToggles: map[string]bool{},
|
FeatureToggles: map[string]bool{},
|
||||||
|
PluginSettings: setting.PluginSettings{
|
||||||
|
"test-app": map[string]string{
|
||||||
|
"path": "testdata/test-app",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
err = pm.Init()
|
err := pm.Init()
|
||||||
So(err, ShouldBeNil)
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
bus.AddHandler("test", func(query *models.GetDashboardQuery) error {
|
bus.AddHandler("test", func(query *models.GetDashboardQuery) error {
|
||||||
|
|||||||
@@ -153,19 +153,14 @@ func (pm *PluginManager) Run(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (pm *PluginManager) checkPluginPaths() error {
|
func (pm *PluginManager) checkPluginPaths() error {
|
||||||
for _, section := range setting.Raw.Sections() {
|
for pluginID, settings := range pm.Cfg.PluginSettings {
|
||||||
if !strings.HasPrefix(section.Name(), "plugin.") {
|
path, exists := settings["path"]
|
||||||
continue
|
if !exists || path == "" {
|
||||||
}
|
|
||||||
|
|
||||||
path := section.Key("path").String()
|
|
||||||
if path == "" {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := pm.scan(path); err != nil {
|
if err := pm.scan(path); err != nil {
|
||||||
return errutil.Wrapf(err, "Failed to scan directory configured for plugin '%s': '%s'",
|
return errutil.Wrapf(err, "Failed to scan directory configured for plugin '%s': '%s'", pluginID, path)
|
||||||
section.Name(), path)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,18 +32,17 @@ func TestPluginScans(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
Convey("When reading app plugin definition", t, func() {
|
Convey("When reading app plugin definition", t, func() {
|
||||||
setting.Raw = ini.Empty()
|
|
||||||
sec, err := setting.Raw.NewSection("plugin.nginx-app")
|
|
||||||
So(err, ShouldBeNil)
|
|
||||||
_, err = sec.NewKey("path", "testdata/test-app")
|
|
||||||
So(err, ShouldBeNil)
|
|
||||||
|
|
||||||
pm := &PluginManager{
|
pm := &PluginManager{
|
||||||
Cfg: &setting.Cfg{
|
Cfg: &setting.Cfg{
|
||||||
FeatureToggles: map[string]bool{},
|
FeatureToggles: map[string]bool{},
|
||||||
|
PluginSettings: setting.PluginSettings{
|
||||||
|
"nginx-app": map[string]string{
|
||||||
|
"path": "testdata/test-app",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
err = pm.Init()
|
err := pm.Init()
|
||||||
So(err, ShouldBeNil)
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
So(len(Apps), ShouldBeGreaterThan, 0)
|
So(len(Apps), ShouldBeGreaterThan, 0)
|
||||||
|
|||||||
@@ -259,6 +259,7 @@ type Cfg struct {
|
|||||||
MetricsEndpointDisableTotalStats bool
|
MetricsEndpointDisableTotalStats bool
|
||||||
PluginsEnableAlpha bool
|
PluginsEnableAlpha bool
|
||||||
PluginsAppsSkipVerifyTLS bool
|
PluginsAppsSkipVerifyTLS bool
|
||||||
|
PluginSettings PluginSettings
|
||||||
DisableSanitizeHtml bool
|
DisableSanitizeHtml bool
|
||||||
EnterpriseLicensePath string
|
EnterpriseLicensePath string
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package setting
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gopkg.in/ini.v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PluginSettings maps plugin id to map of key/value settings.
|
||||||
|
type PluginSettings map[string]map[string]string
|
||||||
|
|
||||||
|
func extractPluginSettings(sections []*ini.Section) PluginSettings {
|
||||||
|
psMap := PluginSettings{}
|
||||||
|
for _, section := range sections {
|
||||||
|
sectionName := section.Name()
|
||||||
|
if !strings.HasPrefix(sectionName, "plugin.") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
pluginID := strings.Replace(sectionName, "plugin.", "", 1)
|
||||||
|
psMap[pluginID] = section.KeysHash()
|
||||||
|
}
|
||||||
|
|
||||||
|
return psMap
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package setting
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPluginSettings(t *testing.T) {
|
||||||
|
cfg := NewCfg()
|
||||||
|
sec, err := cfg.Raw.NewSection("plugin")
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, err = sec.NewKey("key", "value")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
sec, err = cfg.Raw.NewSection("plugin.plugin")
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, err = sec.NewKey("key1", "value1")
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, err = sec.NewKey("key2", "value2")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
sec, err = cfg.Raw.NewSection("plugin.plugin2")
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, err = sec.NewKey("key3", "value3")
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, err = sec.NewKey("key4", "value4")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
sec, err = cfg.Raw.NewSection("other")
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, err = sec.NewKey("keySomething", "whatever")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
ps := extractPluginSettings(cfg.Raw.Sections())
|
||||||
|
require.Len(t, ps, 2)
|
||||||
|
require.Len(t, ps["plugin"], 2)
|
||||||
|
require.Equal(t, ps["plugin"]["key1"], "value1")
|
||||||
|
require.Equal(t, ps["plugin"]["key2"], "value2")
|
||||||
|
require.Len(t, ps["plugin2"], 2)
|
||||||
|
require.Equal(t, ps["plugin2"]["key3"], "value3")
|
||||||
|
require.Equal(t, ps["plugin2"]["key4"], "value4")
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user