diff --git a/pkg/api/plugin_resource_test.go b/pkg/api/plugin_resource_test.go index 97c08eaf6e0..b3497f5e6d4 100644 --- a/pkg/api/plugin_resource_test.go +++ b/pkg/api/plugin_resource_test.go @@ -38,6 +38,7 @@ import ( "github.com/grafana/grafana/pkg/services/oauthtoken/oauthtokentest" "github.com/grafana/grafana/pkg/services/pluginsintegration" "github.com/grafana/grafana/pkg/services/pluginsintegration/config" + "github.com/grafana/grafana/pkg/services/pluginsintegration/pipeline" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginaccesscontrol" "github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext" pluginSettings "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings/service" @@ -69,10 +70,14 @@ func TestCallResource(t *testing.T) { reg := registry.ProvideService() angularInspector, err := angularinspector.NewStaticInspector() require.NoError(t, err) + + discovery := pipeline.ProvideDiscoveryStage(pCfg, finder.NewLocalFinder(pCfg.DevMode), reg) + bootstrap := pipeline.ProvideBootstrapStage(pCfg, signature.ProvideService(statickey.New()), assetpath.ProvideService(pluginscdn.ProvideService(pCfg))) + l := loader.ProvideService(pCfg, fakes.NewFakeLicensingService(), signature.NewUnsignedAuthorizer(pCfg), - reg, provider.ProvideService(coreRegistry), finder.NewLocalFinder(pCfg.DevMode), fakes.NewFakeRoleRegistry(), - assetpath.ProvideService(pluginscdn.ProvideService(pCfg)), signature.ProvideService(statickey.New()), - angularInspector, &fakes.FakeOauthService{}) + reg, provider.ProvideService(coreRegistry), fakes.NewFakeRoleRegistry(), + assetpath.ProvideService(pluginscdn.ProvideService(pCfg)), + angularInspector, &fakes.FakeOauthService{}, discovery, bootstrap) srcs := sources.ProvideService(cfg, pCfg) ps, err := store.ProvideService(reg, srcs, l) require.NoError(t, err) diff --git a/pkg/plugins/manager/loader/assetpath/assetpath.go b/pkg/plugins/manager/loader/assetpath/assetpath.go index 76ee0637805..0b093b0b753 100644 --- a/pkg/plugins/manager/loader/assetpath/assetpath.go +++ b/pkg/plugins/manager/loader/assetpath/assetpath.go @@ -8,6 +8,7 @@ import ( "strings" "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/plugins/config" "github.com/grafana/grafana/pkg/plugins/pluginscdn" ) @@ -22,6 +23,10 @@ func ProvideService(cdn *pluginscdn.Service) *Service { return &Service{cdn: cdn} } +func DefaultService(cfg *config.Cfg) *Service { + return &Service{cdn: pluginscdn.ProvideService(cfg)} +} + // Base returns the base path for the specified plugin. func (s *Service) Base(pluginJSON plugins.JSONData, class plugins.Class, pluginDir string) (string, error) { if class == plugins.ClassCore { diff --git a/pkg/plugins/manager/loader/loader.go b/pkg/plugins/manager/loader/loader.go index 9852efcf9e7..88b7e722e8e 100644 --- a/pkg/plugins/manager/loader/loader.go +++ b/pkg/plugins/manager/loader/loader.go @@ -3,38 +3,35 @@ package loader import ( "context" "errors" - "fmt" - "path" - "strings" "time" "github.com/grafana/grafana/pkg/infra/metrics" - "github.com/grafana/grafana/pkg/infra/slugify" "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/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/loader/initializer" + "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/process" "github.com/grafana/grafana/pkg/plugins/manager/registry" "github.com/grafana/grafana/pkg/plugins/manager/signature" "github.com/grafana/grafana/pkg/plugins/oauth" "github.com/grafana/grafana/pkg/services/featuremgmt" - "github.com/grafana/grafana/pkg/util" ) var _ plugins.ErrorResolver = (*Loader)(nil) type Loader struct { - pluginFinder finder.Finder + discovery discovery.Discoverer + bootstrap bootstrap.Bootstrapper + processManager process.Service pluginRegistry registry.Service roleRegistry plugins.RoleRegistry pluginInitializer initializer.Initializer signatureValidator signature.Validator - signatureCalculator plugins.SignatureCalculator externalServiceRegistry oauth.ExternalServiceRegistry assetPath *assetpath.Service log log.Logger @@ -46,24 +43,23 @@ type Loader struct { } func ProvideService(cfg *config.Cfg, license plugins.Licensing, authorizer plugins.PluginLoaderAuthorizer, - pluginRegistry registry.Service, backendProvider plugins.BackendFactoryProvider, pluginFinder finder.Finder, - roleRegistry plugins.RoleRegistry, assetPath *assetpath.Service, signatureCalculator plugins.SignatureCalculator, - angularInspector angularinspector.Inspector, externalServiceRegistry oauth.ExternalServiceRegistry) *Loader { + pluginRegistry registry.Service, backendProvider plugins.BackendFactoryProvider, + roleRegistry plugins.RoleRegistry, assetPath *assetpath.Service, + angularInspector angularinspector.Inspector, externalServiceRegistry oauth.ExternalServiceRegistry, + discovery discovery.Discoverer, bootstrap bootstrap.Bootstrapper) *Loader { return New(cfg, license, authorizer, pluginRegistry, backendProvider, process.NewManager(pluginRegistry), - roleRegistry, assetPath, pluginFinder, signatureCalculator, angularInspector, externalServiceRegistry) + roleRegistry, assetPath, angularInspector, externalServiceRegistry, discovery, bootstrap) } func New(cfg *config.Cfg, license plugins.Licensing, authorizer plugins.PluginLoaderAuthorizer, pluginRegistry registry.Service, backendProvider plugins.BackendFactoryProvider, - processManager process.Service, roleRegistry plugins.RoleRegistry, - assetPath *assetpath.Service, pluginFinder finder.Finder, signatureCalculator plugins.SignatureCalculator, - angularInspector angularinspector.Inspector, externalServiceRegistry oauth.ExternalServiceRegistry) *Loader { + processManager process.Service, roleRegistry plugins.RoleRegistry, assetPath *assetpath.Service, + angularInspector angularinspector.Inspector, externalServiceRegistry oauth.ExternalServiceRegistry, + discovery discovery.Discoverer, bootstrap bootstrap.Bootstrapper) *Loader { return &Loader{ - pluginFinder: pluginFinder, pluginRegistry: pluginRegistry, pluginInitializer: initializer.New(cfg, backendProvider, license), signatureValidator: signature.NewValidator(authorizer), - signatureCalculator: signatureCalculator, processManager: processManager, errs: make(map[string]*plugins.SignatureError), log: log.New("plugin.loader"), @@ -72,70 +68,29 @@ func New(cfg *config.Cfg, license plugins.Licensing, authorizer plugins.PluginLo assetPath: assetPath, angularInspector: angularInspector, externalServiceRegistry: externalServiceRegistry, + discovery: discovery, + bootstrap: bootstrap, } } func (l *Loader) Load(ctx context.Context, src plugins.PluginSource) ([]*plugins.Plugin, error) { - found, err := l.pluginFinder.Find(ctx, src) + // + discoveredPlugins, err := l.discovery.Discover(ctx, src) if err != nil { return nil, err } + // - return l.loadPlugins(ctx, src, found) -} - -// nolint:gocyclo -func (l *Loader) loadPlugins(ctx context.Context, src plugins.PluginSource, found []*plugins.FoundBundle) ([]*plugins.Plugin, error) { - loadedPlugins := make([]*plugins.Plugin, 0, len(found)) - - for _, p := range found { - if _, exists := l.pluginRegistry.Plugin(ctx, p.Primary.JSONData.ID); exists { - l.log.Warn("Skipping plugin loading as it's a duplicate", "pluginID", p.Primary.JSONData.ID) - continue - } - - sig, err := l.signatureCalculator.Calculate(ctx, src, p.Primary) - if err != nil { - l.log.Warn("Could not calculate plugin signature state", "pluginID", p.Primary.JSONData.ID, "err", err) - continue - } - plugin, err := l.createPluginBase(p.Primary.JSONData, src.PluginClass(ctx), p.Primary.FS) - if err != nil { - l.log.Error("Could not create primary plugin base", "pluginID", p.Primary.JSONData.ID, "err", err) - continue - } - - plugin.Signature = sig.Status - plugin.SignatureType = sig.Type - plugin.SignatureOrg = sig.SigningOrg - - loadedPlugins = append(loadedPlugins, plugin) - - for _, c := range p.Children { - if _, exists := l.pluginRegistry.Plugin(ctx, c.JSONData.ID); exists { - l.log.Warn("Skipping plugin loading as it's a duplicate", "pluginID", p.Primary.JSONData.ID) - continue - } - - cp, err := l.createPluginBase(c.JSONData, plugin.Class, c.FS) - if err != nil { - l.log.Error("Could not create child plugin base", "pluginID", p.Primary.JSONData.ID, "err", err) - continue - } - cp.Parent = plugin - cp.Signature = sig.Status - cp.SignatureType = sig.Type - cp.SignatureOrg = sig.SigningOrg - - plugin.Children = append(plugin.Children, cp) - - loadedPlugins = append(loadedPlugins, cp) - } + // + bootstrappedPlugins, err := l.bootstrap.Bootstrap(ctx, src, discoveredPlugins) + if err != nil { + return nil, err } + // - // validate signatures - verifiedPlugins := make([]*plugins.Plugin, 0, len(loadedPlugins)) - for _, plugin := range loadedPlugins { + // + verifiedPlugins := make([]*plugins.Plugin, 0, len(bootstrappedPlugins)) + for _, plugin := range bootstrappedPlugins { signingError := l.signatureValidator.Validate(plugin) if signingError != nil { l.log.Warn("Skipping loading plugin due to problem with signature", @@ -149,14 +104,6 @@ func (l *Loader) loadPlugins(ctx context.Context, src plugins.PluginSource, foun // clear plugin error if a pre-existing error has since been resolved delete(l.errs, plugin.ID) - // Hardcoded alias changes - switch plugin.ID { - case "grafana-pyroscope-datasource": // rebranding - plugin.Alias = "phlare" - case "debug": // panel plugin used for testing - plugin.Alias = "debugX" - } - // verify module.js exists for SystemJS to load. // CDN plugins can be loaded with plugin.json only, so do not warn for those. if !plugin.IsRenderer() && !plugin.IsCorePlugin() { @@ -173,37 +120,56 @@ func (l *Loader) loadPlugins(ctx context.Context, src plugins.PluginSource, foun } } - if plugin.IsApp() { - setDefaultNavURL(plugin) - } + // detect angular for external plugins + if plugin.IsExternalPlugin() { + var err error - if plugin.Parent != nil && plugin.Parent.IsApp() { - configureAppChildPlugin(plugin.Parent, plugin) + cctx, canc := context.WithTimeout(ctx, time.Second*10) + plugin.AngularDetected, err = l.angularInspector.Inspect(cctx, plugin) + canc() + + if err != nil { + l.log.Warn("Could not inspect plugin for angular", "pluginID", plugin.ID, "err", err) + } + + // Do not initialize plugins if they're using Angular and Angular support is disabled + if plugin.AngularDetected && !l.cfg.AngularSupportEnabled { + l.log.Error("Refusing to initialize plugin because it's using Angular, which has been disabled", "pluginID", plugin.ID) + continue + } } verifiedPlugins = append(verifiedPlugins, plugin) } + // - // initialize plugins + // initializedPlugins := make([]*plugins.Plugin, 0, len(verifiedPlugins)) for _, p := range verifiedPlugins { - // detect angular for external plugins - if p.IsExternalPlugin() { - var err error + err = l.pluginInitializer.Initialize(ctx, p) + if err != nil { + l.log.Error("Could not initialize plugin", "pluginId", p.ID, "err", err) + continue + } - cctx, canc := context.WithTimeout(ctx, time.Second*10) - p.AngularDetected, err = l.angularInspector.Inspect(cctx, p) - canc() + if err = l.pluginRegistry.Add(ctx, p); err != nil { + l.log.Error("Could not start plugin", "pluginId", p.ID, "err", err) + continue + } - if err != nil { - l.log.Warn("Could not inspect plugin for angular", "pluginID", p.ID, "err", err) - } + if !p.IsCorePlugin() { + l.log.Info("Plugin registered", "pluginID", p.ID) + } - // Do not initialize plugins if they're using Angular and Angular support is disabled - if p.AngularDetected && !l.cfg.AngularSupportEnabled { - l.log.Error("Refusing to initialize plugin because it's using Angular, which has been disabled", "pluginID", p.ID) - continue - } + initializedPlugins = append(initializedPlugins, p) + } + // + + // + for _, p := range initializedPlugins { + if err = l.processManager.Start(ctx, p.ID); err != nil { + l.log.Error("Could not start plugin", "pluginId", p.ID, "err", err) + continue } if p.ExternalServiceRegistration != nil && l.cfg.Features.IsEnabled(featuremgmt.FlagExternalServiceAuth) { @@ -215,27 +181,15 @@ func (l *Loader) loadPlugins(ctx context.Context, src plugins.PluginSource, foun p.ExternalService = s } - err := l.pluginInitializer.Initialize(ctx, p) - if err != nil { - l.log.Error("Could not initialize plugin", "pluginId", p.ID, "err", err) - continue - } - if errDeclareRoles := l.roleRegistry.DeclarePluginRoles(ctx, p.ID, p.Name, p.Roles); errDeclareRoles != nil { - l.log.Warn("Declare plugin roles failed.", "pluginID", p.ID, "err", errDeclareRoles) - } - - initializedPlugins = append(initializedPlugins, p) - } - - for _, p := range initializedPlugins { - if err := l.load(ctx, p); err != nil { - l.log.Error("Could not start plugin", "pluginId", p.ID, "err", err) + if err = l.roleRegistry.DeclarePluginRoles(ctx, p.ID, p.Name, p.Roles); err != nil { + l.log.Warn("Declare plugin roles failed.", "pluginID", p.ID, "err", err) } if !p.IsCorePlugin() && !p.IsBundledPlugin() { metrics.SetPluginBuildInformation(p.ID, string(p.Type), p.Info.Version, string(p.Signature)) } } + // return initializedPlugins, nil } @@ -256,18 +210,6 @@ func (l *Loader) Unload(ctx context.Context, pluginID string) error { return nil } -func (l *Loader) load(ctx context.Context, p *plugins.Plugin) error { - if err := l.pluginRegistry.Add(ctx, p); err != nil { - return err - } - - if !p.IsCorePlugin() { - l.log.Info("Plugin registered", "pluginID", p.ID) - } - - return l.processManager.Start(ctx, p.ID) -} - func (l *Loader) unload(ctx context.Context, p *plugins.Plugin) error { l.log.Debug("Stopping plugin process", "pluginId", p.ID) @@ -289,94 +231,6 @@ func (l *Loader) unload(ctx context.Context, p *plugins.Plugin) error { return nil } -func (l *Loader) createPluginBase(pluginJSON plugins.JSONData, class plugins.Class, files plugins.FS) (*plugins.Plugin, error) { - baseURL, err := l.assetPath.Base(pluginJSON, class, files.Base()) - if err != nil { - return nil, fmt.Errorf("base url: %w", err) - } - moduleURL, err := l.assetPath.Module(pluginJSON, class, files.Base()) - if err != nil { - return nil, fmt.Errorf("module url: %w", err) - } - plugin := &plugins.Plugin{ - JSONData: pluginJSON, - FS: files, - BaseURL: baseURL, - Module: moduleURL, - Class: class, - } - - plugin.SetLogger(log.New(fmt.Sprintf("plugin.%s", plugin.ID))) - if err := l.setImages(plugin); err != nil { - return nil, err - } - - return plugin, nil -} - -func (l *Loader) setImages(p *plugins.Plugin) error { - var err error - for _, dst := range []*string{&p.Info.Logos.Small, &p.Info.Logos.Large} { - *dst, err = l.assetPath.RelativeURL(p, *dst, defaultLogoPath(p.Type)) - if err != nil { - return fmt.Errorf("logo: %w", err) - } - } - for i := 0; i < len(p.Info.Screenshots); i++ { - screenshot := &p.Info.Screenshots[i] - screenshot.Path, err = l.assetPath.RelativeURL(p, screenshot.Path, "") - if err != nil { - return fmt.Errorf("screenshot %d relative url: %w", i, err) - } - } - return nil -} - -func setDefaultNavURL(p *plugins.Plugin) { - // slugify pages - for _, include := range p.Includes { - if include.Slug == "" { - include.Slug = slugify.Slugify(include.Name) - } - - if !include.DefaultNav { - continue - } - - if include.Type == "page" { - p.DefaultNavURL = path.Join("/plugins/", p.ID, "/page/", include.Slug) - } - if include.Type == "dashboard" { - dboardURL := include.DashboardURLPath() - if dboardURL == "" { - p.Logger().Warn("Included dashboard is missing a UID field") - continue - } - - p.DefaultNavURL = dboardURL - } - } -} - -func configureAppChildPlugin(parent *plugins.Plugin, child *plugins.Plugin) { - if !parent.IsApp() { - return - } - appSubPath := strings.ReplaceAll(strings.Replace(child.FS.Base(), parent.FS.Base(), "", 1), "\\", "/") - child.IncludedInAppID = parent.ID - child.BaseURL = parent.BaseURL - - if parent.IsCorePlugin() { - child.Module = util.JoinURLFragments("app/plugins/app/"+parent.ID, appSubPath) + "/module" - } else { - child.Module = util.JoinURLFragments("plugins/"+parent.ID, appSubPath) + "/module" - } -} - -func defaultLogoPath(pluginType plugins.Type) string { - return "public/img/icn-" + string(pluginType) + ".svg" -} - func (l *Loader) PluginErrors() []*plugins.Error { errs := make([]*plugins.Error, 0, len(l.errs)) for _, err := range l.errs { diff --git a/pkg/plugins/manager/loader/loader_test.go b/pkg/plugins/manager/loader/loader_test.go index b93eac7daf0..68f3a3275ab 100644 --- a/pkg/plugins/manager/loader/loader_test.go +++ b/pkg/plugins/manager/loader/loader_test.go @@ -18,10 +18,11 @@ 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/loader/initializer" + "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/registry" "github.com/grafana/grafana/pkg/plugins/manager/signature" - "github.com/grafana/grafana/pkg/plugins/manager/signature/statickey" "github.com/grafana/grafana/pkg/plugins/manager/sources" "github.com/grafana/grafana/pkg/plugins/pluginscdn" "github.com/grafana/grafana/pkg/services/org" @@ -543,61 +544,6 @@ func TestLoader_Load_CustomSource(t *testing.T) { }) } -func TestLoader_setDefaultNavURL(t *testing.T) { - t.Run("When including a dashboard with DefaultNav: true", func(t *testing.T) { - pluginWithDashboard := &plugins.Plugin{ - JSONData: plugins.JSONData{Includes: []*plugins.Includes{ - { - Type: "dashboard", - DefaultNav: true, - UID: "", - }, - }}, - } - logger := log.NewTestLogger() - pluginWithDashboard.SetLogger(logger) - - t.Run("Default nav URL is not set if dashboard UID field not is set", func(t *testing.T) { - setDefaultNavURL(pluginWithDashboard) - require.Equal(t, "", pluginWithDashboard.DefaultNavURL) - require.NotZero(t, logger.WarnLogs.Calls) - require.Equal(t, "Included dashboard is missing a UID field", logger.WarnLogs.Message) - }) - - t.Run("Default nav URL is set if dashboard UID field is set", func(t *testing.T) { - pluginWithDashboard.Includes[0].UID = "a1b2c3" - - setDefaultNavURL(pluginWithDashboard) - require.Equal(t, "/d/a1b2c3", pluginWithDashboard.DefaultNavURL) - }) - }) - - t.Run("When including a page with DefaultNav: true", func(t *testing.T) { - pluginWithPage := &plugins.Plugin{ - JSONData: plugins.JSONData{Includes: []*plugins.Includes{ - { - Type: "page", - DefaultNav: true, - Slug: "testPage", - }, - }}, - } - - t.Run("Default nav URL is set using slug", func(t *testing.T) { - setDefaultNavURL(pluginWithPage) - require.Equal(t, "/plugins/page/testPage", pluginWithPage.DefaultNavURL) - }) - - t.Run("Default nav URL is set using slugified Name field if Slug field is empty", func(t *testing.T) { - pluginWithPage.Includes[0].Slug = "" - pluginWithPage.Includes[0].Name = "My Test Page" - - setDefaultNavURL(pluginWithPage) - require.Equal(t, "/plugins/page/my-test-page", pluginWithPage.DefaultNavURL) - }) - }) -} - func TestLoader_Load_MultiplePlugins(t *testing.T) { parentDir, err := filepath.Abs("../") if err != nil { @@ -1257,13 +1203,19 @@ func TestLoader_Load_NestedPlugins(t *testing.T) { child.Parent = parent t.Run("Load nested External plugins", func(t *testing.T) { - reg := fakes.NewFakePluginRegistry() procPrvdr := fakes.NewFakeBackendProcessProvider() procMgr := fakes.NewFakeProcessManager() l := newLoader(t, &config.Cfg{}, func(l *Loader) { - l.pluginRegistry = reg l.processManager = procMgr l.pluginInitializer = initializer.New(&config.Cfg{}, procPrvdr, fakes.NewFakeLicensingService()) + l.discovery = discovery.New(l.cfg, discovery.Opts{ + FindFilterFuncs: []discovery.FindFilterFunc{ + func(ctx context.Context, class plugins.Class, bundles []*plugins.FoundBundle) ([]*plugins.FoundBundle, error) { + return discovery.NewDuplicatePluginFilterStep(l.pluginRegistry).Filter(ctx, bundles) + }, + }, + }, + ) }) got, err := l.Load(context.Background(), &fakes.FakePluginSource{ @@ -1286,7 +1238,7 @@ func TestLoader_Load_NestedPlugins(t *testing.T) { t.Fatalf("Result mismatch (-want +got):\n%s", cmp.Diff(got, expected, compareOpts...)) } - verifyState(t, expected, reg, procPrvdr, procMgr) + verifyState(t, expected, l.pluginRegistry, procPrvdr, procMgr) t.Run("Load will exclude plugins that already exist", func(t *testing.T) { got, err := l.Load(context.Background(), &fakes.FakePluginSource{ @@ -1308,7 +1260,7 @@ func TestLoader_Load_NestedPlugins(t *testing.T) { t.Fatalf("Result mismatch (-want +got):\n%s", cmp.Diff(got, expected, compareOpts...)) } - verifyState(t, expected, reg, procPrvdr, procMgr) + verifyState(t, expected, l.pluginRegistry, procPrvdr, procMgr) }) }) @@ -1466,36 +1418,15 @@ func TestLoader_Load_NestedPlugins(t *testing.T) { }) } -func Test_setPathsBasedOnApp(t *testing.T) { - t.Run("When setting paths based on core plugin on Windows", func(t *testing.T) { - child := &plugins.Plugin{ - FS: fakes.NewFakePluginFiles("c:\\grafana\\public\\app\\plugins\\app\\testdata-app\\datasources\\datasource"), - } - parent := &plugins.Plugin{ - JSONData: plugins.JSONData{ - Type: plugins.TypeApp, - ID: "testdata-app", - }, - Class: plugins.ClassCore, - FS: fakes.NewFakePluginFiles("c:\\grafana\\public\\app\\plugins\\app\\testdata-app"), - BaseURL: "public/app/plugins/app/testdata-app", - } - - configureAppChildPlugin(parent, child) - - require.Equal(t, "app/plugins/app/testdata-app/datasources/datasource/module", child.Module) - require.Equal(t, "testdata-app", child.IncludedInAppID) - require.Equal(t, "public/app/plugins/app/testdata-app", child.BaseURL) - }) -} - func newLoader(t *testing.T, cfg *config.Cfg, cbs ...func(loader *Loader)) *Loader { angularInspector, err := angularinspector.NewStaticInspector() + reg := fakes.NewFakePluginRegistry() + assets := assetpath.ProvideService(pluginscdn.ProvideService(cfg)) require.NoError(t, err) - l := New(cfg, &fakes.FakeLicensingService{}, signature.NewUnsignedAuthorizer(cfg), fakes.NewFakePluginRegistry(), + l := New(cfg, &fakes.FakeLicensingService{}, signature.NewUnsignedAuthorizer(cfg), reg, fakes.NewFakeBackendProcessProvider(), fakes.NewFakeProcessManager(), fakes.NewFakeRoleRegistry(), - assetpath.ProvideService(pluginscdn.ProvideService(cfg)), finder.NewLocalFinder(cfg.DevMode), - signature.ProvideService(statickey.New()), angularInspector, &fakes.FakeOauthService{}) + assets, angularInspector, &fakes.FakeOauthService{}, + discovery.New(cfg, discovery.Opts{}), bootstrap.New(cfg, bootstrap.Opts{})) for _, cb := range cbs { cb(l) @@ -1504,13 +1435,15 @@ func newLoader(t *testing.T, cfg *config.Cfg, cbs ...func(loader *Loader)) *Load return l } -func verifyState(t *testing.T, ps []*plugins.Plugin, reg *fakes.FakePluginRegistry, +func verifyState(t *testing.T, ps []*plugins.Plugin, reg registry.Service, procPrvdr *fakes.FakeBackendProcessProvider, procMngr *fakes.FakeProcessManager) { t.Helper() for _, p := range ps { - if !cmp.Equal(p, reg.Store[p.ID], compareOpts...) { - t.Fatalf("Result mismatch (-want +got):\n%s", cmp.Diff(p, reg.Store[p.ID], compareOpts...)) + regP, exists := reg.Plugin(context.Background(), p.ID) + require.True(t, exists) + if !cmp.Equal(p, regP, compareOpts...) { + t.Fatalf("Result mismatch (-want +got):\n%s", cmp.Diff(p, regP, compareOpts...)) } if p.Backend { diff --git a/pkg/plugins/manager/manager_integration_test.go b/pkg/plugins/manager/manager_integration_test.go index f87061bbee5..9526f49b7f7 100644 --- a/pkg/plugins/manager/manager_integration_test.go +++ b/pkg/plugins/manager/manager_integration_test.go @@ -10,7 +10,6 @@ import ( "github.com/grafana/grafana-azure-sdk-go/azsettings" "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" - "github.com/grafana/grafana/pkg/plugins/manager/loader/angular/angularinspector" "github.com/stretchr/testify/require" "gopkg.in/ini.v1" @@ -22,6 +21,7 @@ import ( "github.com/grafana/grafana/pkg/plugins/manager/client" "github.com/grafana/grafana/pkg/plugins/manager/fakes" "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/registry" @@ -35,6 +35,7 @@ import ( "github.com/grafana/grafana/pkg/services/licensing" "github.com/grafana/grafana/pkg/services/pluginsintegration/config" plicensing "github.com/grafana/grafana/pkg/services/pluginsintegration/licensing" + "github.com/grafana/grafana/pkg/services/pluginsintegration/pipeline" "github.com/grafana/grafana/pkg/services/searchV2" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/tsdb/azuremonitor" @@ -120,10 +121,14 @@ func TestIntegrationPluginManager(t *testing.T) { lic := plicensing.ProvideLicensing(cfg, &licensing.OSSLicensingService{Cfg: cfg}) angularInspector, err := angularinspector.NewStaticInspector() require.NoError(t, err) + + discovery := pipeline.ProvideDiscoveryStage(pCfg, finder.NewLocalFinder(pCfg.DevMode), reg) + bootstrap := pipeline.ProvideBootstrapStage(pCfg, signature.ProvideService(statickey.New()), assetpath.ProvideService(pluginscdn.ProvideService(pCfg))) + l := loader.ProvideService(pCfg, lic, signature.NewUnsignedAuthorizer(pCfg), - reg, provider.ProvideService(coreRegistry), finder.NewLocalFinder(pCfg.DevMode), fakes.NewFakeRoleRegistry(), - assetpath.ProvideService(pluginscdn.ProvideService(pCfg)), signature.ProvideService(statickey.New()), - angularInspector, &fakes.FakeOauthService{}) + reg, provider.ProvideService(coreRegistry), fakes.NewFakeRoleRegistry(), + assetpath.ProvideService(pluginscdn.ProvideService(pCfg)), + angularInspector, &fakes.FakeOauthService{}, discovery, bootstrap) srcs := sources.ProvideService(cfg, pCfg) ps, err := store.ProvideService(reg, srcs, l) require.NoError(t, err) diff --git a/pkg/plugins/manager/pipeline/bootstrap/bootstrap.go b/pkg/plugins/manager/pipeline/bootstrap/bootstrap.go new file mode 100644 index 00000000000..e4b8f2a45b8 --- /dev/null +++ b/pkg/plugins/manager/pipeline/bootstrap/bootstrap.go @@ -0,0 +1,78 @@ +package bootstrap + +import ( + "context" + + "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/loader/assetpath" + "github.com/grafana/grafana/pkg/plugins/manager/signature" +) + +// Bootstrapper is responsible for the Bootstrap stage of the plugin loader pipeline. +type Bootstrapper interface { + Bootstrap(ctx context.Context, src plugins.PluginSource, bundles []*plugins.FoundBundle) ([]*plugins.Plugin, error) +} + +// ConstructFunc is the function used for the Construct step of the Bootstrap stage. +type ConstructFunc func(ctx context.Context, src plugins.PluginSource, bundles []*plugins.FoundBundle) ([]*plugins.Plugin, error) + +// DecorateFunc is the function used for the Decorate step of the Bootstrap stage. +type DecorateFunc func(ctx context.Context, p *plugins.Plugin) (*plugins.Plugin, error) + +// Bootstrap implements the Bootstrapper interface. +// +// The Bootstrap stage is made up of the following steps (in order): +// - Construct: Create the initial plugin structs based on the plugin(s) found in the Discovery stage. +// - Decorate: Decorate the plugins with additional metadata. +// +// The Construct step is implemented by the ConstructFunc type. +// +// The Decorate step is implemented by the DecorateFunc type. +type Bootstrap struct { + constructStep ConstructFunc + decorateSteps []DecorateFunc + log log.Logger +} + +type Opts struct { + ConstructFunc ConstructFunc + DecorateFuncs []DecorateFunc +} + +// New returns a new Bootstrap stage. +func New(cfg *config.Cfg, opts Opts) *Bootstrap { + if opts.ConstructFunc == nil { + opts.ConstructFunc = DefaultConstructFunc(signature.DefaultCalculator(), assetpath.DefaultService(cfg)) + } + + if len(opts.DecorateFuncs) == 0 { + opts.DecorateFuncs = DefaultDecorateFuncs + } + + return &Bootstrap{ + constructStep: opts.ConstructFunc, + decorateSteps: opts.DecorateFuncs, + log: log.New("plugins.bootstrap"), + } +} + +// Bootstrap will execute the Construct and Decorate steps of the Bootstrap stage. +func (b *Bootstrap) Bootstrap(ctx context.Context, src plugins.PluginSource, found []*plugins.FoundBundle) ([]*plugins.Plugin, error) { + ps, err := b.constructStep(ctx, src, found) + if err != nil { + return nil, err + } + + for _, p := range ps { + for _, decorator := range b.decorateSteps { + p, err = decorator(ctx, p) + if err != nil { + return nil, err + } + } + } + + return ps, nil +} diff --git a/pkg/plugins/manager/pipeline/bootstrap/doc.go b/pkg/plugins/manager/pipeline/bootstrap/doc.go new file mode 100644 index 00000000000..d601296c698 --- /dev/null +++ b/pkg/plugins/manager/pipeline/bootstrap/doc.go @@ -0,0 +1,6 @@ +// Package bootstrap defines the second stage of the plugin loader pipeline. +// +// The Bootstrap stage must implement the Bootstrapper interface. +// - Bootstrap(ctx context.Context, src plugins.PluginSource, bundles []*plugins.FoundBundle) ([]*plugins.Plugin, error) + +package bootstrap diff --git a/pkg/plugins/manager/pipeline/bootstrap/factory.go b/pkg/plugins/manager/pipeline/bootstrap/factory.go new file mode 100644 index 00000000000..5e179b1f31c --- /dev/null +++ b/pkg/plugins/manager/pipeline/bootstrap/factory.go @@ -0,0 +1,76 @@ +package bootstrap + +import ( + "fmt" + + "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/plugins/log" + "github.com/grafana/grafana/pkg/plugins/manager/loader/assetpath" +) + +type pluginFactoryFunc func(p plugins.FoundPlugin, pluginClass plugins.Class, sig plugins.Signature) (*plugins.Plugin, error) + +// DefaultPluginFactory is the default plugin factory used by the Construct step of the Bootstrap stage. +// +// It creates the plugin using plugin information found during the Discovery stage and makes use of the assetPath +// service to set the plugin's BaseURL, Module, Logos and Screenshots fields. +type DefaultPluginFactory struct { + assetPath *assetpath.Service +} + +// NewDefaultPluginFactory returns a new DefaultPluginFactory. +func NewDefaultPluginFactory(assetPath *assetpath.Service) *DefaultPluginFactory { + return &DefaultPluginFactory{assetPath: assetPath} +} + +func (f *DefaultPluginFactory) createPlugin(p plugins.FoundPlugin, class plugins.Class, + sig plugins.Signature) (*plugins.Plugin, error) { + baseURL, err := f.assetPath.Base(p.JSONData, class, p.FS.Base()) + if err != nil { + return nil, fmt.Errorf("base url: %w", err) + } + moduleURL, err := f.assetPath.Module(p.JSONData, class, p.FS.Base()) + if err != nil { + return nil, fmt.Errorf("module url: %w", err) + } + + plugin := &plugins.Plugin{ + JSONData: p.JSONData, + FS: p.FS, + BaseURL: baseURL, + Module: moduleURL, + Class: class, + Signature: sig.Status, + SignatureType: sig.Type, + SignatureOrg: sig.SigningOrg, + } + plugin.SetLogger(log.New(fmt.Sprintf("plugin.%s", plugin.ID))) + + if err = setImages(plugin, f.assetPath); err != nil { + return nil, err + } + + return plugin, nil +} + +func setImages(p *plugins.Plugin, assetPath *assetpath.Service) error { + var err error + for _, dst := range []*string{&p.Info.Logos.Small, &p.Info.Logos.Large} { + *dst, err = assetPath.RelativeURL(p, *dst, defaultLogoPath(p.Type)) + if err != nil { + return fmt.Errorf("logo: %w", err) + } + } + for i := 0; i < len(p.Info.Screenshots); i++ { + screenshot := &p.Info.Screenshots[i] + screenshot.Path, err = assetPath.RelativeURL(p, screenshot.Path, "") + if err != nil { + return fmt.Errorf("screenshot %d relative url: %w", i, err) + } + } + return nil +} + +func defaultLogoPath(pluginType plugins.Type) string { + return fmt.Sprintf("public/img/icn-%s.svg", string(pluginType)) +} diff --git a/pkg/plugins/manager/pipeline/bootstrap/steps.go b/pkg/plugins/manager/pipeline/bootstrap/steps.go new file mode 100644 index 00000000000..4d808b00b73 --- /dev/null +++ b/pkg/plugins/manager/pipeline/bootstrap/steps.go @@ -0,0 +1,146 @@ +package bootstrap + +import ( + "context" + "path" + "strings" + + "github.com/grafana/grafana/pkg/infra/slugify" + "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/plugins/log" + "github.com/grafana/grafana/pkg/plugins/manager/loader/assetpath" + "github.com/grafana/grafana/pkg/util" +) + +// DefaultConstructor implements the default ConstructFunc used for the Construct step of the Bootstrap stage. +// +// It uses a pluginFactoryFunc to create plugins and the signatureCalculator to calculate the plugin's signature state. +type DefaultConstructor struct { + pluginFactoryFunc pluginFactoryFunc + signatureCalculator plugins.SignatureCalculator + log log.Logger +} + +// DefaultConstructFunc is the default ConstructFunc used for the Construct step of the Bootstrap stage. +func DefaultConstructFunc(signatureCalculator plugins.SignatureCalculator, assetPath *assetpath.Service) ConstructFunc { + return NewDefaultConstructor(signatureCalculator, assetPath).Construct +} + +// DefaultDecorateFuncs are the default DecorateFuncs used for the Decorate step of the Bootstrap stage. +var DefaultDecorateFuncs = []DecorateFunc{ + AliasDecorateFunc, + AppDefaultNavURLDecorateFunc, + AppChildDecorateFunc, +} + +// NewDefaultConstructor returns a new DefaultConstructor. +func NewDefaultConstructor(signatureCalculator plugins.SignatureCalculator, assetPath *assetpath.Service) *DefaultConstructor { + return &DefaultConstructor{ + pluginFactoryFunc: NewDefaultPluginFactory(assetPath).createPlugin, + signatureCalculator: signatureCalculator, + log: log.New("plugins.construct"), + } +} + +// Construct will calculate the plugin's signature state and create the plugin using the pluginFactoryFunc. +func (c *DefaultConstructor) Construct(ctx context.Context, src plugins.PluginSource, bundles []*plugins.FoundBundle) ([]*plugins.Plugin, error) { + res := make([]*plugins.Plugin, 0, len(bundles)) + + for _, bundle := range bundles { + sig, err := c.signatureCalculator.Calculate(ctx, src, bundle.Primary) + if err != nil { + c.log.Warn("Could not calculate plugin signature state", "pluginID", bundle.Primary.JSONData.ID, "err", err) + continue + } + plugin, err := c.pluginFactoryFunc(bundle.Primary, src.PluginClass(ctx), sig) + if err != nil { + c.log.Error("Could not create primary plugin base", "pluginID", bundle.Primary.JSONData.ID, "err", err) + continue + } + res = append(res, plugin) + + children := make([]*plugins.Plugin, 0, len(bundle.Children)) + for _, child := range bundle.Children { + cp, err := c.pluginFactoryFunc(*child, plugin.Class, sig) + if err != nil { + c.log.Error("Could not create child plugin base", "pluginID", child.JSONData.ID, "err", err) + continue + } + cp.Parent = plugin + plugin.Children = append(plugin.Children, cp) + + children = append(children, cp) + } + res = append(res, children...) + } + + return res, nil +} + +// AliasDecorateFunc is a DecorateFunc that sets the alias for the plugin. +func AliasDecorateFunc(_ context.Context, p *plugins.Plugin) (*plugins.Plugin, error) { + switch p.ID { + case "grafana-pyroscope-datasource": // rebranding + p.Alias = "phlare" + case "debug": // panel plugin used for testing + p.Alias = "debugX" + } + return p, nil +} + +// AppDefaultNavURLDecorateFunc is a DecorateFunc that sets the default nav URL for app plugins. +func AppDefaultNavURLDecorateFunc(_ context.Context, p *plugins.Plugin) (*plugins.Plugin, error) { + if p.IsApp() { + setDefaultNavURL(p) + } + return p, nil +} + +func setDefaultNavURL(p *plugins.Plugin) { + // slugify pages + for _, include := range p.Includes { + if include.Slug == "" { + include.Slug = slugify.Slugify(include.Name) + } + + if !include.DefaultNav { + continue + } + + if include.Type == "page" { + p.DefaultNavURL = path.Join("/plugins/", p.ID, "/page/", include.Slug) + } + if include.Type == "dashboard" { + dboardURL := include.DashboardURLPath() + if dboardURL == "" { + p.Logger().Warn("Included dashboard is missing a UID field") + continue + } + + p.DefaultNavURL = dboardURL + } + } +} + +// AppChildDecorateFunc is a DecorateFunc that configures child plugins of app plugins. +func AppChildDecorateFunc(_ context.Context, p *plugins.Plugin) (*plugins.Plugin, error) { + if p.Parent != nil && p.Parent.IsApp() { + configureAppChildPlugin(p.Parent, p) + } + return p, nil +} + +func configureAppChildPlugin(parent *plugins.Plugin, child *plugins.Plugin) { + if !parent.IsApp() { + return + } + appSubPath := strings.ReplaceAll(strings.Replace(child.FS.Base(), parent.FS.Base(), "", 1), "\\", "/") + child.IncludedInAppID = parent.ID + child.BaseURL = parent.BaseURL + + if parent.IsCorePlugin() { + child.Module = util.JoinURLFragments("app/plugins/app/"+parent.ID, appSubPath) + "/module" + } else { + child.Module = util.JoinURLFragments("plugins/"+parent.ID, appSubPath) + "/module" + } +} diff --git a/pkg/plugins/manager/pipeline/bootstrap/steps_test.go b/pkg/plugins/manager/pipeline/bootstrap/steps_test.go new file mode 100644 index 00000000000..f22ed0478b1 --- /dev/null +++ b/pkg/plugins/manager/pipeline/bootstrap/steps_test.go @@ -0,0 +1,89 @@ +package bootstrap + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/plugins/log" + "github.com/grafana/grafana/pkg/plugins/manager/fakes" +) + +func TestSetDefaultNavURL(t *testing.T) { + t.Run("When including a dashboard with DefaultNav: true", func(t *testing.T) { + pluginWithDashboard := &plugins.Plugin{ + JSONData: plugins.JSONData{Includes: []*plugins.Includes{ + { + Type: "dashboard", + DefaultNav: true, + UID: "", + }, + }}, + } + logger := log.NewTestLogger() + pluginWithDashboard.SetLogger(logger) + + t.Run("Default nav URL is not set if dashboard UID field not is set", func(t *testing.T) { + setDefaultNavURL(pluginWithDashboard) + require.Equal(t, "", pluginWithDashboard.DefaultNavURL) + require.NotZero(t, logger.WarnLogs.Calls) + require.Equal(t, "Included dashboard is missing a UID field", logger.WarnLogs.Message) + }) + + t.Run("Default nav URL is set if dashboard UID field is set", func(t *testing.T) { + pluginWithDashboard.Includes[0].UID = "a1b2c3" + + setDefaultNavURL(pluginWithDashboard) + require.Equal(t, "/d/a1b2c3", pluginWithDashboard.DefaultNavURL) + }) + }) + + t.Run("When including a page with DefaultNav: true", func(t *testing.T) { + pluginWithPage := &plugins.Plugin{ + JSONData: plugins.JSONData{Includes: []*plugins.Includes{ + { + Type: "page", + DefaultNav: true, + Slug: "testPage", + }, + }}, + } + + t.Run("Default nav URL is set using slug", func(t *testing.T) { + setDefaultNavURL(pluginWithPage) + require.Equal(t, "/plugins/page/testPage", pluginWithPage.DefaultNavURL) + }) + + t.Run("Default nav URL is set using slugified Name field if Slug field is empty", func(t *testing.T) { + pluginWithPage.Includes[0].Slug = "" + pluginWithPage.Includes[0].Name = "My Test Page" + + setDefaultNavURL(pluginWithPage) + require.Equal(t, "/plugins/page/my-test-page", pluginWithPage.DefaultNavURL) + }) + }) +} + +func TestSetPathsBasedOnApp(t *testing.T) { + t.Run("When setting paths based on core plugin on Windows", func(t *testing.T) { + child := &plugins.Plugin{ + FS: fakes.NewFakePluginFiles("c:\\grafana\\public\\app\\plugins\\app\\testdata-app\\datasources\\datasource"), + } + parent := &plugins.Plugin{ + JSONData: plugins.JSONData{ + Type: plugins.TypeApp, + ID: "testdata-app", + }, + Class: plugins.ClassCore, + FS: fakes.NewFakePluginFiles("c:\\grafana\\public\\app\\plugins\\app\\testdata-app"), + BaseURL: "public/app/plugins/app/testdata-app", + } + + configureAppChildPlugin(parent, child) + + require.Equal(t, "app/plugins/app/testdata-app/datasources/datasource/module", child.Module) + require.Equal(t, "testdata-app", child.IncludedInAppID) + require.Equal(t, "public/app/plugins/app/testdata-app", child.BaseURL) + }) +} diff --git a/pkg/plugins/manager/pipeline/discovery/discovery.go b/pkg/plugins/manager/pipeline/discovery/discovery.go new file mode 100644 index 00000000000..f24b2b5bc75 --- /dev/null +++ b/pkg/plugins/manager/pipeline/discovery/discovery.go @@ -0,0 +1,74 @@ +package discovery + +import ( + "context" + + "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/plugins/config" + "github.com/grafana/grafana/pkg/plugins/log" +) + +// Discoverer is responsible for the Discovery stage of the plugin loader pipeline. +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) + +// Discovery implements the Discoverer interface. +// +// The Discovery stage is made up of the following steps (in order): +// - Find: Find plugins (from disk, remote, etc.) +// - 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. +type Discovery struct { + findStep FindFunc + findFilterSteps []FindFilterFunc + log log.Logger +} + +type Opts struct { + FindFunc FindFunc + FindFilterFuncs []FindFilterFunc +} + +// New returns a new Discovery stage. +func New(cfg *config.Cfg, opts Opts) *Discovery { + if opts.FindFunc == nil { + opts.FindFunc = DefaultFindFunc(cfg) + } + + if len(opts.FindFilterFuncs) == 0 { + opts.FindFilterFuncs = []FindFilterFunc{} // no filters by default + } + + return &Discovery{ + findStep: opts.FindFunc, + findFilterSteps: opts.FindFilterFuncs, + log: log.New("plugins.discovery"), + } +} + +// Discover will execute the Find and Filter steps of the Discovery stage. +func (d *Discovery) Discover(ctx context.Context, src plugins.PluginSource) ([]*plugins.FoundBundle, error) { + found, err := d.findStep(ctx, src) + if err != nil { + return nil, err + } + + for _, filterStep := range d.findFilterSteps { + found, err = filterStep(ctx, src.PluginClass(ctx), found) + if err != nil { + return nil, err + } + } + + return found, nil +} diff --git a/pkg/plugins/manager/pipeline/discovery/doc.go b/pkg/plugins/manager/pipeline/discovery/doc.go new file mode 100644 index 00000000000..37d91ce8592 --- /dev/null +++ b/pkg/plugins/manager/pipeline/discovery/doc.go @@ -0,0 +1,6 @@ +// Package discovery defines the first stage of the plugin loader pipeline. + +// The Discovery stage must implement the Discoverer interface. +// - Discover(ctx context.Context, src plugins.PluginSource) ([]*plugins.FoundBundle, error) + +package discovery diff --git a/pkg/plugins/manager/pipeline/discovery/steps.go b/pkg/plugins/manager/pipeline/discovery/steps.go new file mode 100644 index 00000000000..a51d2c4c2eb --- /dev/null +++ b/pkg/plugins/manager/pipeline/discovery/steps.go @@ -0,0 +1,55 @@ +package discovery + +import ( + "context" + + "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/loader/finder" + "github.com/grafana/grafana/pkg/plugins/manager/registry" +) + +// 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.Cfg) FindFunc { + return finder.NewLocalFinder(cfg.DevMode).Find +} + +// DuplicatePluginValidation is a filter step that will filter out any plugins that are already registered with the +// registry. This includes both the primary plugin and any child plugins, which are matched using the plugin ID field. +type DuplicatePluginValidation struct { + registry registry.Service + log log.Logger +} + +// NewDuplicatePluginFilterStep returns a new DuplicatePluginValidation. +func NewDuplicatePluginFilterStep(registry registry.Service) *DuplicatePluginValidation { + return &DuplicatePluginValidation{ + registry: registry, + log: log.New("plugins.dedupe"), + } +} + +// Filter will filter out any plugins that are already registered with the registry. +func (d *DuplicatePluginValidation) Filter(ctx context.Context, bundles []*plugins.FoundBundle) ([]*plugins.FoundBundle, error) { + res := make([]*plugins.FoundBundle, 0, len(bundles)) + for _, b := range bundles { + _, exists := d.registry.Plugin(ctx, b.Primary.JSONData.ID) + if exists { + d.log.Warn("Skipping loading of plugin as it's a duplicate", "pluginID", b.Primary.JSONData.ID) + continue + } + + for _, child := range b.Children { + _, exists = d.registry.Plugin(ctx, child.JSONData.ID) + if exists { + d.log.Warn("Skipping loading of child plugin as it's a duplicate", "pluginID", child.JSONData.ID) + continue + } + } + res = append(res, b) + } + + return res, nil +} diff --git a/pkg/plugins/manager/pipeline/doc.go b/pkg/plugins/manager/pipeline/doc.go new file mode 100644 index 00000000000..9b04096e158 --- /dev/null +++ b/pkg/plugins/manager/pipeline/doc.go @@ -0,0 +1,11 @@ +// Package pipeline defines a load pipeline for Grafana plugins. +// +// A pipeline is a sequence of stages that are executed in order. Each stage is made up of a series of steps. +// A plugin loader pipeline is defined by the following stages: +// Discovery: Find plugins (e.g. from disk, remote, etc.), and [optionally] filter the results based on some criteria. +// Bootstrap: Create the plugins found in the discovery stage and enrich them with metadata. +// Verification: Verify the plugins based on some criteria (e.g. signature validation, angular detection, etc.) +// Initialization: Initialize the plugin for use (e.g. register with Grafana, etc.) +// Post-Initialization: Perform any post-initialization tasks (e.g. start the backend process, declare RBAC roles etc.) + +package pipeline diff --git a/pkg/plugins/manager/signature/manifest.go b/pkg/plugins/manager/signature/manifest.go index ffbc25230ba..93829a76809 100644 --- a/pkg/plugins/manager/signature/manifest.go +++ b/pkg/plugins/manager/signature/manifest.go @@ -23,6 +23,7 @@ import ( "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/log" + "github.com/grafana/grafana/pkg/plugins/manager/signature/statickey" "github.com/grafana/grafana/pkg/setting" ) @@ -64,9 +65,20 @@ type Signature struct { var _ plugins.SignatureCalculator = &Signature{} func ProvideService(kr plugins.KeyRetriever) *Signature { + return NewCalculator(kr) +} + +func NewCalculator(kr plugins.KeyRetriever) *Signature { return &Signature{ kr: kr, - log: log.New("plugin.signature"), + log: log.New("plugins.signature"), + } +} + +func DefaultCalculator() *Signature { + return &Signature{ + kr: statickey.New(), + log: log.New("plugins.signature"), } } diff --git a/pkg/services/pluginsintegration/pipeline/discovery.go b/pkg/services/pluginsintegration/pipeline/discovery.go new file mode 100644 index 00000000000..c4e29033a7b --- /dev/null +++ b/pkg/services/pluginsintegration/pipeline/discovery.go @@ -0,0 +1,33 @@ +package pipeline + +import ( + "context" + + "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/plugins/config" + "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/registry" +) + +func ProvideDiscoveryStage(cfg *config.Cfg, pluginFinder finder.Finder, pluginRegistry registry.Service) *discovery.Discovery { + return discovery.New(cfg, discovery.Opts{ + FindFunc: func(ctx context.Context, src plugins.PluginSource) ([]*plugins.FoundBundle, error) { + return pluginFinder.Find(ctx, src) + }, + FindFilterFuncs: []discovery.FindFilterFunc{ + func(ctx context.Context, _ plugins.Class, bundles []*plugins.FoundBundle) ([]*plugins.FoundBundle, error) { + return discovery.NewDuplicatePluginFilterStep(pluginRegistry).Filter(ctx, bundles) + }, + }, + }) +} + +func ProvideBootstrapStage(cfg *config.Cfg, signatureCalculator plugins.SignatureCalculator, assetPath *assetpath.Service) *bootstrap.Bootstrap { + return bootstrap.New(cfg, bootstrap.Opts{ + ConstructFunc: bootstrap.DefaultConstructFunc(signatureCalculator, assetPath), + DecorateFuncs: bootstrap.DefaultDecorateFuncs, + }) +} diff --git a/pkg/services/pluginsintegration/pluginsintegration.go b/pkg/services/pluginsintegration/pluginsintegration.go index 554b171338a..9630b8290b6 100644 --- a/pkg/services/pluginsintegration/pluginsintegration.go +++ b/pkg/services/pluginsintegration/pluginsintegration.go @@ -2,6 +2,7 @@ package pluginsintegration import ( "github.com/google/wire" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/backendplugin/coreplugin" @@ -14,6 +15,8 @@ import ( 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/process" "github.com/grafana/grafana/pkg/plugins/manager/registry" "github.com/grafana/grafana/pkg/plugins/manager/signature" @@ -34,6 +37,7 @@ import ( "github.com/grafana/grafana/pkg/services/pluginsintegration/keyretriever/dynamic" "github.com/grafana/grafana/pkg/services/pluginsintegration/keystore" "github.com/grafana/grafana/pkg/services/pluginsintegration/licensing" + "github.com/grafana/grafana/pkg/services/pluginsintegration/pipeline" "github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings" pluginSettings "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings/service" @@ -57,6 +61,11 @@ var WireSet = wire.NewSet( pluginscdn.ProvideService, assetpath.ProvideService, + pipeline.ProvideDiscoveryStage, + wire.Bind(new(discovery.Discoverer), new(*discovery.Discovery)), + pipeline.ProvideBootstrapStage, + wire.Bind(new(bootstrap.Bootstrapper), new(*bootstrap.Bootstrap)), + angularpatternsstore.ProvideService, angulardetectorsprovider.ProvideDynamic, angularinspector.ProvideService,