mirror of
https://github.com/grafana/grafana.git
synced 2025-07-30 15:22:08 +08:00
Plugins: Move discovery logic to plugin sources (#106911)
* move finder behaviour to source * tidy * undo go.mod changes * fix comment * tidy unsafe local source
This commit is contained in:
@ -3,22 +3,51 @@ package sources
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/fs"
|
||||
"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/util"
|
||||
)
|
||||
|
||||
var walk = util.Walk
|
||||
|
||||
var (
|
||||
ErrInvalidPluginJSONFilePath = errors.New("invalid plugin.json filepath was provided")
|
||||
)
|
||||
|
||||
type LocalSource struct {
|
||||
paths []string
|
||||
class plugins.Class
|
||||
paths []string
|
||||
class plugins.Class
|
||||
strictMode bool // If true, tracks files via a StaticFS
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
// NewLocalSource represents a plugin with a fixed set of files.
|
||||
func NewLocalSource(class plugins.Class, paths []string) *LocalSource {
|
||||
return &LocalSource{
|
||||
class: class,
|
||||
paths: paths,
|
||||
paths: paths,
|
||||
class: class,
|
||||
strictMode: true,
|
||||
log: log.New("local.source"),
|
||||
}
|
||||
}
|
||||
|
||||
// NewUnsafeLocalSource represents a plugin that has an unbounded set of files. This useful when running in
|
||||
// dev mode whilst developing a plugin.
|
||||
func NewUnsafeLocalSource(class plugins.Class, paths []string) *LocalSource {
|
||||
return &LocalSource{
|
||||
paths: paths,
|
||||
class: class,
|
||||
strictMode: false,
|
||||
log: log.New("local.source"),
|
||||
}
|
||||
}
|
||||
|
||||
@ -26,7 +55,9 @@ func (s *LocalSource) PluginClass(_ context.Context) plugins.Class {
|
||||
return s.class
|
||||
}
|
||||
|
||||
func (s *LocalSource) PluginURIs(_ context.Context) []string {
|
||||
// Paths returns the file system paths that this source will search for plugins.
|
||||
// This method is primarily intended for testing purposes.
|
||||
func (s *LocalSource) Paths() []string {
|
||||
return s.paths
|
||||
}
|
||||
|
||||
@ -41,7 +72,189 @@ func (s *LocalSource) DefaultSignature(_ context.Context, _ string) (plugins.Sig
|
||||
}
|
||||
}
|
||||
|
||||
func DirAsLocalSources(pluginsPath string, class plugins.Class) ([]*LocalSource, error) {
|
||||
func (s *LocalSource) Discover(_ context.Context) ([]*plugins.FoundBundle, error) {
|
||||
if len(s.paths) == 0 {
|
||||
return []*plugins.FoundBundle{}, nil
|
||||
}
|
||||
|
||||
pluginJSONPaths := make([]string, 0, len(s.paths))
|
||||
for _, path := range s.paths {
|
||||
exists, err := fs.Exists(path)
|
||||
if err != nil {
|
||||
s.log.Warn("Skipping finding plugins as an error occurred", "path", path, "error", err)
|
||||
continue
|
||||
}
|
||||
if !exists {
|
||||
s.log.Warn("Skipping finding plugins as directory does not exist", "path", path)
|
||||
continue
|
||||
}
|
||||
|
||||
paths, err := s.getAbsPluginJSONPaths(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pluginJSONPaths = append(pluginJSONPaths, paths...)
|
||||
}
|
||||
|
||||
// load plugin.json files and map directory to JSON data
|
||||
foundPlugins := make(map[string]plugins.JSONData)
|
||||
for _, pluginJSONPath := range pluginJSONPaths {
|
||||
plugin, err := s.readPluginJSON(pluginJSONPath)
|
||||
if err != nil {
|
||||
s.log.Warn("Skipping plugin loading as its plugin.json could not be read", "path", pluginJSONPath, "error", err)
|
||||
continue
|
||||
}
|
||||
|
||||
pluginJSONAbsPath, err := filepath.Abs(pluginJSONPath)
|
||||
if err != nil {
|
||||
s.log.Warn("Skipping plugin loading as absolute plugin.json path could not be calculated", "pluginId", plugin.ID, "error", err)
|
||||
continue
|
||||
}
|
||||
|
||||
foundPlugins[filepath.Dir(pluginJSONAbsPath)] = plugin
|
||||
}
|
||||
|
||||
res := make(map[string]*plugins.FoundBundle)
|
||||
for pluginDir, data := range foundPlugins {
|
||||
var pluginFs plugins.FS
|
||||
pluginFs = plugins.NewLocalFS(pluginDir)
|
||||
if s.strictMode {
|
||||
// Tighten up security by allowing access only to the files present up to this point.
|
||||
// Any new file "sneaked in" won't be allowed and will act as if the file does not exist.
|
||||
var err error
|
||||
pluginFs, err = plugins.NewStaticFS(pluginFs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
res[pluginDir] = &plugins.FoundBundle{
|
||||
Primary: plugins.FoundPlugin{
|
||||
JSONData: data,
|
||||
FS: pluginFs,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Track child plugins and add them to their parent.
|
||||
childPlugins := make(map[string]struct{})
|
||||
for dir, p := range res {
|
||||
// Check if this plugin is the parent of another plugin.
|
||||
for dir2, p2 := range res {
|
||||
if dir == dir2 {
|
||||
continue
|
||||
}
|
||||
|
||||
relPath, err := filepath.Rel(dir, dir2)
|
||||
if err != nil {
|
||||
s.log.Error("Cannot calculate relative path. Skipping", "pluginId", p2.Primary.JSONData.ID, "err", err)
|
||||
continue
|
||||
}
|
||||
if !strings.Contains(relPath, "..") {
|
||||
child := p2.Primary
|
||||
s.log.Debug("Adding child", "parent", p.Primary.JSONData.ID, "child", child.JSONData.ID, "relPath", relPath)
|
||||
p.Children = append(p.Children, &child)
|
||||
childPlugins[dir2] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove child plugins from the result (they are already tracked via their parent).
|
||||
result := make([]*plugins.FoundBundle, 0, len(res))
|
||||
for k := range res {
|
||||
if _, ok := childPlugins[k]; !ok {
|
||||
result = append(result, res[k])
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *LocalSource) readPluginJSON(pluginJSONPath string) (plugins.JSONData, error) {
|
||||
reader, err := s.readFile(pluginJSONPath)
|
||||
defer func() {
|
||||
if reader == nil {
|
||||
return
|
||||
}
|
||||
if err = reader.Close(); err != nil {
|
||||
s.log.Warn("Failed to close plugin JSON file", "path", pluginJSONPath, "error", err)
|
||||
}
|
||||
}()
|
||||
if err != nil {
|
||||
s.log.Warn("Skipping plugin loading as its plugin.json could not be read", "path", pluginJSONPath, "error", err)
|
||||
return plugins.JSONData{}, err
|
||||
}
|
||||
plugin, err := plugins.ReadPluginJSON(reader)
|
||||
if err != nil {
|
||||
s.log.Warn("Skipping plugin loading as its plugin.json could not be read", "path", pluginJSONPath, "error", err)
|
||||
return plugins.JSONData{}, err
|
||||
}
|
||||
|
||||
return plugin, nil
|
||||
}
|
||||
|
||||
func (s *LocalSource) getAbsPluginJSONPaths(path string) ([]string, error) {
|
||||
var pluginJSONPaths []string
|
||||
|
||||
var err error
|
||||
path, err = filepath.Abs(path)
|
||||
if err != nil {
|
||||
return []string{}, err
|
||||
}
|
||||
|
||||
if err = walk(path, true, true,
|
||||
func(currentPath string, fi os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
s.log.Error("Couldn't scan directory since it doesn't exist", "pluginDir", path, "error", err)
|
||||
return nil
|
||||
}
|
||||
if errors.Is(err, os.ErrPermission) {
|
||||
s.log.Error("Couldn't scan directory due to lack of permissions", "pluginDir", path, "error", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("filepath.Walk reported an error for %q: %w", currentPath, err)
|
||||
}
|
||||
|
||||
if fi.Name() == "node_modules" {
|
||||
return util.ErrWalkSkipDir
|
||||
}
|
||||
|
||||
if fi.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
if fi.Name() != "plugin.json" {
|
||||
return nil
|
||||
}
|
||||
|
||||
pluginJSONPaths = append(pluginJSONPaths, currentPath)
|
||||
return nil
|
||||
}); err != nil {
|
||||
return []string{}, err
|
||||
}
|
||||
|
||||
return pluginJSONPaths, nil
|
||||
}
|
||||
|
||||
func (s *LocalSource) readFile(pluginJSONPath string) (io.ReadCloser, error) {
|
||||
s.log.Debug("Loading plugin", "path", pluginJSONPath)
|
||||
|
||||
if !strings.EqualFold(filepath.Ext(pluginJSONPath), ".json") {
|
||||
return nil, ErrInvalidPluginJSONFilePath
|
||||
}
|
||||
|
||||
absPluginJSONPath, err := filepath.Abs(pluginJSONPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Wrapping in filepath.Clean to properly handle
|
||||
// gosec G304 Potential file inclusion via variable rule.
|
||||
return os.Open(filepath.Clean(absPluginJSONPath))
|
||||
}
|
||||
|
||||
func DirAsLocalSources(cfg *config.PluginManagementCfg, pluginsPath string, class plugins.Class) ([]*LocalSource, error) {
|
||||
if pluginsPath == "" {
|
||||
return []*LocalSource{}, errors.New("plugins path not configured")
|
||||
}
|
||||
@ -64,7 +277,11 @@ func DirAsLocalSources(pluginsPath string, class plugins.Class) ([]*LocalSource,
|
||||
|
||||
sources := make([]*LocalSource, len(pluginDirs))
|
||||
for i, dir := range pluginDirs {
|
||||
sources[i] = NewLocalSource(class, []string{dir})
|
||||
if cfg.DevMode {
|
||||
sources[i] = NewUnsafeLocalSource(class, []string{dir})
|
||||
} else {
|
||||
sources[i] = NewLocalSource(class, []string{dir})
|
||||
}
|
||||
}
|
||||
|
||||
return sources, nil
|
||||
|
Reference in New Issue
Block a user