mirror of
https://github.com/grafana/grafana.git
synced 2025-09-25 10:54:49 +08:00
Plugins: Simplify plugin file removal (#66115)
* make explicit class check when attempting to remove plugin * simplify plugin file tracking * fix test * apply feedback * fix linter
This commit is contained in:
@ -12,36 +12,27 @@ import (
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/grafana/grafana/pkg/plugins/log"
|
||||
)
|
||||
|
||||
var _ Manager = (*FS)(nil)
|
||||
var _ ZipExtractor = (*FS)(nil)
|
||||
|
||||
var reGitBuild = regexp.MustCompile("^[a-zA-Z0-9_.-]*/")
|
||||
|
||||
var (
|
||||
ErrUninstallOutsideOfPluginDir = errors.New("cannot uninstall a plugin outside of the plugins directory")
|
||||
ErrUninstallInvalidPluginDir = errors.New("cannot recognize as plugin folder")
|
||||
)
|
||||
|
||||
type FS struct {
|
||||
store map[string]string
|
||||
mu sync.RWMutex
|
||||
pluginsDir string
|
||||
log log.PrettyLogger
|
||||
}
|
||||
|
||||
func FileSystem(logger log.PrettyLogger, pluginsDir string) *FS {
|
||||
return &FS{
|
||||
store: make(map[string]string),
|
||||
pluginsDir: pluginsDir,
|
||||
log: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (fs *FS) Add(ctx context.Context, pluginID string, pluginArchive *zip.ReadCloser) (
|
||||
func (fs *FS) Extract(ctx context.Context, pluginID string, pluginArchive *zip.ReadCloser) (
|
||||
*ExtractedPluginArchive, error) {
|
||||
pluginDir, err := fs.extractFiles(ctx, pluginArchive, pluginID)
|
||||
if err != nil {
|
||||
@ -71,38 +62,6 @@ func (fs *FS) Add(ctx context.Context, pluginID string, pluginArchive *zip.ReadC
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (fs *FS) Register(_ context.Context, pluginID, pluginDir string) error {
|
||||
fs.mu.Lock()
|
||||
fs.store[pluginID] = pluginDir
|
||||
fs.mu.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fs *FS) Remove(_ context.Context, pluginID string) error {
|
||||
fs.mu.RLock()
|
||||
pluginDir, exists := fs.store[pluginID]
|
||||
fs.mu.RUnlock()
|
||||
if !exists {
|
||||
return fmt.Errorf("%s does not exist", pluginID)
|
||||
}
|
||||
|
||||
// extra security check to ensure we only remove plugins that are located in the configured plugins directory
|
||||
path, err := filepath.Rel(fs.pluginsDir, pluginDir)
|
||||
if err != nil || strings.HasPrefix(path, ".."+string(filepath.Separator)) {
|
||||
return ErrUninstallOutsideOfPluginDir
|
||||
}
|
||||
|
||||
if _, err = os.Stat(filepath.Join(pluginDir, "plugin.json")); os.IsNotExist(err) {
|
||||
if _, err = os.Stat(filepath.Join(pluginDir, "dist/plugin.json")); os.IsNotExist(err) {
|
||||
return ErrUninstallInvalidPluginDir
|
||||
}
|
||||
}
|
||||
|
||||
fs.log.Infof("Uninstalling plugin %v", pluginDir)
|
||||
return os.RemoveAll(pluginDir)
|
||||
}
|
||||
|
||||
func (fs *FS) extractFiles(_ context.Context, pluginArchive *zip.ReadCloser, pluginID string) (string, error) {
|
||||
installDir := filepath.Join(fs.pluginsDir, pluginID)
|
||||
if _, err := os.Stat(installDir); !os.IsNotExist(err) {
|
||||
@ -261,7 +220,7 @@ func removeGitBuildFromName(filename, pluginID string) string {
|
||||
return reGitBuild.ReplaceAllString(filename, pluginID+"/")
|
||||
}
|
||||
|
||||
func toPluginDTO(pluginID, pluginDir string) (InstalledPlugin, error) {
|
||||
func toPluginDTO(pluginID, pluginDir string) (installedPlugin, error) {
|
||||
distPluginDataPath := filepath.Join(pluginDir, "dist", "plugin.json")
|
||||
|
||||
// It's safe to ignore gosec warning G304 since the file path suffix is hardcoded
|
||||
@ -273,17 +232,17 @@ func toPluginDTO(pluginID, pluginDir string) (InstalledPlugin, error) {
|
||||
// nolint:gosec
|
||||
data, err = os.ReadFile(pluginDataPath)
|
||||
if err != nil {
|
||||
return InstalledPlugin{}, fmt.Errorf("could not find dist/plugin.json or plugin.json for %s in %s", pluginID, pluginDir)
|
||||
return installedPlugin{}, fmt.Errorf("could not find dist/plugin.json or plugin.json for %s in %s", pluginID, pluginDir)
|
||||
}
|
||||
}
|
||||
|
||||
res := InstalledPlugin{}
|
||||
res := installedPlugin{}
|
||||
if err = json.Unmarshal(data, &res); err != nil {
|
||||
return res, err
|
||||
}
|
||||
|
||||
if res.ID == "" {
|
||||
return InstalledPlugin{}, fmt.Errorf("could not find valid plugin %s in %s", pluginID, pluginDir)
|
||||
return installedPlugin{}, fmt.Errorf("could not find valid plugin %s in %s", pluginID, pluginDir)
|
||||
}
|
||||
|
||||
if res.Info.Version == "" {
|
||||
|
@ -25,7 +25,7 @@ func TestAdd(t *testing.T) {
|
||||
pluginID := "test-app"
|
||||
|
||||
fs := FileSystem(&fakeLogger{}, testDir)
|
||||
archive, err := fs.Add(context.Background(), pluginID, zipFile(t, "./testdata/plugin-with-symlinks.zip"))
|
||||
archive, err := fs.Extract(context.Background(), pluginID, zipFile(t, "./testdata/plugin-with-symlinks.zip"))
|
||||
require.NotNil(t, archive)
|
||||
require.NoError(t, err)
|
||||
|
||||
@ -48,89 +48,6 @@ func TestAdd(t *testing.T) {
|
||||
require.Equal(t, files[5].Name(), "text.txt")
|
||||
}
|
||||
|
||||
func TestRemove(t *testing.T) {
|
||||
pluginDir := t.TempDir()
|
||||
pluginJSON := filepath.Join(pluginDir, "plugin.json")
|
||||
//nolint:gosec
|
||||
_, err := os.Create(pluginJSON)
|
||||
require.NoError(t, err)
|
||||
|
||||
pluginID := "test-datasource"
|
||||
i := &FS{
|
||||
pluginsDir: filepath.Dir(pluginDir),
|
||||
store: map[string]string{
|
||||
pluginID: pluginDir,
|
||||
},
|
||||
log: &fakeLogger{},
|
||||
}
|
||||
|
||||
err = i.Remove(context.Background(), pluginID)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = os.Stat(pluginDir)
|
||||
require.True(t, os.IsNotExist(err))
|
||||
|
||||
t.Run("Uninstall will search in nested dir folder for plugin.json", func(t *testing.T) {
|
||||
pluginDistDir := filepath.Join(t.TempDir(), "dist")
|
||||
err = os.Mkdir(pluginDistDir, os.ModePerm)
|
||||
require.NoError(t, err)
|
||||
pluginJSON = filepath.Join(pluginDistDir, "plugin.json")
|
||||
//nolint:gosec
|
||||
_, err = os.Create(pluginJSON)
|
||||
require.NoError(t, err)
|
||||
|
||||
pluginDir = filepath.Dir(pluginDistDir)
|
||||
|
||||
i = &FS{
|
||||
pluginsDir: filepath.Dir(pluginDir),
|
||||
store: map[string]string{
|
||||
pluginID: pluginDir,
|
||||
},
|
||||
log: &fakeLogger{},
|
||||
}
|
||||
|
||||
err = i.Remove(context.Background(), pluginID)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = os.Stat(pluginDir)
|
||||
require.True(t, os.IsNotExist(err))
|
||||
})
|
||||
|
||||
t.Run("Uninstall will not delete folder if cannot recognize plugin structure", func(t *testing.T) {
|
||||
pluginDir = t.TempDir()
|
||||
i = &FS{
|
||||
pluginsDir: filepath.Dir(pluginDir),
|
||||
store: map[string]string{
|
||||
pluginID: pluginDir,
|
||||
},
|
||||
log: &fakeLogger{},
|
||||
}
|
||||
|
||||
err = i.Remove(context.Background(), pluginID)
|
||||
require.EqualError(t, err, "cannot recognize as plugin folder")
|
||||
|
||||
_, err = os.Stat(pluginDir)
|
||||
require.False(t, os.IsNotExist(err))
|
||||
})
|
||||
|
||||
t.Run("Uninstall will not delete folder if plugin's directory is not a subdirectory of specified plugins directory", func(t *testing.T) {
|
||||
pluginDir = t.TempDir()
|
||||
i = &FS{
|
||||
pluginsDir: "/some/other/path",
|
||||
store: map[string]string{
|
||||
pluginID: pluginDir,
|
||||
},
|
||||
log: &fakeLogger{},
|
||||
}
|
||||
|
||||
err = i.Remove(context.Background(), pluginID)
|
||||
require.EqualError(t, err, "cannot uninstall a plugin outside of the plugins directory")
|
||||
|
||||
_, err = os.Stat(pluginDir)
|
||||
require.False(t, os.IsNotExist(err))
|
||||
})
|
||||
}
|
||||
|
||||
func TestExtractFiles(t *testing.T) {
|
||||
pluginsDir := setupFakePluginsDir(t)
|
||||
|
||||
|
@ -5,8 +5,6 @@ import (
|
||||
"context"
|
||||
)
|
||||
|
||||
type Manager interface {
|
||||
Add(ctx context.Context, pluginID string, rc *zip.ReadCloser) (*ExtractedPluginArchive, error)
|
||||
Register(ctx context.Context, pluginID, pluginDir string) error
|
||||
Remove(ctx context.Context, pluginID string) error
|
||||
type ZipExtractor interface {
|
||||
Extract(ctx context.Context, pluginID string, rc *zip.ReadCloser) (*ExtractedPluginArchive, error)
|
||||
}
|
||||
|
@ -22,27 +22,27 @@ type Dependency struct {
|
||||
Version string
|
||||
}
|
||||
|
||||
type InstalledPlugin struct {
|
||||
type installedPlugin struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Info PluginInfo `json:"info"`
|
||||
Dependencies Dependencies `json:"dependencies"`
|
||||
Info pluginInfo `json:"info"`
|
||||
Dependencies dependencies `json:"dependencies"`
|
||||
}
|
||||
|
||||
type Dependencies struct {
|
||||
type dependencies struct {
|
||||
GrafanaVersion string `json:"grafanaVersion"`
|
||||
Plugins []PluginDependency `json:"plugins"`
|
||||
Plugins []pluginDependency `json:"plugins"`
|
||||
}
|
||||
|
||||
type PluginDependency struct {
|
||||
type pluginDependency struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
type PluginInfo struct {
|
||||
type pluginInfo struct {
|
||||
Version string `json:"version"`
|
||||
Updated string `json:"updated"`
|
||||
}
|
||||
|
Reference in New Issue
Block a user