mirror of https://github.com/grafana/grafana.git
				
				
				
			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:
		
							parent
							
								
									2b21bdf4e1
								
							
						
					
					
						commit
						3d37f969e7
					
				|  | @ -13,7 +13,6 @@ import ( | |||
| 	"github.com/grafana/grafana/pkg/cmd/grafana-cli/logger" | ||||
| 	"github.com/grafana/grafana/pkg/cmd/grafana-cli/models" | ||||
| 	"github.com/grafana/grafana/pkg/plugins" | ||||
| 	"github.com/grafana/grafana/pkg/plugins/manager/loader/finder" | ||||
| 	"github.com/grafana/grafana/pkg/plugins/manager/sources" | ||||
| ) | ||||
| 
 | ||||
|  | @ -77,9 +76,7 @@ func GetLocalPlugin(pluginDir, pluginID string) (plugins.FoundPlugin, error) { | |||
| } | ||||
| 
 | ||||
| func GetLocalPlugins(pluginDir string) []*plugins.FoundBundle { | ||||
| 	f := finder.NewLocalFinder(true) | ||||
| 
 | ||||
| 	res, err := f.Find(context.Background(), sources.NewLocalSource(plugins.ClassExternal, []string{pluginDir})) | ||||
| 	res, err := sources.NewLocalSource(plugins.ClassExternal, []string{pluginDir}).Discover(context.Background()) | ||||
| 	if err != nil { | ||||
| 		logger.Error("Could not get local plugins", err) | ||||
| 		return make([]*plugins.FoundBundle, 0) | ||||
|  |  | |||
|  | @ -18,9 +18,12 @@ type Installer interface { | |||
| } | ||||
| 
 | ||||
| type PluginSource interface { | ||||
| 	// PluginClass is the associated Class of plugin for this source
 | ||||
| 	PluginClass(ctx context.Context) Class | ||||
| 	PluginURIs(ctx context.Context) []string | ||||
| 	// DefaultSignature is the (optional) default signature information for this source
 | ||||
| 	DefaultSignature(ctx context.Context, pluginID string) (Signature, bool) | ||||
| 	// Discover finds and returns plugin bundles from this source
 | ||||
| 	Discover(ctx context.Context) ([]*FoundBundle, error) | ||||
| } | ||||
| 
 | ||||
| type FileStore interface { | ||||
|  |  | |||
|  | @ -480,7 +480,7 @@ func (s *FakeSourceRegistry) List(ctx context.Context) []plugins.PluginSource { | |||
| 
 | ||||
| type FakePluginSource struct { | ||||
| 	PluginClassFunc      func(ctx context.Context) plugins.Class | ||||
| 	PluginURIsFunc       func(ctx context.Context) []string | ||||
| 	DiscoverFunc         func(ctx context.Context) ([]*plugins.FoundBundle, error) | ||||
| 	DefaultSignatureFunc func(ctx context.Context) (plugins.Signature, bool) | ||||
| } | ||||
| 
 | ||||
|  | @ -491,11 +491,11 @@ func (s *FakePluginSource) PluginClass(ctx context.Context) plugins.Class { | |||
| 	return "" | ||||
| } | ||||
| 
 | ||||
| func (s *FakePluginSource) PluginURIs(ctx context.Context) []string { | ||||
| 	if s.PluginURIsFunc != nil { | ||||
| 		return s.PluginURIsFunc(ctx) | ||||
| func (s *FakePluginSource) Discover(ctx context.Context) ([]*plugins.FoundBundle, error) { | ||||
| 	if s.DiscoverFunc != nil { | ||||
| 		return s.DiscoverFunc(ctx) | ||||
| 	} | ||||
| 	return []string{} | ||||
| 	return []*plugins.FoundBundle{}, nil | ||||
| } | ||||
| 
 | ||||
| func (s *FakePluginSource) DefaultSignature(ctx context.Context, _ string) (plugins.Signature, bool) { | ||||
|  |  | |||
|  | @ -25,6 +25,8 @@ type PluginInstaller struct { | |||
| 	pluginStorageDirFunc storage.DirNameGeneratorFunc | ||||
| 	pluginRegistry       registry.Service | ||||
| 	pluginLoader         loader.Service | ||||
| 	cfg                  *config.PluginManagementCfg | ||||
| 
 | ||||
| 	installing      sync.Map | ||||
| 	log             log.Logger | ||||
| 	serviceRegistry auth.ExternalServiceRegistry | ||||
|  | @ -32,12 +34,12 @@ type PluginInstaller struct { | |||
| 
 | ||||
| func ProvideInstaller(cfg *config.PluginManagementCfg, pluginRegistry registry.Service, pluginLoader loader.Service, | ||||
| 	pluginRepo repo.Service, serviceRegistry auth.ExternalServiceRegistry) *PluginInstaller { | ||||
| 	return New(pluginRegistry, pluginLoader, pluginRepo, | ||||
| 	return New(cfg, pluginRegistry, pluginLoader, pluginRepo, | ||||
| 		storage.FileSystem(log.NewPrettyLogger("installer.fs"), cfg.PluginsPath), storage.SimpleDirNameGeneratorFunc, serviceRegistry) | ||||
| } | ||||
| 
 | ||||
| func New(pluginRegistry registry.Service, pluginLoader loader.Service, pluginRepo repo.Service, | ||||
| 	pluginStorage storage.ZipExtractor, pluginStorageDirFunc storage.DirNameGeneratorFunc, | ||||
| func New(cfg *config.PluginManagementCfg, pluginRegistry registry.Service, pluginLoader loader.Service, | ||||
| 	pluginRepo repo.Service, pluginStorage storage.ZipExtractor, pluginStorageDirFunc storage.DirNameGeneratorFunc, | ||||
| 	serviceRegistry auth.ExternalServiceRegistry) *PluginInstaller { | ||||
| 	return &PluginInstaller{ | ||||
| 		pluginLoader:         pluginLoader, | ||||
|  | @ -45,6 +47,7 @@ func New(pluginRegistry registry.Service, pluginLoader loader.Service, pluginRep | |||
| 		pluginRepo:           pluginRepo, | ||||
| 		pluginStorage:        pluginStorage, | ||||
| 		pluginStorageDirFunc: pluginStorageDirFunc, | ||||
| 		cfg:                  cfg, | ||||
| 		installing:           sync.Map{}, | ||||
| 		log:                  log.New("plugin.installer"), | ||||
| 		serviceRegistry:      serviceRegistry, | ||||
|  |  | |||
|  | @ -11,8 +11,10 @@ import ( | |||
| 	"github.com/stretchr/testify/require" | ||||
| 
 | ||||
| 	"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/manager/fakes" | ||||
| 	"github.com/grafana/grafana/pkg/plugins/manager/sources" | ||||
| 	"github.com/grafana/grafana/pkg/plugins/repo" | ||||
| 	"github.com/grafana/grafana/pkg/plugins/storage" | ||||
| ) | ||||
|  | @ -36,11 +38,8 @@ func TestPluginManager_Add_Remove(t *testing.T) { | |||
| 			FileHeader: zip.FileHeader{Name: zipNameV1}, | ||||
| 		}}}} | ||||
| 
 | ||||
| 		var loadedPaths []string | ||||
| 		loader := &fakes.FakeLoader{ | ||||
| 			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 | ||||
| 			}, | ||||
| 			UnloadFunc: func(_ context.Context, p *plugins.Plugin) (*plugins.Plugin, error) { | ||||
|  | @ -70,7 +69,7 @@ func TestPluginManager_Add_Remove(t *testing.T) { | |||
| 			}, | ||||
| 		} | ||||
| 
 | ||||
| 		inst := New(fakes.NewFakePluginRegistry(), loader, pluginRepo, fs, storage.SimpleDirNameGeneratorFunc, &fakes.FakeAuthService{}) | ||||
| 		inst := New(&config.PluginManagementCfg{}, fakes.NewFakePluginRegistry(), loader, pluginRepo, fs, storage.SimpleDirNameGeneratorFunc, &fakes.FakeAuthService{}) | ||||
| 		err := inst.Add(context.Background(), pluginID, v1, testCompatOpts()) | ||||
| 		require.NoError(t, err) | ||||
| 
 | ||||
|  | @ -98,7 +97,7 @@ func TestPluginManager_Add_Remove(t *testing.T) { | |||
| 					}, nil | ||||
| 				}, | ||||
| 			} | ||||
| 			inst := New(fakes.NewFakePluginRegistry(), loader, pluginRepo, fs, storage.SimpleDirNameGeneratorFunc, &fakes.FakeAuthService{}) | ||||
| 			inst := New(&config.PluginManagementCfg{}, fakes.NewFakePluginRegistry(), loader, pluginRepo, fs, storage.SimpleDirNameGeneratorFunc, &fakes.FakeAuthService{}) | ||||
| 			err := inst.Add(context.Background(), pluginID, v1, plugins.NewAddOpts(v1, runtime.GOOS, runtime.GOARCH, url)) | ||||
| 			require.NoError(t, err) | ||||
| 		}) | ||||
|  | @ -114,7 +113,6 @@ func TestPluginManager_Add_Remove(t *testing.T) { | |||
| 			}}}} | ||||
| 			loader.LoadFunc = func(ctx context.Context, src plugins.PluginSource) ([]*plugins.Plugin, error) { | ||||
| 				require.Equal(t, plugins.ClassExternal, src.PluginClass(ctx)) | ||||
| 				require.Equal(t, []string{zipNameV2}, src.PluginURIs(ctx)) | ||||
| 				return []*plugins.Plugin{pluginV2}, nil | ||||
| 			} | ||||
| 			pluginRepo.GetPluginArchiveInfoFunc = func(_ context.Context, _, _ string, _ repo.CompatOpts) (*repo.PluginArchiveInfo, error) { | ||||
|  | @ -154,7 +152,6 @@ func TestPluginManager_Add_Remove(t *testing.T) { | |||
| 			}}}} | ||||
| 			loader.LoadFunc = func(ctx context.Context, src plugins.PluginSource) ([]*plugins.Plugin, error) { | ||||
| 				require.Equal(t, plugins.ClassExternal, src.PluginClass(ctx)) | ||||
| 				require.Equal(t, []string{zipNameV2}, src.PluginURIs(ctx)) | ||||
| 				return []*plugins.Plugin{pluginV2}, nil | ||||
| 			} | ||||
| 			pluginRepo.GetPluginArchiveInfoFunc = func(_ context.Context, _, _ string, _ repo.CompatOpts) (*repo.PluginArchiveInfo, error) { | ||||
|  | @ -223,7 +220,7 @@ func TestPluginManager_Add_Remove(t *testing.T) { | |||
| 			}, | ||||
| 		} | ||||
| 
 | ||||
| 		pm := New(reg, &fakes.FakeLoader{}, &fakes.FakePluginRepo{}, &fakes.FakePluginStorage{}, storage.SimpleDirNameGeneratorFunc, &fakes.FakeAuthService{}) | ||||
| 		pm := New(&config.PluginManagementCfg{}, reg, &fakes.FakeLoader{}, &fakes.FakePluginRepo{}, &fakes.FakePluginStorage{}, storage.SimpleDirNameGeneratorFunc, &fakes.FakeAuthService{}) | ||||
| 		err := pm.Add(context.Background(), p.ID, "3.2.0", testCompatOpts()) | ||||
| 		require.ErrorIs(t, err, plugins.ErrInstallCorePlugin) | ||||
| 
 | ||||
|  | @ -246,7 +243,10 @@ func TestPluginManager_Add_Remove(t *testing.T) { | |||
| 		var loadedPaths []string | ||||
| 		loader := &fakes.FakeLoader{ | ||||
| 			LoadFunc: func(ctx context.Context, src plugins.PluginSource) ([]*plugins.Plugin, error) { | ||||
| 				loadedPaths = append(loadedPaths, src.PluginURIs(ctx)...) | ||||
| 				// Check if this is a LocalSource and get its paths
 | ||||
| 				if localSrc, ok := src.(*sources.LocalSource); ok { | ||||
| 					loadedPaths = append(loadedPaths, localSrc.Paths()...) | ||||
| 				} | ||||
| 				return []*plugins.Plugin{}, nil | ||||
| 			}, | ||||
| 		} | ||||
|  | @ -285,7 +285,7 @@ func TestPluginManager_Add_Remove(t *testing.T) { | |||
| 			}, | ||||
| 		} | ||||
| 
 | ||||
| 		inst := New(fakes.NewFakePluginRegistry(), loader, pluginRepo, fs, storage.SimpleDirNameGeneratorFunc, &fakes.FakeAuthService{}) | ||||
| 		inst := New(&config.PluginManagementCfg{}, fakes.NewFakePluginRegistry(), loader, pluginRepo, fs, storage.SimpleDirNameGeneratorFunc, &fakes.FakeAuthService{}) | ||||
| 		err := inst.Add(context.Background(), p3, "", testCompatOpts()) | ||||
| 		require.NoError(t, err) | ||||
| 		require.Equal(t, []string{p1Zip, p2Zip, p3Zip}, loadedPaths) | ||||
|  | @ -300,7 +300,10 @@ func TestPluginManager_Add_Remove(t *testing.T) { | |||
| 		var loadedPaths []string | ||||
| 		loader := &fakes.FakeLoader{ | ||||
| 			LoadFunc: func(ctx context.Context, src plugins.PluginSource) ([]*plugins.Plugin, error) { | ||||
| 				loadedPaths = append(loadedPaths, src.PluginURIs(ctx)...) | ||||
| 				// Check if this is a LocalSource and get its paths
 | ||||
| 				if localSrc, ok := src.(*sources.LocalSource); ok { | ||||
| 					loadedPaths = append(loadedPaths, localSrc.Paths()...) | ||||
| 				} | ||||
| 				return []*plugins.Plugin{}, nil | ||||
| 			}, | ||||
| 		} | ||||
|  | @ -334,7 +337,7 @@ func TestPluginManager_Add_Remove(t *testing.T) { | |||
| 			}, | ||||
| 		} | ||||
| 
 | ||||
| 		inst := New(fakes.NewFakePluginRegistry(), loader, pluginRepo, fs, storage.SimpleDirNameGeneratorFunc, &fakes.FakeAuthService{}) | ||||
| 		inst := New(&config.PluginManagementCfg{}, fakes.NewFakePluginRegistry(), loader, pluginRepo, fs, storage.SimpleDirNameGeneratorFunc, &fakes.FakeAuthService{}) | ||||
| 		err := inst.Add(context.Background(), p1, "", testCompatOpts()) | ||||
| 		require.NoError(t, err) | ||||
| 		require.Equal(t, []string{p2Zip, p1Zip}, loadedPaths) | ||||
|  | @ -351,7 +354,10 @@ func TestPluginManager_Add_Remove(t *testing.T) { | |||
| 		var loadedPaths []string | ||||
| 		loader := &fakes.FakeLoader{ | ||||
| 			LoadFunc: func(ctx context.Context, src plugins.PluginSource) ([]*plugins.Plugin, error) { | ||||
| 				loadedPaths = append(loadedPaths, src.PluginURIs(ctx)...) | ||||
| 				// Check if this is a LocalSource and get its paths
 | ||||
| 				if localSrc, ok := src.(*sources.LocalSource); ok { | ||||
| 					loadedPaths = append(loadedPaths, localSrc.Paths()...) | ||||
| 				} | ||||
| 				return []*plugins.Plugin{}, nil | ||||
| 			}, | ||||
| 		} | ||||
|  | @ -379,7 +385,7 @@ func TestPluginManager_Add_Remove(t *testing.T) { | |||
| 			}, | ||||
| 		} | ||||
| 
 | ||||
| 		inst := New(reg, loader, pluginRepo, fs, storage.SimpleDirNameGeneratorFunc, &fakes.FakeAuthService{}) | ||||
| 		inst := New(&config.PluginManagementCfg{}, reg, loader, pluginRepo, fs, storage.SimpleDirNameGeneratorFunc, &fakes.FakeAuthService{}) | ||||
| 		err := inst.Add(context.Background(), testPluginID, "", testCompatOpts()) | ||||
| 		require.NoError(t, err) | ||||
| 		require.Equal(t, []string{"test-plugin.zip"}, loadedPaths) | ||||
|  |  | |||
|  | @ -1,11 +0,0 @@ | |||
| package finder | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 
 | ||||
| 	"github.com/grafana/grafana/pkg/plugins" | ||||
| ) | ||||
| 
 | ||||
| type Finder interface { | ||||
| 	Find(ctx context.Context, src plugins.PluginSource) ([]*plugins.FoundBundle, error) | ||||
| } | ||||
|  | @ -1,222 +0,0 @@ | |||
| package finder | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| 	"os" | ||||
| 	"path/filepath" | ||||
| 	"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 Local struct { | ||||
| 	log        log.Logger | ||||
| 	production bool | ||||
| } | ||||
| 
 | ||||
