mirror of
https://github.com/grafana/grafana.git
synced 2025-08-02 23:53:10 +08:00

* it's cdn time * tidy body closing * auto signed * fix close * update log name * remove comments
264 lines
6.4 KiB
Go
264 lines
6.4 KiB
Go
package finder
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
|
|
"github.com/grafana/grafana/pkg/infra/fs"
|
|
"github.com/grafana/grafana/pkg/plugins"
|
|
"github.com/grafana/grafana/pkg/plugins/log"
|
|
"github.com/grafana/grafana/pkg/util"
|
|
)
|
|
|
|
var walk = util.Walk
|
|
|
|
var (
|
|
ErrInvalidPluginJSON = errors.New("did not find valid type or id properties in plugin.json")
|
|
ErrInvalidPluginJSONFilePath = errors.New("invalid plugin.json filepath was provided")
|
|
)
|
|
|
|
type Local struct {
|
|
log log.Logger
|
|
}
|
|
|
|
func NewLocalFinder() *Local {
|
|
return &Local{
|
|
log: log.New("local.finder"),
|
|
}
|
|
}
|
|
|
|
func (l *Local) Find(ctx context.Context, src plugins.PluginSource) ([]*plugins.FoundBundle, error) {
|
|
if len(src.PluginURIs(ctx)) == 0 {
|
|
return []*plugins.FoundBundle{}, nil
|
|
}
|
|
|
|
var pluginJSONPaths []string
|
|
for _, path := range src.PluginURIs(ctx) {
|
|
exists, err := fs.Exists(path)
|
|
if err != nil {
|
|
l.log.Warn("Skipping finding plugins as an error occurred", "path", path, "err", err)
|
|
continue
|
|
}
|
|
if !exists {
|
|
l.log.Warn("Skipping finding plugins as directory does not exist", "path", path)
|
|
continue
|
|
}
|
|
|
|
paths, err := l.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 := l.readPluginJSON(pluginJSONPath)
|
|
if err != nil {
|
|
l.log.Warn("Skipping plugin loading as its plugin.json could not be read", "path", pluginJSONPath, "err", err)
|
|
continue
|
|
}
|
|
|
|
pluginJSONAbsPath, err := filepath.Abs(pluginJSONPath)
|
|
if err != nil {
|
|
l.log.Warn("Skipping plugin loading as absolute plugin.json path could not be calculated", "pluginID", plugin.ID, "err", err)
|
|
continue
|
|
}
|
|
|
|
if _, dupe := foundPlugins[filepath.Dir(pluginJSONAbsPath)]; dupe {
|
|
l.log.Warn("Skipping plugin loading as it's a duplicate", "pluginID", plugin.ID)
|
|
continue
|
|
}
|
|
foundPlugins[filepath.Dir(pluginJSONAbsPath)] = plugin
|
|
}
|
|
|
|
var res = make(map[string]*plugins.FoundBundle)
|
|
for pluginDir, data := range foundPlugins {
|
|
files, err := collectFilesWithin(pluginDir)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
res[pluginDir] = &plugins.FoundBundle{
|
|
Primary: plugins.FoundPlugin{
|
|
JSONData: data,
|
|
FS: plugins.NewLocalFS(files, pluginDir),
|
|
},
|
|
}
|
|
}
|
|
|
|
var result []*plugins.FoundBundle
|
|
for dir := range foundPlugins {
|
|
ancestors := strings.Split(dir, string(filepath.Separator))
|
|
ancestors = ancestors[0 : len(ancestors)-1]
|
|
|
|
pluginPath := ""
|
|
if runtime.GOOS != "windows" && filepath.IsAbs(dir) {
|
|
pluginPath = "/"
|
|
}
|
|
add := true
|
|
for _, ancestor := range ancestors {
|
|
pluginPath = filepath.Join(pluginPath, ancestor)
|
|
if _, ok := foundPlugins[pluginPath]; ok {
|
|
if fp, exists := res[pluginPath]; exists {
|
|
fp.Children = append(fp.Children, &res[dir].Primary)
|
|
add = false
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if add {
|
|
result = append(result, res[dir])
|
|
}
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
func (l *Local) readPluginJSON(pluginJSONPath string) (plugins.JSONData, error) {
|
|
reader, err := l.readFile(pluginJSONPath)
|
|
defer func() {
|
|
if reader == nil {
|
|
return
|
|
}
|
|
if err = reader.Close(); err != nil {
|
|
l.log.Warn("Failed to close plugin JSON file", "path", pluginJSONPath, "err", err)
|
|
}
|
|
}()
|
|
if err != nil {
|
|
l.log.Warn("Skipping plugin loading as its plugin.json could not be read", "path", pluginJSONPath, "err", err)
|
|
return plugins.JSONData{}, err
|
|
}
|
|
plugin, err := ReadPluginJSON(reader)
|
|
if err != nil {
|
|
l.log.Warn("Skipping plugin loading as its plugin.json could not be read", "path", pluginJSONPath, "err", err)
|
|
return plugins.JSONData{}, err
|
|
}
|
|
|
|
return plugin, nil
|
|
}
|
|
|
|
func (l *Local) 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) {
|
|
l.log.Error("Couldn't scan directory since it doesn't exist", "pluginDir", path, "err", err)
|
|
return nil
|
|
}
|
|
if errors.Is(err, os.ErrPermission) {
|
|
l.log.Error("Couldn't scan directory due to lack of permissions", "pluginDir", path, "err", 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 collectFilesWithin(dir string) (map[string]struct{}, error) {
|
|
files := map[string]struct{}{}
|
|
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if info.Mode()&os.ModeSymlink == os.ModeSymlink {
|
|
symlinkPath, err := filepath.EvalSymlinks(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
symlink, err := os.Stat(symlinkPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// verify that symlinked file is within plugin directory
|
|
p, err := filepath.Rel(dir, symlinkPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if p == ".." || strings.HasPrefix(p, ".."+string(filepath.Separator)) {
|
|
return fmt.Errorf("file '%s' not inside of plugin directory", p)
|
|
}
|
|
|
|
// skip adding symlinked directories
|
|
if symlink.IsDir() {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// skip directories
|
|
if info.IsDir() {
|
|
return nil
|
|
}
|
|
|
|
// verify that file is within plugin directory
|
|
file, err := filepath.Rel(dir, path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if strings.HasPrefix(file, ".."+string(filepath.Separator)) {
|
|
return fmt.Errorf("file '%s' not inside of plugin directory", file)
|
|
}
|
|
|
|
files[path] = struct{}{}
|
|
|
|
return nil
|
|
})
|
|
|
|
return files, err
|
|
}
|
|
|
|
func (l *Local) readFile(pluginJSONPath string) (io.ReadCloser, error) {
|
|
l.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))
|
|
}
|