From 7694fff0efae1d6a59e3ad595f47cb4a6986f174 Mon Sep 17 00:00:00 2001 From: Will Browne Date: Fri, 14 Jan 2022 13:30:39 +0100 Subject: [PATCH] [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 --- .../backendplugin/grpcplugin/client_v2.go | 18 +- .../backendplugin/provider/provider.go | 48 ++++ pkg/plugins/config.go | 57 ++++ pkg/plugins/ifaces.go | 9 +- pkg/plugins/manager/dashboard_import_test.go | 7 +- pkg/plugins/manager/dashboards_test.go | 7 +- pkg/plugins/manager/loader/finder/finder.go | 6 +- .../manager/loader/finder/finder_test.go | 2 +- .../manager/loader/initializer/initializer.go | 173 ++---------- .../loader/initializer/initializer_test.go | 168 +++--------- pkg/plugins/manager/loader/loader.go | 178 ++++++++++--- pkg/plugins/manager/loader/loader_test.go | 250 ++++++++++-------- pkg/plugins/manager/manager.go | 91 +++---- .../manager/manager_integration_test.go | 6 +- pkg/plugins/manager/manager_test.go | 68 +---- pkg/plugins/manager/signature/authorizer.go | 6 +- pkg/plugins/manager/store.go | 6 +- .../dashboards/memory.json | 5 + .../test-app-with-includes/plugin.json | 38 +++ pkg/server/wireexts_oss.go | 3 + 20 files changed, 566 insertions(+), 580 deletions(-) create mode 100644 pkg/plugins/backendplugin/provider/provider.go create mode 100644 pkg/plugins/config.go create mode 100644 pkg/plugins/manager/testdata/test-app-with-includes/dashboards/memory.json create mode 100644 pkg/plugins/manager/testdata/test-app-with-includes/plugin.json diff --git a/pkg/plugins/backendplugin/grpcplugin/client_v2.go b/pkg/plugins/backendplugin/grpcplugin/client_v2.go index 8ea5c766ad2..30f6399bca0 100644 --- a/pkg/plugins/backendplugin/grpcplugin/client_v2.go +++ b/pkg/plugins/backendplugin/grpcplugin/client_v2.go @@ -18,7 +18,7 @@ import ( "google.golang.org/grpc/status" ) -type clientV2 struct { +type ClientV2 struct { grpcplugin.DiagnosticsClient grpcplugin.ResourceClient grpcplugin.DataClient @@ -52,7 +52,7 @@ func newClientV2(descriptor PluginDescriptor, logger log.Logger, rpcClient plugi return nil, err } - c := clientV2{} + c := ClientV2{} if rawDiagnostics != nil { if diagnosticsClient, ok := rawDiagnostics.(grpcplugin.DiagnosticsClient); ok { c.DiagnosticsClient = diagnosticsClient @@ -92,7 +92,7 @@ func newClientV2(descriptor PluginDescriptor, logger log.Logger, rpcClient plugi return &c, nil } -func (c *clientV2) CollectMetrics(ctx context.Context) (*backend.CollectMetricsResult, error) { +func (c *ClientV2) CollectMetrics(ctx context.Context) (*backend.CollectMetricsResult, error) { if c.DiagnosticsClient == nil { return &backend.CollectMetricsResult{}, nil } @@ -109,7 +109,7 @@ func (c *clientV2) CollectMetrics(ctx context.Context) (*backend.CollectMetricsR return backend.FromProto().CollectMetricsResponse(protoResp), nil } -func (c *clientV2) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) { +func (c *ClientV2) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) { if c.DiagnosticsClient == nil { return nil, backendplugin.ErrMethodNotImplemented } @@ -130,7 +130,7 @@ func (c *clientV2) CheckHealth(ctx context.Context, req *backend.CheckHealthRequ return backend.FromProto().CheckHealthResponse(protoResp), nil } -func (c *clientV2) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { +func (c *ClientV2) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { if c.DataClient == nil { return nil, backendplugin.ErrMethodNotImplemented } @@ -149,7 +149,7 @@ func (c *clientV2) QueryData(ctx context.Context, req *backend.QueryDataRequest) return backend.FromProto().QueryDataResponse(protoResp) } -func (c *clientV2) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { +func (c *ClientV2) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { if c.ResourceClient == nil { return backendplugin.ErrMethodNotImplemented } @@ -184,7 +184,7 @@ func (c *clientV2) CallResource(ctx context.Context, req *backend.CallResourceRe } } -func (c *clientV2) SubscribeStream(ctx context.Context, req *backend.SubscribeStreamRequest) (*backend.SubscribeStreamResponse, error) { +func (c *ClientV2) SubscribeStream(ctx context.Context, req *backend.SubscribeStreamRequest) (*backend.SubscribeStreamResponse, error) { if c.StreamClient == nil { return nil, backendplugin.ErrMethodNotImplemented } @@ -195,7 +195,7 @@ func (c *clientV2) SubscribeStream(ctx context.Context, req *backend.SubscribeSt return backend.FromProto().SubscribeStreamResponse(protoResp), nil } -func (c *clientV2) PublishStream(ctx context.Context, req *backend.PublishStreamRequest) (*backend.PublishStreamResponse, error) { +func (c *ClientV2) PublishStream(ctx context.Context, req *backend.PublishStreamRequest) (*backend.PublishStreamResponse, error) { if c.StreamClient == nil { return nil, backendplugin.ErrMethodNotImplemented } @@ -206,7 +206,7 @@ func (c *clientV2) PublishStream(ctx context.Context, req *backend.PublishStream return backend.FromProto().PublishStreamResponse(protoResp), nil } -func (c *clientV2) RunStream(ctx context.Context, req *backend.RunStreamRequest, sender *backend.StreamSender) error { +func (c *ClientV2) RunStream(ctx context.Context, req *backend.RunStreamRequest, sender *backend.StreamSender) error { if c.StreamClient == nil { return backendplugin.ErrMethodNotImplemented } diff --git a/pkg/plugins/backendplugin/provider/provider.go b/pkg/plugins/backendplugin/provider/provider.go new file mode 100644 index 00000000000..eb01d8a9063 --- /dev/null +++ b/pkg/plugins/backendplugin/provider/provider.go @@ -0,0 +1,48 @@ +package provider + +import ( + "context" + "path/filepath" + + "github.com/grafana/grafana/pkg/infra/log" + "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" +) + +type Service struct{} + +func ProvideService() *Service { + return &Service{} +} + +func (*Service) BackendFactory(ctx context.Context, p *plugins.Plugin) backendplugin.PluginFactoryFunc { + for _, provider := range []PluginBackendProvider{RendererProvider, DefaultProvider} { + if factory := provider(ctx, p); factory != nil { + return factory + } + } + return nil +} + +// PluginBackendProvider is a function type for initializing a Plugin backend. +type PluginBackendProvider func(_ context.Context, _ *plugins.Plugin) backendplugin.PluginFactoryFunc + +var RendererProvider PluginBackendProvider = func(_ context.Context, p *plugins.Plugin) backendplugin.PluginFactoryFunc { + if !p.IsRenderer() { + return nil + } + cmd := plugins.ComposeRendererStartCommand() + return grpcplugin.NewRendererPlugin(p.ID, filepath.Join(p.PluginDir, cmd), + func(pluginID string, renderer pluginextensionv2.RendererPlugin, logger log.Logger) error { + p.Renderer = renderer + return nil + }, + ) +} + +var DefaultProvider PluginBackendProvider = func(_ context.Context, p *plugins.Plugin) backendplugin.PluginFactoryFunc { + cmd := plugins.ComposePluginStartCommand(p.Executable) + return grpcplugin.NewBackendPlugin(p.ID, filepath.Join(p.PluginDir, cmd)) +} diff --git a/pkg/plugins/config.go b/pkg/plugins/config.go new file mode 100644 index 00000000000..e25f18066e2 --- /dev/null +++ b/pkg/plugins/config.go @@ -0,0 +1,57 @@ +package plugins + +import ( + "github.com/grafana/grafana/pkg/setting" +) + +type Cfg struct { + DevMode bool + + PluginsPath string + + PluginSettings setting.PluginSettings + PluginsAllowUnsigned []string + + EnterpriseLicensePath string + + // AWS Plugin Auth + AWSAllowedAuthProviders []string + AWSAssumeRoleEnabled bool + + // Azure Cloud settings + Azure setting.AzureSettings + + CheckForUpdates bool + + BuildVersion string // TODO Remove + AppSubURL string // TODO Remove +} + +func NewCfg() *Cfg { + return &Cfg{} +} + +func FromGrafanaCfg(grafanaCfg *setting.Cfg) *Cfg { + cfg := &Cfg{} + + cfg.DevMode = grafanaCfg.Env == setting.Dev + cfg.PluginsPath = grafanaCfg.PluginsPath + + cfg.PluginSettings = grafanaCfg.PluginSettings + cfg.PluginsAllowUnsigned = grafanaCfg.PluginsAllowUnsigned + cfg.EnterpriseLicensePath = grafanaCfg.EnterpriseLicensePath + + // AWS + cfg.AWSAllowedAuthProviders = grafanaCfg.AWSAllowedAuthProviders + cfg.AWSAssumeRoleEnabled = grafanaCfg.AWSAssumeRoleEnabled + + // Azure + cfg.Azure = grafanaCfg.Azure + + cfg.CheckForUpdates = grafanaCfg.CheckForUpdates + + cfg.BuildVersion = grafanaCfg.BuildVersion + cfg.AppSubURL = grafanaCfg.AppSubURL + + return cfg +} diff --git a/pkg/plugins/ifaces.go b/pkg/plugins/ifaces.go index 8716b64ba21..2e6ad26605d 100644 --- a/pkg/plugins/ifaces.go +++ b/pkg/plugins/ifaces.go @@ -34,10 +34,10 @@ type AddOpts struct { // Loader is responsible for loading plugins from the file system. type Loader interface { // Load will return a list of plugins found in the provided file system paths. - Load(paths []string, ignore map[string]struct{}) ([]*Plugin, error) + Load(ctx context.Context, class Class, paths []string, ignore map[string]struct{}) ([]*Plugin, error) // LoadWithFactory will return a plugin found in the provided file system path and use the provided factory to // construct the plugin backend client. - LoadWithFactory(path string, factory backendplugin.PluginFactoryFunc) (*Plugin, error) + LoadWithFactory(ctx context.Context, class Class, path string, factory backendplugin.PluginFactoryFunc) (*Plugin, error) } // Installer is responsible for managing plugins (add / remove) on the file system. @@ -65,6 +65,11 @@ type Client interface { CollectMetrics(ctx context.Context, pluginID string) (*backend.CollectMetricsResult, error) } +// BackendFactoryProvider provides a backend factory for a provided plugin. +type BackendFactoryProvider interface { + BackendFactory(ctx context.Context, p *Plugin) backendplugin.PluginFactoryFunc +} + type RendererManager interface { // Renderer returns a renderer plugin. Renderer() *Plugin diff --git a/pkg/plugins/manager/dashboard_import_test.go b/pkg/plugins/manager/dashboard_import_test.go index 41a08c623f1..f42bc070494 100644 --- a/pkg/plugins/manager/dashboard_import_test.go +++ b/pkg/plugins/manager/dashboard_import_test.go @@ -8,6 +8,7 @@ import ( "github.com/grafana/grafana/pkg/components/simplejson" "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" "github.com/grafana/grafana/pkg/plugins/manager/signature" "github.com/grafana/grafana/pkg/services/dashboards" @@ -91,8 +92,10 @@ func pluginScenario(t *testing.T, desc string, fn func(*testing.T, *PluginManage }, }, } - pm := newManager(cfg, nil, loader.New(nil, cfg, &signature.UnsignedPluginAuthorizer{Cfg: cfg}), &sqlstore.SQLStore{}) - err := pm.init() + + pmCfg := plugins.FromGrafanaCfg(cfg) + pm, err := ProvideService(cfg, nil, loader.New(pmCfg, nil, + &signature.UnsignedPluginAuthorizer{Cfg: pmCfg}, &provider.Service{}), &sqlstore.SQLStore{}) require.NoError(t, err) t.Run(desc, func(t *testing.T) { diff --git a/pkg/plugins/manager/dashboards_test.go b/pkg/plugins/manager/dashboards_test.go index 34980f3dcea..bc384e81234 100644 --- a/pkg/plugins/manager/dashboards_test.go +++ b/pkg/plugins/manager/dashboards_test.go @@ -7,6 +7,8 @@ import ( "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" "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" "github.com/grafana/grafana/pkg/plugins/manager/signature" "github.com/grafana/grafana/pkg/services/sqlstore" @@ -23,8 +25,9 @@ func TestGetPluginDashboards(t *testing.T) { }, }, } - pm := newManager(cfg, nil, loader.New(nil, cfg, &signature.UnsignedPluginAuthorizer{Cfg: cfg}), &sqlstore.SQLStore{}) - err := pm.init() + pmCfg := plugins.FromGrafanaCfg(cfg) + pm, err := ProvideService(cfg, nil, loader.New(pmCfg, nil, + &signature.UnsignedPluginAuthorizer{Cfg: pmCfg}, &provider.Service{}), &sqlstore.SQLStore{}) require.NoError(t, err) bus.AddHandler("test", func(ctx context.Context, query *models.GetDashboardQuery) error { diff --git a/pkg/plugins/manager/loader/finder/finder.go b/pkg/plugins/manager/loader/finder/finder.go index e8f0bbccd87..98ff7155f06 100644 --- a/pkg/plugins/manager/loader/finder/finder.go +++ b/pkg/plugins/manager/loader/finder/finder.go @@ -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) { diff --git a/pkg/plugins/manager/loader/finder/finder_test.go b/pkg/plugins/manager/loader/finder/finder_test.go index db401563985..94782b05c3e 100644 --- a/pkg/plugins/manager/loader/finder/finder_test.go +++ b/pkg/plugins/manager/loader/finder/finder_test.go @@ -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) diff --git a/pkg/plugins/manager/loader/initializer/initializer.go b/pkg/plugins/manager/loader/initializer/initializer.go index 17b065c6160..af693e9456c 100644 --- a/pkg/plugins/manager/loader/initializer/initializer.go +++ b/pkg/plugins/manager/loader/initializer/initializer.go @@ -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,86 +48,18 @@ func (i *Initializer) Initialize(p *plugins.Plugin) error { } func (i *Initializer) InitializeWithFactory(p *plugins.Plugin, factory backendplugin.PluginFactoryFunc) error { - err := i.Initialize(p) - 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) + if factory == nil { return fmt.Errorf("could not initialize plugin %s", p.ID) } - 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// - // 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 + f, err := factory(p.ID, log.New("pluginID", p.ID), []string{}) + if err != nil { + return err } - // is set as default or has already been prefixed with base path - if pathStr == defaultLogoPath(pluginType) || strings.HasPrefix(pathStr, baseURL) { - return pathStr - } + p.RegisterClient(f) - return path.Join(baseURL, pathStr) + return nil } func (i *Initializer) envVars(plugin *plugins.Plugin) []string { @@ -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" { diff --git a/pkg/plugins/manager/loader/initializer/initializer_test.go b/pkg/plugins/manager/loader/initializer/initializer_test.go index 8e66c9d6755..f74f9bbb967 100644 --- a/pkg/plugins/manager/loader/initializer/initializer_test.go +++ b/pkg/plugins/manager/loader/initializer/initializer_test.go @@ -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 + } +} diff --git a/pkg/plugins/manager/loader/loader.go b/pkg/plugins/manager/loader/loader.go index f04bff52ec1..620185151b5 100644 --- a/pkg/plugins/manager/loader/loader.go +++ b/pkg/plugins/manager/loader/loader.go @@ -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// + // 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 } diff --git a/pkg/plugins/manager/loader/loader_test.go b/pkg/plugins/manager/loader/loader_test.go index f1f158a9514..13c401304f0 100644 --- a/pkg/plugins/manager/loader/loader_test.go +++ b/pkg/plugins/manager/loader/loader_test.go @@ -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", - }, - }, - 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", - }, +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.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{}) { } diff --git a/pkg/plugins/manager/manager.go b/pkg/plugins/manager/manager.go index 1582905d276..56ad6186bc0 100644 --- a/pkg/plugins/manager/manager.go +++ b/pkg/plugins/manager/manager.go @@ -5,14 +5,12 @@ import ( "errors" "fmt" "net/http" - "os" "path/filepath" "sync" "time" "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana/pkg/infra/fs" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" @@ -35,79 +33,52 @@ var _ plugins.StaticRouteResolver = (*PluginManager)(nil) var _ plugins.RendererManager = (*PluginManager)(nil) type PluginManager struct { - cfg *setting.Cfg + cfg *plugins.Cfg requestValidator models.PluginRequestValidator sqlStore *sqlstore.SQLStore store map[string]*plugins.Plugin pluginInstaller plugins.Installer pluginLoader plugins.Loader pluginsMu sync.RWMutex + pluginPaths map[plugins.Class][]string log log.Logger } -func ProvideService(cfg *setting.Cfg, requestValidator models.PluginRequestValidator, pluginLoader plugins.Loader, +func ProvideService(grafanaCfg *setting.Cfg, requestValidator models.PluginRequestValidator, pluginLoader plugins.Loader, sqlStore *sqlstore.SQLStore) (*PluginManager, error) { - pm := newManager(cfg, requestValidator, pluginLoader, sqlStore) - if err := pm.init(); err != nil { + pm := New(plugins.FromGrafanaCfg(grafanaCfg), requestValidator, map[plugins.Class][]string{ + plugins.Core: corePluginPaths(grafanaCfg), + plugins.Bundled: {grafanaCfg.BundledPluginsPath}, + plugins.External: append([]string{grafanaCfg.PluginsPath}, pluginSettingPaths(grafanaCfg)...), + }, pluginLoader, sqlStore) + if err := pm.Init(); err != nil { return nil, err } return pm, nil } -func newManager(cfg *setting.Cfg, pluginRequestValidator models.PluginRequestValidator, pluginLoader plugins.Loader, - sqlStore *sqlstore.SQLStore) *PluginManager { +func New(cfg *plugins.Cfg, requestValidator models.PluginRequestValidator, pluginPaths map[plugins.Class][]string, + pluginLoader plugins.Loader, sqlStore *sqlstore.SQLStore) *PluginManager { return &PluginManager{ cfg: cfg, - requestValidator: pluginRequestValidator, - sqlStore: sqlStore, + requestValidator: requestValidator, pluginLoader: pluginLoader, - store: map[string]*plugins.Plugin{}, + pluginPaths: pluginPaths, + store: make(map[string]*plugins.Plugin), log: log.New("plugin.manager"), pluginInstaller: installer.New(false, cfg.BuildVersion, newInstallerLogger("plugin.installer", true)), + sqlStore: sqlStore, } } -func (m *PluginManager) init() error { - // create external plugin's path if not exists - exists, err := fs.Exists(m.cfg.PluginsPath) - if err != nil { - return err - } - - if !exists { - if err = os.MkdirAll(m.cfg.PluginsPath, os.ModePerm); err != nil { - m.log.Error("Failed to create external plugins directory", "dir", m.cfg.PluginsPath, "error", err) - } else { - m.log.Debug("External plugins directory created", "dir", m.cfg.PluginsPath) +func (m *PluginManager) Init() error { + for class, paths := range m.pluginPaths { + err := m.loadPlugins(context.Background(), class, paths...) + if err != nil { + return err } } - m.log.Info("Initialising plugins") - - // install Core plugins - err = m.loadPlugins(m.corePluginPaths()...) - if err != nil { - return err - } - - // install Bundled plugins - err = m.loadPlugins(m.cfg.BundledPluginsPath) - if err != nil { - return err - } - - // install External plugins - err = m.loadPlugins(m.cfg.PluginsPath) - if err != nil { - return err - } - - // install plugins from cfg.PluginSettings - err = m.loadPlugins(m.pluginSettingPaths()...) - if err != nil { - return err - } - return nil } @@ -161,7 +132,7 @@ func (m *PluginManager) plugins() []*plugins.Plugin { return res } -func (m *PluginManager) loadPlugins(paths ...string) error { +func (m *PluginManager) loadPlugins(ctx context.Context, class plugins.Class, paths ...string) error { if len(paths) == 0 { return nil } @@ -173,7 +144,7 @@ func (m *PluginManager) loadPlugins(paths ...string) error { } } - loadedPlugins, err := m.pluginLoader.Load(pluginPaths, m.registeredPlugins()) + loadedPlugins, err := m.pluginLoader.Load(ctx, class, pluginPaths, m.registeredPlugins()) if err != nil { m.log.Error("Could not load plugins", "paths", pluginPaths, "err", err) return err @@ -499,24 +470,24 @@ func (m *PluginManager) shutdown(ctx context.Context) { } // corePluginPaths provides a list of the Core plugin paths which need to be scanned on init() -func (m *PluginManager) corePluginPaths() []string { +func corePluginPaths(cfg *setting.Cfg) []string { datasourcePaths := []string{ - filepath.Join(m.cfg.StaticRootPath, "app/plugins/datasource/alertmanager"), - filepath.Join(m.cfg.StaticRootPath, "app/plugins/datasource/dashboard"), - filepath.Join(m.cfg.StaticRootPath, "app/plugins/datasource/jaeger"), - filepath.Join(m.cfg.StaticRootPath, "app/plugins/datasource/mixed"), - filepath.Join(m.cfg.StaticRootPath, "app/plugins/datasource/zipkin"), + filepath.Join(cfg.StaticRootPath, "app/plugins/datasource/alertmanager"), + filepath.Join(cfg.StaticRootPath, "app/plugins/datasource/dashboard"), + filepath.Join(cfg.StaticRootPath, "app/plugins/datasource/jaeger"), + filepath.Join(cfg.StaticRootPath, "app/plugins/datasource/mixed"), + filepath.Join(cfg.StaticRootPath, "app/plugins/datasource/zipkin"), } - panelsPath := filepath.Join(m.cfg.StaticRootPath, "app/plugins/panel") + panelsPath := filepath.Join(cfg.StaticRootPath, "app/plugins/panel") return append(datasourcePaths, panelsPath) } // pluginSettingPaths provides a plugin paths defined in cfg.PluginSettings which need to be scanned on init() -func (m *PluginManager) pluginSettingPaths() []string { +func pluginSettingPaths(cfg *setting.Cfg) []string { var pluginSettingDirs []string - for _, settings := range m.cfg.PluginSettings { + for _, settings := range cfg.PluginSettings { path, exists := settings["path"] if !exists || path == "" { continue diff --git a/pkg/plugins/manager/manager_integration_test.go b/pkg/plugins/manager/manager_integration_test.go index d7283b7649d..82d2f559958 100644 --- a/pkg/plugins/manager/manager_integration_test.go +++ b/pkg/plugins/manager/manager_integration_test.go @@ -7,6 +7,7 @@ import ( "testing" "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/plugins/backendplugin/provider" "github.com/grafana/grafana/pkg/plugins/manager/loader" "github.com/grafana/grafana/pkg/plugins/manager/signature" "github.com/grafana/grafana/pkg/services/licensing" @@ -42,9 +43,10 @@ func TestPluginManager_int_init(t *testing.T) { license := &licensing.OSSLicensingService{ Cfg: cfg, } - pm := newManager(cfg, nil, loader.New(license, cfg, &signature.UnsignedPluginAuthorizer{Cfg: cfg}), nil) - err = pm.init() + pmCfg := plugins.FromGrafanaCfg(cfg) + pm, err := ProvideService(cfg, nil, loader.New(pmCfg, license, + &signature.UnsignedPluginAuthorizer{Cfg: pmCfg}, &provider.Service{}), nil) require.NoError(t, err) verifyCorePluginCatalogue(t, pm) diff --git a/pkg/plugins/manager/manager_test.go b/pkg/plugins/manager/manager_test.go index 26409d397b6..fd67c58b068 100644 --- a/pkg/plugins/manager/manager_test.go +++ b/pkg/plugins/manager/manager_test.go @@ -3,56 +3,24 @@ package manager import ( "context" "net/http" - "os" - "path/filepath" "sync" "testing" "time" "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana/pkg/infra/fs" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/backendplugin" "github.com/grafana/grafana/pkg/services/sqlstore" - "github.com/grafana/grafana/pkg/setting" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "gopkg.in/ini.v1" ) const ( testPluginID = "test-plugin" ) -func TestPluginManager_init(t *testing.T) { - t.Run("Plugin folder will be created if not exists", func(t *testing.T) { - testDir := "plugin-test-dir" - - exists, err := fs.Exists(testDir) - require.NoError(t, err) - assert.False(t, exists) - - pm := createManager(t, func(pm *PluginManager) { - pm.cfg.PluginsPath = testDir - }) - - err = pm.init() - require.NoError(t, err) - - exists, err = fs.Exists(testDir) - require.NoError(t, err) - assert.True(t, exists) - - t.Cleanup(func() { - err = os.Remove(testDir) - require.NoError(t, err) - }) - }) -} - func TestPluginManager_loadPlugins(t *testing.T) { t.Run("Managed backend plugin", func(t *testing.T) { p, pc := createPlugin(testPluginID, "", plugins.External, true, true) @@ -64,7 +32,7 @@ func TestPluginManager_loadPlugins(t *testing.T) { pm := createManager(t, func(pm *PluginManager) { pm.pluginLoader = loader }) - err := pm.loadPlugins("test/path") + err := pm.loadPlugins(context.Background(), plugins.External, "test/path") require.NoError(t, err) assert.Equal(t, 1, pc.startCount) @@ -90,7 +58,7 @@ func TestPluginManager_loadPlugins(t *testing.T) { pm := createManager(t, func(pm *PluginManager) { pm.pluginLoader = loader }) - err := pm.loadPlugins("test/path") + err := pm.loadPlugins(context.Background(), plugins.External, "test/path") require.NoError(t, err) assert.Equal(t, 0, pc.startCount) @@ -116,7 +84,7 @@ func TestPluginManager_loadPlugins(t *testing.T) { pm := createManager(t, func(pm *PluginManager) { pm.pluginLoader = loader }) - err := pm.loadPlugins("test/path") + err := pm.loadPlugins(context.Background(), plugins.External, "test/path") require.NoError(t, err) assert.Equal(t, 0, pc.startCount) @@ -142,7 +110,7 @@ func TestPluginManager_loadPlugins(t *testing.T) { pm := createManager(t, func(pm *PluginManager) { pm.pluginLoader = loader }) - err := pm.loadPlugins("test/path") + err := pm.loadPlugins(context.Background(), plugins.External, "test/path") require.NoError(t, err) assert.Equal(t, 0, pc.startCount) @@ -257,7 +225,7 @@ func TestPluginManager_Installer(t *testing.T) { pm := createManager(t, func(pm *PluginManager) { pm.pluginLoader = loader }) - err := pm.loadPlugins("test/path") + err := pm.loadPlugins(context.Background(), plugins.Core, "test/path") require.NoError(t, err) assert.Equal(t, 1, pc.startCount) @@ -291,7 +259,7 @@ func TestPluginManager_Installer(t *testing.T) { pm := createManager(t, func(pm *PluginManager) { pm.pluginLoader = loader }) - err := pm.loadPlugins("test/path") + err := pm.loadPlugins(context.Background(), plugins.Bundled, "test/path") require.NoError(t, err) assert.Equal(t, 1, pc.startCount) @@ -499,18 +467,8 @@ func TestPluginManager_lifecycle_unmanaged(t *testing.T) { func createManager(t *testing.T, cbs ...func(*PluginManager)) *PluginManager { t.Helper() - staticRootPath, err := filepath.Abs("../../../public/") - require.NoError(t, err) - - cfg := &setting.Cfg{ - Raw: ini.Empty(), - Env: setting.Prod, - StaticRootPath: staticRootPath, - } - requestValidator := &testPluginRequestValidator{} - loader := &fakeLoader{} - pm := newManager(cfg, requestValidator, loader, &sqlstore.SQLStore{}) + pm := New(&plugins.Cfg{}, requestValidator, nil, &fakeLoader{}, &sqlstore.SQLStore{}) for _, cb := range cbs { cb(pm) @@ -555,7 +513,7 @@ type managerScenarioCtx struct { func newScenario(t *testing.T, managed bool, fn func(t *testing.T, ctx *managerScenarioCtx)) { t.Helper() - cfg := setting.NewCfg() + cfg := &plugins.Cfg{} cfg.AWSAllowedAuthProviders = []string{"keys", "credentials"} cfg.AWSAssumeRoleEnabled = true @@ -563,13 +521,9 @@ func newScenario(t *testing.T, managed bool, fn func(t *testing.T, ctx *managerS cfg.Azure.Cloud = "AzureCloud" cfg.Azure.ManagedIdentityClientId = "client-id" - staticRootPath, err := filepath.Abs("../../../public") - require.NoError(t, err) - cfg.StaticRootPath = staticRootPath - requestValidator := &testPluginRequestValidator{} loader := &fakeLoader{} - manager := newManager(cfg, requestValidator, loader, nil) + manager := New(cfg, requestValidator, nil, loader, nil) manager.pluginLoader = loader ctx := &managerScenarioCtx{ manager: manager, @@ -616,13 +570,13 @@ type fakeLoader struct { plugins.Loader } -func (l *fakeLoader) Load(paths []string, _ map[string]struct{}) ([]*plugins.Plugin, error) { +func (l *fakeLoader) Load(_ context.Context, _ plugins.Class, paths []string, _ map[string]struct{}) ([]*plugins.Plugin, error) { l.loadedPaths = append(l.loadedPaths, paths...) return l.mockedLoadedPlugins, nil } -func (l *fakeLoader) LoadWithFactory(path string, _ backendplugin.PluginFactoryFunc) (*plugins.Plugin, error) { +func (l *fakeLoader) LoadWithFactory(_ context.Context, _ plugins.Class, path string, _ backendplugin.PluginFactoryFunc) (*plugins.Plugin, error) { l.loadedPaths = append(l.loadedPaths, path) return l.mockedFactoryLoadedPlugin, nil diff --git a/pkg/plugins/manager/signature/authorizer.go b/pkg/plugins/manager/signature/authorizer.go index ac4b4f19f9e..e0bd7d8da05 100644 --- a/pkg/plugins/manager/signature/authorizer.go +++ b/pkg/plugins/manager/signature/authorizer.go @@ -7,12 +7,12 @@ import ( func ProvideService(cfg *setting.Cfg) (*UnsignedPluginAuthorizer, error) { return &UnsignedPluginAuthorizer{ - Cfg: cfg, + Cfg: plugins.FromGrafanaCfg(cfg), }, nil } type UnsignedPluginAuthorizer struct { - Cfg *setting.Cfg + Cfg *plugins.Cfg } func (u *UnsignedPluginAuthorizer) CanLoadPlugin(p *plugins.Plugin) bool { @@ -20,7 +20,7 @@ func (u *UnsignedPluginAuthorizer) CanLoadPlugin(p *plugins.Plugin) bool { return true } - if u.Cfg.Env == setting.Dev { + if u.Cfg.DevMode { return true } diff --git a/pkg/plugins/manager/store.go b/pkg/plugins/manager/store.go index 931b93e5df4..31c51d5567d 100644 --- a/pkg/plugins/manager/store.go +++ b/pkg/plugins/manager/store.go @@ -75,7 +75,7 @@ func (m *PluginManager) Add(ctx context.Context, pluginID, version string) error return err } - err = m.loadPlugins(m.cfg.PluginsPath) + err = m.loadPlugins(context.Background(), plugins.External, m.cfg.PluginsPath) if err != nil { return err } @@ -83,7 +83,7 @@ func (m *PluginManager) Add(ctx context.Context, pluginID, version string) error return nil } -func (m *PluginManager) AddWithFactory(_ context.Context, pluginID string, factory backendplugin.PluginFactoryFunc, +func (m *PluginManager) AddWithFactory(ctx context.Context, pluginID string, factory backendplugin.PluginFactoryFunc, pathResolver plugins.PluginPathResolver) error { if m.isRegistered(pluginID) { return fmt.Errorf("plugin %s is already registered", pluginID) @@ -94,7 +94,7 @@ func (m *PluginManager) AddWithFactory(_ context.Context, pluginID string, facto return err } - p, err := m.pluginLoader.LoadWithFactory(path, factory) + p, err := m.pluginLoader.LoadWithFactory(ctx, plugins.Core, path, factory) if err != nil { return err } diff --git a/pkg/plugins/manager/testdata/test-app-with-includes/dashboards/memory.json b/pkg/plugins/manager/testdata/test-app-with-includes/dashboards/memory.json new file mode 100644 index 00000000000..983b994b40e --- /dev/null +++ b/pkg/plugins/manager/testdata/test-app-with-includes/dashboards/memory.json @@ -0,0 +1,5 @@ +{ + "title": "Nginx Memory", + "revision": 2, + "schemaVersion": 11 +} diff --git a/pkg/plugins/manager/testdata/test-app-with-includes/plugin.json b/pkg/plugins/manager/testdata/test-app-with-includes/plugin.json new file mode 100644 index 00000000000..043e7544bf1 --- /dev/null +++ b/pkg/plugins/manager/testdata/test-app-with-includes/plugin.json @@ -0,0 +1,38 @@ +{ + "type": "app", + "name": "Test App", + "id": "test-app", + "info": { + "description": "Official Grafana Test App & Dashboard bundle", + "author": { + "name": "Test Inc.", + "url": "http://test.com" + }, + "keywords": ["test"], + "links": [ + {"name": "Project site", "url": "http://project.com"}, + {"name": "License & Terms", "url": "http://license.com"} + ], + "version": "1.0.0", + "updated": "2015-02-10" + }, + "includes": [ + { + "type": "dashboard", + "name": "Nginx Memory", + "path": "dashboards/memory.json", + "defaultNav": true + }, + { + "type": "page", + "name": "Root Page (react)", + "path": "/a/my-simple-app", + "role": "Viewer", + "addToNav": true, + "defaultNav": true + } + ], + "dependencies": { + "grafanaDependency": ">=8.0.0" + } +} diff --git a/pkg/server/wireexts_oss.go b/pkg/server/wireexts_oss.go index 9abecb69d7b..24d0ab27f15 100644 --- a/pkg/server/wireexts_oss.go +++ b/pkg/server/wireexts_oss.go @@ -7,6 +7,7 @@ import ( "github.com/google/wire" "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/signature" "github.com/grafana/grafana/pkg/registry" "github.com/grafana/grafana/pkg/server/backgroundsvcs" @@ -61,6 +62,8 @@ var wireExtsBasicSet = wire.NewSet( wire.Bind(new(searchusers.Service), new(*searchusers.OSSService)), signature.ProvideService, wire.Bind(new(plugins.PluginLoaderAuthorizer), new(*signature.UnsignedPluginAuthorizer)), + provider.ProvideService, + wire.Bind(new(plugins.BackendFactoryProvider), new(*provider.Service)), acdb.ProvideService, wire.Bind(new(accesscontrol.ResourcePermissionsStore), new(*acdb.AccessControlStore)), wire.Bind(new(accesscontrol.PermissionsProvider), new(*acdb.AccessControlStore)),