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
pull/55677/head
Will Browne 3 years ago committed by GitHub
parent 003a1cdaa0
commit d0d8544ded
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 12
      pkg/api/fakes.go
  2. 6
      pkg/api/http_server.go
  3. 4
      pkg/api/plugins.go
  4. 16
      pkg/api/plugins_test.go
  5. 8
      pkg/cmd/grafana-cli/runner/wire.go
  6. 3
      pkg/plugins/config/config.go
  7. 6
      pkg/plugins/ifaces.go
  8. 2
      pkg/plugins/logger/logger.go
  9. 135
      pkg/plugins/manager/fakes/fakes.go
  10. 157
      pkg/plugins/manager/installer.go
  11. 161
      pkg/plugins/manager/installer_test.go
  12. 4
      pkg/plugins/manager/loader/ifaces.go
  13. 103
      pkg/plugins/manager/loader/loader.go
  14. 265
      pkg/plugins/manager/loader/loader_test.go
  15. 298
      pkg/plugins/manager/manager.go
  16. 9
      pkg/plugins/manager/manager_integration_test.go
  17. 62
      pkg/plugins/manager/store/store.go
  18. 112
      pkg/plugins/manager/store/store_test.go
  19. 4
      pkg/plugins/plugins.go
  20. 2
      pkg/plugins/storage/fs.go
  21. 8
      pkg/server/wire.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
}

@ -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,

@ -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)

@ -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\" }")

@ -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)),

@ -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),

@ -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
}

@ -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),
}

@ -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
}

@ -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
}

@ -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()

@ -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
}

@ -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")
}

@ -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])
}
}

@ -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
}

@ -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))
}

@ -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
}

@ -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,

@ -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
}

@ -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 (

@ -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)),

Loading…
Cancel
Save