mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Plugins: Use suffix for plugin directory (#71375)
* plugin dir suffix * fix whitespace * fix cli * fix tests * fixup * simplify * undo uninstall changes
This commit is contained in:
@@ -70,7 +70,7 @@ func TestCallResource(t *testing.T) {
|
|||||||
angularInspector, err := angularinspector.NewStaticInspector()
|
angularInspector, err := angularinspector.NewStaticInspector()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
l := loader.ProvideService(pCfg, fakes.NewFakeLicensingService(), signature.NewUnsignedAuthorizer(pCfg),
|
l := loader.ProvideService(pCfg, fakes.NewFakeLicensingService(), signature.NewUnsignedAuthorizer(pCfg),
|
||||||
reg, provider.ProvideService(coreRegistry), finder.NewLocalFinder(pCfg), fakes.NewFakeRoleRegistry(),
|
reg, provider.ProvideService(coreRegistry), finder.NewLocalFinder(pCfg.DevMode), fakes.NewFakeRoleRegistry(),
|
||||||
assetpath.ProvideService(pluginscdn.ProvideService(pCfg)), signature.ProvideService(statickey.New()),
|
assetpath.ProvideService(pluginscdn.ProvideService(pCfg)), signature.ProvideService(statickey.New()),
|
||||||
angularInspector, &fakes.FakeOauthService{})
|
angularInspector, &fakes.FakeOauthService{})
|
||||||
srcs := sources.ProvideService(cfg, pCfg)
|
srcs := sources.ProvideService(cfg, pCfg)
|
||||||
|
|||||||
@@ -5,11 +5,11 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/fatih/color"
|
"github.com/fatih/color"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/cmd/grafana-cli/logger"
|
"github.com/grafana/grafana/pkg/cmd/grafana-cli/logger"
|
||||||
"github.com/grafana/grafana/pkg/cmd/grafana-cli/models"
|
"github.com/grafana/grafana/pkg/cmd/grafana-cli/models"
|
||||||
"github.com/grafana/grafana/pkg/cmd/grafana-cli/services"
|
"github.com/grafana/grafana/pkg/cmd/grafana-cli/services"
|
||||||
@@ -73,6 +73,14 @@ func installCommand(c utils.CommandLine) error {
|
|||||||
// installPlugin downloads the plugin code as a zip file from the Grafana.com API
|
// installPlugin downloads the plugin code as a zip file from the Grafana.com API
|
||||||
// and then extracts the zip into the plugin's directory.
|
// and then extracts the zip into the plugin's directory.
|
||||||
func installPlugin(ctx context.Context, pluginID, version string, c utils.CommandLine) error {
|
func installPlugin(ctx context.Context, pluginID, version string, c utils.CommandLine) error {
|
||||||
|
// If a version is specified, check if it is already installed
|
||||||
|
if version != "" {
|
||||||
|
if services.PluginVersionInstalled(pluginID, version, c.PluginDirectory()) {
|
||||||
|
services.Logger.Successf("Plugin %s v%s already installed.", pluginID, version)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
repository := repo.NewManager(repo.ManagerCfg{
|
repository := repo.NewManager(repo.ManagerCfg{
|
||||||
SkipTLSVerify: c.Bool("insecure"),
|
SkipTLSVerify: c.Bool("insecure"),
|
||||||
BaseURL: c.PluginRepoURL(),
|
BaseURL: c.PluginRepoURL(),
|
||||||
@@ -95,7 +103,7 @@ func installPlugin(ctx context.Context, pluginID, version string, c utils.Comman
|
|||||||
}
|
}
|
||||||
|
|
||||||
pluginFs := storage.FileSystem(services.Logger, c.PluginDirectory())
|
pluginFs := storage.FileSystem(services.Logger, c.PluginDirectory())
|
||||||
extractedArchive, err := pluginFs.Extract(ctx, pluginID, archive.File)
|
extractedArchive, err := pluginFs.Extract(ctx, pluginID, storage.SimpleDirNameGeneratorFunc, archive.File)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -107,7 +115,7 @@ func installPlugin(ctx context.Context, pluginID, version string, c utils.Comman
|
|||||||
return fmt.Errorf("%v: %w", fmt.Sprintf("failed to download plugin %s from repository", dep.ID), err)
|
return fmt.Errorf("%v: %w", fmt.Sprintf("failed to download plugin %s from repository", dep.ID), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = pluginFs.Extract(ctx, dep.ID, d.File)
|
_, err = pluginFs.Extract(ctx, dep.ID, storage.SimpleDirNameGeneratorFunc, d.File)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -117,16 +125,21 @@ func installPlugin(ctx context.Context, pluginID, version string, c utils.Comman
|
|||||||
|
|
||||||
// uninstallPlugin removes the plugin directory
|
// uninstallPlugin removes the plugin directory
|
||||||
func uninstallPlugin(_ context.Context, pluginID string, c utils.CommandLine) error {
|
func uninstallPlugin(_ context.Context, pluginID string, c utils.CommandLine) error {
|
||||||
logger.Infof("Removing plugin: %v\n", pluginID)
|
for _, bundle := range services.GetLocalPlugins(c.PluginDirectory()) {
|
||||||
|
if bundle.Primary.JSONData.ID == pluginID {
|
||||||
pluginPath := filepath.Join(c.PluginDirectory(), pluginID)
|
logger.Infof("Removing plugin: %v\n", pluginID)
|
||||||
fs := plugins.NewLocalFS(pluginPath)
|
if remover, ok := bundle.Primary.FS.(plugins.FSRemover); ok {
|
||||||
|
logger.Debugf("Removing directory %v\n\n", bundle.Primary.FS.Base())
|
||||||
logger.Debugf("Removing directory %v\n", pluginPath)
|
if err := remover.Remove(); err != nil {
|
||||||
err := fs.Remove()
|
return err
|
||||||
if err != nil {
|
}
|
||||||
return err
|
return nil
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("plugin %v is immutable and therefore cannot be uninstalled", pluginID)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/cmd/grafana-cli/logger"
|
"github.com/grafana/grafana/pkg/cmd/grafana-cli/logger"
|
||||||
"github.com/grafana/grafana/pkg/cmd/grafana-cli/models"
|
"github.com/grafana/grafana/pkg/cmd/grafana-cli/models"
|
||||||
"github.com/grafana/grafana/pkg/plugins"
|
"github.com/grafana/grafana/pkg/plugins"
|
||||||
"github.com/grafana/grafana/pkg/plugins/config"
|
|
||||||
"github.com/grafana/grafana/pkg/plugins/manager/loader/finder"
|
"github.com/grafana/grafana/pkg/plugins/manager/loader/finder"
|
||||||
"github.com/grafana/grafana/pkg/plugins/manager/sources"
|
"github.com/grafana/grafana/pkg/plugins/manager/sources"
|
||||||
)
|
)
|
||||||
@@ -78,7 +77,7 @@ func GetLocalPlugin(pluginDir, pluginID string) (plugins.FoundPlugin, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func GetLocalPlugins(pluginDir string) []*plugins.FoundBundle {
|
func GetLocalPlugins(pluginDir string) []*plugins.FoundBundle {
|
||||||
f := finder.NewLocalFinder(&config.Cfg{})
|
f := finder.NewLocalFinder(true)
|
||||||
|
|
||||||
res, err := f.Find(context.Background(), sources.NewLocalSource(plugins.ClassExternal, []string{pluginDir}))
|
res, err := f.Find(context.Background(), sources.NewLocalSource(plugins.ClassExternal, []string{pluginDir}))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -88,3 +87,15 @@ func GetLocalPlugins(pluginDir string) []*plugins.FoundBundle {
|
|||||||
|
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func PluginVersionInstalled(pluginID, version, pluginDir string) bool {
|
||||||
|
for _, bundle := range GetLocalPlugins(pluginDir) {
|
||||||
|
pJSON := bundle.Primary.JSONData
|
||||||
|
if pJSON.ID == pluginID {
|
||||||
|
if pJSON.Info.Version == version {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|||||||
@@ -233,16 +233,16 @@ func (r *FakePluginRepo) GetPluginArchiveInfo(ctx context.Context, pluginID, ver
|
|||||||
}
|
}
|
||||||
|
|
||||||
type FakePluginStorage struct {
|
type FakePluginStorage struct {
|
||||||
ExtractFunc func(_ context.Context, pluginID string, z *zip.ReadCloser) (*storage.ExtractedPluginArchive, error)
|
ExtractFunc func(_ context.Context, pluginID string, dirNameFunc storage.DirNameGeneratorFunc, z *zip.ReadCloser) (*storage.ExtractedPluginArchive, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewFakePluginStorage() *FakePluginStorage {
|
func NewFakePluginStorage() *FakePluginStorage {
|
||||||
return &FakePluginStorage{}
|
return &FakePluginStorage{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *FakePluginStorage) Extract(ctx context.Context, pluginID string, z *zip.ReadCloser) (*storage.ExtractedPluginArchive, error) {
|
func (s *FakePluginStorage) Extract(ctx context.Context, pluginID string, dirNameFunc storage.DirNameGeneratorFunc, z *zip.ReadCloser) (*storage.ExtractedPluginArchive, error) {
|
||||||
if s.ExtractFunc != nil {
|
if s.ExtractFunc != nil {
|
||||||
return s.ExtractFunc(ctx, pluginID, z)
|
return s.ExtractFunc(ctx, pluginID, dirNameFunc, z)
|
||||||
}
|
}
|
||||||
return &storage.ExtractedPluginArchive{}, nil
|
return &storage.ExtractedPluginArchive{}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,27 +18,29 @@ import (
|
|||||||
var _ plugins.Installer = (*PluginInstaller)(nil)
|
var _ plugins.Installer = (*PluginInstaller)(nil)
|
||||||
|
|
||||||
type PluginInstaller struct {
|
type PluginInstaller struct {
|
||||||
pluginRepo repo.Service
|
pluginRepo repo.Service
|
||||||
pluginStorage storage.ZipExtractor
|
pluginStorage storage.ZipExtractor
|
||||||
pluginRegistry registry.Service
|
pluginStorageDirFunc storage.DirNameGeneratorFunc
|
||||||
pluginLoader loader.Service
|
pluginRegistry registry.Service
|
||||||
log log.Logger
|
pluginLoader loader.Service
|
||||||
|
log log.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
func ProvideInstaller(cfg *config.Cfg, pluginRegistry registry.Service, pluginLoader loader.Service,
|
func ProvideInstaller(cfg *config.Cfg, pluginRegistry registry.Service, pluginLoader loader.Service,
|
||||||
pluginRepo repo.Service) *PluginInstaller {
|
pluginRepo repo.Service) *PluginInstaller {
|
||||||
return New(pluginRegistry, pluginLoader, pluginRepo,
|
return New(pluginRegistry, pluginLoader, pluginRepo,
|
||||||
storage.FileSystem(log.NewPrettyLogger("installer.fs"), cfg.PluginsPath))
|
storage.FileSystem(log.NewPrettyLogger("installer.fs"), cfg.PluginsPath), storage.SimpleDirNameGeneratorFunc)
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(pluginRegistry registry.Service, pluginLoader loader.Service, pluginRepo repo.Service,
|
func New(pluginRegistry registry.Service, pluginLoader loader.Service, pluginRepo repo.Service,
|
||||||
pluginStorage storage.ZipExtractor) *PluginInstaller {
|
pluginStorage storage.ZipExtractor, pluginStorageDirFunc storage.DirNameGeneratorFunc) *PluginInstaller {
|
||||||
return &PluginInstaller{
|
return &PluginInstaller{
|
||||||
pluginLoader: pluginLoader,
|
pluginLoader: pluginLoader,
|
||||||
pluginRegistry: pluginRegistry,
|
pluginRegistry: pluginRegistry,
|
||||||
pluginRepo: pluginRepo,
|
pluginRepo: pluginRepo,
|
||||||
pluginStorage: pluginStorage,
|
pluginStorage: pluginStorage,
|
||||||
log: log.New("plugin.installer"),
|
pluginStorageDirFunc: pluginStorageDirFunc,
|
||||||
|
log: log.New("plugin.installer"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,7 +104,7 @@ func (m *PluginInstaller) Add(ctx context.Context, pluginID, version string, opt
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extractedArchive, err := m.pluginStorage.Extract(ctx, pluginID, pluginArchive.File)
|
extractedArchive, err := m.pluginStorage.Extract(ctx, pluginID, m.pluginStorageDirFunc, pluginArchive.File)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -116,7 +118,7 @@ func (m *PluginInstaller) Add(ctx context.Context, pluginID, version string, opt
|
|||||||
return fmt.Errorf("%v: %w", fmt.Sprintf("failed to download plugin %s from repository", dep.ID), err)
|
return fmt.Errorf("%v: %w", fmt.Sprintf("failed to download plugin %s from repository", dep.ID), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
depArchive, err := m.pluginStorage.Extract(ctx, dep.ID, d.File)
|
depArchive, err := m.pluginStorage.Extract(ctx, dep.ID, m.pluginStorageDirFunc, d.File)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ func TestPluginManager_Add_Remove(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fs := &fakes.FakePluginStorage{
|
fs := &fakes.FakePluginStorage{
|
||||||
ExtractFunc: func(_ context.Context, id string, z *zip.ReadCloser) (*storage.ExtractedPluginArchive, error) {
|
ExtractFunc: func(_ context.Context, id string, _ storage.DirNameGeneratorFunc, z *zip.ReadCloser) (*storage.ExtractedPluginArchive, error) {
|
||||||
require.Equal(t, pluginID, id)
|
require.Equal(t, pluginID, id)
|
||||||
require.Equal(t, mockZipV1, z)
|
require.Equal(t, mockZipV1, z)
|
||||||
return &storage.ExtractedPluginArchive{
|
return &storage.ExtractedPluginArchive{
|
||||||
@@ -62,7 +62,7 @@ func TestPluginManager_Add_Remove(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
inst := New(fakes.NewFakePluginRegistry(), loader, pluginRepo, fs)
|
inst := New(fakes.NewFakePluginRegistry(), loader, pluginRepo, fs, storage.SimpleDirNameGeneratorFunc)
|
||||||
err := inst.Add(context.Background(), pluginID, v1, testCompatOpts())
|
err := inst.Add(context.Background(), pluginID, v1, testCompatOpts())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
@@ -108,7 +108,7 @@ func TestPluginManager_Add_Remove(t *testing.T) {
|
|||||||
File: mockZipV2,
|
File: mockZipV2,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
fs.ExtractFunc = func(_ context.Context, pluginID string, z *zip.ReadCloser) (*storage.ExtractedPluginArchive, error) {
|
fs.ExtractFunc = func(_ context.Context, pluginID string, _ storage.DirNameGeneratorFunc, z *zip.ReadCloser) (*storage.ExtractedPluginArchive, error) {
|
||||||
require.Equal(t, pluginV1.ID, pluginID)
|
require.Equal(t, pluginV1.ID, pluginID)
|
||||||
require.Equal(t, mockZipV2, z)
|
require.Equal(t, mockZipV2, z)
|
||||||
return &storage.ExtractedPluginArchive{
|
return &storage.ExtractedPluginArchive{
|
||||||
@@ -168,7 +168,7 @@ func TestPluginManager_Add_Remove(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
pm := New(reg, &fakes.FakeLoader{}, &fakes.FakePluginRepo{}, &fakes.FakePluginStorage{})
|
pm := New(reg, &fakes.FakeLoader{}, &fakes.FakePluginRepo{}, &fakes.FakePluginStorage{}, storage.SimpleDirNameGeneratorFunc)
|
||||||
err := pm.Add(context.Background(), p.ID, "3.2.0", testCompatOpts())
|
err := pm.Add(context.Background(), p.ID, "3.2.0", testCompatOpts())
|
||||||
require.ErrorIs(t, err, plugins.ErrInstallCorePlugin)
|
require.ErrorIs(t, err, plugins.ErrInstallCorePlugin)
|
||||||
|
|
||||||
|
|||||||
@@ -28,15 +28,15 @@ type Local struct {
|
|||||||
production bool
|
production bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewLocalFinder(cfg *config.Cfg) *Local {
|
func NewLocalFinder(devMode bool) *Local {
|
||||||
return &Local{
|
return &Local{
|
||||||
production: !cfg.DevMode,
|
production: !devMode,
|
||||||
log: log.New("local.finder"),
|
log: log.New("local.finder"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func ProvideLocalFinder(cfg *config.Cfg) *Local {
|
func ProvideLocalFinder(cfg *config.Cfg) *Local {
|
||||||
return NewLocalFinder(cfg)
|
return NewLocalFinder(cfg.DevMode)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *Local) Find(ctx context.Context, src plugins.PluginSource) ([]*plugins.FoundBundle, error) {
|
func (l *Local) Find(ctx context.Context, src plugins.PluginSource) ([]*plugins.FoundBundle, error) {
|
||||||
|
|||||||
@@ -255,7 +255,7 @@ func TestFinder_Find(t *testing.T) {
|
|||||||
}
|
}
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
f := NewLocalFinder(pCfg)
|
f := NewLocalFinder(pCfg.DevMode)
|
||||||
pluginBundles, err := f.Find(context.Background(), &fakes.FakePluginSource{
|
pluginBundles, err := f.Find(context.Background(), &fakes.FakePluginSource{
|
||||||
PluginURIsFunc: func(ctx context.Context) []string {
|
PluginURIsFunc: func(ctx context.Context) []string {
|
||||||
return tc.pluginDirs
|
return tc.pluginDirs
|
||||||
@@ -292,7 +292,7 @@ func TestFinder_getAbsPluginJSONPaths(t *testing.T) {
|
|||||||
walk = origWalk
|
walk = origWalk
|
||||||
})
|
})
|
||||||
|
|
||||||
finder := NewLocalFinder(pCfg)
|
finder := NewLocalFinder(pCfg.DevMode)
|
||||||
paths, err := finder.getAbsPluginJSONPaths("test")
|
paths, err := finder.getAbsPluginJSONPaths("test")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Empty(t, paths)
|
require.Empty(t, paths)
|
||||||
@@ -307,7 +307,7 @@ func TestFinder_getAbsPluginJSONPaths(t *testing.T) {
|
|||||||
walk = origWalk
|
walk = origWalk
|
||||||
})
|
})
|
||||||
|
|
||||||
finder := NewLocalFinder(pCfg)
|
finder := NewLocalFinder(pCfg.DevMode)
|
||||||
paths, err := finder.getAbsPluginJSONPaths("test")
|
paths, err := finder.getAbsPluginJSONPaths("test")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Empty(t, paths)
|
require.Empty(t, paths)
|
||||||
@@ -322,7 +322,7 @@ func TestFinder_getAbsPluginJSONPaths(t *testing.T) {
|
|||||||
walk = origWalk
|
walk = origWalk
|
||||||
})
|
})
|
||||||
|
|
||||||
finder := NewLocalFinder(pCfg)
|
finder := NewLocalFinder(pCfg.DevMode)
|
||||||
paths, err := finder.getAbsPluginJSONPaths("test")
|
paths, err := finder.getAbsPluginJSONPaths("test")
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
require.Empty(t, paths)
|
require.Empty(t, paths)
|
||||||
|
|||||||
@@ -1494,7 +1494,7 @@ func newLoader(t *testing.T, cfg *config.Cfg, cbs ...func(loader *Loader)) *Load
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
l := New(cfg, &fakes.FakeLicensingService{}, signature.NewUnsignedAuthorizer(cfg), fakes.NewFakePluginRegistry(),
|
l := New(cfg, &fakes.FakeLicensingService{}, signature.NewUnsignedAuthorizer(cfg), fakes.NewFakePluginRegistry(),
|
||||||
fakes.NewFakeBackendProcessProvider(), fakes.NewFakeProcessManager(), fakes.NewFakeRoleRegistry(),
|
fakes.NewFakeBackendProcessProvider(), fakes.NewFakeProcessManager(), fakes.NewFakeRoleRegistry(),
|
||||||
assetpath.ProvideService(pluginscdn.ProvideService(cfg)), finder.NewLocalFinder(cfg),
|
assetpath.ProvideService(pluginscdn.ProvideService(cfg)), finder.NewLocalFinder(cfg.DevMode),
|
||||||
signature.ProvideService(statickey.New()), angularInspector, &fakes.FakeOauthService{})
|
signature.ProvideService(statickey.New()), angularInspector, &fakes.FakeOauthService{})
|
||||||
|
|
||||||
for _, cb := range cbs {
|
for _, cb := range cbs {
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ func TestIntegrationPluginManager(t *testing.T) {
|
|||||||
angularInspector, err := angularinspector.NewStaticInspector()
|
angularInspector, err := angularinspector.NewStaticInspector()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
l := loader.ProvideService(pCfg, lic, signature.NewUnsignedAuthorizer(pCfg),
|
l := loader.ProvideService(pCfg, lic, signature.NewUnsignedAuthorizer(pCfg),
|
||||||
reg, provider.ProvideService(coreRegistry), finder.NewLocalFinder(pCfg), fakes.NewFakeRoleRegistry(),
|
reg, provider.ProvideService(coreRegistry), finder.NewLocalFinder(pCfg.DevMode), fakes.NewFakeRoleRegistry(),
|
||||||
assetpath.ProvideService(pluginscdn.ProvideService(pCfg)), signature.ProvideService(statickey.New()),
|
assetpath.ProvideService(pluginscdn.ProvideService(pCfg)), signature.ProvideService(statickey.New()),
|
||||||
angularInspector, &fakes.FakeOauthService{})
|
angularInspector, &fakes.FakeOauthService{})
|
||||||
srcs := sources.ProvideService(cfg, pCfg)
|
srcs := sources.ProvideService(cfg, pCfg)
|
||||||
|
|||||||
@@ -32,14 +32,18 @@ func FileSystem(logger log.PrettyLogger, pluginsDir string) *FS {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fs *FS) Extract(ctx context.Context, pluginID string, pluginArchive *zip.ReadCloser) (
|
var SimpleDirNameGeneratorFunc = func(pluginID string) string {
|
||||||
|
return pluginID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fs *FS) Extract(ctx context.Context, pluginID string, dirNameFunc DirNameGeneratorFunc, pluginArchive *zip.ReadCloser) (
|
||||||
*ExtractedPluginArchive, error) {
|
*ExtractedPluginArchive, error) {
|
||||||
pluginDir, err := fs.extractFiles(ctx, pluginArchive, pluginID)
|
pluginDir, err := fs.extractFiles(ctx, pluginArchive, pluginID, dirNameFunc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%v: %w", "failed to extract plugin archive", err)
|
return nil, fmt.Errorf("%v: %w", "failed to extract plugin archive", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
pluginJSON, err := readPluginJSON(pluginID, pluginDir)
|
pluginJSON, err := readPluginJSON(pluginDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%v: %w", "failed to convert to plugin DTO", err)
|
return nil, fmt.Errorf("%v: %w", "failed to convert to plugin DTO", err)
|
||||||
}
|
}
|
||||||
@@ -62,8 +66,9 @@ func (fs *FS) Extract(ctx context.Context, pluginID string, pluginArchive *zip.R
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fs *FS) extractFiles(_ context.Context, pluginArchive *zip.ReadCloser, pluginID string) (string, error) {
|
func (fs *FS) extractFiles(_ context.Context, pluginArchive *zip.ReadCloser, pluginID string, dirNameFunc DirNameGeneratorFunc) (string, error) {
|
||||||
installDir := filepath.Join(fs.pluginsDir, pluginID)
|
pluginDirName := dirNameFunc(pluginID)
|
||||||
|
installDir := filepath.Join(fs.pluginsDir, pluginDirName)
|
||||||
if _, err := os.Stat(installDir); !os.IsNotExist(err) {
|
if _, err := os.Stat(installDir); !os.IsNotExist(err) {
|
||||||
fs.log.Debugf("Removing existing installation of plugin %s", installDir)
|
fs.log.Debugf("Removing existing installation of plugin %s", installDir)
|
||||||
err = os.RemoveAll(installDir)
|
err = os.RemoveAll(installDir)
|
||||||
@@ -92,7 +97,7 @@ func (fs *FS) extractFiles(_ context.Context, pluginArchive *zip.ReadCloser, plu
|
|||||||
zf.Name, fs.pluginsDir)
|
zf.Name, fs.pluginsDir)
|
||||||
}
|
}
|
||||||
|
|
||||||
dstPath := filepath.Clean(filepath.Join(fs.pluginsDir, removeGitBuildFromName(zf.Name, pluginID))) // lgtm[go/zipslip]
|
dstPath := filepath.Clean(filepath.Join(fs.pluginsDir, removeGitBuildFromName(zf.Name, pluginDirName))) // lgtm[go/zipslip]
|
||||||
|
|
||||||
if zf.FileInfo().IsDir() {
|
if zf.FileInfo().IsDir() {
|
||||||
// We can ignore gosec G304 here since it makes sense to give all users read access
|
// We can ignore gosec G304 here since it makes sense to give all users read access
|
||||||
@@ -220,7 +225,7 @@ func removeGitBuildFromName(filename, pluginID string) string {
|
|||||||
return reGitBuild.ReplaceAllString(filename, pluginID+"/")
|
return reGitBuild.ReplaceAllString(filename, pluginID+"/")
|
||||||
}
|
}
|
||||||
|
|
||||||
func readPluginJSON(pluginID, pluginDir string) (plugins.JSONData, error) {
|
func readPluginJSON(pluginDir string) (plugins.JSONData, error) {
|
||||||
pluginPath := filepath.Join(pluginDir, "plugin.json")
|
pluginPath := filepath.Join(pluginDir, "plugin.json")
|
||||||
|
|
||||||
// It's safe to ignore gosec warning G304 since the file path suffix is hardcoded
|
// It's safe to ignore gosec warning G304 since the file path suffix is hardcoded
|
||||||
@@ -232,7 +237,7 @@ func readPluginJSON(pluginID, pluginDir string) (plugins.JSONData, error) {
|
|||||||
// nolint:gosec
|
// nolint:gosec
|
||||||
data, err = os.ReadFile(pluginPath)
|
data, err = os.ReadFile(pluginPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return plugins.JSONData{}, fmt.Errorf("could not find plugin.json or dist/plugin.json for %s in %s", pluginID, pluginDir)
|
return plugins.JSONData{}, fmt.Errorf("could not find plugin.json or dist/plugin.json for in %s", pluginDir)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,25 +27,24 @@ func TestAdd(t *testing.T) {
|
|||||||
pluginID := "test-app"
|
pluginID := "test-app"
|
||||||
|
|
||||||
fs := FileSystem(log.NewTestPrettyLogger(), testDir)
|
fs := FileSystem(log.NewTestPrettyLogger(), testDir)
|
||||||
archive, err := fs.Extract(context.Background(), pluginID, zipFile(t, "./testdata/plugin-with-symlinks.zip"))
|
archive, err := fs.Extract(context.Background(), pluginID, SimpleDirNameGeneratorFunc, zipFile(t, "./testdata/plugin-with-symlinks.zip"))
|
||||||
require.NotNil(t, archive)
|
require.NotNil(t, archive)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// verify extracted contents
|
// verify extracted contents
|
||||||
files, err := os.ReadDir(filepath.Join(testDir, pluginID))
|
files, err := os.ReadDir(archive.Path)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
file2, err := files[2].Info()
|
|
||||||
require.NoError(t, err)
|
|
||||||
file4, err := files[4].Info()
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
require.Len(t, files, 6)
|
require.Len(t, files, 6)
|
||||||
require.Equal(t, files[0].Name(), "MANIFEST.txt")
|
require.Equal(t, files[0].Name(), "MANIFEST.txt")
|
||||||
require.Equal(t, files[1].Name(), "dashboards")
|
require.Equal(t, files[1].Name(), "dashboards")
|
||||||
require.Equal(t, files[2].Name(), "extra")
|
require.Equal(t, files[2].Name(), "extra")
|
||||||
|
file2, err := files[2].Info()
|
||||||
|
require.NoError(t, err)
|
||||||
require.Equal(t, os.ModeSymlink, file2.Mode()&os.ModeSymlink)
|
require.Equal(t, os.ModeSymlink, file2.Mode()&os.ModeSymlink)
|
||||||
require.Equal(t, files[3].Name(), "plugin.json")
|
require.Equal(t, files[3].Name(), "plugin.json")
|
||||||
require.Equal(t, files[4].Name(), "symlink_to_txt")
|
require.Equal(t, files[4].Name(), "symlink_to_txt")
|
||||||
|
file4, err := files[4].Info()
|
||||||
|
require.NoError(t, err)
|
||||||
require.Equal(t, os.ModeSymlink, file4.Mode()&os.ModeSymlink)
|
require.Equal(t, os.ModeSymlink, file4.Mode()&os.ModeSymlink)
|
||||||
require.Equal(t, files[5].Name(), "text.txt")
|
require.Equal(t, files[5].Name(), "text.txt")
|
||||||
}
|
}
|
||||||
@@ -59,27 +58,27 @@ func TestExtractFiles(t *testing.T) {
|
|||||||
skipWindows(t)
|
skipWindows(t)
|
||||||
|
|
||||||
pluginID := "grafana-simple-json-datasource"
|
pluginID := "grafana-simple-json-datasource"
|
||||||
path, err := i.extractFiles(context.Background(), zipFile(t, "testdata/grafana-simple-json-datasource-ec18fa4da8096a952608a7e4c7782b4260b41bcf.zip"), pluginID)
|
path, err := i.extractFiles(context.Background(), zipFile(t, "testdata/grafana-simple-json-datasource-ec18fa4da8096a952608a7e4c7782b4260b41bcf.zip"), pluginID, SimpleDirNameGeneratorFunc)
|
||||||
require.Equal(t, filepath.Join(pluginsDir, pluginID), path)
|
require.Equal(t, filepath.Join(pluginsDir, pluginID), path)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// File in zip has permissions 755
|
// File in zip has permissions 755
|
||||||
fileInfo, err := os.Stat(filepath.Join(pluginsDir, "grafana-simple-json-datasource", "simple-plugin_darwin_amd64"))
|
fileInfo, err := os.Stat(filepath.Join(path, "simple-plugin_darwin_amd64"))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, "-rwxr-xr-x", fileInfo.Mode().String())
|
require.Equal(t, "-rwxr-xr-x", fileInfo.Mode().String())
|
||||||
|
|
||||||
// File in zip has permission 755
|
// File in zip has permission 755
|
||||||
fileInfo, err = os.Stat(pluginsDir + "/grafana-simple-json-datasource/simple-plugin_linux_amd64")
|
fileInfo, err = os.Stat(filepath.Join(path, "simple-plugin_linux_amd64"))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, "-rwxr-xr-x", fileInfo.Mode().String())
|
require.Equal(t, "-rwxr-xr-x", fileInfo.Mode().String())
|
||||||
|
|
||||||
// File in zip has permission 644
|
// File in zip has permission 644
|
||||||
fileInfo, err = os.Stat(pluginsDir + "/grafana-simple-json-datasource/simple-plugin_windows_amd64.exe")
|
fileInfo, err = os.Stat(filepath.Join(path, "simple-plugin_windows_amd64.exe"))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, "-rwxr-xr-x", fileInfo.Mode().String())
|
require.Equal(t, "-rwxr-xr-x", fileInfo.Mode().String())
|
||||||
|
|
||||||
// File in zip has permission 755
|
// File in zip has permission 755
|
||||||
fileInfo, err = os.Stat(pluginsDir + "/grafana-simple-json-datasource/non-plugin-binary")
|
fileInfo, err = os.Stat(filepath.Join(path, "non-plugin-binary"))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, "-rwxr-xr-x", fileInfo.Mode().String())
|
require.Equal(t, "-rwxr-xr-x", fileInfo.Mode().String())
|
||||||
})
|
})
|
||||||
@@ -88,43 +87,43 @@ func TestExtractFiles(t *testing.T) {
|
|||||||
skipWindows(t)
|
skipWindows(t)
|
||||||
|
|
||||||
pluginID := "plugin-with-symlink"
|
pluginID := "plugin-with-symlink"
|
||||||
path, err := i.extractFiles(context.Background(), zipFile(t, "testdata/plugin-with-symlink.zip"), pluginID)
|
path, err := i.extractFiles(context.Background(), zipFile(t, "testdata/plugin-with-symlink.zip"), pluginID, SimpleDirNameGeneratorFunc)
|
||||||
require.Equal(t, filepath.Join(pluginsDir, pluginID), path)
|
require.Equal(t, filepath.Join(pluginsDir, pluginID), path)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
_, err = os.Stat(pluginsDir + "/plugin-with-symlink/symlink_to_txt")
|
_, err = os.Stat(filepath.Join(path, "symlink_to_txt"))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
target, err := filepath.EvalSymlinks(pluginsDir + "/plugin-with-symlink/symlink_to_txt")
|
target, err := filepath.EvalSymlinks(filepath.Join(path, "symlink_to_txt"))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, pluginsDir+"/plugin-with-symlink/text.txt", target)
|
require.Equal(t, filepath.Join(path, "text.txt"), target)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Should extract directory with relative symlink", func(t *testing.T) {
|
t.Run("Should extract directory with relative symlink", func(t *testing.T) {
|
||||||
skipWindows(t)
|
skipWindows(t)
|
||||||
|
|
||||||
pluginID := "plugin-with-symlink-dir"
|
pluginID := "plugin-with-symlink-dir"
|
||||||
path, err := i.extractFiles(context.Background(), zipFile(t, "testdata/plugin-with-symlink-dir.zip"), pluginID)
|
path, err := i.extractFiles(context.Background(), zipFile(t, "testdata/plugin-with-symlink-dir.zip"), pluginID, SimpleDirNameGeneratorFunc)
|
||||||
require.Equal(t, filepath.Join(pluginsDir, pluginID), path)
|
require.Equal(t, filepath.Join(pluginsDir, pluginID), path)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
_, err = os.Stat(pluginsDir + "/plugin-with-symlink-dir/symlink_to_dir")
|
_, err = os.Stat(filepath.Join(path, "symlink_to_dir"))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
target, err := filepath.EvalSymlinks(pluginsDir + "/plugin-with-symlink-dir/symlink_to_dir")
|
target, err := filepath.EvalSymlinks(filepath.Join(path, "symlink_to_dir"))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, pluginsDir+"/plugin-with-symlink-dir/dir", target)
|
require.Equal(t, filepath.Join(path, "dir"), target)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Should not extract file with absolute symlink", func(t *testing.T) {
|
t.Run("Should not extract file with absolute symlink", func(t *testing.T) {
|
||||||
skipWindows(t)
|
skipWindows(t)
|
||||||
|
|
||||||
pluginID := "plugin-with-absolute-symlink"
|
pluginID := "plugin-with-absolute-symlink"
|
||||||
path, err := i.extractFiles(context.Background(), zipFile(t, "testdata/plugin-with-absolute-symlink.zip"), pluginID)
|
path, err := i.extractFiles(context.Background(), zipFile(t, "testdata/plugin-with-absolute-symlink.zip"), pluginID, SimpleDirNameGeneratorFunc)
|
||||||
require.Equal(t, filepath.Join(pluginsDir, pluginID), path)
|
require.Equal(t, filepath.Join(pluginsDir, pluginID), path)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
_, err = os.Stat(pluginsDir + "/plugin-with-absolute-symlink/test.txt")
|
_, err = os.Stat(filepath.Join(path, "/test.txt"))
|
||||||
require.True(t, os.IsNotExist(err))
|
require.True(t, os.IsNotExist(err))
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -132,16 +131,16 @@ func TestExtractFiles(t *testing.T) {
|
|||||||
skipWindows(t)
|
skipWindows(t)
|
||||||
|
|
||||||
pluginID := "plugin-with-absolute-symlink-dir"
|
pluginID := "plugin-with-absolute-symlink-dir"
|
||||||
path, err := i.extractFiles(context.Background(), zipFile(t, "testdata/plugin-with-absolute-symlink-dir.zip"), pluginID)
|
path, err := i.extractFiles(context.Background(), zipFile(t, "testdata/plugin-with-absolute-symlink-dir.zip"), pluginID, SimpleDirNameGeneratorFunc)
|
||||||
require.Equal(t, filepath.Join(pluginsDir, pluginID), path)
|
require.Equal(t, filepath.Join(pluginsDir, pluginID), path)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
_, err = os.Stat(pluginsDir + "/plugin-with-absolute-symlink-dir/target")
|
_, err = os.Stat(filepath.Join(path, "/plugin-with-absolute-symlink-dir/target"))
|
||||||
require.True(t, os.IsNotExist(err))
|
require.True(t, os.IsNotExist(err))
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Should detect if archive members point outside of the destination directory", func(t *testing.T) {
|
t.Run("Should detect if archive members point outside of the destination directory", func(t *testing.T) {
|
||||||
path, err := i.extractFiles(context.Background(), zipFile(t, "testdata/plugin-with-parent-member.zip"), "plugin-with-parent-member")
|
path, err := i.extractFiles(context.Background(), zipFile(t, "testdata/plugin-with-parent-member.zip"), "plugin-with-parent-member", SimpleDirNameGeneratorFunc)
|
||||||
require.Empty(t, path)
|
require.Empty(t, path)
|
||||||
require.EqualError(t, err, fmt.Sprintf(
|
require.EqualError(t, err, fmt.Sprintf(
|
||||||
`archive member "../member.txt" tries to write outside of plugin directory: %q, this can be a security risk`,
|
`archive member "../member.txt" tries to write outside of plugin directory: %q, this can be a security risk`,
|
||||||
@@ -150,7 +149,7 @@ func TestExtractFiles(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Should detect if archive members are absolute", func(t *testing.T) {
|
t.Run("Should detect if archive members are absolute", func(t *testing.T) {
|
||||||
path, err := i.extractFiles(context.Background(), zipFile(t, "testdata/plugin-with-absolute-member.zip"), "plugin-with-absolute-member")
|
path, err := i.extractFiles(context.Background(), zipFile(t, "testdata/plugin-with-absolute-member.zip"), "plugin-with-absolute-member", SimpleDirNameGeneratorFunc)
|
||||||
require.Empty(t, path)
|
require.Empty(t, path)
|
||||||
require.EqualError(t, err, fmt.Sprintf(
|
require.EqualError(t, err, fmt.Sprintf(
|
||||||
`archive member "/member.txt" tries to write outside of plugin directory: %q, this can be a security risk`,
|
`archive member "/member.txt" tries to write outside of plugin directory: %q, this can be a security risk`,
|
||||||
|
|||||||
@@ -6,5 +6,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type ZipExtractor interface {
|
type ZipExtractor interface {
|
||||||
Extract(ctx context.Context, pluginID string, rc *zip.ReadCloser) (*ExtractedPluginArchive, error)
|
Extract(ctx context.Context, pluginID string, destDir DirNameGeneratorFunc, rc *zip.ReadCloser) (*ExtractedPluginArchive, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DirNameGeneratorFunc = func(pluginID string) string
|
||||||
|
|||||||
Reference in New Issue
Block a user