Plugins: Refactor loader + finder to support multiple sourcing methods (#64735)

* it's cdn time

* tidy body closing

* auto signed

* fix close

* update log name

* remove comments
This commit is contained in:
Will Browne 2023-03-20 13:35:49 +00:00 committed by GitHub
parent eba2c7b522
commit ee2dd62a1f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 389 additions and 223 deletions

View File

@ -23,6 +23,7 @@ import (
"github.com/grafana/grafana/pkg/plugins/manager/fakes"
"github.com/grafana/grafana/pkg/plugins/manager/loader"
"github.com/grafana/grafana/pkg/plugins/manager/loader/assetpath"
"github.com/grafana/grafana/pkg/plugins/manager/loader/finder"
"github.com/grafana/grafana/pkg/plugins/manager/registry"
"github.com/grafana/grafana/pkg/plugins/manager/signature"
"github.com/grafana/grafana/pkg/plugins/manager/sources"
@ -60,7 +61,7 @@ func TestCallResource(t *testing.T) {
reg := registry.ProvideService()
cdn := pluginscdn.ProvideService(pCfg)
l := loader.ProvideService(pCfg, fakes.NewFakeLicensingService(), signature.NewUnsignedAuthorizer(pCfg),
reg, provider.ProvideService(coreRegistry), fakes.NewFakeRoleRegistry(), cdn, assetpath.ProvideService(cdn))
reg, provider.ProvideService(coreRegistry), finder.NewLocalFinder(), fakes.NewFakeRoleRegistry(), cdn, assetpath.ProvideService(cdn))
srcs := sources.ProvideService(cfg, pCfg)
ps, err := store.ProvideService(reg, srcs, l)
require.NoError(t, err)

View File

@ -24,9 +24,10 @@ type Installer interface {
Remove(ctx context.Context, pluginID string) error
}
type PluginSource struct {
Class Class
Paths []string
type PluginSource interface {
PluginClass(ctx context.Context) Class
PluginURIs(ctx context.Context) []string
DefaultSignature(ctx context.Context) (Signature, bool)
}
type CompatOpts struct {

View File

@ -1,6 +1,7 @@
package plugins
import (
"errors"
"io/fs"
"os"
"path/filepath"
@ -40,7 +41,14 @@ func (f LocalFS) Open(name string) (fs.File, error) {
if kv.f != nil {
return kv.f, nil
}
return os.Open(kv.path)
file, err := os.Open(kv.path)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
return nil, ErrFileNotExist
}
return nil, ErrPluginFileRead
}
return file, nil
}
return nil, ErrFileNotExist
}
@ -70,14 +78,24 @@ type LocalFile struct {
}
func (p *LocalFile) Stat() (fs.FileInfo, error) {
return os.Stat(p.path)
fi, err := os.Stat(p.path)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
return nil, ErrFileNotExist
}
return nil, ErrPluginFileRead
}
return fi, nil
}
func (p *LocalFile) Read(bytes []byte) (int, error) {
var err error
p.f, err = os.Open(p.path)
if err != nil {
return 0, err
if errors.Is(err, fs.ErrNotExist) {
return 0, ErrFileNotExist
}
return 0, ErrPluginFileRead
}
return p.f.Read(bytes)
}

View File

