Plugins: Plugins loader pipeline (#71438)

* discovery

* flesh out

* add docs

* remove unused func

* bootstrap stage

* fix docs

* update docs

* undo unnecessary changes

* add end tag

* update doc

* fix linter

* fix

* tidy

* update docs

* add class to filter func

* apply PR feedback

* fix test
This commit is contained in:
Will Browne
2023-07-27 15:29:13 +02:00
committed by GitHub
parent 5e5e617693
commit 758d9884bc
17 changed files with 709 additions and 312 deletions

View File

@ -38,6 +38,7 @@ import (
"github.com/grafana/grafana/pkg/services/oauthtoken/oauthtokentest" "github.com/grafana/grafana/pkg/services/oauthtoken/oauthtokentest"
"github.com/grafana/grafana/pkg/services/pluginsintegration" "github.com/grafana/grafana/pkg/services/pluginsintegration"
"github.com/grafana/grafana/pkg/services/pluginsintegration/config" "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/pluginaccesscontrol"
"github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext" "github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext"
pluginSettings "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings/service" pluginSettings "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings/service"
@ -69,10 +70,14 @@ func TestCallResource(t *testing.T) {
reg := registry.ProvideService() reg := registry.ProvideService()
angularInspector, err := angularinspector.NewStaticInspector() angularInspector, err := angularinspector.NewStaticInspector()
require.NoError(t, err) 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), l := loader.ProvideService(pCfg, fakes.NewFakeLicensingService(), signature.NewUnsignedAuthorizer(pCfg),
reg, provider.ProvideService(coreRegistry), finder.NewLocalFinder(pCfg.DevMode), fakes.NewFakeRoleRegistry(), reg, provider.ProvideService(coreRegistry), fakes.NewFakeRoleRegistry(),
assetpath.ProvideService(pluginscdn.ProvideService(pCfg)), signature.ProvideService(statickey.New()), assetpath.ProvideService(pluginscdn.ProvideService(pCfg)),
angularInspector, &fakes.FakeOauthService{}) angularInspector, &fakes.FakeOauthService{}, discovery, bootstrap)
srcs := sources.ProvideService(cfg, pCfg) srcs := sources.ProvideService(cfg, pCfg)
ps, err := store.ProvideService(reg, srcs, l) ps, err := store.ProvideService(reg, srcs, l)
require.NoError(t, err) require.NoError(t, err)

View File

@ -8,6 +8,7 @@ import (
"strings" "strings"
"github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/config"
"github.com/grafana/grafana/pkg/plugins/pluginscdn" "github.com/grafana/grafana/pkg/plugins/pluginscdn"
) )
@ -22,6 +23,10 @@ func ProvideService(cdn *pluginscdn.Service) *Service {
return &Service{cdn: cdn} 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. // Base returns the base path for the specified plugin.
func (s *Service) Base(pluginJSON plugins.JSONData, class plugins.Class, pluginDir string) (string, error) { func (s *Service) Base(pluginJSON plugins.JSONData, class plugins.Class, pluginDir string) (string, error) {
if class == plugins.ClassCore { if class == plugins.ClassCore {

View File

@ -3,38 +3,35 @@ package loader
import ( import (
"context" "context"
"errors" "errors"
"fmt"
"path"
"strings"
"time" "time"
"github.com/grafana/grafana/pkg/infra/metrics" "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"
"github.com/grafana/grafana/pkg/plugins/config" "github.com/grafana/grafana/pkg/plugins/config"
"github.com/grafana/grafana/pkg/plugins/log" "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/angular/angularinspector"
"github.com/grafana/grafana/pkg/plugins/manager/loader/assetpath" "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/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/process"
"github.com/grafana/grafana/pkg/plugins/manager/registry" "github.com/grafana/grafana/pkg/plugins/manager/registry"
"github.com/grafana/grafana/pkg/plugins/manager/signature" "github.com/grafana/grafana/pkg/plugins/manager/signature"
"github.com/grafana/grafana/pkg/plugins/oauth" "github.com/grafana/grafana/pkg/plugins/oauth"
"github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/util"
) )
var _ plugins.ErrorResolver = (*Loader)(nil) var _ plugins.ErrorResolver = (*Loader)(nil)
type Loader struct { type Loader struct {
pluginFinder finder.Finder discovery discovery.Discoverer
bootstrap bootstrap.Bootstrapper
processManager process.Service processManager process.Service
pluginRegistry registry.Service pluginRegistry registry.Service
roleRegistry plugins.RoleRegistry roleRegistry plugins.RoleRegistry
pluginInitializer initializer.Initializer pluginInitializer initializer.Initializer
signatureValidator signature.Validator signatureValidator signature.Validator
signatureCalculator plugins.SignatureCalculator
externalServiceRegistry oauth.ExternalServiceRegistry externalServiceRegistry oauth.ExternalServiceRegistry
assetPath *assetpath.Service assetPath *assetpath.Service
log log.Logger log log.Logger
@ -46,24 +43,23 @@ type Loader struct {
} }
func ProvideService(cfg *config.Cfg, license plugins.Licensing, authorizer plugins.PluginLoaderAuthorizer, func ProvideService(cfg *config.Cfg, license plugins.Licensing, authorizer plugins.PluginLoaderAuthorizer,
pluginRegistry registry.Service, backendProvider plugins.BackendFactoryProvider, pluginFinder finder.Finder, pluginRegistry registry.Service, backendProvider plugins.BackendFactoryProvider,
roleRegistry plugins.RoleRegistry, assetPath *assetpath.Service, signatureCalculator plugins.SignatureCalculator, roleRegistry plugins.RoleRegistry, assetPath *assetpath.Service,
angularInspector angularinspector.Inspector, externalServiceRegistry oauth.ExternalServiceRegistry) *Loader { angularInspector angularinspector.Inspector, externalServiceRegistry oauth.ExternalServiceRegistry,
discovery discovery.Discoverer, bootstrap bootstrap.Bootstrapper) *Loader {
return New(cfg, license, authorizer, pluginRegistry, backendProvider, process.NewManager(pluginRegistry), 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, func New(cfg *config.Cfg, license plugins.Licensing, authorizer plugins.PluginLoaderAuthorizer,
pluginRegistry registry.Service, backendProvider plugins.BackendFactoryProvider, pluginRegistry registry.Service, backendProvider plugins.BackendFactoryProvider,
processManager process.Service, roleRegistry plugins.RoleRegistry, processManager process.Service, roleRegistry plugins.RoleRegistry, assetPath *assetpath.Service,
assetPath *assetpath.Service, pluginFinder finder.Finder, signatureCalculator plugins.SignatureCalculator, angularInspector angularinspector.Inspector, externalServiceRegistry oauth.ExternalServiceRegistry,
angularInspector angularinspector.Inspector, externalServiceRegistry oauth.ExternalServiceRegistry) *Loader { discovery discovery.Discoverer, bootstrap bootstrap.Bootstrapper) *Loader {
return &Loader{ return &Loader{
pluginFinder: pluginFinder,
pluginRegistry: pluginRegistry, pluginRegistry: pluginRegistry,
pluginInitializer: initializer.New(cfg, backendProvider, license), pluginInitializer: initializer.New(cfg, backendProvider, license),
signatureValidator: signature.NewValidator(authorizer), signatureValidator: signature.NewValidator(authorizer),
signatureCalculator: signatureCalculator,
processManager: processManager, processManager: processManager,
errs: make(map[string]*plugins.SignatureError), errs: make(map[string]*plugins.SignatureError),
log: log.New("plugin.loader"), log: log.New("plugin.loader"),
@ -72,70 +68,29 @@ func New(cfg *config.Cfg, license plugins.Licensing, authorizer plugins.PluginLo
assetPath: assetPath, assetPath: assetPath,
angularInspector: angularInspector, angularInspector: angularInspector,
externalServiceRegistry: externalServiceRegistry, externalServiceRegistry: externalServiceRegistry,
discovery: discovery,
bootstrap: bootstrap,
} }
} }
func (l *Loader) Load(ctx context.Context, src plugins.PluginSource) ([]*plugins.Plugin, error) { func (l *Loader) Load(ctx context.Context, src plugins.PluginSource) ([]*plugins.Plugin, error) {
found, err := l.pluginFinder.Find(ctx, src) // <DISCOVERY STAGE>
discoveredPlugins, err := l.discovery.Discover(ctx, src)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// </DISCOVERY STAGE>
return l.loadPlugins(ctx, src, found) // <BOOTSTRAP STAGE>
} bootstrappedPlugins, err := l.bootstrap.Bootstrap(ctx, src, discoveredPlugins)
// 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 { if err != nil {
l.log.Warn("Could not calculate plugin signature state", "pluginID", p.Primary.JSONData.ID, "err", err) return nil, 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
} }
// </BOOTSTRAP STAGE>
plugin.Signature = sig.Status // <VERIFICATION STAGE>
plugin.SignatureType = sig.Type verifiedPlugins := make([]*plugins.Plugin, 0, len(bootstrappedPlugins))
plugin.SignatureOrg = sig.SigningOrg for _, plugin := range bootstrappedPlugins {
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)
}
}
// validate signatures
verifiedPlugins := make([]*plugins.Plugin, 0, len(loadedPlugins))
for _, plugin := range loadedPlugins {
signingError := l.signatureValidator.Validate(plugin) signingError := l.signatureValidator.Validate(plugin)
if signingError != nil { if signingError != nil {
l.log.Warn("Skipping loading plugin due to problem with signature", 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 // clear plugin error if a pre-existing error has since been resolved
delete(l.errs, plugin.ID) 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. // verify module.js exists for SystemJS to load.
// CDN plugins can be loaded with plugin.json only, so do not warn for those. // CDN plugins can be loaded with plugin.json only, so do not warn for those.
if !plugin.IsRenderer() && !plugin.IsCorePlugin() { if !plugin.IsRenderer() && !plugin.IsCorePlugin() {
@ -173,37 +120,56 @@ func (l *Loader) loadPlugins(ctx context.Context, src plugins.PluginSource, foun
} }
} }
if plugin.IsApp() { // detect angular for external plugins
setDefaultNavURL(plugin) if plugin.IsExternalPlugin() {
var err error
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)
} }
if plugin.Parent != nil && plugin.Parent.IsApp() { // Do not initialize plugins if they're using Angular and Angular support is disabled
configureAppChildPlugin(plugin.Parent, plugin) 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) verifiedPlugins = append(verifiedPlugins, plugin)
} }
// </VERIFICATION STAGE>
// initialize plugins // <INITIALIZATION STAGE>
initializedPlugins := make([]*plugins.Plugin, 0, len(verifiedPlugins)) initializedPlugins := make([]*plugins.Plugin, 0, len(verifiedPlugins))
for _, p := range verifiedPlugins { for _, p := range verifiedPlugins {
// detect angular for external plugins err = l.pluginInitializer.Initialize(ctx, p)
if p.IsExternalPlugin() {
var err error
cctx, canc := context.WithTimeout(ctx, time.Second*10)
p.AngularDetected, err = l.angularInspector.Inspect(cctx, p)
canc()
if err != nil { if err != nil {
l.log.Warn("Could not inspect plugin for angular", "pluginID", p.ID, "err", err) l.log.Error("Could not initialize plugin", "pluginId", p.ID, "err", err)
}
// 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 continue
} }
if err = l.pluginRegistry.Add(ctx, p); err != nil {
l.log.Error("Could not start plugin", "pluginId", p.ID, "err", err)
continue
}
if !p.IsCorePlugin() {
l.log.Info("Plugin registered", "pluginID", p.ID)
}
initializedPlugins = append(initializedPlugins, p)
}
// </INITIALIZATION STAGE>
// <POST-INITIALIZATION STAGE>
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) { 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 p.ExternalService = s
} }
err := l.pluginInitializer.Initialize(ctx, p) if err = l.roleRegistry.DeclarePluginRoles(ctx, p.ID, p.Name, p.Roles); err != nil {
if err != nil { l.log.Warn("Declare plugin roles failed.", "pluginID", p.ID, "err", err)
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 !p.IsCorePlugin() && !p.IsBundledPlugin() { if !p.IsCorePlugin() && !p.IsBundledPlugin() {
metrics.SetPluginBuildInformation(p.ID, string(p.Type), p.Info.Version, string(p.Signature)) metrics.SetPluginBuildInformation(p.ID, string(p.Type), p.Info.Version, string(p.Signature))
} }
} }
// </POST-INITIALIZATION STAGE>
return initializedPlugins, nil return initializedPlugins, nil
} }
@ -256,18 +210,6 @@ func (l *Loader) Unload(ctx context.Context, pluginID string) error {
return nil 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 { func (l *Loader) unload(ctx context.Context, p *plugins.Plugin) error {
l.log.Debug("Stopping plugin process", "pluginId", p.ID) 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 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 { func (l *Loader) PluginErrors() []*plugins.Error {
errs := make([]*plugins.Error, 0, len(l.errs)) errs := make([]*plugins.Error, 0, len(l.errs))
for _, err := range l.errs { for _, err := range l.errs {

View File

@ -18,10 +18,11 @@ import (
"github.com/grafana/grafana/pkg/plugins/manager/fakes" "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/angular/angularinspector"
"github.com/grafana/grafana/pkg/plugins/manager/loader/assetpath" "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/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"
"github.com/grafana/grafana/pkg/plugins/manager/signature/statickey"
"github.com/grafana/grafana/pkg/plugins/manager/sources" "github.com/grafana/grafana/pkg/plugins/manager/sources"
"github.com/grafana/grafana/pkg/plugins/pluginscdn" "github.com/grafana/grafana/pkg/plugins/pluginscdn"
"github.com/grafana/grafana/pkg/services/org" "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) { func TestLoader_Load_MultiplePlugins(t *testing.T) {
parentDir, err := filepath.Abs("../") parentDir, err := filepath.Abs("../")
if err != nil { if err != nil {
@ -1257,13 +1203,19 @@ func TestLoader_Load_NestedPlugins(t *testing.T) {
child.Parent = parent child.Parent = parent
t.Run("Load nested External plugins", func(t *testing.T) { t.Run("Load nested External plugins", func(t *testing.T) {
reg := fakes.NewFakePluginRegistry()
procPrvdr := fakes.NewFakeBackendProcessProvider() procPrvdr := fakes.NewFakeBackendProcessProvider()
procMgr := fakes.NewFakeProcessManager() procMgr := fakes.NewFakeProcessManager()
l := newLoader(t, &config.Cfg{}, func(l *Loader) { l := newLoader(t, &config.Cfg{}, func(l *Loader) {
l.pluginRegistry = reg
l.processManager = procMgr l.processManager = procMgr
l.pluginInitializer = initializer.New(&config.Cfg{}, procPrvdr, fakes.NewFakeLicensingService()) 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{ 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...)) 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) { t.Run("Load will exclude plugins that already exist", func(t *testing.T) {
got, err := l.Load(context.Background(), &fakes.FakePluginSource{ 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...)) 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 { func newLoader(t *testing.T, cfg *config.Cfg, cbs ...func(loader *Loader)) *Loader {
angularInspector, err := angularinspector.NewStaticInspector() angularInspector, err := angularinspector.NewStaticInspector()
reg := fakes.NewFakePluginRegistry()
assets := assetpath.ProvideService(pluginscdn.ProvideService(cfg))
require.NoError(t, err) 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(), fakes.NewFakeBackendProcessProvider(), fakes.NewFakeProcessManager(), fakes.NewFakeRoleRegistry(),
assetpath.ProvideService(pluginscdn.ProvideService(cfg)), finder.NewLocalFinder(cfg.DevMode), assets, angularInspector, &fakes.FakeOauthService{},
signature.ProvideService(statickey.New()), angularInspector, &fakes.FakeOauthService{}) discovery.New(cfg, discovery.Opts{}), bootstrap.New(cfg, bootstrap.Opts{}))
for _, cb := range cbs { for _, cb := range cbs {
cb(l) cb(l)
@ -1504,13 +1435,15 @@ func newLoader(t *testing.T, cfg *config.Cfg, cbs ...func(loader *Loader)) *Load
return l 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) { procPrvdr *fakes.FakeBackendProcessProvider, procMngr *fakes.FakeProcessManager) {
t.Helper() t.Helper()
for _, p := range ps { for _, p := range ps {
if !cmp.Equal(p, reg.Store[p.ID], compareOpts...) { regP, exists := reg.Plugin(context.Background(), p.ID)
t.Fatalf("Result mismatch (-want +got):\n%s", cmp.Diff(p, reg.Store[p.ID], compareOpts...)) 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 { if p.Backend {

View File

@ -10,7 +10,6 @@ import (
"github.com/grafana/grafana-azure-sdk-go/azsettings" "github.com/grafana/grafana-azure-sdk-go/azsettings"
"github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
"github.com/grafana/grafana/pkg/plugins/manager/loader/angular/angularinspector"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"gopkg.in/ini.v1" "gopkg.in/ini.v1"
@ -22,6 +21,7 @@ import (
"github.com/grafana/grafana/pkg/plugins/manager/client" "github.com/grafana/grafana/pkg/plugins/manager/client"
"github.com/grafana/grafana/pkg/plugins/manager/fakes" "github.com/grafana/grafana/pkg/plugins/manager/fakes"
"github.com/grafana/grafana/pkg/plugins/manager/loader" "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/assetpath"
"github.com/grafana/grafana/pkg/plugins/manager/loader/finder" "github.com/grafana/grafana/pkg/plugins/manager/loader/finder"
"github.com/grafana/grafana/pkg/plugins/manager/registry" "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/licensing"
"github.com/grafana/grafana/pkg/services/pluginsintegration/config" "github.com/grafana/grafana/pkg/services/pluginsintegration/config"
plicensing "github.com/grafana/grafana/pkg/services/pluginsintegration/licensing" 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/services/searchV2"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/tsdb/azuremonitor" "github.com/grafana/grafana/pkg/tsdb/azuremonitor"
@ -120,10 +121,14 @@ func TestIntegrationPluginManager(t *testing.T) {
lic := plicensing.ProvideLicensing(cfg, &licensing.OSSLicensingService{Cfg: cfg}) lic := plicensing.ProvideLicensing(cfg, &licensing.OSSLicensingService{Cfg: cfg})
angularInspector, err := angularinspector.NewStaticInspector() angularInspector, err := angularinspector.NewStaticInspector()
require.NoError(t, err) 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), l := loader.ProvideService(pCfg, lic, signature.NewUnsignedAuthorizer(pCfg),
reg, provider.ProvideService(coreRegistry), finder.NewLocalFinder(pCfg.DevMode), fakes.NewFakeRoleRegistry(), reg, provider.ProvideService(coreRegistry), fakes.NewFakeRoleRegistry(),
assetpath.ProvideService(pluginscdn.ProvideService(pCfg)), signature.ProvideService(statickey.New()), assetpath.ProvideService(pluginscdn.ProvideService(pCfg)),
angularInspector, &fakes.FakeOauthService{}) angularInspector, &fakes.FakeOauthService{}, discovery, bootstrap)
srcs := sources.ProvideService(cfg, pCfg) srcs := sources.ProvideService(cfg, pCfg)
ps, err := store.ProvideService(reg, srcs, l) ps, err := store.ProvideService(reg, srcs, l)
require.NoError(t, err) require.NoError(t, err)

View File

@ -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
}

View File

@ -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

View File

@ -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))
}

View File

@ -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"
}
}

View File

@ -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)
})
}

View File

@ -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
}

View File

@ -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

View File

@ -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
}

View File

@ -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

View File

@ -23,6 +23,7 @@ import (
"github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/log" "github.com/grafana/grafana/pkg/plugins/log"
"github.com/grafana/grafana/pkg/plugins/manager/signature/statickey"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
) )
@ -64,9 +65,20 @@ type Signature struct {
var _ plugins.SignatureCalculator = &Signature{} var _ plugins.SignatureCalculator = &Signature{}
func ProvideService(kr plugins.KeyRetriever) *Signature { func ProvideService(kr plugins.KeyRetriever) *Signature {
return NewCalculator(kr)
}
func NewCalculator(kr plugins.KeyRetriever) *Signature {
return &Signature{ return &Signature{
kr: kr, 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"),
} }
} }

View File

@ -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,
})
}

View File

@ -2,6 +2,7 @@ package pluginsintegration
import ( import (
"github.com/google/wire" "github.com/google/wire"
"github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/backendplugin/coreplugin" "github.com/grafana/grafana/pkg/plugins/backendplugin/coreplugin"
@ -14,6 +15,8 @@ import (
pAngularInspector "github.com/grafana/grafana/pkg/plugins/manager/loader/angular/angularinspector" 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/assetpath"
"github.com/grafana/grafana/pkg/plugins/manager/loader/finder" "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/process"
"github.com/grafana/grafana/pkg/plugins/manager/registry" "github.com/grafana/grafana/pkg/plugins/manager/registry"
"github.com/grafana/grafana/pkg/plugins/manager/signature" "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/keyretriever/dynamic"
"github.com/grafana/grafana/pkg/services/pluginsintegration/keystore" "github.com/grafana/grafana/pkg/services/pluginsintegration/keystore"
"github.com/grafana/grafana/pkg/services/pluginsintegration/licensing" "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/plugincontext"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings"
pluginSettings "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings/service" pluginSettings "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings/service"
@ -57,6 +61,11 @@ var WireSet = wire.NewSet(
pluginscdn.ProvideService, pluginscdn.ProvideService,
assetpath.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, angularpatternsstore.ProvideService,
angulardetectorsprovider.ProvideDynamic, angulardetectorsprovider.ProvideDynamic,
angularinspector.ProvideService, angularinspector.ProvideService,