Plugins: Fix loading of dist folders (#80015)

* end to end

* tidy

* fix whitespace

* remove unused code

* fix linter

* fix gosec + add sort

* fix test

* apply cr feedback
This commit is contained in:
Will Browne 2024-01-08 11:45:03 +01:00 committed by GitHub
parent 0440b29ebf
commit 78ae795e06
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 114 additions and 93 deletions

View File

@ -354,27 +354,7 @@ func TestFinder_getAbsPluginJSONPaths(t *testing.T) {
require.Empty(t, paths) require.Empty(t, paths)
}) })
t.Run("should forward if the dist folder should be evaluated", func(t *testing.T) { t.Run("The followDistFolder state controls whether certain folders are followed", func(t *testing.T) {
origWalk := walk
walk = func(path string, followSymlinks, detectSymlinkInfiniteLoop, followDistFolder bool, walkFn util.WalkFunc) error {
if followDistFolder {
return walkFn(path, nil, errors.New("unexpected followDistFolder"))
}
return walkFn(path, nil, filepath.SkipDir)
}
t.Cleanup(func() {
walk = origWalk
})
finder := NewLocalFinder(false, featuremgmt.WithFeatures())
paths, err := finder.getAbsPluginJSONPaths("test", false)
require.ErrorIs(t, err, filepath.SkipDir)
require.Empty(t, paths)
})
}
func TestFinder_getAbsPluginJSONPaths_PluginClass(t *testing.T) {
t.Run("When a dist folder exists as a direct child of the plugins path, it will always be resolved", func(t *testing.T) {
dir, err := filepath.Abs("../../testdata/pluginRootWithDist") dir, err := filepath.Abs("../../testdata/pluginRootWithDist")
require.NoError(t, err) require.NoError(t, err)
@ -384,20 +364,17 @@ func TestFinder_getAbsPluginJSONPaths_PluginClass(t *testing.T) {
expected []string expected []string
}{ }{
{ {
name: "When followDistFolder is enabled, a nested dist folder will also be resolved", name: "When followDistFolder is enabled, only the nested dist folder will be followed",
followDist: true, followDist: true,
expected: []string{ expected: []string{
filepath.Join(dir, "datasource/plugin.json"),
filepath.Join(dir, "dist/plugin.json"), filepath.Join(dir, "dist/plugin.json"),
filepath.Join(dir, "panel/dist/plugin.json"),
}, },
}, },
{ {
name: "When followDistFolder is disabled, a nested dist folder will not be resolved", name: "When followDistFolder is disabled, no dist folders will be followed",
followDist: false, followDist: false,
expected: []string{ expected: []string{
filepath.Join(dir, "datasource/plugin.json"), filepath.Join(dir, "datasource/plugin.json"),
filepath.Join(dir, "dist/plugin.json"),
filepath.Join(dir, "panel/src/plugin.json"), filepath.Join(dir, "panel/src/plugin.json"),
}, },
}, },

View File

@ -2,34 +2,77 @@ package sources
import ( import (
"context" "context"
"os"
"path/filepath" "path/filepath"
"slices"
"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/log" "github.com/grafana/grafana/pkg/plugins/log"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
) )
type Service struct { type Service struct {
gCfg *setting.Cfg cfg *setting.Cfg
cfg *config.Cfg
log log.Logger log log.Logger
} }
func ProvideService(gCfg *setting.Cfg, cfg *config.Cfg) *Service { func ProvideService(cfg *setting.Cfg) *Service {
return &Service{ return &Service{
gCfg: gCfg,
cfg: cfg, cfg: cfg,
log: log.New("plugin.sources"), log: log.New("plugin.sources"),
} }
} }
func (s *Service) List(_ context.Context) []plugins.PluginSource { func (s *Service) List(_ context.Context) []plugins.PluginSource {
return []plugins.PluginSource{ r := []plugins.PluginSource{
NewLocalSource(plugins.ClassCore, corePluginPaths(s.gCfg.StaticRootPath)), NewLocalSource(plugins.ClassCore, corePluginPaths(s.cfg.StaticRootPath)),
NewLocalSource(plugins.ClassBundled, []string{s.gCfg.BundledPluginsPath}), NewLocalSource(plugins.ClassBundled, []string{s.cfg.BundledPluginsPath}),
NewLocalSource(plugins.ClassExternal, append([]string{s.cfg.PluginsPath}, pluginFSPaths(s.cfg.PluginSettings)...)),
} }
r = append(r, s.externalPluginSources()...)
r = append(r, s.pluginSettingSources()...)
return r
}
func (s *Service) externalPluginSources() []plugins.PluginSource {
var sources []plugins.PluginSource
if s.cfg.PluginsPath == "" {
return sources
}
pluginsPath := s.cfg.PluginsPath
// It's safe to ignore gosec warning G304 since the variable part of the file path comes from a configuration
// variable.
// nolint:gosec
d, err := os.ReadDir(pluginsPath)
if err != nil {
s.log.Error("Failed to open plugins path", "path", pluginsPath, "error", err)
return sources
}
var pluginDirs []string
for _, dir := range d {
if dir.IsDir() {
pluginDirs = append(pluginDirs, filepath.Join(pluginsPath, dir.Name()))
}
}
slices.Sort(pluginDirs)
for _, dir := range pluginDirs {
sources = append(sources, NewLocalSource(plugins.ClassExternal, []string{dir}))
}
return sources
}
func (s *Service) pluginSettingSources() []plugins.PluginSource {
var sources []plugins.PluginSource
for _, ps := range s.cfg.PluginSettings {
path, exists := ps["path"]
if !exists || path == "" {
continue
}
sources = append(sources, NewLocalSource(plugins.ClassExternal, []string{path}))
}
return sources
} }
// corePluginPaths provides a list of the Core plugin file system paths // corePluginPaths provides a list of the Core plugin file system paths
@ -38,16 +81,3 @@ func corePluginPaths(staticRootPath string) []string {
panelsPath := filepath.Join(staticRootPath, "app/plugins/panel") panelsPath := filepath.Join(staticRootPath, "app/plugins/panel")
return []string{datasourcePaths, panelsPath} return []string{datasourcePaths, panelsPath}
} }
// pluginSettingPaths provides plugin file system paths defined in cfg.PluginSettings
func pluginFSPaths(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
}

View File

@ -8,20 +8,21 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/config"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
) )
func TestSources_List(t *testing.T) { func TestSources_List(t *testing.T) {
t.Run("Plugin sources are populated by default and listed in specific order", func(t *testing.T) { t.Run("Plugin sources are populated by default and listed in specific order", func(t *testing.T) {
testdata, err := filepath.Abs("../testdata")
require.NoError(t, err)
cfg := &setting.Cfg{ cfg := &setting.Cfg{
BundledPluginsPath: "path1", StaticRootPath: testdata,
} PluginsPath: filepath.Join(testdata, "pluginRootWithDist"),
pCfg := &config.Cfg{ BundledPluginsPath: filepath.Join(testdata, "unsigned-panel"),
PluginsPath: "path2",
PluginSettings: setting.PluginSettings{ PluginSettings: setting.PluginSettings{
"foo": map[string]string{ "foo": map[string]string{
"path": "path3", "path": filepath.Join(testdata, "test-app"),
}, },
"bar": map[string]string{ "bar": map[string]string{
"url": "https://grafana.plugin", "url": "https://grafana.plugin",
@ -29,15 +30,18 @@ func TestSources_List(t *testing.T) {
}, },
} }
s := ProvideService(cfg, pCfg) s := ProvideService(cfg)
srcs := s.List(context.Background()) srcs := s.List(context.Background())
ctx := context.Background() ctx := context.Background()
require.Len(t, srcs, 3) require.Len(t, srcs, 6)
require.Equal(t, srcs[0].PluginClass(ctx), plugins.ClassCore) require.Equal(t, srcs[0].PluginClass(ctx), plugins.ClassCore)
require.Equal(t, srcs[0].PluginURIs(ctx), []string{filepath.Join("app", "plugins", "datasource"), filepath.Join("app", "plugins", "panel")}) require.Equal(t, srcs[0].PluginURIs(ctx), []string{
filepath.Join(testdata, "app", "plugins", "datasource"),
filepath.Join(testdata, "app", "plugins", "panel"),
})
sig, exists := srcs[0].DefaultSignature(ctx) sig, exists := srcs[0].DefaultSignature(ctx)
require.True(t, exists) require.True(t, exists)
require.Equal(t, plugins.SignatureStatusInternal, sig.Status) require.Equal(t, plugins.SignatureStatusInternal, sig.Status)
@ -45,15 +49,33 @@ func TestSources_List(t *testing.T) {
require.Equal(t, "", sig.SigningOrg) require.Equal(t, "", sig.SigningOrg)
require.Equal(t, srcs[1].PluginClass(ctx), plugins.ClassBundled) require.Equal(t, srcs[1].PluginClass(ctx), plugins.ClassBundled)
require.Equal(t, srcs[1].PluginURIs(ctx), []string{"path1"}) require.Equal(t, srcs[1].PluginURIs(ctx), []string{filepath.Join(testdata, "unsigned-panel")})
sig, exists = srcs[1].DefaultSignature(ctx) sig, exists = srcs[1].DefaultSignature(ctx)
require.False(t, exists) require.False(t, exists)
require.Equal(t, plugins.Signature{}, sig) require.Equal(t, plugins.Signature{}, sig)
require.Equal(t, srcs[2].PluginClass(ctx), plugins.ClassExternal) require.Equal(t, srcs[2].PluginClass(ctx), plugins.ClassExternal)
require.Equal(t, srcs[2].PluginURIs(ctx), []string{"path2", "path3"}) require.Equal(t, srcs[2].PluginURIs(ctx), []string{
filepath.Join(testdata, "pluginRootWithDist", "datasource"),
})
sig, exists = srcs[2].DefaultSignature(ctx) sig, exists = srcs[2].DefaultSignature(ctx)
require.False(t, exists) require.False(t, exists)
require.Equal(t, plugins.Signature{}, sig) require.Equal(t, plugins.Signature{}, sig)
require.Equal(t, srcs[3].PluginClass(ctx), plugins.ClassExternal)
require.Equal(t, srcs[3].PluginURIs(ctx), []string{
filepath.Join(testdata, "pluginRootWithDist", "dist"),
})
sig, exists = srcs[3].DefaultSignature(ctx)
require.False(t, exists)
require.Equal(t, plugins.Signature{}, sig)
require.Equal(t, srcs[4].PluginClass(ctx), plugins.ClassExternal)
require.Equal(t, srcs[4].PluginURIs(ctx), []string{
filepath.Join(testdata, "pluginRootWithDist", "panel"),
})
sig, exists = srcs[4].DefaultSignature(ctx)
require.False(t, exists)
require.Equal(t, plugins.Signature{}, sig)
}) })
} }

View File

@ -51,30 +51,20 @@ func TestIntegrationPluginManager(t *testing.T) {
bundledPluginsPath, err := filepath.Abs("../../../plugins-bundled/internal") bundledPluginsPath, err := filepath.Abs("../../../plugins-bundled/internal")
require.NoError(t, err) require.NoError(t, err)
// We use the raw config here as it forms the basis for the setting.Provider implementation
// The plugin manager also relies directly on the setting.Cfg struct to provide Grafana specific
// properties such as the loading paths
raw, err := ini.Load([]byte(`
app_mode = production
[plugin.test-app]
path=../../plugins/manager/testdata/test-app
[plugin.test-panel]
not=included
`),
)
require.NoError(t, err)
features := featuremgmt.WithFeatures() features := featuremgmt.WithFeatures()
cfg := &setting.Cfg{ cfg := &setting.Cfg{
Raw: raw, Raw: ini.Empty(),
StaticRootPath: staticRootPath, StaticRootPath: staticRootPath,
BundledPluginsPath: bundledPluginsPath, BundledPluginsPath: bundledPluginsPath,
Azure: &azsettings.AzureSettings{}, Azure: &azsettings.AzureSettings{},
PluginSettings: map[string]map[string]string{
// nolint:staticcheck "test-app": {
IsFeatureToggleEnabled: features.IsEnabledGlobally, "path": "../../plugins/manager/testdata/test-app",
},
"test-panel": {
"not": "included",
},
},
} }
tracer := tracing.InitializeTracerForTest() tracer := tracing.InitializeTracerForTest()

View File

@ -66,7 +66,7 @@ func CreateIntegrationTestCtx(t *testing.T, cfg *setting.Cfg, coreRegistry *core
Terminator: term, Terminator: term,
}) })
ps, err := pluginstore.ProvideService(reg, sources.ProvideService(cfg, pCfg), l) ps, err := pluginstore.ProvideService(reg, sources.ProvideService(cfg), l)
require.NoError(t, err) require.NoError(t, err)
return &IntegrationTestCtx{ return &IntegrationTestCtx{

View File

@ -27,7 +27,7 @@ func newWalker(rootDir string) *walker {
// it calls the walkFn passed. // it calls the walkFn passed.
// //
// It is similar to filepath.Walk, except that it supports symbolic links and // It is similar to filepath.Walk, except that it supports symbolic links and
// can detect infinite loops while following sym links. // can detect infinite loops while following symlinks.
// It solves the issue where your WalkFunc needs a path relative to the symbolic link // It solves the issue where your WalkFunc needs a path relative to the symbolic link
// (resolving links within walkfunc loses the path to the symbolic link for each traversal). // (resolving links within walkfunc loses the path to the symbolic link for each traversal).
func Walk(path string, followSymlinks bool, detectSymlinkInfiniteLoop bool, followDistFolder bool, walkFn WalkFunc) error { func Walk(path string, followSymlinks bool, detectSymlinkInfiniteLoop bool, followDistFolder bool, walkFn WalkFunc) error {
@ -112,20 +112,22 @@ func (w *walker) walk(path string, info os.FileInfo, resolvedPath string, symlin
subFiles = append(subFiles, subFile{path: path2, resolvedPath: resolvedPath2, fileInfo: fileInfo}) subFiles = append(subFiles, subFile{path: path2, resolvedPath: resolvedPath2, fileInfo: fileInfo})
} }
// If we have found a dist directory in a subdirectory (IE not at root path), and followDistFolder is true, if followDistFolder && w.containsDistFolder(subFiles) {
// then we want to follow only the dist directory and ignore all other subdirectories. err := w.walk(
atRootDir := w.rootDir == path filepath.Join(path, "dist"),
if followDistFolder && w.containsDistFolder(subFiles) && !atRootDir { info,
return w.walk(filepath.Join(path, "dist"), info, filepath.Join(resolvedPath, "dist"), symlinkPathsFollowed, filepath.Join(resolvedPath, "dist"),
followDistFolder, walkFn) symlinkPathsFollowed,
followDistFolder,
walkFn)
if err != nil {
return err
}
} else { } else {
// Follow all subdirectories, with special handling for dist directories.
for _, p := range subFiles { for _, p := range subFiles {
// We only want to skip a dist directory if it is not in the root directory, and followDistFolder is false. if p.isDistDir() && !followDistFolder {
if p.isDistDir() && !atRootDir && !followDistFolder {
continue continue
} }
err = w.walk(p.path, p.fileInfo, p.resolvedPath, symlinkPathsFollowed, followDistFolder, walkFn) err = w.walk(p.path, p.fileInfo, p.resolvedPath, symlinkPathsFollowed, followDistFolder, walkFn)
if err != nil { if err != nil {
return err return err