[WIP] Plugins: Refactoring backend initialization flow (#42247)

* refactoring store interface and init flow

* fix import

* fix linter

* refactor resource calling

* load with class

* re-order args

* fix tests

* fix linter

* remove old creator

* add custom config struct

* fix some tests

* cleanup

* fix linter

* add connect failure error

* remove unused err

* convert test over
This commit is contained in:
Will Browne
2022-01-14 13:30:39 +01:00
committed by GitHub
parent 7ffefc069f
commit 7694fff0ef
20 changed files with 566 additions and 580 deletions
+2 -4
View File
@@ -8,17 +8,15 @@ import (
"github.com/grafana/grafana/pkg/infra/fs"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
)
type Finder struct {
cfg *setting.Cfg
log log.Logger
}
func New(cfg *setting.Cfg) Finder {
return Finder{cfg: cfg, log: log.New("plugin.finder")}
func New() Finder {
return Finder{log: log.New("plugin.finder")}
}
func (f *Finder) Find(pluginDirs []string) ([]string, error) {
@@ -51,7 +51,7 @@ func TestFinder_Find(t *testing.T) {
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
f := New(tc.cfg)
f := New()
pluginPaths, err := f.Find(tc.pluginDirs)
if (err != nil) && !errors.Is(err, tc.err) {
t.Errorf("Find() error = %v, expected error %v", err, tc.err)
@@ -1,104 +1,43 @@
package initializer
import (
"context"
"fmt"
"net/url"
"os"
"path"
"path/filepath"
"strings"
"github.com/grafana/grafana-aws-sdk/pkg/awsds"
"github.com/gosimple/slug"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/metrics"
"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/grpcplugin"
"github.com/grafana/grafana/pkg/plugins/backendplugin/pluginextensionv2"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
)
type Initializer struct {
cfg *setting.Cfg
license models.Licensing
log log.Logger
cfg *plugins.Cfg
license models.Licensing
backendProvider plugins.BackendFactoryProvider
log log.Logger
}
func New(cfg *setting.Cfg, license models.Licensing) Initializer {
func New(cfg *plugins.Cfg, backendProvider plugins.BackendFactoryProvider, license models.Licensing) Initializer {
return Initializer{
cfg: cfg,
license: license,
log: log.New("plugin.initializer"),
cfg: cfg,
license: license,
backendProvider: backendProvider,
log: log.New("plugin.initializer"),
}
}
func (i *Initializer) Initialize(p *plugins.Plugin) error {
if len(p.Dependencies.Plugins) == 0 {
p.Dependencies.Plugins = []plugins.Dependency{}
}
if p.Dependencies.GrafanaVersion == "" {
p.Dependencies.GrafanaVersion = "*"
}
for _, include := range p.Includes {
if include.Role == "" {
include.Role = models.ROLE_VIEWER
}
}
i.handleModuleDefaults(p)
p.Info.Logos.Small = pluginLogoURL(p.Type, p.Info.Logos.Small, p.BaseURL)
p.Info.Logos.Large = pluginLogoURL(p.Type, p.Info.Logos.Large, p.BaseURL)
for i := 0; i < len(p.Info.Screenshots); i++ {
p.Info.Screenshots[i].Path = evalRelativePluginURLPath(p.Info.Screenshots[i].Path, p.BaseURL, p.Type)
}
if p.IsApp() {
for _, child := range p.Children {
i.setPathsBasedOnApp(p, child)
}
// slugify pages
for _, include := range p.Includes {
if include.Slug == "" {
include.Slug = slug.Make(include.Name)
}
if include.Type == "page" && include.DefaultNav {
p.DefaultNavURL = i.cfg.AppSubURL + "/plugins/" + p.ID + "/page/" + include.Slug
}
if include.Type == "dashboard" && include.DefaultNav {
p.DefaultNavURL = i.cfg.AppSubURL + "/dashboard/db/" + include.Slug
}
}
}
pluginLog := i.log.New("pluginID", p.ID)
p.SetLogger(pluginLog)
func (i *Initializer) Initialize(ctx context.Context, p *plugins.Plugin) error {
if p.Backend {
var backendFactory backendplugin.PluginFactoryFunc
if p.IsRenderer() {
cmd := plugins.ComposeRendererStartCommand()
backendFactory = grpcplugin.NewRendererPlugin(p.ID, filepath.Join(p.PluginDir, cmd),
func(pluginID string, renderer pluginextensionv2.RendererPlugin, logger log.Logger) error {
p.Renderer = renderer
return nil
},
)
} else {
cmd := plugins.ComposePluginStartCommand(p.Executable)
backendFactory = grpcplugin.NewBackendPlugin(p.ID, filepath.Join(p.PluginDir, cmd))
backendFactory := i.backendProvider.BackendFactory(ctx, p)
if backendFactory == nil {
return fmt.Errorf("could not find backend factory for plugin")
}
if backendClient, err := backendFactory(p.ID, pluginLog, i.envVars(p)); err != nil {
if backendClient, err := backendFactory(p.ID, p.Logger(), i.envVars(p)); err != nil {
return err
} else {
p.RegisterClient(backendClient)
@@ -109,88 +48,20 @@ func (i *Initializer) Initialize(p *plugins.Plugin) error {
}
func (i *Initializer) InitializeWithFactory(p *plugins.Plugin, factory backendplugin.PluginFactoryFunc) error {
err := i.Initialize(p)
if factory == nil {
return fmt.Errorf("could not initialize plugin %s", p.ID)
}
f, err := factory(p.ID, log.New("pluginID", p.ID), []string{})
if err != nil {
return err
}
if factory != nil {
var err error
f, err := factory(p.ID, log.New("pluginID", p.ID), []string{})
if err != nil {
return err
}
p.RegisterClient(f)
} else {
i.log.Warn("Could not initialize core plugin process", "pluginID", p.ID)
return fmt.Errorf("could not initialize plugin %s", p.ID)
}
p.RegisterClient(f)
return nil
}
func (i *Initializer) handleModuleDefaults(p *plugins.Plugin) {
if p.IsCorePlugin() {
// Previously there was an assumption that the Core plugins directory
// should be public/app/plugins/<plugin type>/<plugin id>
// However this can be an issue if the Core plugins directory is renamed
baseDir := filepath.Base(p.PluginDir)
// use path package for the following statements because these are not file paths
p.Module = path.Join("app/plugins", string(p.Type), baseDir, "module")
p.BaseURL = path.Join("public/app/plugins", string(p.Type), baseDir)
return
}
metrics.SetPluginBuildInformation(p.ID, string(p.Type), p.Info.Version, string(p.Signature))
p.Module = path.Join("plugins", p.ID, "module")
p.BaseURL = path.Join("public/plugins", p.ID)
}
func (i *Initializer) setPathsBasedOnApp(parent *plugins.Plugin, child *plugins.Plugin) {
appSubPath := strings.ReplaceAll(strings.Replace(child.PluginDir, parent.PluginDir, "", 1), "\\", "/")
child.IncludedInAppID = parent.ID
child.BaseURL = parent.BaseURL
if parent.IsCorePlugin() {
child.Module = util.JoinURLFragments("app/plugins/app/"+parent.ID, appSubPath) + "/module"
} else {
child.Module = util.JoinURLFragments("plugins/"+parent.ID, appSubPath) + "/module"
}
}
func pluginLogoURL(pluginType plugins.Type, path, baseURL string) string {
if path == "" {
return defaultLogoPath(pluginType)
}
return evalRelativePluginURLPath(path, baseURL, pluginType)
}
func defaultLogoPath(pluginType plugins.Type) string {
return "public/img/icn-" + string(pluginType) + ".svg"
}
func evalRelativePluginURLPath(pathStr, baseURL string, pluginType plugins.Type) string {
if pathStr == "" {
return ""
}
u, _ := url.Parse(pathStr)
if u.IsAbs() {
return pathStr
}
// is set as default or has already been prefixed with base path
if pathStr == defaultLogoPath(pluginType) || strings.HasPrefix(pathStr, baseURL) {
return pathStr
}
return path.Join(baseURL, pathStr)
}
func (i *Initializer) envVars(plugin *plugins.Plugin) []string {
hostEnv := []string{
fmt.Sprintf("GF_VERSION=%s", i.cfg.BuildVersion),
@@ -260,7 +131,7 @@ func (ps pluginSettings) asEnvVar(prefix string, hostEnv []string) []string {
return env
}
func getPluginSettings(pluginID string, cfg *setting.Cfg) pluginSettings {
func getPluginSettings(pluginID string, cfg *plugins.Cfg) pluginSettings {
ps := pluginSettings{}
for k, v := range cfg.PluginSettings[pluginID] {
if k == "path" || strings.ToLower(k) == "id" {
@@ -1,17 +1,15 @@
package initializer
import (
"path"
"context"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"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/setting"
)
func TestInitializer_Initialize(t *testing.T) {
@@ -36,21 +34,16 @@ func TestInitializer_Initialize(t *testing.T) {
}
i := &Initializer{
cfg: setting.NewCfg(),
cfg: plugins.NewCfg(),
log: &fakeLogger{},
backendProvider: &fakeBackendProvider{
plugin: p,
},
}
err := i.Initialize(p)
err := i.Initialize(context.Background(), p)
assert.NoError(t, err)
assert.Equal(t, "public/img/icn-datasource.svg", p.Info.Logos.Small)
assert.Equal(t, "public/img/icn-datasource.svg", p.Info.Logos.Large)
assert.Equal(t, "*", p.Dependencies.GrafanaVersion)
assert.Len(t, p.Includes, 1)
assert.Equal(t, models.ROLE_VIEWER, p.Includes[0].Role)
assert.Equal(t, filepath.Join("app/plugins/datasource", filepath.Base(p.PluginDir), "module"), p.Module)
assert.Equal(t, path.Join("public/app/plugins/datasource", filepath.Base(p.PluginDir)), p.BaseURL)
assert.NotNil(t, p.Logger())
c, exists := p.Client()
assert.True(t, exists)
assert.NotNil(t, c)
@@ -71,101 +64,50 @@ func TestInitializer_Initialize(t *testing.T) {
}
i := &Initializer{
cfg: setting.NewCfg(),
cfg: plugins.NewCfg(),
log: fakeLogger{},
backendProvider: &fakeBackendProvider{
plugin: p,
},
}
err := i.Initialize(p)
err := i.Initialize(context.Background(), p)
assert.NoError(t, err)
// TODO add default img to project
assert.Equal(t, "public/img/icn-renderer.svg", p.Info.Logos.Small)
assert.Equal(t, "public/img/icn-renderer.svg", p.Info.Logos.Large)
assert.Equal(t, ">=8.x", p.Dependencies.GrafanaVersion)
assert.Equal(t, "plugins/test/module", p.Module)
assert.Equal(t, "public/plugins/test", p.BaseURL)
assert.NotNil(t, p.Logger())
c, exists := p.Client()
assert.True(t, exists)
assert.NotNil(t, c)
})
t.Run("external app", func(t *testing.T) {
t.Run("non backend plugin app", func(t *testing.T) {
p := &plugins.Plugin{
JSONData: plugins.JSONData{
ID: "parent-plugin",
Type: plugins.App,
Includes: []*plugins.Includes{
{
Type: "page",
DefaultNav: true,
Slug: "myCustomSlug",
},
},
},
PluginDir: absCurPath,
Class: plugins.External,
Children: []*plugins.Plugin{
{
JSONData: plugins.JSONData{
ID: "child-plugin",
},
PluginDir: absCurPath,
},
Backend: false,
},
}
i := &Initializer{
cfg: &setting.Cfg{
AppSubURL: "appSubURL",
},
cfg: &plugins.Cfg{},
log: fakeLogger{},
backendProvider: &fakeBackendProvider{
plugin: p,
},
}
err := i.Initialize(p)
err := i.Initialize(context.Background(), p)
assert.NoError(t, err)
assert.Equal(t, "public/img/icn-app.svg", p.Info.Logos.Small)
assert.Equal(t, "public/img/icn-app.svg", p.Info.Logos.Large)
assert.Equal(t, "*", p.Dependencies.GrafanaVersion)
assert.Len(t, p.Includes, 1)
assert.Equal(t, models.ROLE_VIEWER, p.Includes[0].Role)
assert.Equal(t, filepath.Join("plugins", p.ID, "module"), p.Module)
assert.Equal(t, "public/plugins/parent-plugin", p.BaseURL)
assert.NotNil(t, p.Logger())
c, exists := p.Client()
assert.False(t, exists)
assert.Nil(t, c)
assert.Len(t, p.Children, 1)
assert.Equal(t, p.ID, p.Children[0].IncludedInAppID)
assert.Equal(t, "public/plugins/parent-plugin", p.Children[0].BaseURL)
assert.Equal(t, "plugins/parent-plugin/module", p.Children[0].Module)
assert.Equal(t, "appSubURL/plugins/parent-plugin/page/myCustomSlug", p.DefaultNavURL)
})
}
func TestInitializer_InitializeWithFactory(t *testing.T) {
t.Run("happy path", func(t *testing.T) {
p := &plugins.Plugin{
JSONData: plugins.JSONData{
ID: "test-plugin",
Type: plugins.App,
Includes: []*plugins.Includes{
{
Type: "page",
DefaultNav: true,
Slug: "myCustomSlug",
},
},
},
PluginDir: "test/folder",
Class: plugins.External,
}
p := &plugins.Plugin{}
i := &Initializer{
cfg: &setting.Cfg{
AppSubURL: "appSubURL",
},
cfg: &plugins.Cfg{},
log: fakeLogger{},
}
@@ -180,33 +122,19 @@ func TestInitializer_InitializeWithFactory(t *testing.T) {
assert.NoError(t, err)
assert.True(t, factoryInvoked)
assert.NotNil(t, p.Logger())
client, exists := p.Client()
assert.True(t, exists)
assert.NotNil(t, client.(testPlugin))
})
t.Run("invalid factory", func(t *testing.T) {
p := &plugins.Plugin{
JSONData: plugins.JSONData{
ID: "test-plugin",
Type: plugins.App,
Includes: []*plugins.Includes{
{
Type: "page",
DefaultNav: true,
Slug: "myCustomSlug",
},
},
},
PluginDir: "test/folder",
Class: plugins.External,
}
p := &plugins.Plugin{}
i := &Initializer{
cfg: &setting.Cfg{
AppSubURL: "appSubURL",
},
cfg: &plugins.Cfg{},
log: fakeLogger{},
backendProvider: &fakeBackendProvider{
plugin: p,
},
}
err := i.InitializeWithFactory(p, nil)
@@ -232,7 +160,7 @@ func TestInitializer_envVars(t *testing.T) {
}
i := &Initializer{
cfg: &setting.Cfg{
cfg: &plugins.Cfg{
EnterpriseLicensePath: "/path/to/ent/license",
PluginSettings: map[string]map[string]string{
"test": {
@@ -242,6 +170,9 @@ func TestInitializer_envVars(t *testing.T) {
},
license: licensing,
log: fakeLogger{},
backendProvider: &fakeBackendProvider{
plugin: p,
},
}
envVars := i.envVars(p)
@@ -254,33 +185,6 @@ func TestInitializer_envVars(t *testing.T) {
})
}
func TestInitializer_setPathsBasedOnApp(t *testing.T) {
t.Run("When setting paths based on core plugin on Windows", func(t *testing.T) {
i := &Initializer{
cfg: setting.NewCfg(),
log: fakeLogger{},
}
child := &plugins.Plugin{
PluginDir: "c:\\grafana\\public\\app\\plugins\\app\\testdata\\datasources\\datasource",
}
parent := &plugins.Plugin{
JSONData: plugins.JSONData{
ID: "testdata",
},
Class: plugins.Core,
PluginDir: "c:\\grafana\\public\\app\\plugins\\app\\testdata",
BaseURL: "public/app/plugins/app/testdata",
}
i.setPathsBasedOnApp(parent, child)
assert.Equal(t, "app/plugins/app/testdata/datasources/datasource/module", child.Module)
assert.Equal(t, "testdata", child.IncludedInAppID)
assert.Equal(t, "public/app/plugins/app/testdata", child.BaseURL)
})
}
func TestInitializer_getAWSEnvironmentVariables(t *testing.T) {
}
@@ -334,7 +238,7 @@ func (t *testLicensingService) ContentDeliveryPrefix() string {
return ""
}
func (t *testLicensingService) LicenseURL(showAdminLicensingPage bool) string {
func (t *testLicensingService) LicenseURL(_ bool) string {
return ""
}
@@ -365,3 +269,15 @@ func (f fakeLogger) New(_ ...interface{}) log.MultiLoggers {
func (f fakeLogger) Warn(_ string, _ ...interface{}) {
}
type fakeBackendProvider struct {
plugins.BackendFactoryProvider
plugin *plugins.Plugin
}
func (f *fakeBackendProvider) BackendFactory(_ context.Context, _ *plugins.Plugin) backendplugin.PluginFactoryFunc {
return func(_ string, _ log.Logger, _ []string) (backendplugin.Plugin, error) {
return f.plugin, nil
}
}
+134 -44
View File
@@ -1,16 +1,22 @@
package loader
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/url"
"os"
"path"
"path/filepath"
"runtime"
"strings"
"github.com/gosimple/slug"
"github.com/grafana/grafana/pkg/infra/fs"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/metrics"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/backendplugin"
@@ -18,6 +24,7 @@ import (
"github.com/grafana/grafana/pkg/plugins/manager/loader/initializer"
"github.com/grafana/grafana/pkg/plugins/manager/signature"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
)
var (
@@ -28,7 +35,7 @@ var (
var _ plugins.ErrorResolver = (*Loader)(nil)
type Loader struct {
cfg *setting.Cfg
cfg *plugins.Cfg
pluginFinder finder.Finder
pluginInitializer initializer.Initializer
signatureValidator signature.Validator
@@ -37,32 +44,34 @@ type Loader struct {
errs map[string]*plugins.SignatureError
}
func ProvideService(license models.Licensing, cfg *setting.Cfg, authorizer plugins.PluginLoaderAuthorizer) (*Loader, error) {
return New(license, cfg, authorizer), nil
func ProvideService(cfg *setting.Cfg, license models.Licensing, authorizer plugins.PluginLoaderAuthorizer,
backendProvider plugins.BackendFactoryProvider) (*Loader, error) {
return New(plugins.FromGrafanaCfg(cfg), license, authorizer, backendProvider), nil
}
func New(license models.Licensing, cfg *setting.Cfg, authorizer plugins.PluginLoaderAuthorizer) *Loader {
func New(cfg *plugins.Cfg, license models.Licensing, authorizer plugins.PluginLoaderAuthorizer,
backendProvider plugins.BackendFactoryProvider) *Loader {
return &Loader{
cfg: cfg,
pluginFinder: finder.New(cfg),
pluginInitializer: initializer.New(cfg, license),
pluginFinder: finder.New(),
pluginInitializer: initializer.New(cfg, backendProvider, license),
signatureValidator: signature.NewValidator(authorizer),
errs: make(map[string]*plugins.SignatureError),
log: log.New("plugin.loader"),
}
}
func (l *Loader) Load(paths []string, ignore map[string]struct{}) ([]*plugins.Plugin, error) {
func (l *Loader) Load(ctx context.Context, class plugins.Class, paths []string, ignore map[string]struct{}) ([]*plugins.Plugin, error) {
pluginJSONPaths, err := l.pluginFinder.Find(paths)
if err != nil {
l.log.Error("plugin finder encountered an error", "err", err)
}
return l.loadPlugins(pluginJSONPaths, ignore)
return l.loadPlugins(ctx, class, pluginJSONPaths, ignore)
}
func (l *Loader) LoadWithFactory(path string, factory backendplugin.PluginFactoryFunc) (*plugins.Plugin, error) {
p, err := l.load(path, map[string]struct{}{})
func (l *Loader) LoadWithFactory(ctx context.Context, class plugins.Class, path string, factory backendplugin.PluginFactoryFunc) (*plugins.Plugin, error) {
p, err := l.load(ctx, class, path, map[string]struct{}{})
if err != nil {
l.log.Error("failed to load core plugin", "err", err)
return nil, err
@@ -73,14 +82,14 @@ func (l *Loader) LoadWithFactory(path string, factory backendplugin.PluginFactor
return p, err
}
func (l *Loader) load(path string, ignore map[string]struct{}) (*plugins.Plugin, error) {
func (l *Loader) load(ctx context.Context, class plugins.Class, path string, ignore map[string]struct{}) (*plugins.Plugin, error) {
pluginJSONPaths, err := l.pluginFinder.Find([]string{path})
if err != nil {
l.log.Error("failed to find plugin", "err", err)
return nil, err
}
loadedPlugins, err := l.loadPlugins(pluginJSONPaths, ignore)
loadedPlugins, err := l.loadPlugins(ctx, class, pluginJSONPaths, ignore)
if err != nil {
return nil, err
}
@@ -92,7 +101,7 @@ func (l *Loader) load(path string, ignore map[string]struct{}) (*plugins.Plugin,
return loadedPlugins[0], nil
}
func (l *Loader) loadPlugins(pluginJSONPaths []string, existingPlugins map[string]struct{}) ([]*plugins.Plugin, error) {
func (l *Loader) loadPlugins(ctx context.Context, class plugins.Class, pluginJSONPaths []string, existingPlugins map[string]struct{}) ([]*plugins.Plugin, error) {
var foundPlugins = foundPlugins{}
// load plugin.json files and map directory to JSON data
@@ -124,8 +133,10 @@ func (l *Loader) loadPlugins(pluginJSONPaths []string, existingPlugins map[strin
plugin := &plugins.Plugin{
JSONData: pluginJSON,
PluginDir: pluginDir,
Class: l.pluginClass(pluginDir),
Class: class,
}
l.setDefaults(plugin)
plugin.SetLogger(l.log.New("pluginID", plugin.ID))
sig, err := signature.Calculate(l.log, plugin)
if err != nil {
@@ -160,7 +171,7 @@ func (l *Loader) loadPlugins(pluginJSONPaths []string, existingPlugins map[strin
}
// validate signatures
verifiedPlugins := []*plugins.Plugin{}
verifiedPlugins := make([]*plugins.Plugin, 0)
for _, plugin := range loadedPlugins {
signingError := l.signatureValidator.Validate(plugin)
if signingError != nil {
@@ -192,7 +203,7 @@ func (l *Loader) loadPlugins(pluginJSONPaths []string, existingPlugins map[strin
}
for _, p := range verifiedPlugins {
err := l.pluginInitializer.Initialize(p)
err := l.pluginInitializer.Initialize(ctx, p)
if err != nil {
return nil, err
}
@@ -233,9 +244,114 @@ func (l *Loader) readPluginJSON(pluginJSONPath string) (plugins.JSONData, error)
plugin.Name = "Pie Chart (old)"
}
if len(plugin.Dependencies.Plugins) == 0 {
plugin.Dependencies.Plugins = []plugins.Dependency{}
}
if plugin.Dependencies.GrafanaVersion == "" {
plugin.Dependencies.GrafanaVersion = "*"
}
for _, include := range plugin.Includes {
if include.Role == "" {
include.Role = models.ROLE_VIEWER
}
}
return plugin, nil
}
func (l *Loader) setDefaults(p *plugins.Plugin) {
setModule(p)
p.Info.Logos.Small = pluginLogoURL(p.Type, p.Info.Logos.Small, p.BaseURL)
p.Info.Logos.Large = pluginLogoURL(p.Type, p.Info.Logos.Large, p.BaseURL)
for i := 0; i < len(p.Info.Screenshots); i++ {
p.Info.Screenshots[i].Path = evalRelativePluginURLPath(p.Info.Screenshots[i].Path, p.BaseURL, p.Type)
}
if p.IsApp() {
for _, child := range p.Children {
setChildModule(p, child)
}
// slugify pages
for _, include := range p.Includes {
if include.Slug == "" {
include.Slug = slug.Make(include.Name)
}
if include.Type == "page" && include.DefaultNav {
p.DefaultNavURL = l.cfg.AppSubURL + "/plugins/" + p.ID + "/page/" + include.Slug
}
if include.Type == "dashboard" && include.DefaultNav {
p.DefaultNavURL = l.cfg.AppSubURL + "/dashboard/db/" + include.Slug
}
}
}
}
func setModule(p *plugins.Plugin) {
if p.IsCorePlugin() {
// Previously there was an assumption that the Core plugins directory
// should be public/app/plugins/<plugin type>/<plugin id>
// However this can be an issue if the Core plugins directory is renamed
baseDir := filepath.Base(p.PluginDir)
// use path package for the following statements because these are not file paths
p.Module = path.Join("app/plugins", string(p.Type), baseDir, "module")
p.BaseURL = path.Join("public/app/plugins", string(p.Type), baseDir)
return
}
metrics.SetPluginBuildInformation(p.ID, string(p.Type), p.Info.Version, string(p.Signature))
p.Module = path.Join("plugins", p.ID, "module")
p.BaseURL = path.Join("public/plugins", p.ID)
}
func setChildModule(parent *plugins.Plugin, child *plugins.Plugin) {
appSubPath := strings.ReplaceAll(strings.Replace(child.PluginDir, parent.PluginDir, "", 1), "\\", "/")
child.IncludedInAppID = parent.ID
child.BaseURL = parent.BaseURL
if parent.IsCorePlugin() {
child.Module = util.JoinURLFragments("app/plugins/app/"+parent.ID, appSubPath) + "/module"
} else {
child.Module = util.JoinURLFragments("plugins/"+parent.ID, appSubPath) + "/module"
}
}
func pluginLogoURL(pluginType plugins.Type, path, baseURL string) string {
if path == "" {
return defaultLogoPath(pluginType)
}
return evalRelativePluginURLPath(path, baseURL, pluginType)
}
func defaultLogoPath(pluginType plugins.Type) string {
return "public/img/icn-" + string(pluginType) + ".svg"
}
func evalRelativePluginURLPath(pathStr, baseURL string, pluginType plugins.Type) string {
if pathStr == "" {
return ""
}
u, _ := url.Parse(pathStr)
if u.IsAbs() {
return pathStr
}
// is set as default or has already been prefixed with base path
if pathStr == defaultLogoPath(pluginType) || strings.HasPrefix(pathStr, baseURL) {
return pathStr
}
return path.Join(baseURL, pathStr)
}
func (l *Loader) PluginErrors() []*plugins.Error {
errs := make([]*plugins.Error, 0)
for _, err := range l.errs {
@@ -255,41 +371,15 @@ func validatePluginJSON(data plugins.JSONData) error {
return nil
}
func (l *Loader) pluginClass(pluginDir string) plugins.Class {
isSubDir := func(base, target string) bool {
path, err := filepath.Rel(base, target)
if err != nil {
return false
}
if !strings.HasPrefix(path, "..") {
return true
}
return false
}
corePluginsDir := filepath.Join(l.cfg.StaticRootPath, "app/plugins")
if isSubDir(corePluginsDir, pluginDir) {
return plugins.Core
}
if isSubDir(l.cfg.BundledPluginsPath, pluginDir) {
return plugins.Bundled
}
return plugins.External
}
type foundPlugins map[string]plugins.JSONData
// stripDuplicates will strip duplicate plugins or plugins that already exist
func (f *foundPlugins) stripDuplicates(existingPlugins map[string]struct{}, log log.Logger) {
pluginsByID := make(map[string]struct{})
for path, scannedPlugin := range *f {
for k, scannedPlugin := range *f {
if _, existing := existingPlugins[scannedPlugin.ID]; existing {
log.Debug("Skipping plugin as it's already installed", "plugin", scannedPlugin.ID)
delete(*f, path)
delete(*f, k)
continue
}
+136 -114
View File
@@ -1,19 +1,21 @@
package loader
import (
"context"
"errors"
"path/filepath"
"sort"
"testing"
"github.com/stretchr/testify/require"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"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/provider"
"github.com/grafana/grafana/pkg/plugins/manager/loader/finder"
"github.com/grafana/grafana/pkg/plugins/manager/loader/initializer"
"github.com/grafana/grafana/pkg/plugins/manager/signature"
@@ -35,16 +37,18 @@ func TestLoader_Load(t *testing.T) {
}
tests := []struct {
name string
cfg *setting.Cfg
class plugins.Class
cfg *plugins.Cfg
pluginPaths []string
existingPlugins map[string]struct{}
want []*plugins.Plugin
pluginErrors map[string]*plugins.Error
}{
{
name: "Load a Core plugin",
cfg: &setting.Cfg{
StaticRootPath: corePluginDir,
name: "Load a Core plugin",
class: plugins.Core,
cfg: &plugins.Cfg{
PluginsPath: corePluginDir,
},
pluginPaths: []string{filepath.Join(corePluginDir, "app/plugins/datasource/cloudwatch")},
want: []*plugins.Plugin{
@@ -86,15 +90,16 @@ func TestLoader_Load(t *testing.T) {
Module: "app/plugins/datasource/cloudwatch/module",
BaseURL: "public/app/plugins/datasource/cloudwatch",
PluginDir: filepath.Join(corePluginDir, "app/plugins/datasource/cloudwatch"),
Signature: "internal",
Class: "core",
Signature: plugins.SignatureInternal,
Class: plugins.Core,
},
},
},
{
name: "Load a Bundled plugin",
cfg: &setting.Cfg{
BundledPluginsPath: filepath.Join(parentDir, "testdata"),
name: "Load a Bundled plugin",
class: plugins.Bundled,
cfg: &plugins.Cfg{
PluginsPath: filepath.Join(parentDir, "testdata"),
},
pluginPaths: []string{"../testdata/valid-v2-signature"},
want: []*plugins.Plugin{
@@ -129,12 +134,13 @@ func TestLoader_Load(t *testing.T) {
Signature: "valid",
SignatureType: plugins.GrafanaSignature,
SignatureOrg: "Grafana Labs",
Class: "bundled",
Class: plugins.Bundled,
},
},
}, {
name: "Load an External plugin",
cfg: &setting.Cfg{
name: "Load plugin with symbolic links",
class: plugins.External,
cfg: &plugins.Cfg{
PluginsPath: filepath.Join(parentDir),
},
pluginPaths: []string{"../testdata/symbolic-plugin-dirs"},
@@ -210,10 +216,11 @@ func TestLoader_Load(t *testing.T) {
},
},
}, {
name: "Load an unsigned plugin (development)",
cfg: &setting.Cfg{
name: "Load an unsigned plugin (development)",
class: plugins.External,
cfg: &plugins.Cfg{
DevMode: true,
PluginsPath: filepath.Join(parentDir),
Env: "development",
},
pluginPaths: []string{"../testdata/unsigned-datasource"},
want: []*plugins.Plugin{
@@ -248,10 +255,10 @@ func TestLoader_Load(t *testing.T) {
},
},
}, {
name: "Load an unsigned plugin (production)",
cfg: &setting.Cfg{
name: "Load an unsigned plugin (production)",
class: plugins.External,
cfg: &plugins.Cfg{
PluginsPath: filepath.Join(parentDir),
Env: "production",
},
pluginPaths: []string{"../testdata/unsigned-datasource"},
want: []*plugins.Plugin{},
@@ -263,10 +270,10 @@ func TestLoader_Load(t *testing.T) {
},
},
{
name: "Load an unsigned plugin using PluginsAllowUnsigned config (production)",
cfg: &setting.Cfg{
name: "Load an unsigned plugin using PluginsAllowUnsigned config (production)",
class: plugins.External,
cfg: &plugins.Cfg{
PluginsPath: filepath.Join(parentDir),
Env: "production",
PluginsAllowUnsigned: []string{"test"},
},
pluginPaths: []string{"../testdata/unsigned-datasource"},
@@ -298,15 +305,15 @@ func TestLoader_Load(t *testing.T) {
Module: "plugins/test/module",
BaseURL: "public/plugins/test",
PluginDir: filepath.Join(parentDir, "testdata/unsigned-datasource/plugin"),
Signature: "unsigned",
Signature: plugins.SignatureUnsigned,
},
},
},
{
name: "Load an unsigned plugin with modified signature (production)",
cfg: &setting.Cfg{
name: "Load an unsigned plugin with modified signature (production)",
class: plugins.External,
cfg: &plugins.Cfg{
PluginsPath: filepath.Join(parentDir),
Env: "production",
},
pluginPaths: []string{"../testdata/lacking-files"},
want: []*plugins.Plugin{},
@@ -318,10 +325,10 @@ func TestLoader_Load(t *testing.T) {
},
},
{
name: "Load an unsigned plugin with modified signature using PluginsAllowUnsigned config (production) still includes a signing error",
cfg: &setting.Cfg{
name: "Load an unsigned plugin with modified signature using PluginsAllowUnsigned config (production) still includes a signing error",
class: plugins.External,
cfg: &plugins.Cfg{
PluginsPath: filepath.Join(parentDir),
Env: "production",
PluginsAllowUnsigned: []string{"test"},
},
pluginPaths: []string{"../testdata/lacking-files"},
@@ -333,11 +340,61 @@ func TestLoader_Load(t *testing.T) {
},
},
},
{
name: "Load an app with includes",
class: plugins.External,
cfg: &plugins.Cfg{
PluginsPath: filepath.Join(parentDir),
PluginsAllowUnsigned: []string{"test-app"},
},
pluginPaths: []string{"../testdata/test-app-with-includes"},
want: []*plugins.Plugin{
{JSONData: plugins.JSONData{
ID: "test-app",
Type: "app",
Name: "Test App",
Info: plugins.Info{
Author: plugins.InfoLink{
Name: "Test Inc.",
URL: "http://test.com",
},
Description: "Official Grafana Test App & Dashboard bundle",
Version: "1.0.0",
Links: []plugins.InfoLink{
{Name: "Project site", URL: "http://project.com"},
{Name: "License & Terms", URL: "http://license.com"},
},
Logos: plugins.Logos{
Small: "public/img/icn-app.svg",
Large: "public/img/icn-app.svg",
},
Updated: "2015-02-10",
},
Dependencies: plugins.Dependencies{
GrafanaDependency: ">=8.0.0",
GrafanaVersion: "*",
Plugins: []plugins.Dependency{},
},
Includes: []*plugins.Includes{
{Name: "Nginx Memory", Path: "dashboards/memory.json", Type: "dashboard", Role: "Viewer", Slug: "nginx-memory", DefaultNav: true},
{Name: "Root Page (react)", Type: "page", Role: "Viewer", Path: "/a/my-simple-app", DefaultNav: true, AddToNav: true, Slug: "root-page-react"},
},
Backend: false,
},
DefaultNavURL: "/plugins/test-app/page/root-page-react",
PluginDir: filepath.Join(parentDir, "testdata/test-app-with-includes"),
Class: plugins.External,
Signature: plugins.SignatureUnsigned,
Module: "plugins/test-app/module",
BaseURL: "public/plugins/test-app",
},
},
},
}
for _, tt := range tests {
l := newLoader(tt.cfg)
t.Run(tt.name, func(t *testing.T) {
got, err := l.Load(tt.pluginPaths, tt.existingPlugins)
got, err := l.Load(context.Background(), tt.class, tt.pluginPaths, tt.existingPlugins)
require.NoError(t, err)
if !cmp.Equal(got, tt.want, compareOpts) {
t.Fatalf("Result mismatch (-want +got):\n%s", cmp.Diff(got, tt.want, compareOpts))
@@ -362,7 +419,7 @@ func TestLoader_Load_MultiplePlugins(t *testing.T) {
t.Run("Load multiple", func(t *testing.T) {
tests := []struct {
name string
cfg *setting.Cfg
cfg *plugins.Cfg
pluginPaths []string
appURL string
existingPlugins map[string]struct{}
@@ -371,8 +428,7 @@ func TestLoader_Load_MultiplePlugins(t *testing.T) {
}{
{
name: "Load multiple plugins (broken, valid, unsigned)",
cfg: &setting.Cfg{
Env: "production",
cfg: &plugins.Cfg{
PluginsPath: filepath.Join(parentDir),
},
appURL: "http://localhost:3000",
@@ -434,7 +490,7 @@ func TestLoader_Load_MultiplePlugins(t *testing.T) {
})
setting.AppUrl = tt.appURL
got, err := l.Load(tt.pluginPaths, tt.existingPlugins)
got, err := l.Load(context.Background(), plugins.External, tt.pluginPaths, tt.existingPlugins)
require.NoError(t, err)
sort.SliceStable(got, func(i, j int) bool {
return got[i].ID < got[j].ID
@@ -488,17 +544,17 @@ func TestLoader_Signature_RootURL(t *testing.T) {
Executable: "test",
},
PluginDir: filepath.Join(parentDir, "/testdata/valid-v2-pvt-signature-root-url-uri/plugin"),
Class: "external",
Signature: "valid",
SignatureType: "private",
Class: plugins.External,
Signature: plugins.SignatureValid,
SignatureType: plugins.PrivateSignature,
SignatureOrg: "Will Browne",
Module: "plugins/test/module",
BaseURL: "public/plugins/test",
},
}
l := newLoader(&setting.Cfg{PluginsPath: filepath.Join(parentDir)})
got, err := l.Load(paths, map[string]struct{}{})
l := newLoader(&plugins.Cfg{PluginsPath: filepath.Join(parentDir)})
got, err := l.Load(context.Background(), plugins.External, paths, map[string]struct{}{})
assert.NoError(t, err)
if !cmp.Equal(got, expected, compareOpts) {
@@ -566,11 +622,11 @@ func TestLoader_Load_DuplicatePlugins(t *testing.T) {
},
}
l := newLoader(&setting.Cfg{
l := newLoader(&plugins.Cfg{
PluginsPath: filepath.Dir(pluginDir),
})
got, err := l.Load([]string{pluginDir, pluginDir}, map[string]struct{}{})
got, err := l.Load(context.Background(), plugins.External, []string{pluginDir, pluginDir}, map[string]struct{}{})
assert.NoError(t, err)
if !cmp.Equal(got, expected, compareOpts) {
@@ -612,10 +668,10 @@ func TestLoader_loadNestedPlugins(t *testing.T) {
Module: "plugins/test-ds/module",
BaseURL: "public/plugins/test-ds",
PluginDir: filepath.Join(parentDir, "testdata/nested-plugins/parent"),
Signature: "valid",
Signature: plugins.SignatureValid,
SignatureType: plugins.GrafanaSignature,
SignatureOrg: "Grafana Labs",
Class: "external",
Class: plugins.External,
}
child := &plugins.Plugin{
@@ -644,10 +700,10 @@ func TestLoader_loadNestedPlugins(t *testing.T) {
Module: "plugins/test-panel/module",
BaseURL: "public/plugins/test-panel",
PluginDir: filepath.Join(parentDir, "testdata/nested-plugins/parent/nested"),
Signature: "valid",
Signature: plugins.SignatureValid,
SignatureType: plugins.GrafanaSignature,
SignatureOrg: "Grafana Labs",
Class: "external",
Class: plugins.External,
}
parent.Children = []*plugins.Plugin{child}
@@ -655,11 +711,11 @@ func TestLoader_loadNestedPlugins(t *testing.T) {
t.Run("Load nested External plugins", func(t *testing.T) {
expected := []*plugins.Plugin{parent, child}
l := newLoader(&setting.Cfg{
l := newLoader(&plugins.Cfg{
PluginsPath: parentDir,
})
got, err := l.Load([]string{"../testdata/nested-plugins"}, map[string]struct{}{})
got, err := l.Load(context.Background(), plugins.External, []string{"../testdata/nested-plugins"}, map[string]struct{}{})
assert.NoError(t, err)
// to ensure we can compare with expected
@@ -677,11 +733,11 @@ func TestLoader_loadNestedPlugins(t *testing.T) {
parent.Children = nil
expected := []*plugins.Plugin{parent}
l := newLoader(&setting.Cfg{
l := newLoader(&plugins.Cfg{
PluginsPath: parentDir,
})
got, err := l.Load([]string{"../testdata/nested-plugins"}, map[string]struct{}{
got, err := l.Load(context.Background(), plugins.External, []string{"../testdata/nested-plugins"}, map[string]struct{}{
"test-panel": {},
})
assert.NoError(t, err)
@@ -740,10 +796,10 @@ func TestLoader_readPluginJSON(t *testing.T) {
},
},
Includes: []*plugins.Includes{
{Name: "Nginx Connections", Path: "dashboards/connections.json", Type: "dashboard"},
{Name: "Nginx Memory", Path: "dashboards/memory.json", Type: "dashboard"},
{Name: "Nginx Panel", Type: "panel"},
{Name: "Nginx Datasource", Type: "datasource"},
{Name: "Nginx Connections", Path: "dashboards/connections.json", Type: "dashboard", Role: models.ROLE_VIEWER},
{Name: "Nginx Memory", Path: "dashboards/memory.json", Type: "dashboard", Role: models.ROLE_VIEWER},
{Name: "Nginx Panel", Type: "panel", Role: models.ROLE_VIEWER},
{Name: "Nginx Datasource", Type: "datasource", Role: models.ROLE_VIEWER},
},
Backend: false,
},
@@ -822,71 +878,33 @@ func Test_validatePluginJSON(t *testing.T) {
}
}
func Test_pluginClass(t *testing.T) {
type args struct {
pluginDir string
cfg *setting.Cfg
}
tests := []struct {
name string
args args
expected plugins.Class
}{
{
name: "Core plugin class",
args: args{
pluginDir: "/root/app/plugins/test-app",
cfg: &setting.Cfg{
StaticRootPath: "/root",
},
func Test_setPathsBasedOnApp(t *testing.T) {
t.Run("When setting paths based on core plugin on Windows", func(t *testing.T) {
child := &plugins.Plugin{
PluginDir: "c:\\grafana\\public\\app\\plugins\\app\\testdata\\datasources\\datasource",
}
parent := &plugins.Plugin{
JSONData: plugins.JSONData{
ID: "testdata",
},
expected: plugins.Core,
},
{
name: "Bundled plugin class",
args: args{
pluginDir: "/test-app",
cfg: &setting.Cfg{
BundledPluginsPath: "/test-app",
},
},
expected: plugins.Bundled,
},
{
name: "External plugin class",
args: args{
pluginDir: "/test-app",
cfg: &setting.Cfg{
PluginsPath: "/test-app",
},
},
expected: plugins.External,
},
{
name: "External plugin class",
args: args{
pluginDir: "/test-app",
cfg: &setting.Cfg{
PluginsPath: "/root",
},
},
expected: plugins.External,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
l := newLoader(tt.args.cfg)
got := l.pluginClass(tt.args.pluginDir)
assert.Equal(t, tt.expected, got)
})
}
Class: plugins.Core,
PluginDir: "c:\\grafana\\public\\app\\plugins\\app\\testdata",
BaseURL: "public/app/plugins/app/testdata",
}
setChildModule(parent, child)
assert.Equal(t, "app/plugins/app/testdata/datasources/datasource/module", child.Module)
assert.Equal(t, "testdata", child.IncludedInAppID)
assert.Equal(t, "public/app/plugins/app/testdata", child.BaseURL)
})
}
func newLoader(cfg *setting.Cfg) *Loader {
func newLoader(cfg *plugins.Cfg) *Loader {
return &Loader{
cfg: cfg,
pluginFinder: finder.New(cfg),
pluginInitializer: initializer.New(cfg, &fakeLicensingService{}),
pluginFinder: finder.New(),
pluginInitializer: initializer.New(cfg, &provider.Service{}, &fakeLicensingService{}),
signatureValidator: signature.NewValidator(&signature.UnsignedPluginAuthorizer{Cfg: cfg}),
errs: make(map[string]*plugins.SignatureError),
log: &fakeLogger{},
@@ -934,6 +952,10 @@ type fakeLogger struct {
log.Logger
}
func (fl fakeLogger) New(_ ...interface{}) log.MultiLoggers {
return log.MultiLoggers{}
}
func (fl fakeLogger) Info(_ string, _ ...interface{}) {
}