mirror of
https://github.com/grafana/grafana.git
synced 2025-07-29 15:12:46 +08:00
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
This commit is contained in:
@ -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)
|
||||
|
||||
LoadedPaths []string
|
||||
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
|
||||
}
|
||||
|
||||
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) Add(ctx context.Context, pluginID, version string, opts plugins.CompatOpts) error {
|
||||
if i.AddFunc != nil {
|
||||
return i.AddFunc(ctx, pluginID, version, opts)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
l.LoadedPaths = append(l.LoadedPaths, paths...)
|
||||
func (i *FakePluginInstaller) Remove(ctx context.Context, pluginID string) error {
|
||||
if i.RemoveFunc != nil {
|
||||
return i.RemoveFunc(ctx, pluginID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
157
pkg/plugins/manager/installer.go
Normal file
157
pkg/plugins/manager/installer.go
Normal file
@ -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)
|
||||
inst.pluginRegistry = &fakes.FakePluginRegistry{
|
||||
Store: map[string]*plugins.Plugin{
|
||||
pluginID: pluginV1,
|
||||
},
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
require.Equal(t, 2, proc.Stopped[pluginID])
|
||||
require.Equal(t, 2, fs.Removed[pluginID])
|
||||
|
||||
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"
|
||||
@ -43,7 +39,6 @@ func TestLoader_Load(t *testing.T) {
|
||||
class plugins.Class
|
||||
cfg *config.Cfg
|
||||
pluginPaths []string
|
||||
existingPlugins map[string]struct{}
|
||||
want []*plugins.Plugin
|
||||
pluginErrors map[string]*plugins.Error
|
||||
}{
|
||||
@ -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))
|
||||
}
|
||||
})
|
||||
|
||||
verifyState(t, expected, reg, procPrvdr, storage, procMgr)
|
||||
|
||||
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}
|
||||
|
||||
l := newLoader(&config.Cfg{})
|
||||
|
||||
got, err := l.Load(context.Background(), plugins.External, []string{"../testdata/nested-plugins"}, map[string]struct{}{
|
||||
"test-panel": {},
|
||||
})
|
||||
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
|
||||
})
|
||||
|
||||
if !cmp.Equal(got, expected, compareOpts) {
|
||||
if !cmp.Equal(got, []*plugins.Plugin{}, 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{},
|
||||
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())
|
||||
|
||||
for _, cb := range cbs {
|
||||
cb(l)
|
||||
}
|
||||
|
||||
return l
|
||||
}
|
||||
|
||||
func verifyState(t *testing.T, ps []*plugins.Plugin, reg *fakes.FakePluginRegistry,
|
||||
procPrvdr *fakes.FakeBackendProcessProvider, storage *fakes.FakePluginStorage, procMngr *fakes.FakeProcessManager) {
|
||||
t.Helper()
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
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])
|
||||
}
|
||||
|
||||
_, exists := storage.Store[p.ID]
|
||||
if p.IsExternalPlugin() {
|
||||
require.True(t, exists)
|
||||
} else {
|
||||
require.False(t, exists)
|
||||
}
|
||||
|
||||
require.Equal(t, 1, procMngr.Started[p.ID])
|
||||
require.Zero(t, procMngr.Stopped[p.ID])
|
||||
}
|
||||
}
|
||||
|
||||
type fakeLicensingService struct {
|
||||
edition string
|
||||
tokenRaw string
|
||||
}
|
||||
|
||||
func (t *fakeLicensingService) Expiry() int64 {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (t *fakeLicensingService) Edition() string {
|
||||
return t.edition
|
||||
}
|
||||
|
||||
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(feature string) bool {
|
||||
return false
|
||||
}
|
||||
|
@ -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{
|
||||
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{
|
||||
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{
|
||||
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)),
|
||||
|
Reference in New Issue
Block a user