@ -37,13 +37,13 @@ func (i *FakePluginInstaller) Remove(ctx context.Context, pluginID string) error
}
type FakeLoader struct {
LoadFunc func(_ context.Context, _ plugins.Class, paths []string) ([]*plugins.Plugin, error)
LoadFunc func(_ context.Context, _ plugins.PluginSource) ([]*plugins.Plugin, error)
UnloadFunc func(_ context.Context, _ string) error
}
func (l *FakeLoader) Load(ctx context.Context, class plugins.Class, paths []string) ([]*plugins.Plugin, error) {
func (l *FakeLoader) Load(ctx context.Context, src plugins.PluginSource) ([]*plugins.Plugin, error) {
if l.LoadFunc != nil {
return l.LoadFunc(ctx, class, paths)
return l.LoadFunc(ctx, src)
}
return nil, nil
}
@ -381,13 +381,40 @@ func (f *FakePluginFiles) Files() []string {
return []string{}
}
type FakeSources struct {
type FakeSourceRegistry struct {
ListFunc func(_ context.Context) []plugins.PluginSource
}
func (s *FakeSources) List(ctx context.Context) []plugins.PluginSource {
func (s *FakeSourceRegistry) List(ctx context.Context) []plugins.PluginSource {
if s.ListFunc != nil {
return s.ListFunc(ctx)
}
return []plugins.PluginSource{}
}
type FakePluginSource struct {
PluginClassFunc func(ctx context.Context) plugins.Class
PluginURIsFunc func(ctx context.Context) []string
DefaultSignatureFunc func(ctx context.Context) (plugins.Signature, bool)
}
func (s *FakePluginSource) PluginClass(ctx context.Context) plugins.Class {
if s.PluginClassFunc != nil {
return s.PluginClassFunc(ctx)
}
return ""
}
func (s *FakePluginSource) PluginURIs(ctx context.Context) []string {
if s.PluginURIsFunc != nil {
return s.PluginURIsFunc(ctx)
}
return []string{}
}
func (s *FakePluginSource) DefaultSignature(ctx context.Context) (plugins.Signature, bool) {
if s.DefaultSignatureFunc != nil {
return s.DefaultSignatureFunc(ctx)
}
return plugins.Signature{}, false
}

View File

@ -9,6 +9,7 @@ import (
"github.com/grafana/grafana/pkg/plugins/log"
"github.com/grafana/grafana/pkg/plugins/manager/loader"
"github.com/grafana/grafana/pkg/plugins/manager/registry"
"github.com/grafana/grafana/pkg/plugins/manager/sources"
"github.com/grafana/grafana/pkg/plugins/repo"
"github.com/grafana/grafana/pkg/plugins/storage"
)
@ -118,7 +119,7 @@ func (m *PluginInstaller) Add(ctx context.Context, pluginID, version string, opt
pathsToScan = append(pathsToScan, depArchive.Path)
}
_, err = m.pluginLoader.Load(ctx, plugins.External, pathsToScan)
_, err = m.pluginLoader.Load(ctx, sources.NewLocalSource(plugins.External, pathsToScan))
if err != nil {
m.log.Error("Could not load plugins", "paths", pathsToScan, "err", err)
return err

View File

@ -34,9 +34,9 @@ func TestPluginManager_Add_Remove(t *testing.T) {
var loadedPaths []string
loader := &fakes.FakeLoader{
LoadFunc: func(_ context.Context, _ plugins.Class, paths []string) ([]*plugins.Plugin, error) {
loadedPaths = append(loadedPaths, paths...)
require.Equal(t, []string{zipNameV1}, paths)
LoadFunc: func(ctx context.Context, src plugins.PluginSource) ([]*plugins.Plugin, error) {
loadedPaths = append(loadedPaths, src.PluginURIs(ctx)...)
require.Equal(t, []string{zipNameV1}, src.PluginURIs(ctx))
return []*plugins.Plugin{pluginV1}, nil
},
}
@ -96,9 +96,9 @@ func TestPluginManager_Add_Remove(t *testing.T) {
mockZipV2 := &zip.ReadCloser{Reader: zip.Reader{File: []*zip.File{{
FileHeader: zip.FileHeader{Name: zipNameV2},
}}}}
loader.LoadFunc = func(_ context.Context, class plugins.Class, paths []string) ([]*plugins.Plugin, error) {
require.Equal(t, plugins.External, class)
require.Equal(t, []string{zipNameV2}, paths)
loader.LoadFunc = func(ctx context.Context, src plugins.PluginSource) ([]*plugins.Plugin, error) {
require.Equal(t, plugins.External, src.PluginClass(ctx))
require.Equal(t, []string{zipNameV2}, src.PluginURIs(ctx))
return []*plugins.Plugin{pluginV2}, nil
}
pluginRepo.GetPluginDownloadOptionsFunc = func(_ context.Context, pluginID, version string, _ repo.CompatOpts) (*repo.PluginDownloadOptions, error) {

View File

@ -1,44 +0,0 @@
package finder
import (
"context"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/log"
)
type Service struct {
local *FS
log log.Logger
}
func NewService() *Service {
logger := log.New("plugin.finder")
return &Service{
local: newFS(logger),
log: logger,
}
}
func (f *Service) Find(ctx context.Context, pluginPaths ...string) ([]*plugins.FoundBundle, error) {
if len(pluginPaths) == 0 {
return []*plugins.FoundBundle{}, nil
}
fbs := make(map[string][]*plugins.FoundBundle)
for _, path := range pluginPaths {
local, err := f.local.Find(ctx, path)
if err != nil {
f.log.Warn("Error occurred when trying to find plugin", "path", path)
continue
}
fbs[path] = local
}
var found []*plugins.FoundBundle
for _, fb := range fbs {
found = append(found, fb...)
}
return found, nil
}

View File

@ -7,5 +7,5 @@ import (
)
type Finder interface {
Find(ctx context.Context, uris ...string) ([]*plugins.FoundBundle, error)
Find(ctx context.Context, src plugins.PluginSource) ([]*plugins.FoundBundle, error)
}

View File

@ -2,9 +2,9 @@ package finder
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"runtime"
@ -13,7 +13,6 @@ import (
"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/services/org"
"github.com/grafana/grafana/pkg/util"
)
@ -24,32 +23,34 @@ var (
ErrInvalidPluginJSONFilePath = errors.New("invalid plugin.json filepath was provided")
)
type FS struct {
type Local struct {
log log.Logger
}
func newFS(logger log.Logger) *FS {
return &FS{log: logger.New("fs")}
func NewLocalFinder() *Local {
return &Local{
log: log.New("local.finder"),
}
}
func (f *FS) Find(_ context.Context, pluginPaths ...string) ([]*plugins.FoundBundle, error) {
if len(pluginPaths) == 0 {
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 pluginPaths {
for _, path := range src.PluginURIs(ctx) {
exists, err := fs.Exists(path)
if err != nil {
f.log.Warn("Skipping finding plugins as an error occurred", "path", path, "err", err)
l.log.Warn("Skipping finding plugins as an error occurred", "path", path, "err", err)
continue
}
if !exists {
f.log.Warn("Skipping finding plugins as directory does not exist", "path", path)
l.log.Warn("Skipping finding plugins as directory does not exist", "path", path)
continue
}
paths, err := f.getAbsPluginJSONPaths(path)
paths, err := l.getAbsPluginJSONPaths(path)
if err != nil {
return nil, err
}
@ -59,20 +60,20 @@ func (f *FS) Find(_ context.Context, pluginPaths ...string) ([]*plugins.FoundBun
// load plugin.json files and map directory to JSON data
foundPlugins := make(map[string]plugins.JSONData)
for _, pluginJSONPath := range pluginJSONPaths {
plugin, err := f.readPluginJSON(pluginJSONPath)
plugin, err := l.readPluginJSON(pluginJSONPath)
if err != nil {
f.log.Warn("Skipping plugin loading as its plugin.json could not be read", "path", pluginJSONPath, "err", err)
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 {
f.log.Warn("Skipping plugin loading as absolute plugin.json path could not be calculated", "pluginID", plugin.ID, "err", err)
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 {
f.log.Warn("Skipping plugin loading as it's a duplicate", "pluginID", plugin.ID)
l.log.Warn("Skipping plugin loading as it's a duplicate", "pluginID", plugin.ID)
continue
}
foundPlugins[filepath.Dir(pluginJSONAbsPath)] = plugin
@ -121,7 +122,30 @@ func (f *FS) Find(_ context.Context, pluginPaths ...string) ([]*plugins.FoundBun
return result, nil
}
func (f *FS) getAbsPluginJSONPaths(path string) ([]string, error) {
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
@ -134,11 +158,11 @@ func (f *FS) getAbsPluginJSONPaths(path string) ([]string, error) {
func(currentPath string, fi os.FileInfo, err error) error {
if err != nil {
if errors.Is(err, os.ErrNotExist) {
f.log.Error("Couldn't scan directory since it doesn't exist", "pluginDir", path, "err", err)
l.log.Error("Couldn't scan directory since it doesn't exist", "pluginDir", path, "err", err)
return nil
}
if errors.Is(err, os.ErrPermission) {
f.log.Error("Couldn't scan directory due to lack of permissions", "pluginDir", path, "err", err)
l.log.Error("Couldn't scan directory due to lack of permissions", "pluginDir", path, "err", err)
return nil
}
@ -166,70 +190,6 @@ func (f *FS) getAbsPluginJSONPaths(path string) ([]string, error) {
return pluginJSONPaths, nil
}
func (f *FS) readPluginJSON(pluginJSONPath string) (plugins.JSONData, error) {
f.log.Debug("Loading plugin", "path", pluginJSONPath)
if !strings.EqualFold(filepath.Ext(pluginJSONPath), ".json") {
return plugins.JSONData{}, ErrInvalidPluginJSONFilePath
}
absPluginJSONPath, err := filepath.Abs(pluginJSONPath)
if err != nil {
return plugins.JSONData{}, err
}
// Wrapping in filepath.Clean to properly handle
// gosec G304 Potential file inclusion via variable rule.
reader, err := os.Open(filepath.Clean(absPluginJSONPath))
if err != nil {
return plugins.JSONData{}, err
}
defer func() {
if reader == nil {
return
}
if err = reader.Close(); err != nil {
f.log.Warn("Failed to close JSON file", "path", pluginJSONPath, "err", err)
}
}()
plugin := plugins.JSONData{}
if err = json.NewDecoder(reader).Decode(&plugin); err != nil {
return plugins.JSONData{}, err
}
if err = validatePluginJSON(plugin); err != nil {
return plugins.JSONData{}, err
}
if plugin.ID == "grafana-piechart-panel" {
plugin.Name = "Pie Chart (old)"
}
if len(plugin.Dependencies.Plugins) == 0 {
plugin.Dependencies.Plugins = []plugins.Dependency{}
}
if plugin.Dependencies.GrafanaVersion == "" {
plugin.Dependencies.GrafanaVersion = "*"
}
for _, include := range plugin.Includes {
if include.Role == "" {
include.Role = org.RoleViewer
}
}
return plugin, nil
}
func validatePluginJSON(data plugins.JSONData) error {
if data.ID == "" || !data.Type.IsValid() {
return ErrInvalidPluginJSON
}
return 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 {
@ -284,3 +244,20 @@ func collectFilesWithin(dir string) (map[string]struct{}, error) {
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))
}

View File

@ -13,7 +13,7 @@ import (
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/log"
"github.com/grafana/grafana/pkg/plugins/manager/fakes"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/util"
)
@ -278,8 +278,12 @@ func TestFinder_Find(t *testing.T) {
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
f := newFS(log.NewTestLogger())
pluginBundles, err := f.Find(context.Background(), tc.pluginDirs...)
f := NewLocalFinder()
pluginBundles, err := f.Find(context.Background(), &fakes.FakePluginSource{
PluginURIsFunc: func(ctx context.Context) []string {
return tc.pluginDirs
},
})
if (err != nil) && !errors.Is(err, tc.err) {
t.Errorf("Find() error = %v, expected error %v", err, tc.err)
return
@ -307,7 +311,7 @@ func TestFinder_getAbsPluginJSONPaths(t *testing.T) {
walk = origWalk
})
finder := newFS(log.NewTestLogger())
finder := NewLocalFinder()
paths, err := finder.getAbsPluginJSONPaths("test")
require.NoError(t, err)
require.Empty(t, paths)
@ -322,7 +326,7 @@ func TestFinder_getAbsPluginJSONPaths(t *testing.T) {
walk = origWalk
})
finder := newFS(log.NewTestLogger())
finder := NewLocalFinder()
paths, err := finder.getAbsPluginJSONPaths("test")
require.NoError(t, err)
require.Empty(t, paths)
@ -337,7 +341,7 @@ func TestFinder_getAbsPluginJSONPaths(t *testing.T) {
walk = origWalk
})
finder := newFS(log.NewTestLogger())
finder := NewLocalFinder()
paths, err := finder.getAbsPluginJSONPaths("test")
require.Error(t, err)
require.Empty(t, paths)
@ -396,7 +400,7 @@ func TestFinder_readPluginJSON(t *testing.T) {
name string
pluginPath string
expected plugins.JSONData
failed bool
err error
}{
{
name: "Valid plugin",
@ -444,27 +448,23 @@ func TestFinder_readPluginJSON(t *testing.T) {
},
{
name: "Invalid plugin JSON",
pluginPath: "../testdata/invalid-plugin-json/plugin.json",
failed: true,
},
{
name: "Non-existing JSON file",
pluginPath: "nonExistingFile.json",
failed: true,
pluginPath: "../../testdata/invalid-plugin-json/plugin.json",
err: ErrInvalidPluginJSON,
},
}
f := newFS(log.NewTestLogger())
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := f.readPluginJSON(tt.pluginPath)
if (err != nil) && !tt.failed {
t.Errorf("readPluginJSON() error = %v, failed %v", err, tt.failed)
return
reader, err := os.Open(tt.pluginPath)
require.NoError(t, err)
got, err := ReadPluginJSON(reader)
if tt.err != nil {
require.ErrorIs(t, err, tt.err)
}
if !cmp.Equal(got, tt.expected) {
t.Errorf("Unexpected pluginJSONData: %v", cmp.Diff(got, tt.expected))
}
require.NoError(t, reader.Close())
})
}
}

View File

@ -0,0 +1,47 @@
package finder
import (
"encoding/json"
"io"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/services/org"
)
func ReadPluginJSON(reader io.Reader) (plugins.JSONData, error) {
plugin := plugins.JSONData{}
if err := json.NewDecoder(reader).Decode(&plugin); err != nil {
return plugins.JSONData{}, err
}
if err := validatePluginJSON(plugin); err != nil {
return plugins.JSONData{}, err
}
if plugin.ID == "grafana-piechart-panel" {
plugin.Name = "Pie Chart (old)"
}
if len(plugin.Dependencies.Plugins) == 0 {
plugin.Dependencies.Plugins = []plugins.Dependency{}
}
if plugin.Dependencies.GrafanaVersion == "" {
plugin.Dependencies.GrafanaVersion = "*"
}
for _, include := range plugin.Includes {
if include.Role == "" {
include.Role = org.RoleViewer
}
}
return plugin, nil
}
func validatePluginJSON(data plugins.JSONData) error {
if data.ID == "" || !data.Type.IsValid() {
return ErrInvalidPluginJSON
}
return nil
}

View File

@ -9,7 +9,7 @@ import (
// Service is responsible for loading plugins from the file system.
type Service interface {
// Load will return a list of plugins found in the provided file system paths.
Load(ctx context.Context, class plugins.Class, paths []string) ([]*plugins.Plugin, error)
Load(ctx context.Context, src plugins.PluginSource) ([]*plugins.Plugin, error)
// Unload will unload a specified plugin from the file system.
Unload(ctx context.Context, pluginID string) error
}

View File

@ -42,18 +42,19 @@ type Loader struct {
}
func ProvideService(cfg *config.Cfg, license plugins.Licensing, authorizer plugins.PluginLoaderAuthorizer,
pluginRegistry registry.Service, backendProvider plugins.BackendFactoryProvider,
pluginRegistry registry.Service, backendProvider plugins.BackendFactoryProvider, pluginFinder finder.Finder,
roleRegistry plugins.RoleRegistry, pluginsCDNService *pluginscdn.Service, assetPath *assetpath.Service) *Loader {
return New(cfg, license, authorizer, pluginRegistry, backendProvider, process.NewManager(pluginRegistry),
storage.FileSystem(log.NewPrettyLogger("loader.fs"), cfg.PluginsPath), roleRegistry, pluginsCDNService, assetPath)
storage.FileSystem(log.NewPrettyLogger("loader.fs"), cfg.PluginsPath), roleRegistry, pluginsCDNService,
assetPath, pluginFinder)
}
func New(cfg *config.Cfg, license plugins.Licensing, authorizer plugins.PluginLoaderAuthorizer,
pluginRegistry registry.Service, backendProvider plugins.BackendFactoryProvider,
processManager process.Service, pluginStorage storage.Manager, roleRegistry plugins.RoleRegistry,
pluginsCDNService *pluginscdn.Service, assetPath *assetpath.Service) *Loader {
pluginsCDNService *pluginscdn.Service, assetPath *assetpath.Service, pluginFinder finder.Finder) *Loader {
return &Loader{
pluginFinder: finder.NewService(),
pluginFinder: pluginFinder,
pluginRegistry: pluginRegistry,
pluginInitializer: initializer.New(cfg, backendProvider, license),
signatureValidator: signature.NewValidator(authorizer),
@ -68,16 +69,16 @@ func New(cfg *config.Cfg, license plugins.Licensing, authorizer plugins.PluginLo
}
}
func (l *Loader) Load(ctx context.Context, class plugins.Class, paths []string) ([]*plugins.Plugin, error) {
found, err := l.pluginFinder.Find(ctx, paths...)
func (l *Loader) Load(ctx context.Context, src plugins.PluginSource) ([]*plugins.Plugin, error) {
found, err := l.pluginFinder.Find(ctx, src)
if err != nil {
return nil, err
}
return l.loadPlugins(ctx, class, found)
return l.loadPlugins(ctx, src, found)
}
func (l *Loader) loadPlugins(ctx context.Context, class plugins.Class, found []*plugins.FoundBundle) ([]*plugins.Plugin, error) {
func (l *Loader) loadPlugins(ctx context.Context, src plugins.PluginSource, found []*plugins.FoundBundle) ([]*plugins.Plugin, error) {
var loadedPlugins []*plugins.Plugin
for _, p := range found {
if _, exists := l.pluginRegistry.Plugin(ctx, p.Primary.JSONData.ID); exists {
@ -91,12 +92,13 @@ func (l *Loader) loadPlugins(ctx context.Context, class plugins.Class, found []*
sig = plugins.Signature{Status: plugins.SignatureValid}
} else {
var err error
sig, err = signature.Calculate(l.log, class, p.Primary)
sig, err = signature.Calculate(ctx, l.log, src, p.Primary)
if err != nil {
l.log.Warn("Could not calculate plugin signature state", "pluginID", p.Primary.JSONData.ID, "err", err)
continue
}
}
class := src.PluginClass(ctx)
plugin, err := l.createPluginBase(p.Primary.JSONData, class, p.Primary.FS)
if err != nil {
l.log.Error("Could not create primary plugin base", "pluginID", p.Primary.JSONData.ID, "err", err)

View File

@ -7,9 +7,6 @@ import (
"sort"
"testing"
"github.com/grafana/grafana/pkg/plugins/manager/loader/assetpath"
"github.com/grafana/grafana/pkg/plugins/pluginscdn"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/stretchr/testify/require"
@ -18,8 +15,12 @@ import (
"github.com/grafana/grafana/pkg/plugins/config"
"github.com/grafana/grafana/pkg/plugins/log"
"github.com/grafana/grafana/pkg/plugins/manager/fakes"
"github.com/grafana/grafana/pkg/plugins/manager/loader/assetpath"
"github.com/grafana/grafana/pkg/plugins/manager/loader/finder"
"github.com/grafana/grafana/pkg/plugins/manager/loader/initializer"
"github.com/grafana/grafana/pkg/plugins/manager/signature"
"github.com/grafana/grafana/pkg/plugins/manager/sources"
"github.com/grafana/grafana/pkg/plugins/pluginscdn"
"github.com/grafana/grafana/pkg/setting"
)
@ -29,6 +30,7 @@ var localFSComparer = cmp.Comparer(func(fs1 plugins.LocalFS, fs2 plugins.LocalFS
fs1Files := fs1.Files()
fs2Files := fs2.Files()
finder.NewLocalFinder()
sort.SliceStable(fs1Files, func(i, j int) bool {
return fs1Files[i] < fs1Files[j]
})
@ -516,7 +518,7 @@ func TestLoader_Load(t *testing.T) {
})
t.Run(tt.name, func(t *testing.T) {
got, err := l.Load(context.Background(), tt.class, tt.pluginPaths)
got, err := l.Load(context.Background(), sources.NewLocalSource(tt.class, tt.pluginPaths))
require.NoError(t, err)
if !cmp.Equal(got, tt.want, compareOpts...) {
t.Fatalf("Result mismatch (-want +got):\n%s", cmp.Diff(got, tt.want, compareOpts...))
@ -679,7 +681,14 @@ func TestLoader_Load_MultiplePlugins(t *testing.T) {
})
setting.AppUrl = tt.appURL
got, err := l.Load(context.Background(), plugins.External, tt.pluginPaths)
got, err := l.Load(context.Background(), &fakes.FakePluginSource{
PluginClassFunc: func(ctx context.Context) plugins.Class {
return plugins.External
},
PluginURIsFunc: func(ctx context.Context) []string {
return tt.pluginPaths
},
})
require.NoError(t, err)
sort.SliceStable(got, func(i, j int) bool {
return got[i].ID < got[j].ID
@ -792,7 +801,14 @@ func TestLoader_Load_RBACReady(t *testing.T) {
l.pluginInitializer = initializer.New(tt.cfg, procPrvdr, fakes.NewFakeLicensingService())
})
got, err := l.Load(context.Background(), plugins.External, tt.pluginPaths)
got, err := l.Load(context.Background(), &fakes.FakePluginSource{
PluginClassFunc: func(ctx context.Context) plugins.Class {
return plugins.External
},
PluginURIsFunc: func(ctx context.Context) []string {
return tt.pluginPaths
},
})
require.NoError(t, err)
if !cmp.Equal(got, tt.want, compareOpts...) {
@ -868,7 +884,14 @@ func TestLoader_Load_Signature_RootURL(t *testing.T) {
l.processManager = procMgr
l.pluginInitializer = initializer.New(&config.Cfg{}, procPrvdr, fakes.NewFakeLicensingService())
})
got, err := l.Load(context.Background(), plugins.External, paths)
got, err := l.Load(context.Background(), &fakes.FakePluginSource{
PluginClassFunc: func(ctx context.Context) plugins.Class {
return plugins.External
},
PluginURIsFunc: func(ctx context.Context) []string {
return paths
},
})
require.NoError(t, err)
if !cmp.Equal(got, expected, compareOpts...) {
@ -947,7 +970,14 @@ func TestLoader_Load_DuplicatePlugins(t *testing.T) {
l.processManager = procMgr
l.pluginInitializer = initializer.New(&config.Cfg{}, procPrvdr, fakes.NewFakeLicensingService())
})
got, err := l.Load(context.Background(), plugins.External, []string{pluginDir, pluginDir})
got, err := l.Load(context.Background(), &fakes.FakePluginSource{
PluginClassFunc: func(ctx context.Context) plugins.Class {
return plugins.External
},
PluginURIsFunc: func(ctx context.Context) []string {
return []string{pluginDir, pluginDir}
},
})
require.NoError(t, err)
if !cmp.Equal(got, expected, compareOpts...) {
@ -1046,7 +1076,14 @@ func TestLoader_Load_NestedPlugins(t *testing.T) {
l.pluginInitializer = initializer.New(&config.Cfg{}, procPrvdr, fakes.NewFakeLicensingService())
})
got, err := l.Load(context.Background(), plugins.External, []string{"../testdata/nested-plugins"})
got, err := l.Load(context.Background(), &fakes.FakePluginSource{
PluginClassFunc: func(ctx context.Context) plugins.Class {
return plugins.External
},
PluginURIsFunc: func(ctx context.Context) []string {
return []string{"../testdata/nested-plugins"}
},
})
require.NoError(t, err)
// to ensure we can compare with expected
@ -1062,7 +1099,14 @@ func TestLoader_Load_NestedPlugins(t *testing.T) {
verifyState(t, expected, reg, procPrvdr, storage, procMgr)
t.Run("Load will exclude plugins that already exist", func(t *testing.T) {
got, err := l.Load(context.Background(), plugins.External, []string{"../testdata/nested-plugins"})
got, err := l.Load(context.Background(), &fakes.FakePluginSource{
PluginClassFunc: func(ctx context.Context) plugins.Class {
return plugins.External
},
PluginURIsFunc: func(ctx context.Context) []string {
return []string{"../testdata/nested-plugins"}
},
})
require.NoError(t, err)
// to ensure we can compare with expected
@ -1213,7 +1257,14 @@ func TestLoader_Load_NestedPlugins(t *testing.T) {
l.processManager = procMgr
l.pluginInitializer = initializer.New(&config.Cfg{}, procPrvdr, fakes.NewFakeLicensingService())
})
got, err := l.Load(context.Background(), plugins.External, []string{"../testdata/app-with-child"})
got, err := l.Load(context.Background(), &fakes.FakePluginSource{
PluginClassFunc: func(ctx context.Context) plugins.Class {
return plugins.External
},
PluginURIsFunc: func(ctx context.Context) []string {
return []string{"../testdata/app-with-child"}
},
})
require.NoError(t, err)
// to ensure we can compare with expected
@ -1256,7 +1307,7 @@ func newLoader(cfg *config.Cfg, cbs ...func(loader *Loader)) *Loader {
cdn := pluginscdn.ProvideService(cfg)
l := New(cfg, &fakes.FakeLicensingService{}, signature.NewUnsignedAuthorizer(cfg), fakes.NewFakePluginRegistry(),
fakes.NewFakeBackendProcessProvider(), fakes.NewFakeProcessManager(), fakes.NewFakePluginStorage(),
fakes.NewFakeRoleRegistry(), cdn, assetpath.ProvideService(cdn))
fakes.NewFakeRoleRegistry(), cdn, assetpath.ProvideService(cdn), finder.NewLocalFinder())
for _, cb := range cbs {
cb(l)

View File

@ -24,6 +24,7 @@ import (
"github.com/grafana/grafana/pkg/plugins/manager/fakes"
"github.com/grafana/grafana/pkg/plugins/manager/loader"
"github.com/grafana/grafana/pkg/plugins/manager/loader/assetpath"
"github.com/grafana/grafana/pkg/plugins/manager/loader/finder"
"github.com/grafana/grafana/pkg/plugins/manager/registry"
"github.com/grafana/grafana/pkg/plugins/manager/signature"
"github.com/grafana/grafana/pkg/plugins/manager/sources"
@ -116,7 +117,7 @@ func TestIntegrationPluginManager(t *testing.T) {
lic := plicensing.ProvideLicensing(cfg, &licensing.OSSLicensingService{Cfg: cfg})
l := loader.ProvideService(pCfg, lic, signature.NewUnsignedAuthorizer(pCfg),
reg, provider.ProvideService(coreRegistry), fakes.NewFakeRoleRegistry(),
reg, provider.ProvideService(coreRegistry), finder.NewLocalFinder(), fakes.NewFakeRoleRegistry(),
cdn, assetpath.ProvideService(cdn))
srcs := sources.ProvideService(cfg, pCfg)
ps, err := store.ProvideService(reg, srcs, l)

View File

@ -2,6 +2,7 @@ package signature
import (
"bytes"
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
@ -76,7 +77,7 @@ func (m *PluginManifest) isV2() bool {
return strings.HasPrefix(m.ManifestVersion, "2.")
}
// readPluginManifest attempts to read and verify the plugin manifest
// ReadPluginManifest attempts to read and verify the plugin manifest
// if any error occurs or the manifest is not valid, this will return an error
func ReadPluginManifest(body []byte) (*PluginManifest, error) {
block, _ := clearsign.Decode(body)
@ -98,11 +99,9 @@ func ReadPluginManifest(body []byte) (*PluginManifest, error) {
return &manifest, nil
}
func Calculate(mlog log.Logger, class plugins.Class, plugin plugins.FoundPlugin) (plugins.Signature, error) {
if class == plugins.Core {
return plugins.Signature{
Status: plugins.SignatureInternal,
}, nil
func Calculate(ctx context.Context, mlog log.Logger, src plugins.PluginSource, plugin plugins.FoundPlugin) (plugins.Signature, error) {
if defaultSignature, exists := src.DefaultSignature(ctx); exists {
return defaultSignature, nil
}
if len(plugin.FS.Files()) == 0 {

View File

@ -1,16 +1,19 @@
package signature
import (
"context"
"path/filepath"
"sort"
"strings"
"testing"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/log"
"github.com/grafana/grafana/pkg/setting"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/log"
"github.com/grafana/grafana/pkg/plugins/manager/fakes"
"github.com/grafana/grafana/pkg/setting"
)
func TestReadPluginManifest(t *testing.T) {
@ -152,7 +155,11 @@ func TestCalculate(t *testing.T) {
setting.AppUrl = tc.appURL
basePath := filepath.Join(parentDir, "testdata/non-pvt-with-root-url/plugin")
sig, err := Calculate(log.NewTestLogger(), plugins.External, plugins.FoundPlugin{
sig, err := Calculate(context.Background(), log.NewTestLogger(), &fakes.FakePluginSource{
PluginClassFunc: func(ctx context.Context) plugins.Class {
return plugins.External
},
}, plugins.FoundPlugin{
JSONData: plugins.JSONData{
ID: "test-datasource",
Info: plugins.Info{
@ -178,7 +185,11 @@ func TestCalculate(t *testing.T) {
basePath := "../testdata/renderer-added-file/plugin"
runningWindows = true
sig, err := Calculate(log.NewTestLogger(), plugins.External, plugins.FoundPlugin{
sig, err := Calculate(context.Background(), log.NewTestLogger(), &fakes.FakePluginSource{
PluginClassFunc: func(ctx context.Context) plugins.Class {
return plugins.External
},
}, plugins.FoundPlugin{
JSONData: plugins.JSONData{
ID: "test-renderer",
Type: plugins.Renderer,

View File

@ -6,6 +6,6 @@ import (
"github.com/grafana/grafana/pkg/plugins"
)
type Resolver interface {
type Registry interface {
List(context.Context) []plugins.PluginSource
}

View File

@ -0,0 +1,38 @@
package sources
import (
"context"
"github.com/grafana/grafana/pkg/plugins"
)
type LocalSource struct {
paths []string
class plugins.Class
}
func NewLocalSource(class plugins.Class, paths []string) *LocalSource {
return &LocalSource{
class: class,
paths: paths,
}
}
func (s *LocalSource) PluginClass(_ context.Context) plugins.Class {
return s.class
}
func (s *LocalSource) PluginURIs(_ context.Context) []string {
return s.paths
}
func (s *LocalSource) DefaultSignature(_ context.Context) (plugins.Signature, bool) {
switch s.class {
case plugins.Core:
return plugins.Signature{
Status: plugins.SignatureInternal,
}, true
default:
return plugins.Signature{}, false
}
}

View File

@ -26,9 +26,9 @@ func ProvideService(gCfg *setting.Cfg, cfg *config.Cfg) *Service {
func (s *Service) List(_ context.Context) []plugins.PluginSource {
return []plugins.PluginSource{
{Class: plugins.Core, Paths: corePluginPaths(s.gCfg.StaticRootPath)},
{Class: plugins.Bundled, Paths: []string{s.gCfg.BundledPluginsPath}},
{Class: plugins.External, Paths: append([]string{s.cfg.PluginsPath}, pluginFSPaths(s.cfg.PluginSettings)...)},
NewLocalSource(plugins.Core, corePluginPaths(s.gCfg.StaticRootPath)),
NewLocalSource(plugins.Bundled, []string{s.gCfg.BundledPluginsPath}),
NewLocalSource(plugins.External, append([]string{s.cfg.PluginsPath}, pluginFSPaths(s.cfg.PluginSettings)...)),
}
}

View File

@ -12,7 +12,7 @@ import (
)
func TestSources_List(t *testing.T) {
t.Run("Plugin sources are added in order", func(t *testing.T) {
t.Run("Plugin sources are populated by default and listed in specific order", func(t *testing.T) {
cfg := &setting.Cfg{
BundledPluginsPath: "path1",
}
@ -31,11 +31,28 @@ func TestSources_List(t *testing.T) {
s := ProvideService(cfg, pCfg)
srcs := s.List(context.Background())
expected := []plugins.PluginSource{
{Class: plugins.Core, Paths: []string{"app/plugins/datasource", "app/plugins/panel"}},
{Class: plugins.Bundled, Paths: []string{"path1"}},
{Class: plugins.External, Paths: []string{"path2", "path3"}},
}
require.Equal(t, expected, srcs)
ctx := context.Background()
require.Len(t, srcs, 3)
require.Equal(t, srcs[0].PluginClass(ctx), plugins.Core)
require.Equal(t, srcs[0].PluginURIs(ctx), []string{"app/plugins/datasource", "app/plugins/panel"})
sig, exists := srcs[0].DefaultSignature(ctx)
require.True(t, exists)
require.Equal(t, plugins.SignatureInternal, sig.Status)
require.Equal(t, plugins.SignatureType(""), sig.Type)
require.Equal(t, "", sig.SigningOrg)
require.Equal(t, srcs[1].PluginClass(ctx), plugins.Bundled)
require.Equal(t, srcs[1].PluginURIs(ctx), []string{"path1"})
sig, exists = srcs[1].DefaultSignature(ctx)
require.False(t, exists)
require.Equal(t, plugins.Signature{}, sig)
require.Equal(t, srcs[2].PluginClass(ctx), plugins.External)
require.Equal(t, srcs[2].PluginURIs(ctx), []string{"path2", "path3"})
sig, exists = srcs[2].DefaultSignature(ctx)
require.False(t, exists)
require.Equal(t, plugins.Signature{}, sig)
})
}

View File

@ -16,11 +16,11 @@ type Service struct {
pluginRegistry registry.Service
}
func ProvideService(pluginRegistry registry.Service, pluginSources sources.Resolver,
func ProvideService(pluginRegistry registry.Service, pluginSources sources.Registry,
pluginLoader loader.Service) (*Service, error) {
ctx := context.Background()
for _, ps := range pluginSources.List(ctx) {
if _, err := pluginLoader.Load(ctx, ps.Class, ps.Paths); err != nil {
if _, err := pluginLoader.Load(ctx, ps); err != nil {
return nil, err
}
}

View File

@ -15,21 +15,29 @@ func TestStore_ProvideService(t *testing.T) {
t.Run("Plugin sources are added in order", func(t *testing.T) {
var addedPaths []string
l := &fakes.FakeLoader{
LoadFunc: func(ctx context.Context, class plugins.Class, paths []string) ([]*plugins.Plugin, error) {
addedPaths = append(addedPaths, paths...)
LoadFunc: func(ctx context.Context, src plugins.PluginSource) ([]*plugins.Plugin, error) {
addedPaths = append(addedPaths, src.PluginURIs(ctx)...)
return nil, nil
},
}
srcs := &fakes.FakeSources{ListFunc: func(_ context.Context) []plugins.PluginSource {
srcs := &fakes.FakeSourceRegistry{ListFunc: func(_ context.Context) []plugins.PluginSource {
return []plugins.PluginSource{
{
Class: plugins.Bundled,
Paths: []string{"path1"},
&fakes.FakePluginSource{
PluginClassFunc: func(ctx context.Context) plugins.Class {
return plugins.Bundled
},
PluginURIsFunc: func(ctx context.Context) []string {
return []string{"path1"}
},
},
{
Class: plugins.External,
Paths: []string{"path2", "path3"},
&fakes.FakePluginSource{
PluginClassFunc: func(ctx context.Context) plugins.Class {
return plugins.External
},
PluginURIsFunc: func(ctx context.Context) []string {
return []string{"path2", "path3"}
},
},
}
}}

View File

@ -20,7 +20,10 @@ import (
"github.com/grafana/grafana/pkg/util"
)
var ErrFileNotExist = errors.New("file does not exist")
var (
ErrFileNotExist = errors.New("file does not exist")
ErrPluginFileRead = errors.New("file could not be read")
)
type Plugin struct {
JSONData
@ -427,6 +430,10 @@ const (
External Class = "external"
)
func (c Class) String() string {
return string(c)
}
var PluginTypes = []Type{
DataSource,
Panel,

View File

@ -2,6 +2,7 @@ package pluginsintegration
import (
"github.com/google/wire"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/backendplugin/coreplugin"
"github.com/grafana/grafana/pkg/plugins/backendplugin/provider"
@ -11,6 +12,7 @@ import (
"github.com/grafana/grafana/pkg/plugins/manager/client"
"github.com/grafana/grafana/pkg/plugins/manager/loader"
"github.com/grafana/grafana/pkg/plugins/manager/loader/assetpath"
"github.com/grafana/grafana/pkg/plugins/manager/loader/finder"
"github.com/grafana/grafana/pkg/plugins/manager/process"
"github.com/grafana/grafana/pkg/plugins/manager/registry"
"github.com/grafana/grafana/pkg/plugins/manager/signature"
@ -53,7 +55,7 @@ var WireSet = wire.NewSet(
plugincontext.ProvideService,
licensing.ProvideLicensing,
wire.Bind(new(plugins.Licensing), new(*licensing.Service)),
wire.Bind(new(sources.Resolver), new(*sources.Service)),
wire.Bind(new(sources.Registry), new(*sources.Service)),
sources.ProvideService,
pluginSettings.ProvideService,
wire.Bind(new(pluginsettings.Service), new(*pluginSettings.Service)),
@ -66,6 +68,8 @@ var WireExtensionSet = wire.NewSet(
wire.Bind(new(plugins.BackendFactoryProvider), new(*provider.Service)),
signature.ProvideOSSAuthorizer,
wire.Bind(new(plugins.PluginLoaderAuthorizer), new(*signature.UnsignedPluginAuthorizer)),
wire.Bind(new(finder.Finder), new(*finder.Local)),
finder.NewLocalFinder,
)
func ProvideClientDecorator(cfg *setting.Cfg, pCfg *config.Cfg,