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:
Will Browne
2025-06-19 10:28:23 +01:00
committed by GitHub
parent 2b21bdf4e1
commit 3d37f969e7
23 changed files with 475 additions and 917 deletions
@@ -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")) {
return []*plugins.Plugin{p}, nil
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 {