From d0d8544dedfd67e336a6ba1959e8040b8f732f45 Mon Sep 17 00:00:00 2001 From: Will Browne Date: Fri, 23 Sep 2022 14:27:01 +0200 Subject: [PATCH] Plugins: Create single point of entry for adding / removing plugins (#55463) * split out plugin manager * remove whitespace * fix tests * split up tests * updating naming conventions * simplify manager * tidy * explorations * fix build * tidy * fix tests * add logger helper * pass the tests * tidying * fix tests * tidy and re-add test * store depends on loader * enrich tests * fix test * undo gomod changes --- pkg/api/fakes.go | 12 +- pkg/api/http_server.go | 6 +- pkg/api/plugins.go | 4 +- pkg/api/plugins_test.go | 16 +- pkg/cmd/grafana-cli/runner/wire.go | 8 +- pkg/plugins/config/config.go | 3 + pkg/plugins/ifaces.go | 6 +- pkg/plugins/logger/logger.go | 2 +- pkg/plugins/manager/fakes/fakes.go | 135 +++++++- pkg/plugins/manager/installer.go | 157 +++++++++ .../{manager_test.go => installer_test.go} | 161 +++------- pkg/plugins/manager/loader/ifaces.go | 4 +- pkg/plugins/manager/loader/loader.go | 103 +++++- pkg/plugins/manager/loader/loader_test.go | 265 ++++++++++------ pkg/plugins/manager/manager.go | 298 ------------------ .../manager/manager_integration_test.go | 9 +- pkg/plugins/manager/store/store.go | 62 +++- pkg/plugins/manager/store/store_test.go | 112 +++++-- pkg/plugins/plugins.go | 4 + pkg/plugins/storage/fs.go | 2 + pkg/server/wire.go | 8 +- 21 files changed, 770 insertions(+), 607 deletions(-) create mode 100644 pkg/plugins/manager/installer.go rename pkg/plugins/manager/{manager_test.go => installer_test.go} (51%) delete mode 100644 pkg/plugins/manager/manager.go diff --git a/pkg/api/fakes.go b/pkg/api/fakes.go index 48c765a4a4e..6b1a2c6f34f 100644 --- a/pkg/api/fakes.go +++ b/pkg/api/fakes.go @@ -9,8 +9,8 @@ import ( "github.com/grafana/grafana/pkg/services/pluginsettings" ) -type fakePluginManager struct { - plugins.Manager +type fakePluginInstaller struct { + plugins.Installer plugins map[string]fakePlugin } @@ -20,7 +20,11 @@ type fakePlugin struct { version string } -func (pm *fakePluginManager) Add(_ context.Context, pluginID, version string, _ plugins.CompatOpts) error { +func NewFakePluginInstaller() *fakePluginInstaller { + return &fakePluginInstaller{plugins: map[string]fakePlugin{}} +} + +func (pm *fakePluginInstaller) Add(_ context.Context, pluginID, version string, _ plugins.CompatOpts) error { pm.plugins[pluginID] = fakePlugin{ pluginID: pluginID, version: version, @@ -28,7 +32,7 @@ func (pm *fakePluginManager) Add(_ context.Context, pluginID, version string, _ return nil } -func (pm *fakePluginManager) Remove(_ context.Context, pluginID string) error { +func (pm *fakePluginInstaller) Remove(_ context.Context, pluginID string) error { delete(pm.plugins, pluginID) return nil } diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index fadcb63eb3b..3d515a2df56 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -127,7 +127,7 @@ type HTTPServer struct { PluginRequestValidator models.PluginRequestValidator pluginClient plugins.Client pluginStore plugins.Store - pluginManager plugins.Manager + pluginInstaller plugins.Installer pluginDashboardService plugindashboards.Service pluginStaticRouteResolver plugins.StaticRouteResolver pluginErrorResolver plugins.ErrorResolver @@ -210,7 +210,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi cacheService *localcache.CacheService, sqlStore *sqlstore.SQLStore, alertEngine *alerting.AlertEngine, pluginRequestValidator models.PluginRequestValidator, pluginStaticRouteResolver plugins.StaticRouteResolver, pluginDashboardService plugindashboards.Service, pluginStore plugins.Store, pluginClient plugins.Client, - pluginErrorResolver plugins.ErrorResolver, pluginManager plugins.Manager, settingsProvider setting.Provider, + pluginErrorResolver plugins.ErrorResolver, pluginInstaller plugins.Installer, settingsProvider setting.Provider, dataSourceCache datasources.CacheService, userTokenService models.UserTokenService, cleanUpService *cleanup.CleanUpService, shortURLService shorturls.Service, queryHistoryService queryhistory.Service, correlationsService correlations.Service, thumbService thumbs.Service, remoteCache *remotecache.RemoteCache, provisioningService provisioning.ProvisioningService, @@ -255,7 +255,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi SQLStore: sqlStore, AlertEngine: alertEngine, PluginRequestValidator: pluginRequestValidator, - pluginManager: pluginManager, + pluginInstaller: pluginInstaller, pluginClient: pluginClient, pluginStore: pluginStore, pluginStaticRouteResolver: pluginStaticRouteResolver, diff --git a/pkg/api/plugins.go b/pkg/api/plugins.go index 30620b255c9..031c076ce63 100644 --- a/pkg/api/plugins.go +++ b/pkg/api/plugins.go @@ -413,7 +413,7 @@ func (hs *HTTPServer) InstallPlugin(c *models.ReqContext) response.Response { } pluginID := web.Params(c.Req)[":pluginId"] - err := hs.pluginManager.Add(c.Req.Context(), pluginID, dto.Version, plugins.CompatOpts{ + err := hs.pluginInstaller.Add(c.Req.Context(), pluginID, dto.Version, plugins.CompatOpts{ GrafanaVersion: hs.Cfg.BuildVersion, OS: runtime.GOOS, Arch: runtime.GOARCH, @@ -448,7 +448,7 @@ func (hs *HTTPServer) InstallPlugin(c *models.ReqContext) response.Response { func (hs *HTTPServer) UninstallPlugin(c *models.ReqContext) response.Response { pluginID := web.Params(c.Req)[":pluginId"] - err := hs.pluginManager.Remove(c.Req.Context(), pluginID) + err := hs.pluginInstaller.Remove(c.Req.Context(), pluginID) if err != nil { if errors.Is(err, plugins.ErrPluginNotInstalled) { return response.Error(http.StatusNotFound, "Plugin not installed", err) diff --git a/pkg/api/plugins_test.go b/pkg/api/plugins_test.go index 217eaf165ab..1965d578340 100644 --- a/pkg/api/plugins_test.go +++ b/pkg/api/plugins_test.go @@ -51,16 +51,14 @@ func Test_PluginsInstallAndUninstall(t *testing.T) { action, testCase.expectedHTTPStatus, testCase.pluginAdminEnabled, testCase.pluginAdminExternalManageEnabled) } - pm := &fakePluginManager{ - plugins: make(map[string]fakePlugin), - } + inst := NewFakePluginInstaller() for _, tc := range tcs { srv := SetupAPITestServer(t, func(hs *HTTPServer) { hs.Cfg = &setting.Cfg{ PluginAdminEnabled: tc.pluginAdminEnabled, PluginAdminExternalManageEnabled: tc.pluginAdminExternalManageEnabled, } - hs.pluginManager = pm + hs.pluginInstaller = inst hs.QuotaService = quotatest.NewQuotaServiceFake() }) @@ -78,7 +76,7 @@ func Test_PluginsInstallAndUninstall(t *testing.T) { require.Equal(t, tc.expectedHTTPStatus, resp.StatusCode) if tc.expectedHTTPStatus == 200 { - require.Equal(t, fakePlugin{pluginID: "test", version: "1.0.2"}, pm.plugins["test"]) + require.Equal(t, fakePlugin{pluginID: "test", version: "1.0.2"}, inst.plugins["test"]) } }) @@ -96,7 +94,7 @@ func Test_PluginsInstallAndUninstall(t *testing.T) { require.Equal(t, tc.expectedHTTPStatus, resp.StatusCode) if tc.expectedHTTPStatus == 200 { - require.Empty(t, pm.plugins) + require.Empty(t, inst.plugins) } }) } @@ -125,10 +123,6 @@ func Test_PluginsInstallAndUninstall_AccessControl(t *testing.T) { action, tc.expectedCode, tc.pluginAdminEnabled, tc.pluginAdminExternalManageEnabled, tc.permissions) } - pm := &fakePluginManager{ - plugins: make(map[string]fakePlugin), - } - for _, tc := range tcs { sc := setupHTTPServerWithCfg(t, true, &setting.Cfg{ RBACEnabled: true, @@ -136,7 +130,7 @@ func Test_PluginsInstallAndUninstall_AccessControl(t *testing.T) { PluginAdminExternalManageEnabled: tc.pluginAdminExternalManageEnabled}) setInitCtxSignedInViewer(sc.initCtx) setAccessControlPermissions(sc.acmock, tc.permissions, sc.initCtx.OrgID) - sc.hs.pluginManager = pm + sc.hs.pluginInstaller = NewFakePluginInstaller() t.Run(testName("Install", tc), func(t *testing.T) { input := strings.NewReader("{ \"version\": \"1.0.2\" }") diff --git a/pkg/cmd/grafana-cli/runner/wire.go b/pkg/cmd/grafana-cli/runner/wire.go index 87b22411671..fd9a97837ad 100644 --- a/pkg/cmd/grafana-cli/runner/wire.go +++ b/pkg/cmd/grafana-cli/runner/wire.go @@ -184,14 +184,14 @@ var wireSet = wire.NewSet( wire.Bind(new(registry.Service), new(*registry.InMemory)), repo.ProvideService, wire.Bind(new(repo.Service), new(*repo.Manager)), - manager.ProvideService, - wire.Bind(new(plugins.Manager), new(*manager.PluginManager)), - wire.Bind(new(plugins.RendererManager), new(*manager.PluginManager)), - wire.Bind(new(plugins.SecretsPluginManager), new(*manager.PluginManager)), + manager.ProvideInstaller, + wire.Bind(new(plugins.Installer), new(*manager.PluginInstaller)), client.ProvideService, wire.Bind(new(plugins.Client), new(*client.Service)), managerStore.ProvideService, wire.Bind(new(plugins.Store), new(*managerStore.Service)), + wire.Bind(new(plugins.RendererManager), new(*managerStore.Service)), + wire.Bind(new(plugins.SecretsPluginManager), new(*managerStore.Service)), wire.Bind(new(plugins.StaticRouteResolver), new(*managerStore.Service)), pluginDashboards.ProvideFileStoreManager, wire.Bind(new(pluginDashboards.FileStore), new(*pluginDashboards.FileStoreManager)), diff --git a/pkg/plugins/config/config.go b/pkg/plugins/config/config.go index 73bd5562238..bd8278f6103 100644 --- a/pkg/plugins/config/config.go +++ b/pkg/plugins/config/config.go @@ -14,6 +14,8 @@ type Cfg struct { DevMode bool + PluginsPath string + PluginSettings setting.PluginSettings PluginsAllowUnsigned []string @@ -52,6 +54,7 @@ func NewCfg(settingProvider setting.Provider, grafanaCfg *setting.Cfg) *Cfg { return &Cfg{ log: logger, + PluginsPath: grafanaCfg.PluginsPath, BuildVersion: grafanaCfg.BuildVersion, DevMode: settingProvider.KeyValue("", "app_mode").MustBool(grafanaCfg.Env == setting.Dev), EnterpriseLicensePath: settingProvider.KeyValue("enterprise", "license_path").MustString(grafanaCfg.EnterpriseLicensePath), diff --git a/pkg/plugins/ifaces.go b/pkg/plugins/ifaces.go index 4ce31f17c18..509c84a8ab6 100644 --- a/pkg/plugins/ifaces.go +++ b/pkg/plugins/ifaces.go @@ -16,10 +16,10 @@ type Store interface { Plugins(ctx context.Context, pluginTypes ...Type) []PluginDTO } -type Manager interface { - // Add adds a plugin to the store. +type Installer interface { + // Add adds a new plugin. Add(ctx context.Context, pluginID, version string, opts CompatOpts) error - // Remove removes a plugin from the store. + // Remove removes an existing plugin. Remove(ctx context.Context, pluginID string) error } diff --git a/pkg/plugins/logger/logger.go b/pkg/plugins/logger/logger.go index e56bd19729b..949e5fdff44 100644 --- a/pkg/plugins/logger/logger.go +++ b/pkg/plugins/logger/logger.go @@ -10,7 +10,7 @@ type InfraLogWrapper struct { log log.Logger } -func NewLogger(name string) (l *InfraLogWrapper) { +func NewLogger(name string) *InfraLogWrapper { return &InfraLogWrapper{ log: log.New(name), } diff --git a/pkg/plugins/manager/fakes/fakes.go b/pkg/plugins/manager/fakes/fakes.go index 76baf515bc6..5097f614cbb 100644 --- a/pkg/plugins/manager/fakes/fakes.go +++ b/pkg/plugins/manager/fakes/fakes.go @@ -14,22 +14,45 @@ import ( "github.com/grafana/grafana/pkg/plugins/storage" ) -type FakeLoader struct { - LoadFunc func(_ context.Context, _ plugins.Class, paths []string, _ map[string]struct{}) ([]*plugins.Plugin, error) +type FakePluginInstaller struct { + AddFunc func(ctx context.Context, pluginID, version string, opts plugins.CompatOpts) error + // Remove removes a plugin from the store. + RemoveFunc func(ctx context.Context, pluginID string) error +} - LoadedPaths []string +func (i *FakePluginInstaller) Add(ctx context.Context, pluginID, version string, opts plugins.CompatOpts) error { + if i.AddFunc != nil { + return i.AddFunc(ctx, pluginID, version, opts) + } + return nil } -func (l *FakeLoader) Load(ctx context.Context, class plugins.Class, paths []string, ignore map[string]struct{}) ([]*plugins.Plugin, error) { - if l.LoadFunc != nil { - return l.LoadFunc(ctx, class, paths, ignore) +func (i *FakePluginInstaller) Remove(ctx context.Context, pluginID string) error { + if i.RemoveFunc != nil { + return i.RemoveFunc(ctx, pluginID) } + return nil +} - l.LoadedPaths = append(l.LoadedPaths, paths...) +type FakeLoader struct { + LoadFunc func(_ context.Context, _ plugins.Class, paths []string) ([]*plugins.Plugin, error) + UnloadFunc func(_ context.Context, _ string) error +} +func (l *FakeLoader) Load(ctx context.Context, class plugins.Class, paths []string) ([]*plugins.Plugin, error) { + if l.LoadFunc != nil { + return l.LoadFunc(ctx, class, paths) + } return nil, nil } +func (l *FakeLoader) Unload(ctx context.Context, pluginID string) error { + if l.UnloadFunc != nil { + return l.UnloadFunc(ctx, pluginID) + } + return nil +} + type FakePluginClient struct { ID string Managed bool @@ -139,6 +162,30 @@ func (pc *FakePluginClient) RunStream(_ context.Context, _ *backend.RunStreamReq return backendplugin.ErrMethodNotImplemented } +type FakePluginStore struct { + Store map[string]plugins.PluginDTO +} + +func NewFakePluginStore() *FakePluginStore { + return &FakePluginStore{ + Store: make(map[string]plugins.PluginDTO), + } +} + +func (f *FakePluginStore) Plugin(_ context.Context, id string) (plugins.PluginDTO, bool) { + p, exists := f.Store[id] + return p, exists +} + +func (f *FakePluginStore) Plugins(_ context.Context, _ ...plugins.Type) []plugins.PluginDTO { + var res []plugins.PluginDTO + for _, p := range f.Store { + res = append(res, p) + } + + return res +} + type FakePluginRegistry struct { Store map[string]*plugins.Plugin } @@ -207,15 +254,20 @@ func (r *FakePluginRepo) GetPluginDownloadOptions(ctx context.Context, pluginID, } type FakePluginStorage struct { + Store map[string]struct{} AddFunc func(_ context.Context, pluginID string, z *zip.ReadCloser) (*storage.ExtractedPluginArchive, error) RegisterFunc func(_ context.Context, pluginID, pluginDir string) error RemoveFunc func(_ context.Context, pluginID string) error - Added map[string]string - Removed map[string]int +} + +func NewFakePluginStorage() *FakePluginStorage { + return &FakePluginStorage{ + Store: map[string]struct{}{}, + } } func (s *FakePluginStorage) Register(ctx context.Context, pluginID, pluginDir string) error { - s.Added[pluginID] = pluginDir + s.Store[pluginID] = struct{}{} if s.RegisterFunc != nil { return s.RegisterFunc(ctx, pluginID, pluginDir) } @@ -223,6 +275,7 @@ func (s *FakePluginStorage) Register(ctx context.Context, pluginID, pluginDir st } func (s *FakePluginStorage) Add(ctx context.Context, pluginID string, z *zip.ReadCloser) (*storage.ExtractedPluginArchive, error) { + s.Store[pluginID] = struct{}{} if s.AddFunc != nil { return s.AddFunc(ctx, pluginID, z) } @@ -230,7 +283,7 @@ func (s *FakePluginStorage) Add(ctx context.Context, pluginID string, z *zip.Rea } func (s *FakePluginStorage) Remove(ctx context.Context, pluginID string) error { - s.Removed[pluginID]++ + delete(s.Store, pluginID) if s.RemoveFunc != nil { return s.RemoveFunc(ctx, pluginID) } @@ -266,3 +319,63 @@ func (m *FakeProcessManager) Stop(ctx context.Context, pluginID string) error { } return nil } + +type FakeBackendProcessProvider struct { + Requested map[string]int + Invoked map[string]int +} + +func NewFakeBackendProcessProvider() *FakeBackendProcessProvider { + return &FakeBackendProcessProvider{ + Requested: make(map[string]int), + Invoked: make(map[string]int), + } +} + +func (pr *FakeBackendProcessProvider) BackendFactory(_ context.Context, p *plugins.Plugin) backendplugin.PluginFactoryFunc { + pr.Requested[p.ID]++ + return func(pluginID string, _ log.Logger, _ []string) (backendplugin.Plugin, error) { + pr.Invoked[pluginID]++ + return &FakePluginClient{}, nil + } +} + +type FakeLicensingService struct { + TokenRaw string +} + +func NewFakeLicensingService() *FakeLicensingService { + return &FakeLicensingService{} +} + +func (t *FakeLicensingService) Expiry() int64 { + return 0 +} + +func (t *FakeLicensingService) Edition() string { + return "" +} + +func (t *FakeLicensingService) StateInfo() string { + return "" +} + +func (t *FakeLicensingService) ContentDeliveryPrefix() string { + return "" +} + +func (t *FakeLicensingService) LicenseURL(_ bool) string { + return "" +} + +func (t *FakeLicensingService) Environment() map[string]string { + return map[string]string{"GF_ENTERPRISE_LICENSE_TEXT": t.TokenRaw} +} + +func (*FakeLicensingService) EnabledFeatures() map[string]bool { + return map[string]bool{} +} + +func (*FakeLicensingService) FeatureEnabled(_ string) bool { + return false +} diff --git a/pkg/plugins/manager/installer.go b/pkg/plugins/manager/installer.go new file mode 100644 index 00000000000..0d82f6ee561 --- /dev/null +++ b/pkg/plugins/manager/installer.go @@ -0,0 +1,157 @@ +package manager + +import ( + "context" + "fmt" + + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/plugins/config" + "github.com/grafana/grafana/pkg/plugins/logger" + "github.com/grafana/grafana/pkg/plugins/manager/loader" + "github.com/grafana/grafana/pkg/plugins/manager/registry" + "github.com/grafana/grafana/pkg/plugins/repo" + "github.com/grafana/grafana/pkg/plugins/storage" +) + +var _ plugins.Installer = (*PluginInstaller)(nil) + +type PluginInstaller struct { + pluginRepo repo.Service + pluginStorage storage.Manager + pluginRegistry registry.Service + pluginLoader loader.Service + log log.Logger +} + +func ProvideInstaller(cfg *config.Cfg, pluginRegistry registry.Service, pluginLoader loader.Service, + pluginRepo repo.Service) *PluginInstaller { + return New(pluginRegistry, pluginLoader, pluginRepo, storage.FileSystem(logger.NewLogger("installer.fs"), cfg.PluginsPath)) +} + +func New(pluginRegistry registry.Service, pluginLoader loader.Service, pluginRepo repo.Service, + pluginStorage storage.Manager) *PluginInstaller { + return &PluginInstaller{ + pluginLoader: pluginLoader, + pluginRegistry: pluginRegistry, + pluginRepo: pluginRepo, + pluginStorage: pluginStorage, + log: log.New("plugin.installer"), + } +} + +func (m *PluginInstaller) Add(ctx context.Context, pluginID, version string, opts plugins.CompatOpts) error { + compatOpts := repo.NewCompatOpts(opts.GrafanaVersion, opts.OS, opts.Arch) + + var pluginArchive *repo.PluginArchive + if plugin, exists := m.plugin(ctx, pluginID); exists { + if !plugin.IsExternalPlugin() { + return plugins.ErrInstallCorePlugin + } + + if plugin.Info.Version == version { + return plugins.DuplicateError{ + PluginID: plugin.ID, + ExistingPluginDir: plugin.PluginDir, + } + } + + // get plugin update information to confirm if target update is possible + dlOpts, err := m.pluginRepo.GetPluginDownloadOptions(ctx, pluginID, version, compatOpts) + if err != nil { + return err + } + + // if existing plugin version is the same as the target update version + if dlOpts.Version == plugin.Info.Version { + return plugins.DuplicateError{ + PluginID: plugin.ID, + ExistingPluginDir: plugin.PluginDir, + } + } + + if dlOpts.PluginZipURL == "" && dlOpts.Version == "" { + return fmt.Errorf("could not determine update options for %s", pluginID) + } + + // remove existing installation of plugin + err = m.Remove(ctx, plugin.ID) + if err != nil { + return err + } + + if dlOpts.PluginZipURL != "" { + pluginArchive, err = m.pluginRepo.GetPluginArchiveByURL(ctx, dlOpts.PluginZipURL, compatOpts) + if err != nil { + return err + } + } else { + pluginArchive, err = m.pluginRepo.GetPluginArchive(ctx, pluginID, dlOpts.Version, compatOpts) + if err != nil { + return err + } + } + } else { + var err error + pluginArchive, err = m.pluginRepo.GetPluginArchive(ctx, pluginID, version, compatOpts) + if err != nil { + return err + } + } + + extractedArchive, err := m.pluginStorage.Add(ctx, pluginID, pluginArchive.File) + if err != nil { + return err + } + + // download dependency plugins + pathsToScan := []string{extractedArchive.Path} + for _, dep := range extractedArchive.Dependencies { + m.log.Info("Fetching %s dependencies...", dep.ID) + d, err := m.pluginRepo.GetPluginArchive(ctx, dep.ID, dep.Version, compatOpts) + if err != nil { + return fmt.Errorf("%v: %w", fmt.Sprintf("failed to download plugin %s from repository", dep.ID), err) + } + + depArchive, err := m.pluginStorage.Add(ctx, dep.ID, d.File) + if err != nil { + return err + } + + pathsToScan = append(pathsToScan, depArchive.Path) + } + + _, err = m.pluginLoader.Load(ctx, plugins.External, pathsToScan) + if err != nil { + m.log.Error("Could not load plugins", "paths", pathsToScan, "err", err) + return err + } + + return nil +} + +func (m *PluginInstaller) Remove(ctx context.Context, pluginID string) error { + plugin, exists := m.plugin(ctx, pluginID) + if !exists { + return plugins.ErrPluginNotInstalled + } + + if !plugin.IsExternalPlugin() { + return plugins.ErrUninstallCorePlugin + } + + if err := m.pluginLoader.Unload(ctx, plugin.ID); err != nil { + return err + } + return nil +} + +// plugin finds a plugin with `pluginID` from the store +func (m *PluginInstaller) plugin(ctx context.Context, pluginID string) (*plugins.Plugin, bool) { + p, exists := m.pluginRegistry.Plugin(ctx, pluginID) + if !exists { + return nil, false + } + + return p, true +} diff --git a/pkg/plugins/manager/manager_test.go b/pkg/plugins/manager/installer_test.go similarity index 51% rename from pkg/plugins/manager/manager_test.go rename to pkg/plugins/manager/installer_test.go index 3654387bb08..fe4066f2163 100644 --- a/pkg/plugins/manager/manager_test.go +++ b/pkg/plugins/manager/installer_test.go @@ -3,16 +3,15 @@ package manager import ( "archive/zip" "context" + "fmt" "testing" - "github.com/stretchr/testify/require" - "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/plugins" - "github.com/grafana/grafana/pkg/plugins/config" "github.com/grafana/grafana/pkg/plugins/manager/fakes" "github.com/grafana/grafana/pkg/plugins/repo" "github.com/grafana/grafana/pkg/plugins/storage" + "github.com/stretchr/testify/require" ) const testPluginID = "test-plugin" @@ -34,16 +33,18 @@ func TestPluginManager_Add_Remove(t *testing.T) { FileHeader: zip.FileHeader{Name: zipNameV1}, }}}} + var loadedPaths []string loader := &fakes.FakeLoader{ - LoadFunc: func(_ context.Context, _ plugins.Class, paths []string, _ map[string]struct{}) ([]*plugins.Plugin, error) { + LoadFunc: func(_ context.Context, _ plugins.Class, paths []string) ([]*plugins.Plugin, error) { + loadedPaths = append(loadedPaths, paths...) require.Equal(t, []string{zipNameV1}, paths) return []*plugins.Plugin{pluginV1}, nil }, } pluginRepo := &fakes.FakePluginRepo{ - GetPluginArchiveFunc: func(_ context.Context, pluginID, version string, _ repo.CompatOpts) (*repo.PluginArchive, error) { - require.Equal(t, pluginV1.ID, pluginID) + GetPluginArchiveFunc: func(_ context.Context, id, version string, _ repo.CompatOpts) (*repo.PluginArchive, error) { + require.Equal(t, pluginID, id) require.Equal(t, v1, version) return &repo.PluginArchive{ File: mockZipV1, @@ -52,8 +53,8 @@ func TestPluginManager_Add_Remove(t *testing.T) { } fs := &fakes.FakePluginStorage{ - AddFunc: func(_ context.Context, pluginID string, z *zip.ReadCloser) (*storage.ExtractedPluginArchive, error) { - require.Equal(t, pluginV1.ID, pluginID) + AddFunc: func(_ context.Context, id string, z *zip.ReadCloser) (*storage.ExtractedPluginArchive, error) { + require.Equal(t, pluginID, id) require.Equal(t, mockZipV1, z) return &storage.ExtractedPluginArchive{ Path: zipNameV1, @@ -64,27 +65,21 @@ func TestPluginManager_Add_Remove(t *testing.T) { require.Equal(t, pluginV1.PluginDir, pluginDir) return nil }, - Added: make(map[string]string), - Removed: make(map[string]int), + Store: map[string]struct{}{}, } - proc := fakes.NewFakeProcessManager() - pm := New(&config.Cfg{}, fakes.NewFakePluginRegistry(), []plugins.PluginSource{}, loader, pluginRepo, fs, proc) - err := pm.Add(context.Background(), pluginID, v1, plugins.CompatOpts{}) + inst := New(fakes.NewFakePluginRegistry(), loader, pluginRepo, fs) + err := inst.Add(context.Background(), pluginID, v1, plugins.CompatOpts{}) require.NoError(t, err) - require.Equal(t, pluginV1.PluginDir, fs.Added[pluginID]) - require.Equal(t, 0, fs.Removed[pluginID]) - require.Equal(t, 1, proc.Started[pluginID]) - require.Equal(t, 0, proc.Stopped[pluginID]) - - regPlugin, exists := pm.pluginRegistry.Plugin(context.Background(), pluginID) - require.True(t, exists) - require.Equal(t, pluginV1, regPlugin) - require.Len(t, pm.pluginRegistry.Plugins(context.Background()), 1) - t.Run("Won't add if already exists", func(t *testing.T) { - err = pm.Add(context.Background(), pluginID, v1, plugins.CompatOpts{}) + inst.pluginRegistry = &fakes.FakePluginRegistry{ + Store: map[string]*plugins.Plugin{ + pluginID: pluginV1, + }, + } + + err = inst.Add(context.Background(), pluginID, v1, plugins.CompatOpts{}) require.Equal(t, plugins.DuplicateError{ PluginID: pluginV1.ID, ExistingPluginDir: pluginV1.PluginDir, @@ -106,9 +101,8 @@ func TestPluginManager_Add_Remove(t *testing.T) { mockZipV2 := &zip.ReadCloser{Reader: zip.Reader{File: []*zip.File{{ FileHeader: zip.FileHeader{Name: zipNameV2}, }}}} - loader.LoadFunc = func(_ context.Context, class plugins.Class, paths []string, ignore map[string]struct{}) ([]*plugins.Plugin, error) { + loader.LoadFunc = func(_ context.Context, class plugins.Class, paths []string) ([]*plugins.Plugin, error) { require.Equal(t, plugins.External, class) - require.Empty(t, ignore) require.Equal(t, []string{zipNameV2}, paths) return []*plugins.Plugin{pluginV2}, nil } @@ -136,33 +130,34 @@ func TestPluginManager_Add_Remove(t *testing.T) { return nil } - err = pm.Add(context.Background(), pluginID, v2, plugins.CompatOpts{}) + err = inst.Add(context.Background(), pluginID, v2, plugins.CompatOpts{}) require.NoError(t, err) - - require.Equal(t, pluginDirV2, fs.Added[pluginID]) - require.Equal(t, 1, fs.Removed[pluginID]) - require.Equal(t, 2, proc.Started[pluginID]) - require.Equal(t, 1, proc.Stopped[pluginID]) - - regPlugin, exists = pm.pluginRegistry.Plugin(context.Background(), pluginID) - require.True(t, exists) - require.Equal(t, pluginV2, regPlugin) - require.Len(t, pm.pluginRegistry.Plugins(context.Background()), 1) }) t.Run("Removing an existing plugin", func(t *testing.T) { - err = pm.Remove(context.Background(), pluginID) - require.NoError(t, err) + inst.pluginRegistry = &fakes.FakePluginRegistry{ + Store: map[string]*plugins.Plugin{ + pluginID: pluginV1, + }, + } - require.Equal(t, 2, proc.Stopped[pluginID]) - require.Equal(t, 2, fs.Removed[pluginID]) + var unloadedPlugins []string + inst.pluginLoader = &fakes.FakeLoader{ + UnloadFunc: func(_ context.Context, id string) error { + unloadedPlugins = append(unloadedPlugins, id) + return nil + }, + } + + err = inst.Remove(context.Background(), pluginID) + require.NoError(t, err) - p, exists := pm.pluginRegistry.Plugin(context.Background(), pluginID) - require.False(t, exists) - require.Nil(t, p) + require.Equal(t, []string{pluginID}, unloadedPlugins) t.Run("Won't remove if not exists", func(t *testing.T) { - err := pm.Remove(context.Background(), pluginID) + inst.pluginRegistry = fakes.NewFakePluginRegistry() + + err = inst.Remove(context.Background(), pluginID) require.Equal(t, plugins.ErrPluginNotInstalled, err) }) }) @@ -181,31 +176,20 @@ func TestPluginManager_Add_Remove(t *testing.T) { plugin.Info.Version = "1.0.0" }) - fakes.NewFakePluginRegistry() - reg := &fakes.FakePluginRegistry{ Store: map[string]*plugins.Plugin{ testPluginID: p, }, } - proc := fakes.NewFakeProcessManager() - pm := New(&config.Cfg{}, reg, []plugins.PluginSource{}, &fakes.FakeLoader{}, &fakes.FakePluginRepo{}, &fakes.FakePluginStorage{}, proc) + pm := New(reg, &fakes.FakeLoader{}, &fakes.FakePluginRepo{}, &fakes.FakePluginStorage{}) err := pm.Add(context.Background(), p.ID, "3.2.0", plugins.CompatOpts{}) require.ErrorIs(t, err, plugins.ErrInstallCorePlugin) - require.Equal(t, 0, proc.Started[p.ID]) - require.Equal(t, 0, proc.Stopped[p.ID]) - - regPlugin, exists := pm.pluginRegistry.Plugin(context.Background(), testPluginID) - require.True(t, exists) - require.Equal(t, p, regPlugin) - require.Len(t, pm.pluginRegistry.Plugins(context.Background()), 1) - err = pm.Add(context.Background(), testPluginID, "", plugins.CompatOpts{}) require.Equal(t, plugins.ErrInstallCorePlugin, err) - t.Run("Can't uninstall core plugin", func(t *testing.T) { + t.Run(fmt.Sprintf("Can't uninstall %s plugin", tc.class), func(t *testing.T) { err = pm.Remove(context.Background(), p.ID) require.Equal(t, plugins.ErrUninstallCorePlugin, err) }) @@ -213,67 +197,6 @@ func TestPluginManager_Add_Remove(t *testing.T) { }) } -func TestPluginManager_Run(t *testing.T) { - t.Run("Plugin sources are loaded in order", func(t *testing.T) { - loader := &fakes.FakeLoader{} - pm := New(&config.Cfg{}, fakes.NewFakePluginRegistry(), []plugins.PluginSource{ - {Class: plugins.Bundled, Paths: []string{"path1"}}, - {Class: plugins.Core, Paths: []string{"path2"}}, - {Class: plugins.External, Paths: []string{"path3"}}, - }, loader, &fakes.FakePluginRepo{}, &fakes.FakePluginStorage{}, &fakes.FakeProcessManager{}) - - err := pm.Init(context.Background()) - require.NoError(t, err) - require.Equal(t, []string{"path1", "path2", "path3"}, loader.LoadedPaths) - }) -} - -func TestManager_Renderer(t *testing.T) { - t.Run("Renderer returns a single (non-decommissioned) renderer plugin", func(t *testing.T) { - p1 := &plugins.Plugin{JSONData: plugins.JSONData{ID: "test-renderer", Type: plugins.Renderer}} - p2 := &plugins.Plugin{JSONData: plugins.JSONData{ID: "test-panel", Type: plugins.Panel}} - p3 := &plugins.Plugin{JSONData: plugins.JSONData{ID: "test-app", Type: plugins.App}} - - reg := &fakes.FakePluginRegistry{ - Store: map[string]*plugins.Plugin{ - p1.ID: p1, - p2.ID: p2, - p3.ID: p3, - }, - } - - pm := New(&config.Cfg{}, reg, []plugins.PluginSource{}, &fakes.FakeLoader{}, &fakes.FakePluginRepo{}, - &fakes.FakePluginStorage{}, &fakes.FakeProcessManager{}) - - r := pm.Renderer(context.Background()) - require.Equal(t, p1, r) - }) -} - -func TestManager_SecretsManager(t *testing.T) { - t.Run("Renderer returns a single (non-decommissioned) secrets manager plugin", func(t *testing.T) { - p1 := &plugins.Plugin{JSONData: plugins.JSONData{ID: "test-renderer", Type: plugins.Renderer}} - p2 := &plugins.Plugin{JSONData: plugins.JSONData{ID: "test-panel", Type: plugins.Panel}} - p3 := &plugins.Plugin{JSONData: plugins.JSONData{ID: "test-secrets", Type: plugins.SecretsManager}} - p4 := &plugins.Plugin{JSONData: plugins.JSONData{ID: "test-datasource", Type: plugins.DataSource}} - - reg := &fakes.FakePluginRegistry{ - Store: map[string]*plugins.Plugin{ - p1.ID: p1, - p2.ID: p2, - p3.ID: p3, - p4.ID: p4, - }, - } - - pm := New(&config.Cfg{}, reg, []plugins.PluginSource{}, &fakes.FakeLoader{}, &fakes.FakePluginRepo{}, - &fakes.FakePluginStorage{}, &fakes.FakeProcessManager{}) - - r := pm.SecretsManager(context.Background()) - require.Equal(t, p3, r) - }) -} - func createPlugin(t *testing.T, pluginID string, class plugins.Class, managed, backend bool, cbs ...func(*plugins.Plugin)) *plugins.Plugin { t.Helper() diff --git a/pkg/plugins/manager/loader/ifaces.go b/pkg/plugins/manager/loader/ifaces.go index 02645adeb67..fde77628e31 100644 --- a/pkg/plugins/manager/loader/ifaces.go +++ b/pkg/plugins/manager/loader/ifaces.go @@ -9,5 +9,7 @@ import ( // Service is responsible for loading plugins from the file system. type Service interface { // Load will return a list of plugins found in the provided file system paths. - Load(ctx context.Context, class plugins.Class, paths []string, ignore map[string]struct{}) ([]*plugins.Plugin, error) + Load(ctx context.Context, class plugins.Class, paths []string) ([]*plugins.Plugin, error) + // Unload will unload a specified plugin from the file system. + Unload(ctx context.Context, pluginID string) error } diff --git a/pkg/plugins/manager/loader/loader.go b/pkg/plugins/manager/loader/loader.go index f200267c9b5..c7ca2672b0b 100644 --- a/pkg/plugins/manager/loader/loader.go +++ b/pkg/plugins/manager/loader/loader.go @@ -20,9 +20,13 @@ import ( "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/config" + "github.com/grafana/grafana/pkg/plugins/logger" "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/process" + "github.com/grafana/grafana/pkg/plugins/manager/registry" "github.com/grafana/grafana/pkg/plugins/manager/signature" + "github.com/grafana/grafana/pkg/plugins/storage" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/util" ) @@ -36,39 +40,47 @@ var _ plugins.ErrorResolver = (*Loader)(nil) type Loader struct { pluginFinder finder.Finder + processManager process.Service + pluginRegistry registry.Service pluginInitializer initializer.Initializer signatureValidator signature.Validator + pluginStorage storage.Manager log log.Logger errs map[string]*plugins.SignatureError } func ProvideService(cfg *config.Cfg, license models.Licensing, authorizer plugins.PluginLoaderAuthorizer, - backendProvider plugins.BackendFactoryProvider) (*Loader, error) { - return New(cfg, license, authorizer, backendProvider), nil + pluginRegistry registry.Service, backendProvider plugins.BackendFactoryProvider) *Loader { + return New(cfg, license, authorizer, pluginRegistry, backendProvider, process.NewManager(pluginRegistry), + storage.FileSystem(logger.NewLogger("loader.fs"), cfg.PluginsPath)) } func New(cfg *config.Cfg, license models.Licensing, authorizer plugins.PluginLoaderAuthorizer, - backendProvider plugins.BackendFactoryProvider) *Loader { + pluginRegistry registry.Service, backendProvider plugins.BackendFactoryProvider, + processManager process.Service, pluginStorage storage.Manager) *Loader { return &Loader{ pluginFinder: finder.New(), + pluginRegistry: pluginRegistry, pluginInitializer: initializer.New(cfg, backendProvider, license), signatureValidator: signature.NewValidator(authorizer), + processManager: processManager, + pluginStorage: pluginStorage, errs: make(map[string]*plugins.SignatureError), log: log.New("plugin.loader"), } } -func (l *Loader) Load(ctx context.Context, class plugins.Class, paths []string, ignore map[string]struct{}) ([]*plugins.Plugin, error) { +func (l *Loader) Load(ctx context.Context, class plugins.Class, paths []string) ([]*plugins.Plugin, error) { pluginJSONPaths, err := l.pluginFinder.Find(paths) if err != nil { return nil, err } - return l.loadPlugins(ctx, class, pluginJSONPaths, ignore) + return l.loadPlugins(ctx, class, pluginJSONPaths) } -func (l *Loader) loadPlugins(ctx context.Context, class plugins.Class, pluginJSONPaths []string, existingPlugins map[string]struct{}) ([]*plugins.Plugin, error) { +func (l *Loader) loadPlugins(ctx context.Context, class plugins.Class, pluginJSONPaths []string) ([]*plugins.Plugin, error) { var foundPlugins = foundPlugins{} // load plugin.json files and map directory to JSON data @@ -92,12 +104,18 @@ func (l *Loader) loadPlugins(ctx context.Context, class plugins.Class, pluginJSO foundPlugins[filepath.Dir(pluginJSONAbsPath)] = plugin } - foundPlugins.stripDuplicates(existingPlugins, l.log) + // get all registered plugins + registeredPlugins := make(map[string]struct{}) + for _, p := range l.pluginRegistry.Plugins(ctx) { + registeredPlugins[p.ID] = struct{}{} + } + + foundPlugins.stripDuplicates(registeredPlugins, l.log) // calculate initial signature state loadedPlugins := make(map[string]*plugins.Plugin) for pluginDir, pluginJSON := range foundPlugins { - plugin := createPluginBase(pluginJSON, class, pluginDir, l.log) + plugin := createPluginBase(pluginJSON, class, pluginDir) sig, err := signature.Calculate(l.log, plugin) if err != nil { @@ -179,9 +197,68 @@ func (l *Loader) loadPlugins(ctx context.Context, class plugins.Class, pluginJSO metrics.SetPluginBuildInformation(p.ID, string(p.Type), p.Info.Version, string(p.Signature)) } + for _, p := range verifiedPlugins { + if err := l.load(ctx, p); err != nil { + l.log.Error("Could not start plugin", "pluginId", p.ID, "err", err) + } + } + return verifiedPlugins, nil } +func (l *Loader) Unload(ctx context.Context, pluginID string) error { + plugin, exists := l.pluginRegistry.Plugin(ctx, pluginID) + if !exists { + return plugins.ErrPluginNotInstalled + } + + if !plugin.IsExternalPlugin() { + return plugins.ErrUninstallCorePlugin + } + + if err := l.unload(ctx, plugin); err != nil { + return err + } + return nil +} + +func (l *Loader) load(ctx context.Context, p *plugins.Plugin) error { + if err := l.pluginRegistry.Add(ctx, p); err != nil { + return err + } + + if !p.IsCorePlugin() { + l.log.Info("Plugin registered", "pluginID", p.ID) + } + + if p.IsExternalPlugin() { + if err := l.pluginStorage.Register(ctx, p.ID, p.PluginDir); err != nil { + return err + } + } + + return l.processManager.Start(ctx, p.ID) +} + +func (l *Loader) unload(ctx context.Context, p *plugins.Plugin) error { + l.log.Debug("Stopping plugin process", "pluginId", p.ID) + + // TODO confirm the sequence of events is safe + if err := l.processManager.Stop(ctx, p.ID); err != nil { + return err + } + + if err := l.pluginRegistry.Remove(ctx, p.ID); err != nil { + return err + } + l.log.Debug("Plugin unregistered", "pluginId", p.ID) + + if err := l.pluginStorage.Remove(ctx, p.ID); err != nil { + return err + } + return nil +} + func (l *Loader) readPluginJSON(pluginJSONPath string) (plugins.JSONData, error) { l.log.Debug("Loading plugin", "path", pluginJSONPath) @@ -198,15 +275,15 @@ func (l *Loader) readPluginJSON(pluginJSONPath string) (plugins.JSONData, error) } plugin := plugins.JSONData{} - if err := json.NewDecoder(reader).Decode(&plugin); err != nil { + if err = json.NewDecoder(reader).Decode(&plugin); err != nil { return plugins.JSONData{}, err } - if err := reader.Close(); err != nil { + if err = reader.Close(); err != nil { l.log.Warn("Failed to close JSON file", "path", pluginJSONPath, "err", err) } - if err := validatePluginJSON(plugin); err != nil { + if err = validatePluginJSON(plugin); err != nil { return plugins.JSONData{}, err } @@ -231,7 +308,7 @@ func (l *Loader) readPluginJSON(pluginJSONPath string) (plugins.JSONData, error) return plugin, nil } -func createPluginBase(pluginJSON plugins.JSONData, class plugins.Class, pluginDir string, logger log.Logger) *plugins.Plugin { +func createPluginBase(pluginJSON plugins.JSONData, class plugins.Class, pluginDir string) *plugins.Plugin { plugin := &plugins.Plugin{ JSONData: pluginJSON, PluginDir: pluginDir, @@ -342,7 +419,6 @@ func baseURL(pluginJSON plugins.JSONData, class plugins.Class, pluginDir string) if class == plugins.Core { return path.Join("public/app/plugins", string(pluginJSON.Type), filepath.Base(pluginDir)) } - return path.Join("public/plugins", pluginJSON.ID) } @@ -350,7 +426,6 @@ func module(pluginJSON plugins.JSONData, class plugins.Class, pluginDir string) if class == plugins.Core { return path.Join("app/plugins", string(pluginJSON.Type), filepath.Base(pluginDir), "module") } - return path.Join("plugins", pluginJSON.ID, "module") } diff --git a/pkg/plugins/manager/loader/loader_test.go b/pkg/plugins/manager/loader/loader_test.go index 5ed9288584b..9247d23d479 100644 --- a/pkg/plugins/manager/loader/loader_test.go +++ b/pkg/plugins/manager/loader/loader_test.go @@ -9,16 +9,12 @@ import ( "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/logtest" "github.com/grafana/grafana/pkg/plugins" - "github.com/grafana/grafana/pkg/plugins/backendplugin" - "github.com/grafana/grafana/pkg/plugins/backendplugin/coreplugin" - "github.com/grafana/grafana/pkg/plugins/backendplugin/provider" "github.com/grafana/grafana/pkg/plugins/config" - "github.com/grafana/grafana/pkg/plugins/manager/loader/finder" + "github.com/grafana/grafana/pkg/plugins/manager/fakes" "github.com/grafana/grafana/pkg/plugins/manager/loader/initializer" "github.com/grafana/grafana/pkg/plugins/manager/signature" "github.com/grafana/grafana/pkg/services/org" @@ -39,13 +35,12 @@ func TestLoader_Load(t *testing.T) { return } tests := []struct { - name string - class plugins.Class - cfg *config.Cfg - pluginPaths []string - existingPlugins map[string]struct{} - want []*plugins.Plugin - pluginErrors map[string]*plugins.Error + name string + class plugins.Class + cfg *config.Cfg + pluginPaths []string + want []*plugins.Plugin + pluginErrors map[string]*plugins.Error }{ { name: "Load a Core plugin", @@ -411,19 +406,31 @@ func TestLoader_Load(t *testing.T) { }, } for _, tt := range tests { - l := newLoader(tt.cfg) + reg := fakes.NewFakePluginRegistry() + storage := fakes.NewFakePluginStorage() + procPrvdr := fakes.NewFakeBackendProcessProvider() + procMgr := fakes.NewFakeProcessManager() + l := newLoader(tt.cfg, func(l *Loader) { + l.pluginRegistry = reg + l.pluginStorage = storage + l.processManager = procMgr + l.pluginInitializer = initializer.New(tt.cfg, procPrvdr, &fakes.FakeLicensingService{}) + }) + t.Run(tt.name, func(t *testing.T) { - got, err := l.Load(context.Background(), tt.class, tt.pluginPaths, tt.existingPlugins) + got, err := l.Load(context.Background(), tt.class, tt.pluginPaths) 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)) } pluginErrs := l.PluginErrors() - assert.Equal(t, len(tt.pluginErrors), len(pluginErrs)) + require.Equal(t, len(tt.pluginErrors), len(pluginErrs)) for _, pluginErr := range pluginErrs { - assert.Equal(t, tt.pluginErrors[pluginErr.PluginID], pluginErr) + require.Equal(t, tt.pluginErrors[pluginErr.PluginID], pluginErr) } + + verifyState(t, tt.want, reg, procPrvdr, storage, procMgr) }) } } @@ -554,7 +561,16 @@ func TestLoader_Load_MultiplePlugins(t *testing.T) { } for _, tt := range tests { - l := newLoader(tt.cfg) + reg := fakes.NewFakePluginRegistry() + storage := fakes.NewFakePluginStorage() + procPrvdr := fakes.NewFakeBackendProcessProvider() + procMgr := fakes.NewFakeProcessManager() + l := newLoader(tt.cfg, func(l *Loader) { + l.pluginRegistry = reg + l.pluginStorage = storage + l.processManager = procMgr + l.pluginInitializer = initializer.New(tt.cfg, procPrvdr, fakes.NewFakeLicensingService()) + }) t.Run(tt.name, func(t *testing.T) { origAppURL := setting.AppUrl t.Cleanup(func() { @@ -562,7 +578,7 @@ func TestLoader_Load_MultiplePlugins(t *testing.T) { }) setting.AppUrl = tt.appURL - got, err := l.Load(context.Background(), plugins.External, tt.pluginPaths, tt.existingPlugins) + got, err := l.Load(context.Background(), plugins.External, tt.pluginPaths) require.NoError(t, err) sort.SliceStable(got, func(i, j int) bool { return got[i].ID < got[j].ID @@ -575,12 +591,13 @@ func TestLoader_Load_MultiplePlugins(t *testing.T) { for _, pluginErr := range pluginErrs { require.Equal(t, tt.pluginErrors[pluginErr.PluginID], pluginErr) } + verifyState(t, tt.want, reg, procPrvdr, storage, procMgr) }) } }) } -func TestLoader_Signature_RootURL(t *testing.T) { +func TestLoader_Load_Signature_RootURL(t *testing.T) { const defaultAppURL = "http://localhost:3000/grafana" parentDir, err := filepath.Abs("../") @@ -630,13 +647,23 @@ func TestLoader_Signature_RootURL(t *testing.T) { }, } - l := newLoader(&config.Cfg{}) - got, err := l.Load(context.Background(), plugins.External, paths, map[string]struct{}{}) - assert.NoError(t, err) + reg := fakes.NewFakePluginRegistry() + storage := fakes.NewFakePluginStorage() + procPrvdr := fakes.NewFakeBackendProcessProvider() + procMgr := fakes.NewFakeProcessManager() + l := newLoader(&config.Cfg{}, func(l *Loader) { + l.pluginRegistry = reg + l.pluginStorage = storage + l.processManager = procMgr + l.pluginInitializer = initializer.New(&config.Cfg{}, procPrvdr, fakes.NewFakeLicensingService()) + }) + got, err := l.Load(context.Background(), plugins.External, paths) + require.NoError(t, err) if !cmp.Equal(got, expected, compareOpts) { t.Fatalf("Result mismatch (-want +got):\n%s", cmp.Diff(got, expected, compareOpts)) } + verifyState(t, expected, reg, procPrvdr, storage, procMgr) }) } @@ -699,18 +726,28 @@ func TestLoader_Load_DuplicatePlugins(t *testing.T) { }, } - l := newLoader(&config.Cfg{}) - - got, err := l.Load(context.Background(), plugins.External, []string{pluginDir, pluginDir}, map[string]struct{}{}) - assert.NoError(t, err) + reg := fakes.NewFakePluginRegistry() + storage := fakes.NewFakePluginStorage() + procPrvdr := fakes.NewFakeBackendProcessProvider() + procMgr := fakes.NewFakeProcessManager() + l := newLoader(&config.Cfg{}, func(l *Loader) { + l.pluginRegistry = reg + l.pluginStorage = storage + l.processManager = procMgr + l.pluginInitializer = initializer.New(&config.Cfg{}, procPrvdr, fakes.NewFakeLicensingService()) + }) + got, err := l.Load(context.Background(), plugins.External, []string{pluginDir, pluginDir}) + require.NoError(t, err) if !cmp.Equal(got, expected, compareOpts) { t.Fatalf("Result mismatch (-want +got):\n%s", cmp.Diff(got, expected, compareOpts)) } + + verifyState(t, expected, reg, procPrvdr, storage, procMgr) }) } -func TestLoader_loadNestedPlugins(t *testing.T) { +func TestLoader_Load_NestedPlugins(t *testing.T) { rootDir, err := filepath.Abs("../") if err != nil { t.Errorf("could not construct absolute path of root dir") @@ -785,42 +822,47 @@ func TestLoader_loadNestedPlugins(t *testing.T) { child.Parent = parent t.Run("Load nested External plugins", func(t *testing.T) { - expected := []*plugins.Plugin{parent, child} - l := newLoader(&config.Cfg{}) + reg := fakes.NewFakePluginRegistry() + storage := fakes.NewFakePluginStorage() + procPrvdr := fakes.NewFakeBackendProcessProvider() + procMgr := fakes.NewFakeProcessManager() + l := newLoader(&config.Cfg{}, func(l *Loader) { + l.pluginRegistry = reg + l.pluginStorage = storage + l.processManager = procMgr + l.pluginInitializer = initializer.New(&config.Cfg{}, procPrvdr, fakes.NewFakeLicensingService()) + }) - got, err := l.Load(context.Background(), plugins.External, []string{"../testdata/nested-plugins"}, map[string]struct{}{}) - assert.NoError(t, err) + got, err := l.Load(context.Background(), plugins.External, []string{"../testdata/nested-plugins"}) + require.NoError(t, err) // to ensure we can compare with expected sort.SliceStable(got, func(i, j int) bool { return got[i].ID < got[j].ID }) + expected := []*plugins.Plugin{parent, child} if !cmp.Equal(got, expected, compareOpts) { t.Fatalf("Result mismatch (-want +got):\n%s", cmp.Diff(got, expected, compareOpts)) } - }) - t.Run("Load will exclude plugins that already exist", func(t *testing.T) { - // parent/child links will not be created when either plugins are provided in the existingPlugins map - parent.Children = nil - expected := []*plugins.Plugin{parent} + verifyState(t, expected, reg, procPrvdr, storage, procMgr) - l := newLoader(&config.Cfg{}) + t.Run("Load will exclude plugins that already exist", func(t *testing.T) { + got, err := l.Load(context.Background(), plugins.External, []string{"../testdata/nested-plugins"}) + require.NoError(t, err) - got, err := l.Load(context.Background(), plugins.External, []string{"../testdata/nested-plugins"}, map[string]struct{}{ - "test-panel": {}, - }) - assert.NoError(t, err) + // to ensure we can compare with expected + sort.SliceStable(got, func(i, j int) bool { + return got[i].ID < got[j].ID + }) - // to ensure we can compare with expected - sort.SliceStable(got, func(i, j int) bool { - return got[i].ID < got[j].ID - }) + if !cmp.Equal(got, []*plugins.Plugin{}, compareOpts) { + t.Fatalf("Result mismatch (-want +got):\n%s", cmp.Diff(got, expected, compareOpts)) + } - if !cmp.Equal(got, expected, compareOpts) { - t.Fatalf("Result mismatch (-want +got):\n%s", cmp.Diff(got, expected, compareOpts)) - } + verifyState(t, expected, reg, procPrvdr, storage, procMgr) + }) }) t.Run("Plugin child field `IncludedInAppID` is set to parent app's plugin ID", func(t *testing.T) { @@ -944,12 +986,20 @@ func TestLoader_loadNestedPlugins(t *testing.T) { parent.Children = []*plugins.Plugin{child} child.Parent = parent - expected := []*plugins.Plugin{parent, child} - l := newLoader(&config.Cfg{}) - got, err := l.Load(context.Background(), plugins.External, []string{"../testdata/app-with-child"}, map[string]struct{}{}) - assert.NoError(t, err) + reg := fakes.NewFakePluginRegistry() + storage := fakes.NewFakePluginStorage() + procPrvdr := fakes.NewFakeBackendProcessProvider() + procMgr := fakes.NewFakeProcessManager() + l := newLoader(&config.Cfg{}, func(l *Loader) { + l.pluginRegistry = reg + l.pluginStorage = storage + l.processManager = procMgr + l.pluginInitializer = initializer.New(&config.Cfg{}, procPrvdr, fakes.NewFakeLicensingService()) + }) + got, err := l.Load(context.Background(), plugins.External, []string{"../testdata/app-with-child"}) + require.NoError(t, err) // to ensure we can compare with expected sort.SliceStable(got, func(i, j int) bool { @@ -960,14 +1010,24 @@ func TestLoader_loadNestedPlugins(t *testing.T) { t.Fatalf("Result mismatch (-want +got):\n%s", cmp.Diff(got, expected, compareOpts)) } + verifyState(t, expected, reg, procPrvdr, storage, procMgr) + t.Run("order of loaded parent and child plugins gives same output", func(t *testing.T) { parentPluginJSON := filepath.Join(rootDir, "testdata/app-with-child/dist/plugin.json") childPluginJSON := filepath.Join(rootDir, "testdata/app-with-child/dist/child/plugin.json") - got, err := l.loadPlugins(context.Background(), plugins.External, []string{ - parentPluginJSON, childPluginJSON}, - map[string]struct{}{}) - assert.NoError(t, err) + reg = fakes.NewFakePluginRegistry() + storage = fakes.NewFakePluginStorage() + procPrvdr = fakes.NewFakeBackendProcessProvider() + procMgr = fakes.NewFakeProcessManager() + l = newLoader(&config.Cfg{}, func(l *Loader) { + l.pluginRegistry = reg + l.pluginStorage = storage + l.processManager = procMgr + l.pluginInitializer = initializer.New(&config.Cfg{}, procPrvdr, fakes.NewFakeLicensingService()) + }) + got, err = l.loadPlugins(context.Background(), plugins.External, []string{parentPluginJSON, childPluginJSON}) + require.NoError(t, err) // to ensure we can compare with expected sort.SliceStable(got, func(i, j int) bool { @@ -978,10 +1038,20 @@ func TestLoader_loadNestedPlugins(t *testing.T) { t.Fatalf("Result mismatch (-want +got):\n%s", cmp.Diff(got, expected, compareOpts)) } - got, err = l.loadPlugins(context.Background(), plugins.External, []string{ - childPluginJSON, parentPluginJSON}, - map[string]struct{}{}) - assert.NoError(t, err) + verifyState(t, expected, reg, procPrvdr, storage, procMgr) + + reg = fakes.NewFakePluginRegistry() + storage = fakes.NewFakePluginStorage() + procPrvdr = fakes.NewFakeBackendProcessProvider() + procMgr = fakes.NewFakeProcessManager() + l = newLoader(&config.Cfg{}, func(l *Loader) { + l.pluginRegistry = reg + l.pluginStorage = storage + l.processManager = procMgr + l.pluginInitializer = initializer.New(&config.Cfg{}, procPrvdr, fakes.NewFakeLicensingService()) + }) + got, err = l.loadPlugins(context.Background(), plugins.External, []string{childPluginJSON, parentPluginJSON}) + require.NoError(t, err) // to ensure we can compare with expected sort.SliceStable(got, func(i, j int) bool { @@ -991,6 +1061,8 @@ func TestLoader_loadNestedPlugins(t *testing.T) { if !cmp.Equal(got, expected, compareOpts) { t.Fatalf("Result mismatch (-want +got):\n%s", cmp.Diff(got, expected, compareOpts)) } + + verifyState(t, expected, reg, procPrvdr, storage, procMgr) }) }) } @@ -1137,55 +1209,48 @@ func Test_setPathsBasedOnApp(t *testing.T) { configureAppChildOPlugin(parent, child) - assert.Equal(t, "app/plugins/app/testdata-app/datasources/datasource/module", child.Module) - assert.Equal(t, "testdata-app", child.IncludedInAppID) - assert.Equal(t, "public/app/plugins/app/testdata-app", child.BaseURL) + require.Equal(t, "app/plugins/app/testdata-app/datasources/datasource/module", child.Module) + require.Equal(t, "testdata-app", child.IncludedInAppID) + require.Equal(t, "public/app/plugins/app/testdata-app", child.BaseURL) }) } -func newLoader(cfg *config.Cfg) *Loader { - return &Loader{ - pluginFinder: finder.New(), - pluginInitializer: initializer.New(cfg, provider.ProvideService(coreplugin.NewRegistry(make(map[string]backendplugin.PluginFactoryFunc))), &fakeLicensingService{}), - signatureValidator: signature.NewValidator(signature.NewUnsignedAuthorizer(cfg)), - errs: make(map[string]*plugins.SignatureError), - log: &logtest.Fake{}, - } -} - -type fakeLicensingService struct { - edition string - tokenRaw string -} - -func (t *fakeLicensingService) Expiry() int64 { - return 0 -} +func newLoader(cfg *config.Cfg, cbs ...func(loader *Loader)) *Loader { + l := New(cfg, &fakes.FakeLicensingService{}, signature.NewUnsignedAuthorizer(cfg), fakes.NewFakePluginRegistry(), + fakes.NewFakeBackendProcessProvider(), fakes.NewFakeProcessManager(), fakes.NewFakePluginStorage()) -func (t *fakeLicensingService) Edition() string { - return t.edition -} + for _, cb := range cbs { + cb(l) + } -func (t *fakeLicensingService) StateInfo() string { - return "" + return l } -func (t *fakeLicensingService) ContentDeliveryPrefix() string { - return "" -} +func verifyState(t *testing.T, ps []*plugins.Plugin, reg *fakes.FakePluginRegistry, + procPrvdr *fakes.FakeBackendProcessProvider, storage *fakes.FakePluginStorage, procMngr *fakes.FakeProcessManager) { + t.Helper() -func (t *fakeLicensingService) LicenseURL(_ bool) string { - return "" -} + for _, p := range ps { + if !cmp.Equal(p, reg.Store[p.ID], compareOpts) { + t.Fatalf("Result mismatch (-want +got):\n%s", cmp.Diff(p, reg.Store[p.ID], compareOpts)) + } -func (t *fakeLicensingService) Environment() map[string]string { - return map[string]string{"GF_ENTERPRISE_LICENSE_TEXT": t.tokenRaw} -} + if p.Backend { + require.Equal(t, 1, procPrvdr.Requested[p.ID]) + require.Equal(t, 1, procPrvdr.Invoked[p.ID]) + } else { + require.Zero(t, procPrvdr.Requested[p.ID]) + require.Zero(t, procPrvdr.Invoked[p.ID]) + } -func (*fakeLicensingService) EnabledFeatures() map[string]bool { - return map[string]bool{} -} + _, exists := storage.Store[p.ID] + if p.IsExternalPlugin() { + require.True(t, exists) + } else { + require.False(t, exists) + } -func (*fakeLicensingService) FeatureEnabled(feature string) bool { - return false + require.Equal(t, 1, procMngr.Started[p.ID]) + require.Zero(t, procMngr.Stopped[p.ID]) + } } diff --git a/pkg/plugins/manager/manager.go b/pkg/plugins/manager/manager.go deleted file mode 100644 index 818d59d690f..00000000000 --- a/pkg/plugins/manager/manager.go +++ /dev/null @@ -1,298 +0,0 @@ -package manager - -import ( - "context" - "fmt" - "path/filepath" - - "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/plugins" - "github.com/grafana/grafana/pkg/plugins/config" - "github.com/grafana/grafana/pkg/plugins/logger" - "github.com/grafana/grafana/pkg/plugins/manager/loader" - "github.com/grafana/grafana/pkg/plugins/manager/process" - "github.com/grafana/grafana/pkg/plugins/manager/registry" - "github.com/grafana/grafana/pkg/plugins/repo" - "github.com/grafana/grafana/pkg/plugins/storage" - "github.com/grafana/grafana/pkg/setting" -) - -var _ plugins.Manager = (*PluginManager)(nil) -var _ plugins.RendererManager = (*PluginManager)(nil) -var _ plugins.SecretsPluginManager = (*PluginManager)(nil) - -type PluginManager struct { - cfg *config.Cfg - pluginSources []plugins.PluginSource - pluginRepo repo.Service - pluginStorage storage.Manager - processManager process.Service - pluginRegistry registry.Service - pluginLoader loader.Service - log log.Logger -} - -func ProvideService(cfg *config.Cfg, grafCfg *setting.Cfg, pluginRegistry registry.Service, pluginLoader loader.Service, - pluginRepo repo.Service) (*PluginManager, error) { - pm := New(cfg, pluginRegistry, - pluginSources(pathData{ - pluginsPath: grafCfg.PluginsPath, - bundledPluginsPath: grafCfg.BundledPluginsPath, - staticRootPath: grafCfg.StaticRootPath, - }, cfg.PluginSettings), - pluginLoader, pluginRepo, storage.FileSystem(logger.NewLogger("plugin.fs"), grafCfg.PluginsPath), - process.NewManager(pluginRegistry), - ) - if err := pm.Init(context.Background()); err != nil { - return nil, err - } - return pm, nil -} - -func New(cfg *config.Cfg, pluginRegistry registry.Service, pluginSources []plugins.PluginSource, - pluginLoader loader.Service, pluginRepo repo.Service, pluginStorage storage.Manager, - processManager process.Service) *PluginManager { - return &PluginManager{ - cfg: cfg, - pluginSources: pluginSources, - pluginRepo: pluginRepo, - pluginLoader: pluginLoader, - pluginRegistry: pluginRegistry, - processManager: processManager, - pluginStorage: pluginStorage, - log: log.New("plugin.manager"), - } -} - -func (m *PluginManager) Init(ctx context.Context) error { - for _, ps := range m.pluginSources { - if err := m.loadPlugins(ctx, ps.Class, ps.Paths...); err != nil { - return err - } - } - return nil -} - -func (m *PluginManager) Add(ctx context.Context, pluginID, version string, opts plugins.CompatOpts) error { - compatOpts := repo.NewCompatOpts(opts.GrafanaVersion, opts.OS, opts.Arch) - - var pluginArchive *repo.PluginArchive - if plugin, exists := m.plugin(ctx, pluginID); exists { - if !plugin.IsExternalPlugin() { - return plugins.ErrInstallCorePlugin - } - - if plugin.Info.Version == version { - return plugins.DuplicateError{ - PluginID: plugin.ID, - ExistingPluginDir: plugin.PluginDir, - } - } - - // get plugin update information to confirm if target update is possible - dlOpts, err := m.pluginRepo.GetPluginDownloadOptions(ctx, pluginID, version, compatOpts) - if err != nil { - return err - } - - // if existing plugin version is the same as the target update version - if dlOpts.Version == plugin.Info.Version { - return plugins.DuplicateError{ - PluginID: plugin.ID, - ExistingPluginDir: plugin.PluginDir, - } - } - - if dlOpts.PluginZipURL == "" && dlOpts.Version == "" { - return fmt.Errorf("could not determine update options for %s", pluginID) - } - - // remove existing installation of plugin - err = m.Remove(ctx, plugin.ID) - if err != nil { - return err - } - - if dlOpts.PluginZipURL != "" { - pluginArchive, err = m.pluginRepo.GetPluginArchiveByURL(ctx, dlOpts.PluginZipURL, compatOpts) - if err != nil { - return err - } - } else { - pluginArchive, err = m.pluginRepo.GetPluginArchive(ctx, pluginID, dlOpts.Version, compatOpts) - if err != nil { - return err - } - } - } else { - var err error - pluginArchive, err = m.pluginRepo.GetPluginArchive(ctx, pluginID, version, compatOpts) - if err != nil { - return err - } - } - - extractedArchive, err := m.pluginStorage.Add(ctx, pluginID, pluginArchive.File) - if err != nil { - return err - } - - // download dependency plugins - pathsToScan := []string{extractedArchive.Path} - for _, dep := range extractedArchive.Dependencies { - m.log.Info("Fetching %s dependencies...", dep.ID) - d, err := m.pluginRepo.GetPluginArchive(ctx, dep.ID, dep.Version, compatOpts) - if err != nil { - return fmt.Errorf("%v: %w", fmt.Sprintf("failed to download plugin %s from repository", dep.ID), err) - } - - depArchive, err := m.pluginStorage.Add(ctx, dep.ID, d.File) - if err != nil { - return err - } - - pathsToScan = append(pathsToScan, depArchive.Path) - } - - err = m.loadPlugins(context.Background(), plugins.External, pathsToScan...) - if err != nil { - m.log.Error("Could not load plugins", "paths", pathsToScan, "err", err) - return err - } - - return nil -} - -func (m *PluginManager) Remove(ctx context.Context, pluginID string) error { - plugin, exists := m.plugin(ctx, pluginID) - if !exists { - return plugins.ErrPluginNotInstalled - } - - if !plugin.IsExternalPlugin() { - return plugins.ErrUninstallCorePlugin - } - - if err := m.unregisterAndStop(ctx, plugin); err != nil { - return err - } - - return m.pluginStorage.Remove(ctx, plugin.ID) -} - -func (m *PluginManager) Renderer(ctx context.Context) *plugins.Plugin { - for _, p := range m.pluginRegistry.Plugins(ctx) { - if p.IsRenderer() && !p.IsDecommissioned() { - return p - } - } - return nil -} - -func (m *PluginManager) SecretsManager(ctx context.Context) *plugins.Plugin { - for _, p := range m.pluginRegistry.Plugins(ctx) { - if p.IsSecretsManager() && !p.IsDecommissioned() { - return p - } - } - return nil -} - -// plugin finds a plugin with `pluginID` from the registry that is not decommissioned -func (m *PluginManager) plugin(ctx context.Context, pluginID string) (*plugins.Plugin, bool) { - p, exists := m.pluginRegistry.Plugin(ctx, pluginID) - if !exists { - return nil, false - } - - if p.IsDecommissioned() { - return nil, false - } - - return p, true -} - -func (m *PluginManager) loadPlugins(ctx context.Context, class plugins.Class, pluginPaths ...string) error { - registeredPlugins := make(map[string]struct{}) - for _, p := range m.pluginRegistry.Plugins(ctx) { - registeredPlugins[p.ID] = struct{}{} - } - - loadedPlugins, err := m.pluginLoader.Load(ctx, class, pluginPaths, registeredPlugins) - if err != nil { - m.log.Error("Could not load plugins", "paths", pluginPaths, "err", err) - return err - } - - for _, p := range loadedPlugins { - if err = m.registerAndStart(context.Background(), p); err != nil { - m.log.Error("Could not start plugin", "pluginID", p.ID, "err", err) - } - } - - return nil -} - -func (m *PluginManager) registerAndStart(ctx context.Context, p *plugins.Plugin) error { - if err := m.pluginRegistry.Add(ctx, p); err != nil { - return err - } - - if !p.IsCorePlugin() { - m.log.Info("Plugin registered", "pluginID", p.ID) - } - - if p.IsExternalPlugin() { - if err := m.pluginStorage.Register(ctx, p.ID, p.PluginDir); err != nil { - return err - } - } - - return m.processManager.Start(ctx, p.ID) -} - -func (m *PluginManager) unregisterAndStop(ctx context.Context, p *plugins.Plugin) error { - m.log.Debug("Stopping plugin process", "pluginID", p.ID) - - if err := m.processManager.Stop(ctx, p.ID); err != nil { - return err - } - - if err := m.pluginRegistry.Remove(ctx, p.ID); err != nil { - return err - } - m.log.Debug("Plugin unregistered", "pluginID", p.ID) - return nil -} - -type pathData struct { - pluginsPath, bundledPluginsPath, staticRootPath string -} - -func pluginSources(p pathData, ps map[string]map[string]string) []plugins.PluginSource { - return []plugins.PluginSource{ - {Class: plugins.Core, Paths: corePluginPaths(p.staticRootPath)}, - {Class: plugins.Bundled, Paths: []string{p.bundledPluginsPath}}, - {Class: plugins.External, Paths: append([]string{p.pluginsPath}, pluginSettingPaths(ps)...)}, - } -} - -// corePluginPaths provides a list of the Core plugin paths which need to be scanned on init() -func corePluginPaths(staticRootPath string) []string { - datasourcePaths := filepath.Join(staticRootPath, "app/plugins/datasource") - panelsPath := filepath.Join(staticRootPath, "app/plugins/panel") - return []string{datasourcePaths, panelsPath} -} - -// pluginSettingPaths provides a plugin paths defined in cfg.PluginSettings which need to be scanned on init() -func pluginSettingPaths(ps map[string]map[string]string) []string { - var pluginSettingDirs []string - for _, s := range ps { - path, exists := s["path"] - if !exists || path == "" { - continue - } - pluginSettingDirs = append(pluginSettingDirs, path) - } - return pluginSettingDirs -} diff --git a/pkg/plugins/manager/manager_integration_test.go b/pkg/plugins/manager/manager_integration_test.go index c14ae9036f5..beffba0c4ba 100644 --- a/pkg/plugins/manager/manager_integration_test.go +++ b/pkg/plugins/manager/manager_integration_test.go @@ -49,7 +49,7 @@ import ( "github.com/grafana/grafana/pkg/tsdb/testdatasource" ) -func TestIntegrationPluginManager_Run(t *testing.T) { +func TestIntegrationPluginManager(t *testing.T) { t.Helper() staticRootPath, err := filepath.Abs("../../../public/") @@ -110,16 +110,15 @@ func TestIntegrationPluginManager_Run(t *testing.T) { pCfg := config.ProvideConfig(setting.ProvideProvider(cfg), cfg) reg := registry.ProvideService() - pm, err := ProvideService(pCfg, cfg, reg, loader.New(pCfg, license, signature.NewUnsignedAuthorizer(pCfg), - provider.ProvideService(coreRegistry)), nil) + l := loader.ProvideService(pCfg, license, signature.NewUnsignedAuthorizer(pCfg), reg, provider.ProvideService(coreRegistry)) + ps, err := store.ProvideService(cfg, pCfg, reg, l) require.NoError(t, err) - ps := store.ProvideService(reg) ctx := context.Background() verifyCorePluginCatalogue(t, ctx, ps) verifyBundledPlugins(t, ctx, ps) verifyPluginStaticRoutes(t, ctx, ps) - verifyBackendProcesses(t, pm.pluginRegistry.Plugins(ctx)) + verifyBackendProcesses(t, reg.Plugins(ctx)) verifyPluginQuery(t, ctx, client.ProvideService(reg)) } diff --git a/pkg/plugins/manager/store/store.go b/pkg/plugins/manager/store/store.go index 4fa6a3d927b..f253b5682e9 100644 --- a/pkg/plugins/manager/store/store.go +++ b/pkg/plugins/manager/store/store.go @@ -2,10 +2,14 @@ package store import ( "context" + "path/filepath" "sort" "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/plugins/config" + "github.com/grafana/grafana/pkg/plugins/manager/loader" "github.com/grafana/grafana/pkg/plugins/manager/registry" + "github.com/grafana/grafana/pkg/setting" ) var _ plugins.Store = (*Service)(nil) @@ -14,7 +18,17 @@ type Service struct { pluginRegistry registry.Service } -func ProvideService(pluginRegistry registry.Service) *Service { +func ProvideService(gCfg *setting.Cfg, cfg *config.Cfg, pluginRegistry registry.Service, + pluginLoader loader.Service) (*Service, error) { + for _, ps := range pluginSources(gCfg, cfg) { + if _, err := pluginLoader.Load(context.Background(), ps.Class, ps.Paths); err != nil { + return nil, err + } + } + return New(pluginRegistry), nil +} + +func New(pluginRegistry registry.Service) *Service { return &Service{ pluginRegistry: pluginRegistry, } @@ -49,6 +63,24 @@ func (s *Service) Plugins(ctx context.Context, pluginTypes ...plugins.Type) []pl return pluginsList } +func (s *Service) Renderer(ctx context.Context) *plugins.Plugin { + for _, p := range s.availablePlugins(ctx) { + if p.IsRenderer() { + return p + } + } + return nil +} + +func (s *Service) SecretsManager(ctx context.Context) *plugins.Plugin { + for _, p := range s.availablePlugins(ctx) { + if p.IsSecretsManager() { + return p + } + } + return nil +} + // plugin finds a plugin with `pluginID` from the registry that is not decommissioned func (s *Service) plugin(ctx context.Context, pluginID string) (*plugins.Plugin, bool) { p, exists := s.pluginRegistry.Plugin(ctx, pluginID) @@ -87,3 +119,31 @@ func (s *Service) Routes() []*plugins.StaticRoute { } return staticRoutes } + +func pluginSources(gCfg *setting.Cfg, cfg *config.Cfg) []plugins.PluginSource { + return []plugins.PluginSource{ + {Class: plugins.Core, Paths: corePluginPaths(gCfg.StaticRootPath)}, + {Class: plugins.Bundled, Paths: []string{gCfg.BundledPluginsPath}}, + {Class: plugins.External, Paths: append([]string{cfg.PluginsPath}, pluginSettingPaths(cfg.PluginSettings)...)}, + } +} + +// corePluginPaths provides a list of the Core plugin paths which need to be scanned on init() +func corePluginPaths(staticRootPath string) []string { + datasourcePaths := filepath.Join(staticRootPath, "app/plugins/datasource") + panelsPath := filepath.Join(staticRootPath, "app/plugins/panel") + return []string{datasourcePaths, panelsPath} +} + +// pluginSettingPaths provides a plugin paths defined in cfg.PluginSettings which need to be scanned on init() +func pluginSettingPaths(ps map[string]map[string]string) []string { + var pluginSettingDirs []string + for _, s := range ps { + path, exists := s["path"] + if !exists || path == "" { + continue + } + pluginSettingDirs = append(pluginSettingDirs, path) + } + return pluginSettingDirs +} diff --git a/pkg/plugins/manager/store/store_test.go b/pkg/plugins/manager/store/store_test.go index da88974d607..a0c2a36f1d8 100644 --- a/pkg/plugins/manager/store/store_test.go +++ b/pkg/plugins/manager/store/store_test.go @@ -8,20 +8,48 @@ import ( "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/backendplugin" + "github.com/grafana/grafana/pkg/plugins/config" + "github.com/grafana/grafana/pkg/plugins/manager/fakes" + "github.com/grafana/grafana/pkg/setting" ) +func TestStore_ProvideService(t *testing.T) { + t.Run("Plugin sources are added in order", func(t *testing.T) { + var addedPaths []string + l := &fakes.FakeLoader{ + LoadFunc: func(ctx context.Context, class plugins.Class, paths []string) ([]*plugins.Plugin, error) { + addedPaths = append(addedPaths, paths...) + return nil, nil + }, + } + cfg := &setting.Cfg{ + BundledPluginsPath: "path1", + } + pCfg := &config.Cfg{ + PluginsPath: "path2", + PluginSettings: setting.PluginSettings{ + "blah": map[string]string{ + "path": "path3", + }, + }, + } + + _, err := ProvideService(cfg, pCfg, fakes.NewFakePluginRegistry(), l) + require.NoError(t, err) + require.Equal(t, []string{"app/plugins/datasource", "app/plugins/panel", "path1", "path2", "path3"}, addedPaths) + }) +} + func TestStore_Plugin(t *testing.T) { t.Run("Plugin returns all non-decommissioned plugins", func(t *testing.T) { p1 := &plugins.Plugin{JSONData: plugins.JSONData{ID: "test-datasource"}} p1.RegisterClient(&DecommissionedPlugin{}) p2 := &plugins.Plugin{JSONData: plugins.JSONData{ID: "test-panel"}} - ps := ProvideService( - newFakePluginRegistry(map[string]*plugins.Plugin{ - p1.ID: p1, - p2.ID: p2, - }), - ) + ps := New(newFakePluginRegistry(map[string]*plugins.Plugin{ + p1.ID: p1, + p2.ID: p2, + })) p, exists := ps.Plugin(context.Background(), p1.ID) require.False(t, exists) @@ -42,15 +70,13 @@ func TestStore_Plugins(t *testing.T) { p5 := &plugins.Plugin{JSONData: plugins.JSONData{ID: "e-test-panel", Type: plugins.Panel}} p5.RegisterClient(&DecommissionedPlugin{}) - ps := ProvideService( - newFakePluginRegistry(map[string]*plugins.Plugin{ - p1.ID: p1, - p2.ID: p2, - p3.ID: p3, - p4.ID: p4, - p5.ID: p5, - }), - ) + ps := New(newFakePluginRegistry(map[string]*plugins.Plugin{ + p1.ID: p1, + p2.ID: p2, + p3.ID: p3, + p4.ID: p4, + p5.ID: p5, + })) pss := ps.Plugins(context.Background()) require.Equal(t, pss, []plugins.PluginDTO{p1.ToDTO(), p2.ToDTO(), p3.ToDTO(), p4.ToDTO()}) @@ -79,16 +105,14 @@ func TestStore_Routes(t *testing.T) { p6 := &plugins.Plugin{JSONData: plugins.JSONData{ID: "f-test-app", Type: plugins.App}} p6.RegisterClient(&DecommissionedPlugin{}) - ps := ProvideService( - newFakePluginRegistry(map[string]*plugins.Plugin{ - p1.ID: p1, - p2.ID: p2, - p3.ID: p3, - p4.ID: p4, - p5.ID: p5, - p6.ID: p6, - }), - ) + ps := New(newFakePluginRegistry(map[string]*plugins.Plugin{ + p1.ID: p1, + p2.ID: p2, + p3.ID: p3, + p4.ID: p4, + p5.ID: p5, + p6.ID: p6, + })) sr := func(p *plugins.Plugin) *plugins.StaticRoute { return &plugins.StaticRoute{PluginID: p.ID, Directory: p.PluginDir} @@ -99,13 +123,49 @@ func TestStore_Routes(t *testing.T) { }) } +func TestStore_Renderer(t *testing.T) { + t.Run("Renderer returns a single (non-decommissioned) renderer plugin", func(t *testing.T) { + p1 := &plugins.Plugin{JSONData: plugins.JSONData{ID: "test-renderer", Type: plugins.Renderer}} + p2 := &plugins.Plugin{JSONData: plugins.JSONData{ID: "test-panel", Type: plugins.Panel}} + p3 := &plugins.Plugin{JSONData: plugins.JSONData{ID: "test-app", Type: plugins.App}} + + ps := New(newFakePluginRegistry(map[string]*plugins.Plugin{ + p1.ID: p1, + p2.ID: p2, + p3.ID: p3, + })) + + r := ps.Renderer(context.Background()) + require.Equal(t, p1, r) + }) +} + +func TestStore_SecretsManager(t *testing.T) { + t.Run("Renderer returns a single (non-decommissioned) secrets manager plugin", func(t *testing.T) { + p1 := &plugins.Plugin{JSONData: plugins.JSONData{ID: "test-renderer", Type: plugins.Renderer}} + p2 := &plugins.Plugin{JSONData: plugins.JSONData{ID: "test-panel", Type: plugins.Panel}} + p3 := &plugins.Plugin{JSONData: plugins.JSONData{ID: "test-secrets", Type: plugins.SecretsManager}} + p4 := &plugins.Plugin{JSONData: plugins.JSONData{ID: "test-datasource", Type: plugins.DataSource}} + + ps := New(newFakePluginRegistry(map[string]*plugins.Plugin{ + p1.ID: p1, + p2.ID: p2, + p3.ID: p3, + p4.ID: p4, + })) + + r := ps.SecretsManager(context.Background()) + require.Equal(t, p3, r) + }) +} + func TestStore_availablePlugins(t *testing.T) { t.Run("Decommissioned plugins are excluded from availablePlugins", func(t *testing.T) { p1 := &plugins.Plugin{JSONData: plugins.JSONData{ID: "test-datasource"}} p1.RegisterClient(&DecommissionedPlugin{}) p2 := &plugins.Plugin{JSONData: plugins.JSONData{ID: "test-app"}} - ps := ProvideService( + ps := New( newFakePluginRegistry(map[string]*plugins.Plugin{ p1.ID: p1, p2.ID: p2, diff --git a/pkg/plugins/plugins.go b/pkg/plugins/plugins.go index ef70a7e03e8..17e6dc6a073 100644 --- a/pkg/plugins/plugins.go +++ b/pkg/plugins/plugins.go @@ -81,6 +81,10 @@ func (p PluginDTO) IsCorePlugin() bool { return p.Class == Core } +func (p PluginDTO) IsExternalPlugin() bool { + return p.Class == External +} + func (p PluginDTO) IsSecretsManager() bool { return p.JSONData.Type == SecretsManager } diff --git a/pkg/plugins/storage/fs.go b/pkg/plugins/storage/fs.go index 43675ca086e..0660e648159 100644 --- a/pkg/plugins/storage/fs.go +++ b/pkg/plugins/storage/fs.go @@ -17,6 +17,8 @@ import ( "github.com/grafana/grafana/pkg/plugins/logger" ) +var _ Manager = (*FS)(nil) + var reGitBuild = regexp.MustCompile("^[a-zA-Z0-9_.-]*/") var ( diff --git a/pkg/server/wire.go b/pkg/server/wire.go index 28240012c10..5811dee4419 100644 --- a/pkg/server/wire.go +++ b/pkg/server/wire.go @@ -187,14 +187,14 @@ var wireBasicSet = wire.NewSet( pluginsCfg.ProvideConfig, repo.ProvideService, wire.Bind(new(repo.Service), new(*repo.Manager)), - manager.ProvideService, - wire.Bind(new(plugins.Manager), new(*manager.PluginManager)), - wire.Bind(new(plugins.RendererManager), new(*manager.PluginManager)), - wire.Bind(new(plugins.SecretsPluginManager), new(*manager.PluginManager)), + manager.ProvideInstaller, + wire.Bind(new(plugins.Installer), new(*manager.PluginInstaller)), client.ProvideService, wire.Bind(new(plugins.Client), new(*client.Service)), managerStore.ProvideService, wire.Bind(new(plugins.Store), new(*managerStore.Service)), + wire.Bind(new(plugins.RendererManager), new(*managerStore.Service)), + wire.Bind(new(plugins.SecretsPluginManager), new(*managerStore.Service)), wire.Bind(new(plugins.StaticRouteResolver), new(*managerStore.Service)), pluginDashboards.ProvideFileStoreManager, wire.Bind(new(pluginDashboards.FileStore), new(*pluginDashboards.FileStoreManager)),