| func NewLocalFinder(devMode bool) *Local { | ||||
| 	return &Local{ | ||||
| 		production: !devMode, | ||||
| 		log:        log.New("local.finder"), | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func ProvideLocalFinder(cfg *config.PluginManagementCfg) *Local { | ||||
| 	return NewLocalFinder(cfg.DevMode) | ||||
| } | ||||
| 
 | ||||
| func (l *Local) Find(ctx context.Context, src plugins.PluginSource) ([]*plugins.FoundBundle, error) { | ||||
| 	if len(src.PluginURIs(ctx)) == 0 { | ||||
| 		return []*plugins.FoundBundle{}, nil | ||||
| 	} | ||||
| 
 | ||||
| 	pluginURIs := src.PluginURIs(ctx) | ||||
| 	pluginJSONPaths := make([]string, 0, len(pluginURIs)) | ||||
| 	for _, path := range pluginURIs { | ||||
| 		exists, err := fs.Exists(path) | ||||
| 		if err != nil { | ||||
| 			l.log.Warn("Skipping finding plugins as an error occurred", "path", path, "error", 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, "error", 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, "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 l.production { | ||||
| 			// In prod, 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 acts as if the file did 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 { | ||||
| 				l.log.Error("Cannot calculate relative path. Skipping", "pluginId", p2.Primary.JSONData.ID, "err", err) | ||||
| 				continue | ||||
| 			} | ||||
| 			if !strings.Contains(relPath, "..") { | ||||
| 				child := p2.Primary | ||||
| 				l.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 (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, "error", err) | ||||
| 		} | ||||
| 	}() | ||||
| 	if err != nil { | ||||
| 		l.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 { | ||||
| 		l.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 (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, "error", err) | ||||
| 					return nil | ||||
| 				} | ||||
| 				if errors.Is(err, os.ErrPermission) { | ||||
| 					l.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 (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)) | ||||
| } | ||||
|  | @ -1,473 +0,0 @@ | |||
| package finder | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"errors" | ||||
| 	"os" | ||||
| 	"path/filepath" | ||||
| 	"sort" | ||||
| 	"testing" | ||||
| 
 | ||||
| 	"github.com/google/go-cmp/cmp" | ||||
| 	"github.com/stretchr/testify/require" | ||||
| 
 | ||||
| 	"github.com/grafana/grafana/pkg/plugins" | ||||
| 	"github.com/grafana/grafana/pkg/plugins/manager/fakes" | ||||
| 	"github.com/grafana/grafana/pkg/util" | ||||
| ) | ||||
| 
 | ||||
| func TestFinder_Find(t *testing.T) { | ||||
| 	testData, err := filepath.Abs("../../testdata") | ||||
| 	if err != nil { | ||||
| 		require.NoError(t, err) | ||||
| 	} | ||||
| 
 | ||||
| 	testCases := []struct { | ||||
| 		name            string | ||||
| 		pluginDirs      []string | ||||
| 		pluginClass     plugins.Class | ||||
| 		expectedBundles []*plugins.FoundBundle | ||||
| 		err             error | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name:       "Dir with single plugin", | ||||
| 			pluginDirs: []string{filepath.Join(testData, "valid-v2-signature")}, | ||||
| 			expectedBundles: []*plugins.FoundBundle{ | ||||
| 				{ | ||||
| 					Primary: plugins.FoundPlugin{ | ||||
| 						JSONData: plugins.JSONData{ | ||||
| 							ID:   "test-datasource", | ||||
| 							Type: plugins.TypeDataSource, | ||||
| 							Name: "Test", | ||||
| 							Info: plugins.Info{ | ||||
| 								Author: plugins.InfoLink{ | ||||
| 									Name: "Will Browne", | ||||
| 									URL:  "https://willbrowne.com", | ||||
| 								}, | ||||
| 								Description: "Test", | ||||
| 								Version:     "1.0.0", | ||||
| 							}, | ||||
| 							Dependencies: plugins.Dependencies{ | ||||
| 								GrafanaVersion: "*", | ||||
| 								Plugins:        []plugins.Dependency{}, | ||||
| 								Extensions: plugins.ExtensionsDependencies{ | ||||
| 									ExposedComponents: []string{}, | ||||
| 								}, | ||||
| 							}, | ||||
| 							Extensions: plugins.Extensions{ | ||||
| 								AddedLinks:        []plugins.AddedLink{}, | ||||
| 								AddedComponents:   []plugins.AddedComponent{}, | ||||
| 								AddedFunctions:    []plugins.AddedFunction{}, | ||||
| 								ExposedComponents: []plugins.ExposedComponent{}, | ||||
| 								ExtensionPoints:   []plugins.ExtensionPoint{}, | ||||
| 							}, | ||||
| 							State:      plugins.ReleaseStateAlpha, | ||||
| 							Backend:    true, | ||||
| 							Executable: "test", | ||||
| 						}, | ||||
| 						FS: mustNewStaticFSForTests(t, filepath.Join(testData, "valid-v2-signature/plugin")), | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:       "Dir with nested plugins", | ||||
| 			pluginDirs: []string{"../../testdata/duplicate-plugins"}, | ||||
| 			expectedBundles: []*plugins.FoundBundle{ | ||||
| 				{ | ||||
| 					Primary: plugins.FoundPlugin{ | ||||
| 						JSONData: plugins.JSONData{ | ||||
| 							ID:   "test-app", | ||||
| 							Type: plugins.TypeDataSource, | ||||
| 							Name: "Parent", | ||||
| 							Info: plugins.Info{ | ||||
| 								Author: plugins.InfoLink{ | ||||
| 									Name: "Grafana Labs", | ||||
| 									URL:  "http://grafana.com", | ||||
| 								}, | ||||
| 								Description: "Parent plugin", | ||||
| 								Version:     "1.0.0", | ||||
| 								Updated:     "2020-10-20", | ||||
| 							}, | ||||
| 							Dependencies: plugins.Dependencies{ | ||||
| 								GrafanaVersion: "*", | ||||
| 								Plugins:        []plugins.Dependency{}, | ||||
| 								Extensions: plugins.ExtensionsDependencies{ | ||||
| 									ExposedComponents: []string{}, | ||||
| 								}, | ||||
| 							}, | ||||
| 							Extensions: plugins.Extensions{ | ||||
| 								AddedLinks:      []plugins.AddedLink{}, | ||||
| 								AddedComponents: []plugins.AddedComponent{}, | ||||
| 								AddedFunctions:  []plugins.AddedFunction{}, | ||||
| 
 | ||||
| 								ExposedComponents: []plugins.ExposedComponent{}, | ||||
| 								ExtensionPoints:   []plugins.ExtensionPoint{}, | ||||
| 							}, | ||||
| 						}, | ||||
| 						FS: mustNewStaticFSForTests(t, filepath.Join(testData, "duplicate-plugins/nested")), | ||||
| 					}, | ||||
| 					Children: []*plugins.FoundPlugin{ | ||||
| 						{ | ||||
| 							JSONData: plugins.JSONData{ | ||||
| 								ID:   "test-app", | ||||
| 								Type: plugins.TypeDataSource, | ||||
| 								Name: "Child", | ||||
| 								Info: plugins.Info{ | ||||
| 									Author: plugins.InfoLink{ | ||||
| 										Name: "Grafana Labs", | ||||
| 										URL:  "http://grafana.com", | ||||
| 									}, | ||||
| 									Description: "Child plugin", | ||||
| 									Version:     "1.0.0", | ||||
| 									Updated:     "2020-10-20", | ||||
| 								}, | ||||
| 								Dependencies: plugins.Dependencies{ | ||||
| 									GrafanaVersion: "*", | ||||
| 									Plugins:        []plugins.Dependency{}, | ||||
| 									Extensions: plugins.ExtensionsDependencies{ | ||||
| 										ExposedComponents: []string{}, | ||||
| 									}, | ||||
| 								}, | ||||
| 								Extensions: plugins.Extensions{ | ||||
| 									AddedLinks:      []plugins.AddedLink{}, | ||||
| 									AddedComponents: []plugins.AddedComponent{}, | ||||
| 									AddedFunctions:  []plugins.AddedFunction{}, | ||||
| 
 | ||||
| 									ExposedComponents: []plugins.ExposedComponent{}, | ||||
| 									ExtensionPoints:   []plugins.ExtensionPoint{}, | ||||
| 								}, | ||||
| 							}, | ||||
| 							FS: mustNewStaticFSForTests(t, filepath.Join(testData, "duplicate-plugins/nested/nested")), | ||||
| 						}, | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:       "Dir with single plugin which has symbolic link root directory", | ||||
| 			pluginDirs: []string{"../../testdata/symbolic-plugin-dirs"}, | ||||
| 			expectedBundles: []*plugins.FoundBundle{ | ||||
| 				{ | ||||
| 					Primary: plugins.FoundPlugin{ | ||||
| 						JSONData: plugins.JSONData{ | ||||
| 							ID:   "test-app", | ||||
| 							Type: plugins.TypeApp, | ||||
| 							Name: "Test App", | ||||
| 							Info: plugins.Info{ | ||||
| 								Author: plugins.InfoLink{ | ||||
| 									Name: "Test Inc.", | ||||
| 									URL:  "http://test.com", | ||||
| 								}, | ||||
| 								Description: "Official Grafana Test App & Dashboard bundle", | ||||
| 								Version:     "1.0.0", | ||||
| 								Links: []plugins.InfoLink{ | ||||
| 									{Name: "Project site", URL: "http://project.com"}, | ||||
| 									{Name: "License & Terms", URL: "http://license.com"}, | ||||
| 								}, | ||||
| 								Updated: "2015-02-10", | ||||
| 								Logos: plugins.Logos{ | ||||
| 									Small: "img/logo_small.png", | ||||
| 									Large: "img/logo_large.png", | ||||
| 								}, | ||||
| 								Screenshots: []plugins.Screenshots{ | ||||
| 									{Name: "img1", Path: "img/screenshot1.png"}, | ||||
| 									{Name: "img2", Path: "img/screenshot2.png"}, | ||||
| 								}, | ||||
| 								Keywords: []string{"test"}, | ||||
| 							}, | ||||
| 							Dependencies: plugins.Dependencies{ | ||||
| 								GrafanaVersion: "3.x.x", | ||||
| 								Plugins: []plugins.Dependency{ | ||||
| 									{ID: "graphite", Type: "datasource", Name: "Graphite"}, | ||||
| 									{ID: "graph", Type: "panel", Name: "Graph"}, | ||||
| 								}, | ||||
| 								Extensions: plugins.ExtensionsDependencies{ | ||||
| 									ExposedComponents: []string{}, | ||||
| 								}, | ||||
| 							}, | ||||
| 							Includes: []*plugins.Includes{ | ||||
| 								{ | ||||
| 									Name:   "Nginx Connections", | ||||
| 									Path:   "dashboards/connections.json", | ||||
| 									Type:   "dashboard", | ||||
| 									Role:   "Viewer", | ||||
| 									Action: "plugins.app:access", | ||||
| 								}, | ||||
| 								{ | ||||
| 									Name:   "Nginx Memory", | ||||
| 									Path:   "dashboards/memory.json", | ||||
| 									Type:   "dashboard", | ||||
| 									Role:   "Viewer", | ||||
| 									Action: "plugins.app:access", | ||||
| 								}, | ||||
| 								{Name: "Nginx Panel", Type: "panel", Role: "Viewer", Action: "plugins.app:access"}, | ||||
| 								{Name: "Nginx Datasource", Type: "datasource", Role: "Viewer", Action: "plugins.app:access"}, | ||||
| 							}, | ||||
| 							Extensions: plugins.Extensions{ | ||||
| 								AddedLinks:      []plugins.AddedLink{}, | ||||
| 								AddedComponents: []plugins.AddedComponent{}, | ||||
| 								AddedFunctions:  []plugins.AddedFunction{}, | ||||
| 
 | ||||
| 								ExposedComponents: []plugins.ExposedComponent{}, | ||||
| 								ExtensionPoints:   []plugins.ExtensionPoint{}, | ||||
| 							}, | ||||
| 						}, | ||||
| 						FS: mustNewStaticFSForTests(t, filepath.Join(testData, "includes-symlinks")), | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:       "Multiple plugin dirs", | ||||
| 			pluginDirs: []string{"../../testdata/duplicate-plugins", "../../testdata/invalid-v1-signature"}, | ||||
| 			expectedBundles: []*plugins.FoundBundle{ | ||||
| 				{ | ||||
| 					Primary: plugins.FoundPlugin{ | ||||
| 						JSONData: plugins.JSONData{ | ||||
| 							ID:   "test-app", | ||||
| 							Type: plugins.TypeDataSource, | ||||
| 							Name: "Parent", | ||||
| 							Info: plugins.Info{ | ||||
| 								Author: plugins.InfoLink{ | ||||
| 									Name: "Grafana Labs", | ||||
| 									URL:  "http://grafana.com", | ||||
| 								}, | ||||
| 								Description: "Parent plugin", | ||||
| 								Version:     "1.0.0", | ||||
| 								Updated:     "2020-10-20", | ||||
| 							}, | ||||
| 							Dependencies: plugins.Dependencies{ | ||||
| 								GrafanaVersion: "*", | ||||
| 								Plugins:        []plugins.Dependency{}, | ||||
| 								Extensions: plugins.ExtensionsDependencies{ | ||||
| 									ExposedComponents: []string{}, | ||||
| 								}, | ||||
| 							}, | ||||
| 							Extensions: plugins.Extensions{ | ||||
| 								AddedLinks:      []plugins.AddedLink{}, | ||||
| 								AddedComponents: []plugins.AddedComponent{}, | ||||
| 								AddedFunctions:  []plugins.AddedFunction{}, | ||||
| 
 | ||||
| 								ExposedComponents: []plugins.ExposedComponent{}, | ||||
| 								ExtensionPoints:   []plugins.ExtensionPoint{}, | ||||
| 							}, | ||||
| 						}, | ||||
| 						FS: mustNewStaticFSForTests(t, filepath.Join(testData, "duplicate-plugins/nested")), | ||||
| 					}, | ||||
| 					Children: []*plugins.FoundPlugin{ | ||||
| 						{ | ||||
| 							JSONData: plugins.JSONData{ | ||||
| 								ID:   "test-app", | ||||
| 								Type: plugins.TypeDataSource, | ||||
| 								Name: "Child", | ||||
| 								Info: plugins.Info{ | ||||
| 									Author: plugins.InfoLink{ | ||||
| 										Name: "Grafana Labs", | ||||
| 										URL:  "http://grafana.com", | ||||
| 									}, | ||||
| 									Description: "Child plugin", | ||||
| 									Version:     "1.0.0", | ||||
| 									Updated:     "2020-10-20", | ||||
| 								}, | ||||
| 								Dependencies: plugins.Dependencies{ | ||||
| 									GrafanaVersion: "*", | ||||
| 									Plugins:        []plugins.Dependency{}, | ||||
| 									Extensions: plugins.ExtensionsDependencies{ | ||||
| 										ExposedComponents: []string{}, | ||||
| 									}, | ||||
| 								}, | ||||
| 								Extensions: plugins.Extensions{ | ||||
| 									AddedLinks:      []plugins.AddedLink{}, | ||||
| 									AddedComponents: []plugins.AddedComponent{}, | ||||
| 									AddedFunctions:  []plugins.AddedFunction{}, | ||||
| 
 | ||||
| 									ExposedComponents: []plugins.ExposedComponent{}, | ||||
| 									ExtensionPoints:   []plugins.ExtensionPoint{}, | ||||
| 								}, | ||||
| 							}, | ||||
| 							FS: mustNewStaticFSForTests(t, filepath.Join(testData, "duplicate-plugins/nested/nested")), | ||||
| 						}, | ||||
| 					}, | ||||
| 				}, | ||||
| 				{ | ||||
| 					Primary: plugins.FoundPlugin{ | ||||
| 						JSONData: plugins.JSONData{ | ||||
| 							ID:   "test-datasource", | ||||
| 							Type: plugins.TypeDataSource, | ||||
| 							Name: "Test", | ||||
| 							Info: plugins.Info{ | ||||
| 								Author: plugins.InfoLink{ | ||||
| 									Name: "Grafana Labs", | ||||
| 									URL:  "https://grafana.com", | ||||
| 								}, | ||||
| 								Description: "Test", | ||||
| 							}, | ||||
| 							Dependencies: plugins.Dependencies{ | ||||
| 								GrafanaVersion: "*", | ||||
| 								Plugins:        []plugins.Dependency{}, | ||||
| 								Extensions: plugins.ExtensionsDependencies{ | ||||
| 									ExposedComponents: []string{}, | ||||
| 								}, | ||||
| 							}, | ||||
| 							Extensions: plugins.Extensions{ | ||||
| 								AddedLinks:      []plugins.AddedLink{}, | ||||
| 								AddedComponents: []plugins.AddedComponent{}, | ||||
| 								AddedFunctions:  []plugins.AddedFunction{}, | ||||
| 
 | ||||
| 								ExposedComponents: []plugins.ExposedComponent{}, | ||||
| 								ExtensionPoints:   []plugins.ExtensionPoint{}, | ||||
| 							}, | ||||
| 							State:   plugins.ReleaseStateAlpha, | ||||
| 							Backend: true, | ||||
| 						}, | ||||
| 						FS: mustNewStaticFSForTests(t, filepath.Join(testData, "invalid-v1-signature/plugin")), | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:        "Plugin with dist folder (core class)", | ||||
| 			pluginDirs:  []string{filepath.Join(testData, "plugin-with-dist")}, | ||||
| 			pluginClass: plugins.ClassCore, | ||||
| 			expectedBundles: []*plugins.FoundBundle{ | ||||
| 				{ | ||||
| 					Primary: plugins.FoundPlugin{ | ||||
| 						JSONData: plugins.JSONData{ | ||||
| 							ID:   "test-datasource", | ||||
| 							Type: plugins.TypeDataSource, | ||||
| 							Name: "Test", | ||||
| 							Info: plugins.Info{ | ||||
| 								Author: plugins.InfoLink{ | ||||
| 									Name: "Will Browne", | ||||
| 									URL:  "https://willbrowne.com", | ||||
| 								}, | ||||
| 								Description: "Test", | ||||
| 								Version:     "1.0.0", | ||||
| 							}, | ||||
| 							Dependencies: plugins.Dependencies{ | ||||
| 								GrafanaVersion: "*", | ||||
| 								Plugins:        []plugins.Dependency{}, | ||||
| 								Extensions: plugins.ExtensionsDependencies{ | ||||
| 									ExposedComponents: []string{}, | ||||
| 								}, | ||||
| 							}, | ||||
| 							Extensions: plugins.Extensions{ | ||||
| 								AddedLinks:      []plugins.AddedLink{}, | ||||
| 								AddedComponents: []plugins.AddedComponent{}, | ||||
| 								AddedFunctions:  []plugins.AddedFunction{}, | ||||
| 
 | ||||
| 								ExposedComponents: []plugins.ExposedComponent{}, | ||||
| 								ExtensionPoints:   []plugins.ExtensionPoint{}, | ||||
| 							}, | ||||
| 							State:      plugins.ReleaseStateAlpha, | ||||
| 							Backend:    true, | ||||
| 							Executable: "test", | ||||
| 						}, | ||||
| 						FS: mustNewStaticFSForTests(t, filepath.Join(testData, "plugin-with-dist/plugin/dist")), | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 	} | ||||
| 	for _, tc := range testCases { | ||||
| 		t.Run(tc.name, func(t *testing.T) { | ||||
| 			f := NewLocalFinder(false) | ||||
| 			pluginBundles, err := f.Find(context.Background(), &fakes.FakePluginSource{ | ||||
| 				PluginClassFunc: func(ctx context.Context) plugins.Class { | ||||
| 					return tc.pluginClass | ||||
| 				}, | ||||
| 				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 | ||||
| 			} | ||||
| 
 | ||||
| 			// to ensure we can compare with expected
 | ||||
| 			sort.SliceStable(pluginBundles, func(i, j int) bool { | ||||
| 				return pluginBundles[i].Primary.JSONData.ID < pluginBundles[j].Primary.JSONData.ID | ||||
| 			}) | ||||
| 
 | ||||
| 			if !cmp.Equal(pluginBundles, tc.expectedBundles, fsComparer) { | ||||
| 				t.Fatalf("Result mismatch (-want +got):\n%s", cmp.Diff(pluginBundles, tc.expectedBundles, fsComparer)) | ||||
| 			} | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestFinder_getAbsPluginJSONPaths(t *testing.T) { | ||||
| 	t.Run("When scanning a folder that doesn't exists shouldn't return an error", func(t *testing.T) { | ||||
| 		origWalk := walk | ||||
| 		walk = func(path string, followSymlinks, detectSymlinkInfiniteLoop bool, walkFn util.WalkFunc) error { | ||||
| 			return walkFn(path, nil, os.ErrNotExist) | ||||
| 		} | ||||
| 		t.Cleanup(func() { | ||||
| 			walk = origWalk | ||||
| 		}) | ||||
| 
 | ||||
| 		finder := NewLocalFinder(false) | ||||
| 		paths, err := finder.getAbsPluginJSONPaths("test") | ||||
| 		require.NoError(t, err) | ||||
| 		require.Empty(t, paths) | ||||
| 	}) | ||||
| 
 | ||||
| 	t.Run("When scanning a folder that lacks permission shouldn't return an error", func(t *testing.T) { | ||||
| 		origWalk := walk | ||||
| 		walk = func(path string, followSymlinks, detectSymlinkInfiniteLoop bool, walkFn util.WalkFunc) error { | ||||
| 			return walkFn(path, nil, os.ErrPermission) | ||||
| 		} | ||||
| 		t.Cleanup(func() { | ||||
| 			walk = origWalk | ||||
| 		}) | ||||
| 
 | ||||
| 		finder := NewLocalFinder(false) | ||||
| 		paths, err := finder.getAbsPluginJSONPaths("test") | ||||
| 		require.NoError(t, err) | ||||
| 		require.Empty(t, paths) | ||||
| 	}) | ||||
| 
 | ||||
| 	t.Run("When scanning a folder that returns a non-handled error should return that error", func(t *testing.T) { | ||||
| 		origWalk := walk | ||||
| 		walk = func(path string, followSymlinks, detectSymlinkInfiniteLoop bool, walkFn util.WalkFunc) error { | ||||
| 			return walkFn(path, nil, errors.New("random error")) | ||||
| 		} | ||||
| 		t.Cleanup(func() { | ||||
| 			walk = origWalk | ||||
| 		}) | ||||
| 
 | ||||
| 		finder := NewLocalFinder(false) | ||||
| 		paths, err := finder.getAbsPluginJSONPaths("test") | ||||
| 		require.Error(t, err) | ||||
| 		require.Empty(t, paths) | ||||
| 	}) | ||||
| } | ||||
| 
 | ||||
| var fsComparer = cmp.Comparer(func(fs1 plugins.FS, fs2 plugins.FS) bool { | ||||
| 	fs1Files, err := fs1.Files() | ||||
| 	if err != nil { | ||||
| 		panic(err) | ||||
| 	} | ||||
| 	fs2Files, err := fs2.Files() | ||||
| 	if err != nil { | ||||
| 		panic(err) | ||||
| 	} | ||||
| 
 | ||||
| 	sort.SliceStable(fs1Files, func(i, j int) bool { | ||||
| 		return fs1Files[i] < fs1Files[j] | ||||
| 	}) | ||||
| 
 | ||||
| 	sort.SliceStable(fs2Files, func(i, j int) bool { | ||||
| 		return fs2Files[i] < fs2Files[j] | ||||
| 	}) | ||||
| 
 | ||||
| 	return cmp.Equal(fs1Files, fs2Files) && fs1.Base() == fs2.Base() | ||||
| }) | ||||
| 
 | ||||
| func mustNewStaticFSForTests(t *testing.T, dir string) plugins.FS { | ||||
| 	sfs, err := plugins.NewStaticFS(plugins.NewLocalFS(dir)) | ||||
| 	require.NoError(t, err) | ||||
| 	return sfs | ||||
| } | ||||
|  | @ -451,9 +451,6 @@ func TestLoader_Load(t *testing.T) { | |||
| 			PluginClassFunc: func(ctx context.Context) plugins.Class { | ||||
| 				return plugins.ClassExternal | ||||
| 			}, | ||||
| 			PluginURIsFunc: func(ctx context.Context) []string { | ||||
| 				return []string{"http://example.com"} | ||||
| 			}, | ||||
| 			DefaultSignatureFunc: func(ctx context.Context) (plugins.Signature, bool) { | ||||
| 				return plugins.Signature{}, false | ||||
| 			}, | ||||
|  | @ -509,9 +506,6 @@ func TestLoader_Load(t *testing.T) { | |||
| 			PluginClassFunc: func(ctx context.Context) plugins.Class { | ||||
| 				return plugins.ClassExternal | ||||
| 			}, | ||||
| 			PluginURIsFunc: func(ctx context.Context) []string { | ||||
| 				return []string{"http://example.com"} | ||||
| 			}, | ||||
| 			DefaultSignatureFunc: func(ctx context.Context) (plugins.Signature, bool) { | ||||
| 				return plugins.Signature{}, false | ||||
| 			}, | ||||
|  | @ -571,9 +565,6 @@ func TestLoader_Load(t *testing.T) { | |||
| 			PluginClassFunc: func(ctx context.Context) plugins.Class { | ||||
| 				return plugins.ClassExternal | ||||
| 			}, | ||||
| 			PluginURIsFunc: func(ctx context.Context) []string { | ||||
| 				return []string{"http://example.com"} | ||||
| 			}, | ||||
| 			DefaultSignatureFunc: func(ctx context.Context) (plugins.Signature, bool) { | ||||
| 				return plugins.Signature{}, false | ||||
| 			}, | ||||
|  |  | |||
|  | @ -13,62 +13,57 @@ type Discoverer interface { | |||
| 	Discover(ctx context.Context, src plugins.PluginSource) ([]*plugins.FoundBundle, error) | ||||
| } | ||||
| 
 | ||||
| // FindFunc is the function used for the Find step of the Discovery stage.
 | ||||
| type FindFunc func(ctx context.Context, src plugins.PluginSource) ([]*plugins.FoundBundle, error) | ||||
| 
 | ||||
| // FindFilterFunc is the function used for the Filter step of the Discovery stage.
 | ||||
| type FindFilterFunc func(ctx context.Context, class plugins.Class, bundles []*plugins.FoundBundle) ([]*plugins.FoundBundle, error) | ||||
| // FilterFunc is the function used for the Filter step of the Discovery stage.
 | ||||
| type FilterFunc func(ctx context.Context, class plugins.Class, bundles []*plugins.FoundBundle) ([]*plugins.FoundBundle, error) | ||||
| 
 | ||||
| // Discovery implements the Discoverer interface.
 | ||||
| //
 | ||||
| // The Discovery stage is made up of the following steps (in order):
 | ||||
| // - Find: Find plugins (from disk, remote, etc.)
 | ||||
| // - Discover: Each source discovers its own plugins
 | ||||
| // - Filter: Filter the results based on some criteria.
 | ||||
| //
 | ||||
| // The Find step is implemented by the FindFunc type.
 | ||||
| //
 | ||||
| // The Filter step is implemented by the FindFilterFunc type.
 | ||||
| // The Filter step is implemented by the FilterFunc type.
 | ||||
| type Discovery struct { | ||||
| 	findStep        FindFunc | ||||
| 	findFilterSteps []FindFilterFunc | ||||
| 	filterSteps []FilterFunc | ||||
| 	log         log.Logger | ||||
| } | ||||
| 
 | ||||
| type Opts struct { | ||||
| 	FindFunc        FindFunc | ||||
| 	FindFilterFuncs []FindFilterFunc | ||||
| 	FilterFuncs []FilterFunc | ||||
| } | ||||
| 
 | ||||
| // New returns a new Discovery stage.
 | ||||
| func New(cfg *config.PluginManagementCfg, opts Opts) *Discovery { | ||||
| 	if opts.FindFunc == nil { | ||||
| 		opts.FindFunc = DefaultFindFunc(cfg) | ||||
| 	} | ||||
| 
 | ||||
| 	if opts.FindFilterFuncs == nil { | ||||
| 		opts.FindFilterFuncs = []FindFilterFunc{} // no filters by default
 | ||||
| func New(_ *config.PluginManagementCfg, opts Opts) *Discovery { | ||||
| 	if opts.FilterFuncs == nil { | ||||
| 		opts.FilterFuncs = []FilterFunc{} // no filters by default
 | ||||
| 	} | ||||
| 
 | ||||
| 	return &Discovery{ | ||||
| 		findStep:        opts.FindFunc, | ||||
| 		findFilterSteps: opts.FindFilterFuncs, | ||||
| 		filterSteps: opts.FilterFuncs, | ||||
| 		log:         log.New("plugins.discovery"), | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // Discover will execute the Find and Filter steps of the Discovery stage.
 | ||||
| // Discover will execute the Filter step of the Discovery stage.
 | ||||
| func (d *Discovery) Discover(ctx context.Context, src plugins.PluginSource) ([]*plugins.FoundBundle, error) { | ||||
| 	discoveredPlugins, err := d.findStep(ctx, src) | ||||
| 	// Use the source's own Discover method
 | ||||
| 	found, err := src.Discover(ctx) | ||||
| 	if err != nil { | ||||
| 		d.log.Warn("Discovery source failed", "class", src.PluginClass(ctx), "error", err) | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	for _, filter := range d.findFilterSteps { | ||||
| 		discoveredPlugins, err = filter(ctx, src.PluginClass(ctx), discoveredPlugins) | ||||
| 	d.log.Debug("Found plugins", "class", src.PluginClass(ctx), "count", len(found)) | ||||
| 
 | ||||
| 	// Apply filtering steps
 | ||||
| 	result := found | ||||
| 	for _, filter := range d.filterSteps { | ||||
| 		result, err = filter(ctx, src.PluginClass(ctx), result) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return discoveredPlugins, nil | ||||
| 	d.log.Debug("Discovery complete", "class", src.PluginClass(ctx), "found", len(found), "filtered", len(result)) | ||||
| 	return result, nil | ||||
| } | ||||
|  |  | |||
|  | @ -5,24 +5,16 @@ import ( | |||
| 	"slices" | ||||
| 
 | ||||
| 	"github.com/grafana/grafana/pkg/plugins" | ||||
| 	"github.com/grafana/grafana/pkg/plugins/config" | ||||
| 	"github.com/grafana/grafana/pkg/plugins/manager/loader/finder" | ||||
| ) | ||||
| 
 | ||||
| // DefaultFindFunc is the default function used for the Find step of the Discovery stage. It will scan the local
 | ||||
| // filesystem for plugins.
 | ||||
| func DefaultFindFunc(cfg *config.PluginManagementCfg) FindFunc { | ||||
| 	return finder.NewLocalFinder(cfg.DevMode).Find | ||||
| } | ||||
| 
 | ||||
| // PermittedPluginTypesFilter is a filter step that will filter out any plugins that are not of a permitted type.
 | ||||
| type PermittedPluginTypesFilter struct { | ||||
| 	permittedTypes []plugins.Type | ||||
| } | ||||
| 
 | ||||
| // NewPermittedPluginTypesFilterStep returns a new FindFilterFunc for filtering out any plugins that are not of a
 | ||||
| // NewPermittedPluginTypesFilterStep returns a new FilterFunc for filtering out any plugins that are not of a
 | ||||
| // permitted type. This includes both the primary plugin and any child plugins.
 | ||||
| func NewPermittedPluginTypesFilterStep(permittedTypes []plugins.Type) FindFilterFunc { | ||||
| func NewPermittedPluginTypesFilterStep(permittedTypes []plugins.Type) FilterFunc { | ||||
| 	f := &PermittedPluginTypesFilter{ | ||||
| 		permittedTypes: permittedTypes, | ||||
| 	} | ||||
|  |  | |||
|  | @ -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 | ||||
| 	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, | ||||
| 		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,8 +277,12 @@ func DirAsLocalSources(pluginsPath string, class plugins.Class) ([]*LocalSource, | |||
| 
 | ||||
| 	sources := make([]*LocalSource, len(pluginDirs)) | ||||
| 	for i, dir := range pluginDirs { | ||||
| 		if cfg.DevMode { | ||||
| 			sources[i] = NewUnsafeLocalSource(class, []string{dir}) | ||||
| 		} else { | ||||
| 			sources[i] = NewLocalSource(class, []string{dir}) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return sources, nil | ||||
| } | ||||
|  |  | |||
|  | @ -5,17 +5,23 @@ import ( | |||
| 	"path/filepath" | ||||
| 	"testing" | ||||
| 
 | ||||
| 	"github.com/google/go-cmp/cmp" | ||||
| 	"github.com/google/go-cmp/cmp/cmpopts" | ||||
| 	"github.com/stretchr/testify/require" | ||||
| 
 | ||||
| 	"github.com/grafana/grafana/pkg/plugins" | ||||
| 	"github.com/grafana/grafana/pkg/plugins/config" | ||||
| ) | ||||
| 
 | ||||
| var compareOpts = []cmp.Option{cmpopts.IgnoreFields(LocalSource{}, "log"), cmp.AllowUnexported(LocalSource{})} | ||||
| 
 | ||||
| func TestDirAsLocalSources(t *testing.T) { | ||||
| 	testdataDir := "../testdata" | ||||
| 
 | ||||
| 	tests := []struct { | ||||
| 		name        string | ||||
| 		pluginsPath string | ||||
| 		cfg         *config.PluginManagementCfg | ||||
| 		expected    []*LocalSource | ||||
| 		err         error | ||||
| 	}{ | ||||
|  | @ -28,44 +34,79 @@ func TestDirAsLocalSources(t *testing.T) { | |||
| 		{ | ||||
| 			name:        "Directory with subdirectories", | ||||
| 			pluginsPath: filepath.Join(testdataDir, "pluginRootWithDist"), | ||||
| 			cfg:         &config.PluginManagementCfg{}, | ||||
| 			expected: []*LocalSource{ | ||||
| 				{ | ||||
| 					paths:      []string{filepath.Join(testdataDir, "pluginRootWithDist", "datasource")}, | ||||
| 					strictMode: true, | ||||
| 					class:      plugins.ClassExternal, | ||||
| 				}, | ||||
| 				{ | ||||
| 					paths:      []string{filepath.Join(testdataDir, "pluginRootWithDist", "dist")}, | ||||
| 					strictMode: true, | ||||
| 					class:      plugins.ClassExternal, | ||||
| 				}, | ||||
| 				{ | ||||
| 					paths:      []string{filepath.Join(testdataDir, "pluginRootWithDist", "panel")}, | ||||
| 					strictMode: true, | ||||
| 					class:      plugins.ClassExternal, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "Dev mode disables strict mode for source", | ||||
| 			cfg: &config.PluginManagementCfg{ | ||||
| 				DevMode: true, | ||||
| 			}, | ||||
| 			pluginsPath: filepath.Join(testdataDir, "pluginRootWithDist"), | ||||
| 			expected: []*LocalSource{ | ||||
| 				{ | ||||
| 					paths:      []string{filepath.Join(testdataDir, "pluginRootWithDist", "datasource")}, | ||||
| 					class:      plugins.ClassExternal, | ||||
| 					strictMode: false, | ||||
| 				}, | ||||
| 				{ | ||||
| 					paths:      []string{filepath.Join(testdataDir, "pluginRootWithDist", "dist")}, | ||||
| 					class:      plugins.ClassExternal, | ||||
| 					strictMode: false, | ||||
| 				}, | ||||
| 				{ | ||||
| 					paths:      []string{filepath.Join(testdataDir, "pluginRootWithDist", "panel")}, | ||||
| 					class:      plugins.ClassExternal, | ||||
| 					strictMode: false, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:        "Directory with no subdirectories", | ||||
| 			cfg:         &config.PluginManagementCfg{}, | ||||
| 			pluginsPath: filepath.Join(testdataDir, "pluginRootWithDist", "datasource"), | ||||
| 			expected:    []*LocalSource{}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:        "Directory with a symlink to a directory", | ||||
| 			pluginsPath: filepath.Join(testdataDir, "symbolic-plugin-dirs"), | ||||
| 			cfg:         &config.PluginManagementCfg{}, | ||||
| 			expected: []*LocalSource{ | ||||
| 				{ | ||||
| 					paths:      []string{filepath.Join(testdataDir, "symbolic-plugin-dirs", "plugin")}, | ||||
| 					class:      plugins.ClassExternal, | ||||
| 					strictMode: true, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 	} | ||||
| 	for _, tt := range tests { | ||||
| 		t.Run(tt.name, func(t *testing.T) { | ||||
| 			got, err := DirAsLocalSources(tt.pluginsPath, plugins.ClassExternal) | ||||
| 			got, err := DirAsLocalSources(tt.cfg, tt.pluginsPath, plugins.ClassExternal) | ||||
| 			if tt.err != nil { | ||||
| 				require.Errorf(t, err, tt.err.Error()) | ||||
| 				return | ||||
| 			} | ||||
| 			require.NoError(t, err) | ||||
| 			if !cmp.Equal(got, tt.expected, compareOpts...) { | ||||
| 				t.Fatalf("Result mismatch (-want +got):\n%s", cmp.Diff(got, tt.expected, compareOpts...)) | ||||
| 			} | ||||
| 			require.Equal(t, tt.expected, got) | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|  |  | |||
|  | @ -5,25 +5,32 @@ import ( | |||
| 	"path/filepath" | ||||
| 
 | ||||
| 	"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/setting" | ||||
| ) | ||||
| 
 | ||||
| type Service struct { | ||||
| 	cfg *setting.Cfg | ||||
| 	cfg            *config.PluginManagementCfg | ||||
| 	staticRootPath string | ||||
| 
 | ||||
| 	log log.Logger | ||||
| } | ||||
| 
 | ||||
| func ProvideService(cfg *setting.Cfg) *Service { | ||||
| func ProvideService(cfg *setting.Cfg, pCcfg *config.PluginManagementCfg) *Service { | ||||
| 	return &Service{ | ||||
| 		cfg: cfg, | ||||
| 		cfg:            pCcfg, | ||||
| 		staticRootPath: cfg.StaticRootPath, | ||||
| 		log:            log.New("plugin.sources"), | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (s *Service) List(_ context.Context) []plugins.PluginSource { | ||||
| 	r := []plugins.PluginSource{ | ||||
| 		NewLocalSource(plugins.ClassCore, corePluginPaths(s.cfg.StaticRootPath)), | ||||
| 		NewLocalSource( | ||||
| 			plugins.ClassCore, | ||||
| 			s.corePluginPaths(), | ||||
| 		), | ||||
| 	} | ||||
| 	r = append(r, s.externalPluginSources()...) | ||||
| 	r = append(r, s.pluginSettingSources()...) | ||||
|  | @ -31,7 +38,7 @@ func (s *Service) List(_ context.Context) []plugins.PluginSource { | |||
| } | ||||
| 
 | ||||
| func (s *Service) externalPluginSources() []plugins.PluginSource { | ||||
| 	localSrcs, err := DirAsLocalSources(s.cfg.PluginsPath, plugins.ClassExternal) | ||||
| 	localSrcs, err := DirAsLocalSources(s.cfg, s.cfg.PluginsPath, plugins.ClassExternal) | ||||
| 	if err != nil { | ||||
| 		s.log.Error("Failed to load external plugins", "error", err) | ||||
| 		return []plugins.PluginSource{} | ||||
|  | @ -52,16 +59,19 @@ func (s *Service) pluginSettingSources() []plugins.PluginSource { | |||
| 		if !exists || path == "" { | ||||
| 			continue | ||||
| 		} | ||||
| 
 | ||||
| 		if s.cfg.DevMode { | ||||
| 			sources = append(sources, NewUnsafeLocalSource(plugins.ClassExternal, []string{path})) | ||||
| 		} else { | ||||
| 			sources = append(sources, NewLocalSource(plugins.ClassExternal, []string{path})) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return sources | ||||
| } | ||||
| 
 | ||||
| // corePluginPaths provides a list of the Core plugin file system paths
 | ||||
| func corePluginPaths(staticRootPath string) []string { | ||||
| 	datasourcePaths := filepath.Join(staticRootPath, "app/plugins/datasource") | ||||
| 	panelsPath := filepath.Join(staticRootPath, "app/plugins/panel") | ||||
| func (s *Service) corePluginPaths() []string { | ||||
| 	datasourcePaths := filepath.Join(s.staticRootPath, "app", "plugins", "datasource") | ||||
| 	panelsPath := filepath.Join(s.staticRootPath, "app", "plugins", "panel") | ||||
| 	return []string{datasourcePaths, panelsPath} | ||||
| } | ||||
|  |  | |||
|  | @ -8,6 +8,7 @@ import ( | |||
| 	"github.com/stretchr/testify/require" | ||||
| 
 | ||||
| 	"github.com/grafana/grafana/pkg/plugins" | ||||
| 	"github.com/grafana/grafana/pkg/plugins/config" | ||||
| 	"github.com/grafana/grafana/pkg/setting" | ||||
| ) | ||||
| 
 | ||||
|  | @ -18,6 +19,9 @@ func TestSources_List(t *testing.T) { | |||
| 
 | ||||
| 		cfg := &setting.Cfg{ | ||||
| 			StaticRootPath: testdata, | ||||
| 		} | ||||
| 
 | ||||
| 		pCfg := &config.PluginManagementCfg{ | ||||
| 			PluginsPath: filepath.Join(testdata, "pluginRootWithDist"), | ||||
| 			PluginSettings: setting.PluginSettings{ | ||||
| 				"foo": map[string]string{ | ||||
|  | @ -29,7 +33,7 @@ func TestSources_List(t *testing.T) { | |||
| 			}, | ||||
| 		} | ||||
| 
 | ||||
| 		s := ProvideService(cfg) | ||||
| 		s := ProvideService(cfg, pCfg) | ||||
| 		srcs := s.List(context.Background()) | ||||
| 
 | ||||
| 		ctx := context.Background() | ||||
|  | @ -37,10 +41,14 @@ func TestSources_List(t *testing.T) { | |||
| 		require.Len(t, srcs, 5) | ||||
| 
 | ||||
| 		require.Equal(t, srcs[0].PluginClass(ctx), plugins.ClassCore) | ||||
| 		require.Equal(t, srcs[0].PluginURIs(ctx), []string{ | ||||
| 		if localSrc, ok := srcs[0].(*LocalSource); ok { | ||||
| 			require.Equal(t, localSrc.Paths(), []string{ | ||||
| 				filepath.Join(testdata, "app", "plugins", "datasource"), | ||||
| 				filepath.Join(testdata, "app", "plugins", "panel"), | ||||
| 			}) | ||||
| 		} else { | ||||
| 			t.Fatalf("Expected LocalSource, got %T", srcs[0]) | ||||
| 		} | ||||
| 		sig, exists := srcs[0].DefaultSignature(ctx, "") | ||||
| 		require.True(t, exists) | ||||
| 		require.Equal(t, plugins.SignatureStatusInternal, sig.Status) | ||||
|  | @ -48,25 +56,37 @@ func TestSources_List(t *testing.T) { | |||
| 		require.Equal(t, "", sig.SigningOrg) | ||||
| 
 | ||||
| 		require.Equal(t, srcs[1].PluginClass(ctx), plugins.ClassExternal) | ||||
| 		require.Equal(t, srcs[1].PluginURIs(ctx), []string{ | ||||
| 		if localSrc, ok := srcs[1].(*LocalSource); ok { | ||||
| 			require.Equal(t, localSrc.Paths(), []string{ | ||||
| 				filepath.Join(testdata, "pluginRootWithDist", "datasource"), | ||||
| 			}) | ||||
| 		} else { | ||||
| 			t.Fatalf("Expected LocalSource, got %T", srcs[1]) | ||||
| 		} | ||||
| 		sig, exists = srcs[1].DefaultSignature(ctx, "") | ||||
| 		require.False(t, exists) | ||||
| 		require.Equal(t, plugins.Signature{}, sig) | ||||
| 
 | ||||
| 		require.Equal(t, srcs[2].PluginClass(ctx), plugins.ClassExternal) | ||||
| 		require.Equal(t, srcs[2].PluginURIs(ctx), []string{ | ||||
| 		if localSrc, ok := srcs[2].(*LocalSource); ok { | ||||
| 			require.Equal(t, localSrc.Paths(), []string{ | ||||
| 				filepath.Join(testdata, "pluginRootWithDist", "dist"), | ||||
| 			}) | ||||
| 		} else { | ||||
| 			t.Fatalf("Expected LocalSource, got %T", srcs[2]) | ||||
| 		} | ||||
| 		sig, exists = srcs[2].DefaultSignature(ctx, "") | ||||
| 		require.False(t, exists) | ||||
| 		require.Equal(t, plugins.Signature{}, sig) | ||||
| 
 | ||||
| 		require.Equal(t, srcs[3].PluginClass(ctx), plugins.ClassExternal) | ||||
| 		require.Equal(t, srcs[3].PluginURIs(ctx), []string{ | ||||
| 		if localSrc, ok := srcs[3].(*LocalSource); ok { | ||||
| 			require.Equal(t, localSrc.Paths(), []string{ | ||||
| 				filepath.Join(testdata, "pluginRootWithDist", "panel"), | ||||
| 			}) | ||||
| 		} else { | ||||
| 			t.Fatalf("Expected LocalSource, got %T", srcs[3]) | ||||
| 		} | ||||
| 		sig, exists = srcs[3].DefaultSignature(ctx, "") | ||||
| 		require.False(t, exists) | ||||
| 		require.Equal(t, plugins.Signature{}, sig) | ||||
|  | @ -78,19 +98,25 @@ func TestSources_List(t *testing.T) { | |||
| 
 | ||||
| 		cfg := &setting.Cfg{ | ||||
| 			StaticRootPath: testdata, | ||||
| 		} | ||||
| 
 | ||||
| 		pCfg := &config.PluginManagementCfg{ | ||||
| 			PluginsPath: filepath.Join(testdata, "symbolic-plugin-dirs"), | ||||
| 		} | ||||
| 		s := ProvideService(cfg) | ||||
| 
 | ||||
| 		s := ProvideService(cfg, pCfg) | ||||
| 		ctx := context.Background() | ||||
| 		srcs := s.List(ctx) | ||||
| 		uris := map[plugins.Class]map[string]struct{}{} | ||||
| 		for _, s := range srcs { | ||||
| 			class := s.PluginClass(ctx) | ||||
| 		for _, src := range srcs { | ||||
| 			class := src.PluginClass(ctx) | ||||
| 			if _, exists := uris[class]; !exists { | ||||
| 				uris[class] = map[string]struct{}{} | ||||
| 			} | ||||
| 			for _, uri := range s.PluginURIs(ctx) { | ||||
| 				uris[class][uri] = struct{}{} | ||||
| 			if localSrc, ok := src.(*LocalSource); ok { | ||||
| 				for _, path := range localSrc.Paths() { | ||||
| 					uris[class][path] = struct{}{} | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
|  |  | |||
|  | @ -21,7 +21,6 @@ import ( | |||
| 	"github.com/grafana/grafana/pkg/plugins/manager/fakes" | ||||
| 	"github.com/grafana/grafana/pkg/plugins/manager/loader/angular/angularinspector" | ||||
| 	"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" | ||||
|  | @ -566,9 +565,7 @@ func TestLoader_Load_ExternalRegistration(t *testing.T) { | |||
| 			PluginClassFunc: func(ctx context.Context) plugins.Class { | ||||
| 				return plugins.ClassExternal | ||||
| 			}, | ||||
| 			PluginURIsFunc: func(ctx context.Context) []string { | ||||
| 				return pluginPaths | ||||
| 			}, | ||||
| 			DiscoverFunc: sources.NewLocalSource(plugins.ClassExternal, pluginPaths).Discover, | ||||
| 			DefaultSignatureFunc: func(ctx context.Context) (plugins.Signature, bool) { | ||||
| 				return plugins.Signature{}, false | ||||
| 			}, | ||||
|  | @ -650,9 +647,7 @@ func TestLoader_Load_CustomSource(t *testing.T) { | |||
| 			PluginClassFunc: func(ctx context.Context) plugins.Class { | ||||
| 				return plugins.ClassExternal | ||||
| 			}, | ||||
| 			PluginURIsFunc: func(ctx context.Context) []string { | ||||
| 				return pluginPaths | ||||
| 			}, | ||||
| 			DiscoverFunc: sources.NewLocalSource(plugins.ClassExternal, pluginPaths).Discover, | ||||
| 			DefaultSignatureFunc: func(ctx context.Context) (plugins.Signature, bool) { | ||||
| 				return plugins.Signature{ | ||||
| 					Status: plugins.SignatureStatusValid, | ||||
|  | @ -754,9 +749,7 @@ func TestLoader_Load_MultiplePlugins(t *testing.T) { | |||
| 					PluginClassFunc: func(ctx context.Context) plugins.Class { | ||||
| 						return plugins.ClassExternal | ||||
| 					}, | ||||
| 					PluginURIsFunc: func(ctx context.Context) []string { | ||||
| 						return tt.pluginPaths | ||||
| 					}, | ||||
| 					DiscoverFunc: sources.NewLocalSource(plugins.ClassExternal, tt.pluginPaths).Discover, | ||||
| 				}) | ||||
| 				require.NoError(t, err) | ||||
| 				sort.SliceStable(got, func(i, j int) bool { | ||||
|  | @ -866,9 +859,7 @@ func TestLoader_Load_RBACReady(t *testing.T) { | |||
| 			PluginClassFunc: func(ctx context.Context) plugins.Class { | ||||
| 				return plugins.ClassExternal | ||||
| 			}, | ||||
| 			PluginURIsFunc: func(ctx context.Context) []string { | ||||
| 				return tt.pluginPaths | ||||
| 			}, | ||||
| 			DiscoverFunc: sources.NewLocalSource(plugins.ClassExternal, tt.pluginPaths).Discover, | ||||
| 		}) | ||||
| 		require.NoError(t, err) | ||||
| 
 | ||||
|  | @ -933,9 +924,7 @@ func TestLoader_Load_Signature_RootURL(t *testing.T) { | |||
| 			PluginClassFunc: func(ctx context.Context) plugins.Class { | ||||
| 				return plugins.ClassExternal | ||||
| 			}, | ||||
| 			PluginURIsFunc: func(ctx context.Context) []string { | ||||
| 				return []string{filepath.Join(testDataDir(t), "valid-v2-pvt-signature-root-url-uri")} | ||||
| 			}, | ||||
| 			DiscoverFunc: sources.NewLocalSource(plugins.ClassExternal, []string{filepath.Join(testDataDir(t), "valid-v2-pvt-signature-root-url-uri")}).Discover, | ||||
| 		}) | ||||
| 		require.NoError(t, err) | ||||
| 
 | ||||
|  | @ -1021,9 +1010,7 @@ func TestLoader_Load_DuplicatePlugins(t *testing.T) { | |||
| 			PluginClassFunc: func(ctx context.Context) plugins.Class { | ||||
| 				return plugins.ClassExternal | ||||
| 			}, | ||||
| 			PluginURIsFunc: func(ctx context.Context) []string { | ||||
| 				return []string{filepath.Join(testDataDir(t), "test-app"), filepath.Join(testDataDir(t), "test-app")} | ||||
| 			}, | ||||
| 			DiscoverFunc: sources.NewLocalSource(plugins.ClassExternal, []string{filepath.Join(testDataDir(t), "test-app"), filepath.Join(testDataDir(t), "test-app")}).Discover, | ||||
| 		}) | ||||
| 		require.NoError(t, err) | ||||
| 
 | ||||
|  | @ -1122,9 +1109,7 @@ func TestLoader_Load_SkipUninitializedPlugins(t *testing.T) { | |||
| 			PluginClassFunc: func(ctx context.Context) plugins.Class { | ||||
| 				return plugins.ClassExternal | ||||
| 			}, | ||||
| 			PluginURIsFunc: func(ctx context.Context) []string { | ||||
| 				return []string{pluginDir1, pluginDir2} | ||||
| 			}, | ||||
| 			DiscoverFunc: sources.NewLocalSource(plugins.ClassExternal, []string{pluginDir1, pluginDir2}).Discover, | ||||
| 		}) | ||||
| 		require.NoError(t, err) | ||||
| 
 | ||||
|  | @ -1163,9 +1148,7 @@ func TestLoader_AngularClass(t *testing.T) { | |||
| 				PluginClassFunc: func(ctx context.Context) plugins.Class { | ||||
| 					return tc.class | ||||
| 				}, | ||||
| 				PluginURIsFunc: func(ctx context.Context) []string { | ||||
| 					return []string{filepath.Join(testDataDir(t), "valid-v2-signature")} | ||||
| 				}, | ||||
| 				DiscoverFunc: sources.NewLocalSource(plugins.ClassExternal, []string{filepath.Join(testDataDir(t), "valid-v2-signature")}).Discover, | ||||
| 			} | ||||
| 			// if angularDetected = true, it means that the detection has run
 | ||||
| 			l := newLoaderWithOpts(t, &config.PluginManagementCfg{}, loaderDepOpts{ | ||||
|  | @ -1188,9 +1171,7 @@ func TestLoader_Load_Angular(t *testing.T) { | |||
| 		PluginClassFunc: func(ctx context.Context) plugins.Class { | ||||
| 			return plugins.ClassExternal | ||||
| 		}, | ||||
| 		PluginURIsFunc: func(ctx context.Context) []string { | ||||
| 			return []string{filepath.Join(testDataDir(t), "valid-v2-signature")} | ||||
| 		}, | ||||
| 		DiscoverFunc: sources.NewLocalSource(plugins.ClassExternal, []string{filepath.Join(testDataDir(t), "valid-v2-signature")}).Discover, | ||||
| 	} | ||||
| 	for _, cfgTc := range []struct { | ||||
| 		name string | ||||
|  | @ -1238,9 +1219,7 @@ func TestLoader_HideAngularDeprecation(t *testing.T) { | |||
| 		PluginClassFunc: func(ctx context.Context) plugins.Class { | ||||
| 			return plugins.ClassExternal | ||||
| 		}, | ||||
| 		PluginURIsFunc: func(ctx context.Context) []string { | ||||
| 			return []string{filepath.Join(testDataDir(t), "valid-v2-signature")} | ||||
| 		}, | ||||
| 		DiscoverFunc: sources.NewLocalSource(plugins.ClassExternal, []string{filepath.Join(testDataDir(t), "valid-v2-signature")}).Discover, | ||||
| 	} | ||||
| 	for _, tc := range []struct { | ||||
| 		name string | ||||
|  | @ -1369,9 +1348,7 @@ func TestLoader_Load_NestedPlugins(t *testing.T) { | |||
| 			PluginClassFunc: func(ctx context.Context) plugins.Class { | ||||
| 				return plugins.ClassExternal | ||||
| 			}, | ||||
| 			PluginURIsFunc: func(ctx context.Context) []string { | ||||
| 				return []string{filepath.Join(testDataDir(t), "nested-plugins")} | ||||
| 			}, | ||||
| 			DiscoverFunc: sources.NewLocalSource(plugins.ClassExternal, []string{filepath.Join(testDataDir(t), "nested-plugins")}).Discover, | ||||
| 		}) | ||||
| 		require.NoError(t, err) | ||||
| 
 | ||||
|  | @ -1392,9 +1369,7 @@ func TestLoader_Load_NestedPlugins(t *testing.T) { | |||
| 				PluginClassFunc: func(ctx context.Context) plugins.Class { | ||||
| 					return plugins.ClassExternal | ||||
| 				}, | ||||
| 				PluginURIsFunc: func(ctx context.Context) []string { | ||||
| 					return []string{filepath.Join(testDataDir(t), "nested-plugins")} | ||||
| 				}, | ||||
| 				DiscoverFunc: sources.NewLocalSource(plugins.ClassExternal, []string{filepath.Join(testDataDir(t), "nested-plugins")}).Discover, | ||||
| 			}) | ||||
| 			require.NoError(t, err) | ||||
| 
 | ||||
|  | @ -1571,9 +1546,7 @@ func TestLoader_Load_NestedPlugins(t *testing.T) { | |||
| 			PluginClassFunc: func(ctx context.Context) plugins.Class { | ||||
| 				return plugins.ClassExternal | ||||
| 			}, | ||||
| 			PluginURIsFunc: func(ctx context.Context) []string { | ||||
| 				return []string{filepath.Join(testDataDir(t), "app-with-child")} | ||||
| 			}, | ||||
| 			DiscoverFunc: sources.NewLocalSource(plugins.ClassExternal, []string{filepath.Join(testDataDir(t), "app-with-child")}).Discover, | ||||
| 		}) | ||||
| 		require.NoError(t, err) | ||||
| 
 | ||||
|  | @ -1605,8 +1578,7 @@ func newLoader(t *testing.T, cfg *config.PluginManagementCfg, reg registry.Servi | |||
| 	terminate, err := pipeline.ProvideTerminationStage(cfg, reg, proc) | ||||
| 	require.NoError(t, err) | ||||
| 
 | ||||
| 	return ProvideService(cfg, pipeline.ProvideDiscoveryStage(cfg, | ||||
| 		finder.NewLocalFinder(false), reg), | ||||
| 	return ProvideService(cfg, pipeline.ProvideDiscoveryStage(cfg, reg), | ||||
| 		pipeline.ProvideBootstrapStage(cfg, signature.DefaultCalculator(cfg), assets), | ||||
| 		pipeline.ProvideValidationStage(cfg, signature.NewValidator(signature.NewUnsignedAuthorizer(cfg)), angularInspector), | ||||
| 		pipeline.ProvideInitializationStage(cfg, reg, backendFactory, proc, &fakes.FakeAuthService{}, fakes.NewFakeRoleRegistry(), fakes.NewFakeActionSetRegistry(), fakes.NewFakePluginEnvProvider(), tracing.InitializeTracerForTest()), | ||||
|  | @ -1637,8 +1609,7 @@ func newLoaderWithOpts(t *testing.T, cfg *config.PluginManagementCfg, opts loade | |||
| 		backendFactoryProvider = fakes.NewFakeBackendProcessProvider() | ||||
| 	} | ||||
| 
 | ||||
| 	return ProvideService(cfg, pipeline.ProvideDiscoveryStage(cfg, | ||||
| 		finder.NewLocalFinder(false), reg), | ||||
| 	return ProvideService(cfg, pipeline.ProvideDiscoveryStage(cfg, reg), | ||||
| 		pipeline.ProvideBootstrapStage(cfg, signature.DefaultCalculator(cfg), assets), | ||||
| 		pipeline.ProvideValidationStage(cfg, signature.NewValidator(signature.NewUnsignedAuthorizer(cfg)), angularInspector), | ||||
| 		pipeline.ProvideInitializationStage(cfg, reg, backendFactoryProvider, proc, authServiceRegistry, fakes.NewFakeRoleRegistry(), fakes.NewFakeActionSetRegistry(), fakes.NewFakePluginEnvProvider(), tracing.InitializeTracerForTest()), | ||||
|  |  | |||
|  | @ -10,7 +10,6 @@ import ( | |||
| 	"github.com/grafana/grafana/pkg/plugins/envvars" | ||||
| 	"github.com/grafana/grafana/pkg/plugins/manager/loader/angular/angularinspector" | ||||
| 	"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/pipeline/bootstrap" | ||||
| 	"github.com/grafana/grafana/pkg/plugins/manager/pipeline/discovery" | ||||
| 	"github.com/grafana/grafana/pkg/plugins/manager/pipeline/initialization" | ||||
|  | @ -22,10 +21,9 @@ import ( | |||
| 	"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginaccesscontrol" | ||||
| ) | ||||
| 
 | ||||
| func ProvideDiscoveryStage(cfg *config.PluginManagementCfg, pf finder.Finder, pr registry.Service) *discovery.Discovery { | ||||
| func ProvideDiscoveryStage(cfg *config.PluginManagementCfg, pr registry.Service) *discovery.Discovery { | ||||
| 	return discovery.New(cfg, discovery.Opts{ | ||||
| 		FindFunc: pf.Find, | ||||
| 		FindFilterFuncs: []discovery.FindFilterFunc{ | ||||
| 		FilterFuncs: []discovery.FilterFunc{ | ||||
| 			discovery.NewPermittedPluginTypesFilterStep([]plugins.Type{ | ||||
| 				plugins.TypeDataSource, plugins.TypeApp, plugins.TypePanel, | ||||
| 			}), | ||||
|  |  | |||
|  | @ -18,7 +18,6 @@ import ( | |||
| 	pluginLoader "github.com/grafana/grafana/pkg/plugins/manager/loader" | ||||
| 	pAngularInspector "github.com/grafana/grafana/pkg/plugins/manager/loader/angular/angularinspector" | ||||
| 	"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/pipeline/bootstrap" | ||||
| 	"github.com/grafana/grafana/pkg/plugins/manager/pipeline/discovery" | ||||
| 	"github.com/grafana/grafana/pkg/plugins/manager/pipeline/initialization" | ||||
|  | @ -144,8 +143,6 @@ var WireExtensionSet = wire.NewSet( | |||
| 	wire.Bind(new(plugins.BackendFactoryProvider), new(*provider.Service)), | ||||
| 	signature.ProvideOSSAuthorizer, | ||||
| 	wire.Bind(new(plugins.PluginLoaderAuthorizer), new(*signature.UnsignedPluginAuthorizer)), | ||||
| 	finder.ProvideLocalFinder, | ||||
| 	wire.Bind(new(finder.Finder), new(*finder.Local)), | ||||
| 	ProvideClientWithMiddlewares, | ||||
| 	wire.Bind(new(plugins.Client), new(*backend.MiddlewareHandler)), | ||||
| 	managedplugins.NewNoop, | ||||
|  |  | |||
|  | @ -15,10 +15,10 @@ import ( | |||
| 
 | ||||
| func TestStore_ProvideService(t *testing.T) { | ||||
| 	t.Run("Plugin sources are added in order", func(t *testing.T) { | ||||
| 		var addedPaths []string | ||||
| 		var loadedSrcs []plugins.Class | ||||
| 		l := &fakes.FakeLoader{ | ||||
| 			LoadFunc: func(ctx context.Context, src plugins.PluginSource) ([]*plugins.Plugin, error) { | ||||
| 				addedPaths = append(addedPaths, src.PluginURIs(ctx)...) | ||||
| 				loadedSrcs = append(loadedSrcs, src.PluginClass(ctx)) | ||||
| 				return nil, nil | ||||
| 			}, | ||||
| 		} | ||||
|  | @ -27,18 +27,17 @@ func TestStore_ProvideService(t *testing.T) { | |||
| 			return []plugins.PluginSource{ | ||||
| 				&fakes.FakePluginSource{ | ||||
| 					PluginClassFunc: func(ctx context.Context) plugins.Class { | ||||
| 						return "foobar" | ||||
| 					}, | ||||
| 					PluginURIsFunc: func(ctx context.Context) []string { | ||||
| 						return []string{"path1"} | ||||
| 						return "1" | ||||
| 					}, | ||||
| 				}, | ||||
| 				&fakes.FakePluginSource{ | ||||
| 					PluginClassFunc: func(ctx context.Context) plugins.Class { | ||||
| 						return plugins.ClassExternal | ||||
| 						return "2" | ||||
| 					}, | ||||
| 					PluginURIsFunc: func(ctx context.Context) []string { | ||||
| 						return []string{"path2", "path3"} | ||||
| 				}, | ||||
| 				&fakes.FakePluginSource{ | ||||
| 					PluginClassFunc: func(ctx context.Context) plugins.Class { | ||||
| 						return "3" | ||||
| 					}, | ||||
| 				}, | ||||
| 			} | ||||
|  | @ -46,7 +45,7 @@ func TestStore_ProvideService(t *testing.T) { | |||
| 
 | ||||
| 		_, err := ProvideService(fakes.NewFakePluginRegistry(), srcs, l) | ||||
| 		require.NoError(t, err) | ||||
| 		require.Equal(t, []string{"path1", "path2", "path3"}, addedPaths) | ||||
| 		require.Equal(t, []plugins.Class{"1", "2", "3"}, loadedSrcs) | ||||
| 	}) | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -12,7 +12,6 @@ import ( | |||
| 	"github.com/stretchr/testify/require" | ||||
| 	"gopkg.in/ini.v1" | ||||
| 
 | ||||
| 	"github.com/grafana/grafana-azure-sdk-go/v2/azsettings" | ||||
| 	"github.com/grafana/grafana-plugin-sdk-go/backend" | ||||
| 	"github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" | ||||
| 
 | ||||
|  | @ -130,21 +129,26 @@ func TestIntegrationPluginManager(t *testing.T) { | |||
| 	staticRootPath, err := filepath.Abs("../../../../public/") | ||||
| 	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() | ||||
| 	cfg := &setting.Cfg{ | ||||
| 		Raw:            ini.Empty(), | ||||
| 		Raw:            raw, | ||||
| 		StaticRootPath: staticRootPath, | ||||
| 		Azure:          &azsettings.AzureSettings{}, | ||||
| 		PluginSettings: map[string]map[string]string{ | ||||
| 			"test-app": { | ||||
| 				"path": "../../../plugins/manager/testdata/test-app", | ||||
| 			}, | ||||
| 			"test-panel": { | ||||
| 				"not": "included", | ||||
| 			}, | ||||
| 		}, | ||||
| 	} | ||||
| 
 | ||||
| 	tracer := tracing.InitializeTracerForTest() | ||||
| 
 | ||||
| 	hcp := httpclient.NewProvider() | ||||
|  |  | |||
|  | @ -4,6 +4,8 @@ import ( | |||
| 	"context" | ||||
| 	"errors" | ||||
| 
 | ||||
| 	"go.opentelemetry.io/otel/trace" | ||||
| 
 | ||||
| 	"github.com/grafana/grafana/pkg/infra/log" | ||||
| 	"github.com/grafana/grafana/pkg/infra/tracing" | ||||
| 	"github.com/grafana/grafana/pkg/plugins" | ||||
|  | @ -23,7 +25,6 @@ import ( | |||
| 	"github.com/grafana/grafana/pkg/services/pluginsintegration/pipeline" | ||||
| 	"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginerrs" | ||||
| 	"github.com/grafana/grafana/pkg/services/rendering" | ||||
| 	"go.opentelemetry.io/otel/trace" | ||||
| ) | ||||
| 
 | ||||
| func ProvideService(cfg *config.PluginManagementCfg, pluginEnvProvider envvars.Provider, | ||||
|  | @ -84,7 +85,7 @@ func (m *Manager) Renderer(ctx context.Context) (rendering.Plugin, bool) { | |||
| 		return m.renderer, true | ||||
| 	} | ||||
| 
 | ||||
| 	srcs, err := sources.DirAsLocalSources(m.cfg.PluginsPath, plugins.ClassExternal) | ||||
| 	srcs, err := sources.DirAsLocalSources(m.cfg, m.cfg.PluginsPath, plugins.ClassExternal) | ||||
| 	if err != nil { | ||||
| 		m.log.Error("Failed to get renderer plugin sources", "error", err) | ||||
| 		return nil, false | ||||
|  | @ -109,7 +110,7 @@ func (m *Manager) Renderer(ctx context.Context) (rendering.Plugin, bool) { | |||
| func createLoader(cfg *config.PluginManagementCfg, pluginEnvProvider envvars.Provider, | ||||
| 	pr registry.Service, tracer trace.Tracer) (loader.Service, error) { | ||||
| 	d := discovery.New(cfg, discovery.Opts{ | ||||
| 		FindFilterFuncs: []discovery.FindFilterFunc{ | ||||
| 		FilterFuncs: []discovery.FilterFunc{ | ||||
| 			discovery.NewPermittedPluginTypesFilterStep([]plugins.Type{plugins.TypeRenderer}), | ||||
| 			func(ctx context.Context, class plugins.Class, bundles []*plugins.FoundBundle) ([]*plugins.FoundBundle, error) { | ||||
| 				return pipeline.NewDuplicatePluginIDFilterStep(pr).Filter(ctx, bundles) | ||||
|  |  | |||
|  | @ -11,6 +11,7 @@ import ( | |||
| 	"github.com/grafana/grafana/pkg/plugins" | ||||
| 	"github.com/grafana/grafana/pkg/plugins/config" | ||||
| 	"github.com/grafana/grafana/pkg/plugins/manager/fakes" | ||||
| 	"github.com/grafana/grafana/pkg/plugins/manager/sources" | ||||
| ) | ||||
| 
 | ||||
| func TestRenderer(t *testing.T) { | ||||
|  | @ -22,8 +23,14 @@ func TestRenderer(t *testing.T) { | |||
| 		loader := &fakes.FakeLoader{ | ||||
| 			LoadFunc: func(ctx context.Context, src plugins.PluginSource) ([]*plugins.Plugin, error) { | ||||
| 				require.True(t, src.PluginClass(ctx) == plugins.ClassExternal) | ||||
| 				require.Len(t, src.PluginURIs(ctx), 1) | ||||
| 				require.True(t, strings.HasPrefix(src.PluginURIs(ctx)[0], testdataDir)) | ||||
| 
 | ||||
| 				if localSrc, ok := src.(*sources.LocalSource); ok { | ||||
| 					paths := localSrc.Paths() | ||||
| 					require.Len(t, paths, 1) | ||||
| 					require.True(t, strings.HasPrefix(paths[0], testdataDir)) | ||||
| 				} else { | ||||
| 					t.Fatalf("Expected LocalSource, got %T", src) | ||||
| 				} | ||||
| 
 | ||||
| 				numLoaded++ | ||||
| 				return []*plugins.Plugin{}, nil | ||||
|  | @ -55,9 +62,16 @@ func TestRenderer(t *testing.T) { | |||
| 		loader := &fakes.FakeLoader{ | ||||
| 			LoadFunc: func(ctx context.Context, src plugins.PluginSource) ([]*plugins.Plugin, error) { | ||||
| 				numLoaded++ | ||||
| 				if strings.HasPrefix(src.PluginURIs(ctx)[0], filepath.Join(testdataDir, "renderer")) { | ||||
| 
 | ||||
| 				if localSrc, ok := src.(*sources.LocalSource); ok { | ||||
| 					paths := localSrc.Paths() | ||||
| 					if strings.HasPrefix(paths[0], filepath.Join(testdataDir, "renderer")) { | ||||
| 						return []*plugins.Plugin{p}, nil | ||||
| 					} | ||||
| 				} else { | ||||
| 					t.Fatalf("Expected LocalSource, got %T", src) | ||||
| 				} | ||||
| 
 | ||||
| 				return []*plugins.Plugin{}, nil | ||||
| 			}, | ||||
| 			UnloadFunc: func(_ context.Context, _ *plugins.Plugin) (*plugins.Plugin, error) { | ||||
|  |  | |||
|  | @ -16,7 +16,6 @@ import ( | |||
| 	"github.com/grafana/grafana/pkg/plugins/manager/loader" | ||||
| 	"github.com/grafana/grafana/pkg/plugins/manager/loader/angular/angularinspector" | ||||
| 	"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/pipeline/bootstrap" | ||||
| 	"github.com/grafana/grafana/pkg/plugins/manager/pipeline/discovery" | ||||
| 	"github.com/grafana/grafana/pkg/plugins/manager/pipeline/initialization" | ||||
|  | @ -51,7 +50,7 @@ func CreateIntegrationTestCtx(t *testing.T, cfg *setting.Cfg, coreRegistry *core | |||
| 	angularInspector := angularinspector.NewStaticInspector() | ||||
| 	proc := process.ProvideService() | ||||
| 
 | ||||
| 	disc := pipeline.ProvideDiscoveryStage(pCfg, finder.NewLocalFinder(true), reg) | ||||
| 	disc := pipeline.ProvideDiscoveryStage(pCfg, reg) | ||||
| 	boot := pipeline.ProvideBootstrapStage(pCfg, signature.ProvideService(pCfg, statickey.New()), assetpath.ProvideService(pCfg, cdn)) | ||||
| 	valid := pipeline.ProvideValidationStage(pCfg, signature.NewValidator(signature.NewUnsignedAuthorizer(pCfg)), angularInspector) | ||||
| 	init := pipeline.ProvideInitializationStage(pCfg, reg, provider.ProvideService(coreRegistry), proc, &fakes.FakeAuthService{}, fakes.NewFakeRoleRegistry(), fakes.NewFakeActionSetRegistry(), nil, tracing.InitializeTracerForTest()) | ||||
|  | @ -66,7 +65,7 @@ func CreateIntegrationTestCtx(t *testing.T, cfg *setting.Cfg, coreRegistry *core | |||
| 		Terminator:   term, | ||||
| 	}) | ||||
| 
 | ||||
| 	ps, err := pluginstore.ProvideService(reg, sources.ProvideService(cfg), l) | ||||
| 	ps, err := pluginstore.ProvideService(reg, sources.ProvideService(cfg, pCfg), l) | ||||
| 	require.NoError(t, err) | ||||
| 
 | ||||
| 	return &IntegrationTestCtx{ | ||||
|  | @ -86,7 +85,7 @@ type LoaderOpts struct { | |||
| 
 | ||||
| func CreateTestLoader(t *testing.T, cfg *pluginsCfg.PluginManagementCfg, opts LoaderOpts) *loader.Loader { | ||||
| 	if opts.Discoverer == nil { | ||||
| 		opts.Discoverer = pipeline.ProvideDiscoveryStage(cfg, finder.NewLocalFinder(cfg.DevMode), registry.ProvideService()) | ||||
| 		opts.Discoverer = pipeline.ProvideDiscoveryStage(cfg, registry.ProvideService()) | ||||
| 	} | ||||
| 
 | ||||
| 	if opts.Bootstrapper == nil { | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue