[WIP] Plugins: Refactoring backend initialization flow (#42247)

* refactoring store interface and init flow

* fix import

* fix linter

* refactor resource calling

* load with class

* re-order args

* fix tests

* fix linter

* remove old creator

* add custom config struct

* fix some tests

* cleanup

* fix linter

* add connect failure error

* remove unused err

* convert test over
This commit is contained in:
Will Browne 2022-01-14 13:30:39 +01:00 committed by GitHub
parent 7ffefc069f
commit 7694fff0ef
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 566 additions and 580 deletions

View File

@ -18,7 +18,7 @@ import (
"google.golang.org/grpc/status"
)
type clientV2 struct {
type ClientV2 struct {
grpcplugin.DiagnosticsClient
grpcplugin.ResourceClient
grpcplugin.DataClient
@ -52,7 +52,7 @@ func newClientV2(descriptor PluginDescriptor, logger log.Logger, rpcClient plugi
return nil, err
}
c := clientV2{}
c := ClientV2{}
if rawDiagnostics != nil {
if diagnosticsClient, ok := rawDiagnostics.(grpcplugin.DiagnosticsClient); ok {
c.DiagnosticsClient = diagnosticsClient
@ -92,7 +92,7 @@ func newClientV2(descriptor PluginDescriptor, logger log.Logger, rpcClient plugi
return &c, nil
}
func (c *clientV2) CollectMetrics(ctx context.Context) (*backend.CollectMetricsResult, error) {
func (c *ClientV2) CollectMetrics(ctx context.Context) (*backend.CollectMetricsResult, error) {
if c.DiagnosticsClient == nil {
return &backend.CollectMetricsResult{}, nil
}
@ -109,7 +109,7 @@ func (c *clientV2) CollectMetrics(ctx context.Context) (*backend.CollectMetricsR
return backend.FromProto().CollectMetricsResponse(protoResp), nil
}
func (c *clientV2) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) {
func (c *ClientV2) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) {
if c.DiagnosticsClient == nil {
return nil, backendplugin.ErrMethodNotImplemented
}
@ -130,7 +130,7 @@ func (c *clientV2) CheckHealth(ctx context.Context, req *backend.CheckHealthRequ
return backend.FromProto().CheckHealthResponse(protoResp), nil
}
func (c *clientV2) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
func (c *ClientV2) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
if c.DataClient == nil {
return nil, backendplugin.ErrMethodNotImplemented
}
@ -149,7 +149,7 @@ func (c *clientV2) QueryData(ctx context.Context, req *backend.QueryDataRequest)
return backend.FromProto().QueryDataResponse(protoResp)
}
func (c *clientV2) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error {
func (c *ClientV2) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error {
if c.ResourceClient == nil {
return backendplugin.ErrMethodNotImplemented
}
@ -184,7 +184,7 @@ func (c *clientV2) CallResource(ctx context.Context, req *backend.CallResourceRe
}
}
func (c *clientV2) SubscribeStream(ctx context.Context, req *backend.SubscribeStreamRequest) (*backend.SubscribeStreamResponse, error) {
func (c *ClientV2) SubscribeStream(ctx context.Context, req *backend.SubscribeStreamRequest) (*backend.SubscribeStreamResponse, error) {
if c.StreamClient == nil {
return nil, backendplugin.ErrMethodNotImplemented
}
@ -195,7 +195,7 @@ func (c *clientV2) SubscribeStream(ctx context.Context, req *backend.SubscribeSt
return backend.FromProto().SubscribeStreamResponse(protoResp), nil
}
func (c *clientV2) PublishStream(ctx context.Context, req *backend.PublishStreamRequest) (*backend.PublishStreamResponse, error) {
func (c *ClientV2) PublishStream(ctx context.Context, req *backend.PublishStreamRequest) (*backend.PublishStreamResponse, error) {
if c.StreamClient == nil {
return nil, backendplugin.ErrMethodNotImplemented
}
@ -206,7 +206,7 @@ func (c *clientV2) PublishStream(ctx context.Context, req *backend.PublishStream
return backend.FromProto().PublishStreamResponse(protoResp), nil
}
func (c *clientV2) RunStream(ctx context.Context, req *backend.RunStreamRequest, sender *backend.StreamSender) error {
func (c *ClientV2) RunStream(ctx context.Context, req *backend.RunStreamRequest, sender *backend.StreamSender) error {
if c.StreamClient == nil {
return backendplugin.ErrMethodNotImplemented
}

View File

@ -0,0 +1,48 @@
package provider
import (
"context"
"path/filepath"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/backendplugin"
"github.com/grafana/grafana/pkg/plugins/backendplugin/grpcplugin"
"github.com/grafana/grafana/pkg/plugins/backendplugin/pluginextensionv2"
)
type Service struct{}
func ProvideService() *Service {
return &Service{}
}
func (*Service) BackendFactory(ctx context.Context, p *plugins.Plugin) backendplugin.PluginFactoryFunc {
for _, provider := range []PluginBackendProvider{RendererProvider, DefaultProvider} {
if factory := provider(ctx, p); factory != nil {
return factory
}
}
return nil
}
// PluginBackendProvider is a function type for initializing a Plugin backend.
type PluginBackendProvider func(_ context.Context, _ *plugins.Plugin) backendplugin.PluginFactoryFunc
var RendererProvider PluginBackendProvider = func(_ context.Context, p *plugins.Plugin) backendplugin.PluginFactoryFunc {
if !p.IsRenderer() {
return nil
}
cmd := plugins.ComposeRendererStartCommand()
return grpcplugin.NewRendererPlugin(p.ID, filepath.Join(p.PluginDir, cmd),
func(pluginID string, renderer pluginextensionv2.RendererPlugin, logger log.Logger) error {
p.Renderer = renderer
return nil
},
)
}
var DefaultProvider PluginBackendProvider = func(_ context.Context, p *plugins.Plugin) backendplugin.PluginFactoryFunc {
cmd := plugins.ComposePluginStartCommand(p.Executable)
return grpcplugin.NewBackendPlugin(p.ID, filepath.Join(p.PluginDir, cmd))
}

57
pkg/plugins/config.go Normal file
View File

@ -0,0 +1,57 @@
package plugins
import (
"github.com/grafana/grafana/pkg/setting"
)
type Cfg struct {
DevMode bool
PluginsPath string
PluginSettings setting.PluginSettings
PluginsAllowUnsigned []string
EnterpriseLicensePath string
// AWS Plugin Auth
AWSAllowedAuthProviders []string
AWSAssumeRoleEnabled bool
// Azure Cloud settings
Azure setting.AzureSettings
CheckForUpdates bool
BuildVersion string // TODO Remove
AppSubURL string // TODO Remove
}
func NewCfg() *Cfg {
return &Cfg{}
}
func FromGrafanaCfg(grafanaCfg *setting.Cfg) *Cfg {
cfg := &Cfg{}
cfg.DevMode = grafanaCfg.Env == setting.Dev
cfg.PluginsPath = grafanaCfg.PluginsPath
cfg.PluginSettings = grafanaCfg.PluginSettings
cfg.PluginsAllowUnsigned = grafanaCfg.PluginsAllowUnsigned
cfg.EnterpriseLicensePath = grafanaCfg.EnterpriseLicensePath
// AWS
cfg.AWSAllowedAuthProviders = grafanaCfg.AWSAllowedAuthProviders
cfg.AWSAssumeRoleEnabled = grafanaCfg.AWSAssumeRoleEnabled
// Azure
cfg.Azure = grafanaCfg.Azure
cfg.CheckForUpdates = grafanaCfg.CheckForUpdates
cfg.BuildVersion = grafanaCfg.BuildVersion
cfg.AppSubURL = grafanaCfg.AppSubURL
return cfg
}

View File

@ -34,10 +34,10 @@ type AddOpts struct {
// Loader is responsible for loading plugins from the file system.
type Loader interface {
// Load will return a list of plugins found in the provided file system paths.
Load(paths []string, ignore map[string]struct{}) ([]*Plugin, error)
Load(ctx context.Context, class Class, paths []string, ignore map[string]struct{}) ([]*Plugin, error)
// LoadWithFactory will return a plugin found in the provided file system path and use the provided factory to
// construct the plugin backend client.
LoadWithFactory(path string, factory backendplugin.PluginFactoryFunc) (*Plugin, error)
LoadWithFactory(ctx context.Context, class Class, path string, factory backendplugin.PluginFactoryFunc) (*Plugin, error)
}
// Installer is responsible for managing plugins (add / remove) on the file system.
@ -65,6 +65,11 @@ type Client interface {
CollectMetrics(ctx context.Context, pluginID string) (*backend.CollectMetricsResult, error)
}
// BackendFactoryProvider provides a backend factory for a provided plugin.
type BackendFactoryProvider interface {
BackendFactory(ctx context.Context, p *Plugin) backendplugin.PluginFactoryFunc
}
type RendererManager interface {
// Renderer returns a renderer plugin.
Renderer() *Plugin

View File

@ -8,6 +8,7 @@ import (
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/backendplugin/provider"
"github.com/grafana/grafana/pkg/plugins/manager/loader"
"github.com/grafana/grafana/pkg/plugins/manager/signature"
"github.com/grafana/grafana/pkg/services/dashboards"
@ -91,8 +92,10 @@ func pluginScenario(t *testing.T, desc string, fn func(*testing.T, *PluginManage
},
},
}
pm := newManager(cfg, nil, loader.New(nil, cfg, &signature.UnsignedPluginAuthorizer{Cfg: cfg}), &sqlstore.SQLStore{})
err := pm.init()
pmCfg := plugins.FromGrafanaCfg(cfg)
pm, err := ProvideService(cfg, nil, loader.New(pmCfg, nil,
&signature.UnsignedPluginAuthorizer{Cfg: pmCfg}, &provider.Service{}), &sqlstore.SQLStore{})
require.NoError(t, err)
t.Run(desc, func(t *testing.T) {

View File

@ -7,6 +7,8 @@ import (
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/backendplugin/provider"
"github.com/grafana/grafana/pkg/plugins/manager/loader"
"github.com/grafana/grafana/pkg/plugins/manager/signature"
"github.com/grafana/grafana/pkg/services/sqlstore"
@ -23,8 +25,9 @@ func TestGetPluginDashboards(t *testing.T) {
},
},
}
pm := newManager(cfg, nil, loader.New(nil, cfg, &signature.UnsignedPluginAuthorizer{Cfg: cfg}), &sqlstore.SQLStore{})
err := pm.init()
pmCfg := plugins.FromGrafanaCfg(cfg)
pm, err := ProvideService(cfg, nil, loader.New(pmCfg, nil,
&signature.UnsignedPluginAuthorizer{Cfg: pmCfg}, &provider.Service{}), &sqlstore.SQLStore{})
require.NoError(t, err)
bus.AddHandler("test", func(ctx context.Context, query *models.GetDashboardQuery) error {

View File

@ -8,17 +8,15 @@ import (
"github.com/grafana/grafana/pkg/infra/fs"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
)
type Finder struct {
cfg *setting.Cfg
log log.Logger
}
func New(cfg *setting.Cfg) Finder {
return Finder{cfg: cfg, log: log.New("plugin.finder")}
func New() Finder {
return Finder{log: log.New("plugin.finder")}
}
func (f *Finder) Find(pluginDirs []string) ([]string, error) {

View File

@ -51,7 +51,7 @@ func TestFinder_Find(t *testing.T) {
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
f := New(tc.cfg)
f := New()
pluginPaths, err := f.Find(tc.pluginDirs)
if (err != nil) && !errors.Is(err, tc.err) {
t.Errorf("Find() error = %v, expected error %v", err, tc.err)

View File

@ -1,104 +1,43 @@
package initializer
import (
"context"
"fmt"
"net/url"
"os"
"path"
"path/filepath"
"strings"
"github.com/grafana/grafana-aws-sdk/pkg/awsds"
"github.com/gosimple/slug"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/metrics"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/backendplugin"
"github.com/grafana/grafana/pkg/plugins/backendplugin/grpcplugin"
"github.com/grafana/grafana/pkg/plugins/backendplugin/pluginextensionv2"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
)
type Initializer struct {
cfg *setting.Cfg
license models.Licensing
log log.Logger
cfg *plugins.Cfg
license models.Licensing
backendProvider plugins.BackendFactoryProvider
log log.Logger
}
func New(cfg *setting.Cfg, license models.Licensing) Initializer {
func New(cfg *plugins.Cfg, backendProvider plugins.BackendFactoryProvider, license models.Licensing) Initializer {
return Initializer{
cfg: cfg,
license: license,
log: log.New("plugin.initializer"),
cfg: cfg,
license: license,
backendProvider: backendProvider,
log: log.New("plugin.initializer"),
}
}
func (i *Initializer) Initialize(p *plugins.Plugin) error {
if len(p.Dependencies.Plugins) == 0 {
p.Dependencies.Plugins = []plugins.Dependency{}
}
if p.Dependencies.GrafanaVersion == "" {
p.Dependencies.GrafanaVersion = "*"
}
for _, include := range p.Includes {
if include.Role == "" {
include.Role = models.ROLE_VIEWER
}
}
i.handleModuleDefaults(p)
p.Info.Logos.Small = pluginLogoURL(p.Type, p.Info.Logos.Small, p.BaseURL)
p.Info.Logos.Large = pluginLogoURL(p.Type, p.Info.Logos.Large, p.BaseURL)
for i := 0; i < len(p.Info.Screenshots); i++ {
p.Info.Screenshots[i].Path = evalRelativePluginURLPath(p.Info.Screenshots[i].Path, p.BaseURL, p.Type)
}
if p.IsApp() {
for _, child := range p.Children {
i.setPathsBasedOnApp(p, child)
}
// slugify pages
for _, include := range p.Includes {
if include.Slug == "" {
include.Slug = slug.Make(include.Name)
}
if include.Type == "page" && include.DefaultNav {
p.DefaultNavURL = i.cfg.AppSubURL + "/plugins/" + p.ID + "/page/" + include.Slug
}
if include.Type == "dashboard" && include.DefaultNav {
p.DefaultNavURL = i.cfg.AppSubURL + "/dashboard/db/" + include.Slug
}
}
}
pluginLog := i.log.New("pluginID", p.ID)
p.SetLogger(pluginLog)
func (i *Initializer) Initialize(ctx context.Context, p *plugins.Plugin) error {
if p.Backend {
var backendFactory backendplugin.PluginFactoryFunc
if p.IsRenderer() {
cmd := plugins.ComposeRendererStartCommand()
backendFactory = grpcplugin.NewRendererPlugin(p.ID, filepath.Join(p.PluginDir, cmd),
func(pluginID string, renderer pluginextensionv2.RendererPlugin, logger log.Logger) error {
p.Renderer = renderer
return nil
},
)
} else {
cmd := plugins.ComposePluginStartCommand(p.Executable)
backendFactory = grpcplugin.NewBackendPlugin(p.ID, filepath.Join(p.PluginDir, cmd))
backendFactory := i.backendProvider.BackendFactory(ctx, p)
if backendFactory == nil {
return fmt.Errorf("could not find backend factory for plugin")
}
if backendClient, err := backendFactory(p.ID, pluginLog, i.envVars(p)); err != nil {
if backendClient, err := backendFactory(p.ID, p.Logger(), i.envVars(p)); err != nil {
return err
} else {
p.RegisterClient(backendClient)
@ -109,88 +48,20 @@ func (i *Initializer) Initialize(p *plugins.Plugin) error {
}
func (i *Initializer) InitializeWithFactory(p *plugins.Plugin, factory backendplugin.PluginFactoryFunc) error {
err := i.Initialize(p)
if factory == nil {
return fmt.Errorf("could not initialize plugin %s", p.ID)
}
f, err := factory(p.ID, log.New("pluginID", p.ID), []string{})
if err != nil {
return err
}
if factory != nil {
var err error
f, err := factory(p.ID, log.New("pluginID", p.ID), []string{})
if err != nil {
return err
}
p.RegisterClient(f)
} else {
i.log.Warn("Could not initialize core plugin process", "pluginID", p.ID)
return fmt.Errorf("could not initialize plugin %s", p.ID)
}
p.RegisterClient(f)
return nil
}
func (i *Initializer) handleModuleDefaults(p *plugins.Plugin) {
if p.IsCorePlugin() {
// Previously there was an assumption that the Core plugins directory
// should be public/app/plugins/<plugin type>/<plugin id>
// However this can be an issue if the Core plugins directory is renamed
baseDir := filepath.Base(p.PluginDir)
// use path package for the following statements because these are not file paths
p.Module = path.Join("app/plugins", string(p.Type), baseDir, "module")
p.BaseURL = path.Join("public/app/plugins", string(p.Type), baseDir)
return
}
metrics.SetPluginBuildInformation(p.ID, string(p.Type), p.Info.Version, string(p.Signature))
p.Module = path.Join("plugins", p.ID, "module")
p.BaseURL = path.Join("public/plugins", p.ID)
}
func (i *Initializer) setPathsBasedOnApp(parent *plugins.Plugin, child *plugins.Plugin) {
appSubPath := strings.ReplaceAll(strings.Replace(child.PluginDir, parent.PluginDir, "", 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 pluginLogoURL(pluginType plugins.Type, path, baseURL string) string {
if path == "" {
return defaultLogoPath(pluginType)
}
return evalRelativePluginURLPath(path, baseURL, pluginType)
}
func defaultLogoPath(pluginType plugins.Type) string {
return "public/img/icn-" + string(pluginType) + ".svg"
}
func evalRelativePluginURLPath(pathStr, baseURL string, pluginType plugins.Type) string {
if pathStr == "" {
return ""
}
u, _ := url.Parse(pathStr)
if u.IsAbs() {
return pathStr
}
// is set as default or has already been prefixed with base path
if pathStr == defaultLogoPath(pluginType) || strings.HasPrefix(pathStr, baseURL) {
return pathStr
}
return path.Join(baseURL, pathStr)
}
func (i *Initializer) envVars(plugin *plugins.Plugin) []string {
hostEnv := []string{
fmt.Sprintf("GF_VERSION=%s", i.cfg.BuildVersion),
@ -260,7 +131,7 @@ func (ps pluginSettings) asEnvVar(prefix string, hostEnv []string) []string {
return env
}
func getPluginSettings(pluginID string, cfg *setting.Cfg) pluginSettings {
func getPluginSettings(pluginID string, cfg *plugins.Cfg) pluginSettings {
ps := pluginSettings{}
for k, v := range cfg.PluginSettings[pluginID] {
if k == "path" || strings.ToLower(k) == "id" {

View File

@ -1,17 +1,15 @@
package initializer
import (
"path"
"context"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/backendplugin"
"github.com/grafana/grafana/pkg/setting"
)
func TestInitializer_Initialize(t *testing.T) {
@ -36,21 +34,16 @@ func TestInitializer_Initialize(t *testing.T) {
}
i := &Initializer{
cfg: setting.NewCfg(),
cfg: plugins.NewCfg(),
log: &fakeLogger{},
backendProvider: &fakeBackendProvider{
plugin: p,
},
}
err := i.Initialize(p)
err := i.Initialize(context.Background(), p)
assert.NoError(t, err)
assert.Equal(t, "public/img/icn-datasource.svg", p.Info.Logos.Small)
assert.Equal(t, "public/img/icn-datasource.svg", p.Info.Logos.Large)
assert.Equal(t, "*", p.Dependencies.GrafanaVersion)
assert.Len(t, p.Includes, 1)
assert.Equal(t, models.ROLE_VIEWER, p.Includes[0].Role)
assert.Equal(t, filepath.Join("app/plugins/datasource", filepath.Base(p.PluginDir), "module"), p.Module)
assert.Equal(t, path.Join("public/app/plugins/datasource", filepath.Base(p.PluginDir)), p.BaseURL)
assert.NotNil(t, p.Logger())
c, exists := p.Client()
assert.True(t, exists)
assert.NotNil(t, c)
@ -71,101 +64,50 @@ func TestInitializer_Initialize(t *testing.T) {
}
i := &Initializer{
cfg: setting.NewCfg(),
cfg: plugins.NewCfg(),
log: fakeLogger{},
backendProvider: &fakeBackendProvider{
plugin: p,
},
}
err := i.Initialize(p)
err := i.Initialize(context.Background(), p)
assert.NoError(t, err)
// TODO add default img to project
assert.Equal(t, "public/img/icn-renderer.svg", p.Info.Logos.Small)
assert.Equal(t, "public/img/icn-renderer.svg", p.Info.Logos.Large)
assert.Equal(t, ">=8.x", p.Dependencies.GrafanaVersion)
assert.Equal(t, "plugins/test/module", p.Module)
assert.Equal(t, "public/plugins/test", p.BaseURL)
assert.NotNil(t, p.Logger())
c, exists := p.Client()
assert.True(t, exists)
assert.NotNil(t, c)
})
t.Run("external app", func(t *testing.T) {
t.Run("non backend plugin app", func(t *testing.T) {
p := &plugins.Plugin{
JSONData: plugins.JSONData{
ID: "parent-plugin",
Type: plugins.App,
Includes: []*plugins.Includes{
{
Type: "page",
DefaultNav: true,
Slug: "myCustomSlug",
},
},
},
PluginDir: absCurPath,
Class: plugins.External,
Children: []*plugins.Plugin{
{
JSONData: plugins.JSONData{
ID: "child-plugin",
},
PluginDir: absCurPath,
},
Backend: false,
},
}
i := &Initializer{
cfg: &setting.Cfg{
AppSubURL: "appSubURL",
},
cfg: &plugins.Cfg{},
log: fakeLogger{},
backendProvider: &fakeBackendProvider{
plugin: p,
},
}
err := i.Initialize(p)
err := i.Initialize(context.Background(), p)
assert.NoError(t, err)
assert.Equal(t, "public/img/icn-app.svg", p.Info.Logos.Small)
assert.Equal(t, "public/img/icn-app.svg", p.Info.Logos.Large)
assert.Equal(t, "*", p.Dependencies.GrafanaVersion)
assert.Len(t, p.Includes, 1)
assert.Equal(t, models.ROLE_VIEWER, p.Includes[0].Role)
assert.Equal(t, filepath.Join("plugins", p.ID, "module"), p.Module)
assert.Equal(t, "public/plugins/parent-plugin", p.BaseURL)
assert.NotNil(t, p.Logger())
c, exists := p.Client()
assert.False(t, exists)
assert.Nil(t, c)
assert.Len(t, p.Children, 1)
assert.Equal(t, p.ID, p.Children[0].IncludedInAppID)
assert.Equal(t, "public/plugins/parent-plugin", p.Children[0].BaseURL)
assert.Equal(t, "plugins/parent-plugin/module", p.Children[0].Module)
assert.Equal(t, "appSubURL/plugins/parent-plugin/page/myCustomSlug", p.DefaultNavURL)
})
}
func TestInitializer_InitializeWithFactory(t *testing.T) {
t.Run("happy path", func(t *testing.T) {
p := &plugins.Plugin{
JSONData: plugins.JSONData{
ID: "test-plugin",
Type: plugins.App,
Includes: []*plugins.Includes{
{
Type: "page",
DefaultNav: true,
Slug: "myCustomSlug",
},
},
},
PluginDir: "test/folder",
Class: plugins.External,
}
p := &plugins.Plugin{}
i := &Initializer{
cfg: &setting.Cfg{
AppSubURL: "appSubURL",
},
cfg: &plugins.Cfg{},
log: fakeLogger{},
}
@ -180,33 +122,19 @@ func TestInitializer_InitializeWithFactory(t *testing.T) {
assert.NoError(t, err)
assert.True(t, factoryInvoked)
assert.NotNil(t, p.Logger())
client, exists := p.Client()
assert.True(t, exists)
assert.NotNil(t, client.(testPlugin))
})
t.Run("invalid factory", func(t *testing.T) {
p := &plugins.Plugin{
JSONData: plugins.JSONData{
ID: "test-plugin",
Type: plugins.App,
Includes: []*plugins.Includes{
{
Type: "page",
DefaultNav: true,
Slug: "myCustomSlug",
},
},
},
PluginDir: "test/folder",
Class: plugins.External,
}
p := &plugins.Plugin{}
i := &Initializer{
cfg: &setting.Cfg{
AppSubURL: "appSubURL",
},
cfg: &plugins.Cfg{},
log: fakeLogger{},
backendProvider: &fakeBackendProvider{
plugin: p,
},
}
err := i.InitializeWithFactory(p, nil)
@ -232,7 +160,7 @@ func TestInitializer_envVars(t *testing.T) {
}
i := &Initializer{
cfg: &setting.Cfg{
cfg: &plugins.Cfg{
EnterpriseLicensePath: "/path/to/ent/license",
PluginSettings: map[string]map[string]string{
"test": {
@ -242,6 +170,9 @@ func TestInitializer_envVars(t *testing.T) {
},
license: licensing,
log: fakeLogger{},
backendProvider: &fakeBackendProvider{
plugin: p,
},
}
envVars := i.envVars(p)
@ -254,33 +185,6 @@ func TestInitializer_envVars(t *testing.T) {
})
}
func TestInitializer_setPathsBasedOnApp(t *testing.T) {
t.Run("When setting paths based on core plugin on Windows", func(t *testing.T) {
i := &Initializer{
cfg: setting.NewCfg(),
log: fakeLogger{},
}
child := &plugins.Plugin{
PluginDir: "c:\\grafana\\public\\app\\plugins\\app\\testdata\\datasources\\datasource",
}
parent := &plugins.Plugin{
JSONData: plugins.JSONData{
ID: "testdata",
},
Class: plugins.Core,
PluginDir: "c:\\grafana\\public\\app\\plugins\\app\\testdata",
BaseURL: "public/app/plugins/app/testdata",
}
i.setPathsBasedOnApp(parent, child)
assert.Equal(t, "app/plugins/app/testdata/datasources/datasource/module", child.Module)
assert.Equal(t, "testdata", child.IncludedInAppID)
assert.Equal(t, "public/app/plugins/app/testdata", child.BaseURL)
})
}
func TestInitializer_getAWSEnvironmentVariables(t *testing.T) {
}
@ -334,7 +238,7 @@ func (t *testLicensingService) ContentDeliveryPrefix() string {
return ""
}
func (t *testLicensingService) LicenseURL(showAdminLicensingPage bool) string {
func (t *testLicensingService) LicenseURL(_ bool) string {
return ""
}
@ -365,3 +269,15 @@ func (f fakeLogger) New(_ ...interface{}) log.MultiLoggers {
func (f fakeLogger) Warn(_ string, _ ...interface{}) {
}
type fakeBackendProvider struct {
plugins.BackendFactoryProvider
plugin *plugins.Plugin
}
func (f *fakeBackendProvider) BackendFactory(_ context.Context, _ *plugins.Plugin) backendplugin.PluginFactoryFunc {
return func(_ string, _ log.Logger, _ []string) (backendplugin.Plugin, error) {
return f.plugin, nil
}
}

View File

@ -1,16 +1,22 @@
package loader
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/url"
"os"
"path"
"path/filepath"
"runtime"
"strings"
"github.com/gosimple/slug"
"github.com/grafana/grafana/pkg/infra/fs"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/metrics"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/backendplugin"
@ -18,6 +24,7 @@ import (
"github.com/grafana/grafana/pkg/plugins/manager/loader/initializer"
"github.com/grafana/grafana/pkg/plugins/manager/signature"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
)
var (
@ -28,7 +35,7 @@ var (
var _ plugins.ErrorResolver = (*Loader)(nil)
type Loader struct {
cfg *setting.Cfg
cfg *plugins.Cfg
pluginFinder finder.Finder
pluginInitializer initializer.Initializer
signatureValidator signature.Validator
@ -37,32 +44,34 @@ type Loader struct {
errs map[string]*plugins.SignatureError
}
func ProvideService(license models.Licensing, cfg *setting.Cfg, authorizer plugins.PluginLoaderAuthorizer) (*Loader, error) {
return New(license, cfg, authorizer), nil
func ProvideService(cfg *setting.Cfg, license models.Licensing, authorizer plugins.PluginLoaderAuthorizer,
backendProvider plugins.BackendFactoryProvider) (*Loader, error) {
return New(plugins.FromGrafanaCfg(cfg), license, authorizer, backendProvider), nil
}
func New(license models.Licensing, cfg *setting.Cfg, authorizer plugins.PluginLoaderAuthorizer) *Loader {
func New(cfg *plugins.Cfg, license models.Licensing, authorizer plugins.PluginLoaderAuthorizer,
backendProvider plugins.BackendFactoryProvider) *Loader {
return &Loader{
cfg: cfg,
pluginFinder: finder.New(cfg),
pluginInitializer: initializer.New(cfg, license),
pluginFinder: finder.New(),
pluginInitializer: initializer.New(cfg, backendProvider, license),
signatureValidator: signature.NewValidator(authorizer),
errs: make(map[string]*plugins.SignatureError),
log: log.New("plugin.loader"),
}
}
func (l *Loader) Load(paths []string, ignore map[string]struct{}) ([]*plugins.Plugin, error) {
func (l *Loader) Load(ctx context.Context, class plugins.Class, paths []string, ignore map[string]struct{}) ([]*plugins.Plugin, error) {
pluginJSONPaths, err := l.pluginFinder.Find(paths)
if err != nil {
l.log.Error("plugin finder encountered an error", "err", err)
}
return l.loadPlugins(pluginJSONPaths, ignore)
return l.loadPlugins(ctx, class, pluginJSONPaths, ignore)
}
func (l *Loader) LoadWithFactory(path string, factory backendplugin.PluginFactoryFunc) (*plugins.Plugin, error) {
p, err := l.load(path, map[string]struct{}{})
func (l *Loader) LoadWithFactory(ctx context.Context, class plugins.Class, path string, factory backendplugin.PluginFactoryFunc) (*plugins.Plugin, error) {
p, err := l.load(ctx, class, path, map[string]struct{}{})
if err != nil {
l.log.Error("failed to load core plugin", "err", err)
return nil, err
@ -73,14 +82,14 @@ func (l *Loader) LoadWithFactory(path string, factory backendplugin.PluginFactor
return p, err
}
func (l *Loader) load(path string, ignore map[string]struct{}) (*plugins.Plugin, error) {
func (l *Loader) load(ctx context.Context, class plugins.Class, path string, ignore map[string]struct{}) (*plugins.Plugin, error) {
pluginJSONPaths, err := l.pluginFinder.Find([]string{path})
if err != nil {
l.log.Error("failed to find plugin", "err", err)
return nil, err
}
loadedPlugins, err := l.loadPlugins(pluginJSONPaths, ignore)
loadedPlugins, err := l.loadPlugins(ctx, class, pluginJSONPaths, ignore)
if err != nil {
return nil, err
}
@ -92,7 +101,7 @@ func (l *Loader) load(path string, ignore map[string]struct{}) (*plugins.Plugin,
return loadedPlugins[0], nil
}
func (l *Loader) loadPlugins(pluginJSONPaths []string, existingPlugins map[string]struct{}) ([]*plugins.Plugin, error) {
func (l *Loader) loadPlugins(ctx context.Context, class plugins.Class, pluginJSONPaths []string, existingPlugins map[string]struct{}) ([]*plugins.Plugin, error) {
var foundPlugins = foundPlugins{}
// load plugin.json files and map directory to JSON data
@ -124,8 +133,10 @@ func (l *Loader) loadPlugins(pluginJSONPaths []string, existingPlugins map[strin
plugin := &plugins.Plugin{
JSONData: pluginJSON,
PluginDir: pluginDir,
Class: l.pluginClass(pluginDir),
Class: class,
}
l.setDefaults(plugin)
plugin.SetLogger(l.log.New("pluginID", plugin.ID))
sig, err := signature.Calculate(l.log, plugin)
if err != nil {
@ -160,7 +171,7 @@ func (l *Loader) loadPlugins(pluginJSONPaths []string, existingPlugins map[strin
}
// validate signatures
verifiedPlugins := []*plugins.Plugin{}
verifiedPlugins := make([]*plugins.Plugin, 0)
for _, plugin := range loadedPlugins {
signingError := l.signatureValidator.Validate(plugin)
if signingError != nil {
@ -192,7 +203,7 @@ func (l *Loader) loadPlugins(pluginJSONPaths []string, existingPlugins map[strin
}
for _, p := range verifiedPlugins {
err := l.pluginInitializer.Initialize(p)
err := l.pluginInitializer.Initialize(ctx, p)
if err != nil {
return nil, err
}
@ -233,9 +244,114 @@ func (l *Loader) readPluginJSON(pluginJSONPath string) (plugins.JSONData, error)
plugin.Name = "Pie Chart (old)"
}
if len(plugin.Dependencies.Plugins) == 0 {
plugin.Dependencies.Plugins = []plugins.Dependency{}
}
if plugin.Dependencies.GrafanaVersion == "" {
plugin.Dependencies.GrafanaVersion = "*"
}
for _, include := range plugin.Includes {
if include.Role == "" {
include.Role = models.ROLE_VIEWER
}
}
return plugin, nil
}
func (l *Loader) setDefaults(p *plugins.Plugin) {
setModule(p)
p.Info.Logos.Small = pluginLogoURL(p.Type, p.Info.Logos.Small, p.BaseURL)
p.Info.Logos.Large = pluginLogoURL(p.Type, p.Info.Logos.Large, p.BaseURL)
for i := 0; i < len(p.Info.Screenshots); i++ {
p.Info.Screenshots[i].Path = evalRelativePluginURLPath(p.Info.Screenshots[i].Path, p.BaseURL, p.Type)
}
if p.IsApp() {
for _, child := range p.Children {
setChildModule(p, child)
}
// slugify pages
for _, include := range p.Includes {
if include.Slug == "" {
include.Slug = slug.Make(include.Name)
}
if include.Type == "page" && include.DefaultNav {
p.DefaultNavURL = l.cfg.AppSubURL + "/plugins/" + p.ID + "/page/" + include.Slug
}
if include.Type == "dashboard" && include.DefaultNav {
p.DefaultNavURL = l.cfg.AppSubURL + "/dashboard/db/" + include.Slug
}
}
}
}
func setModule(p *plugins.Plugin) {
if p.IsCorePlugin() {
// Previously there was an assumption that the Core plugins directory
// should be public/app/plugins/<plugin type>/<plugin id>
// However this can be an issue if the Core plugins directory is renamed
baseDir := filepath.Base(p.PluginDir)
// use path package for the following statements because these are not file paths
p.Module = path.Join("app/plugins", string(p.Type), baseDir, "module")
p.BaseURL = path.Join("public/app/plugins", string(p.Type), baseDir)
return
}
metrics.SetPluginBuildInformation(p.ID, string(p.Type), p.Info.Version, string(p.Signature))
p.Module = path.Join("plugins", p.ID, "module")
p.BaseURL = path.Join("public/plugins", p.ID)
}
func setChildModule(parent *plugins.Plugin, child *plugins.Plugin) {
appSubPath := strings.ReplaceAll(strings.Replace(child.PluginDir, parent.PluginDir, "", 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 pluginLogoURL(pluginType plugins.Type, path, baseURL string) string {
if path == "" {
return defaultLogoPath(pluginType)
}
return evalRelativePluginURLPath(path, baseURL, pluginType)
}
func defaultLogoPath(pluginType plugins.Type) string {
return "public/img/icn-" + string(pluginType) + ".svg"
}
func evalRelativePluginURLPath(pathStr, baseURL string, pluginType plugins.Type) string {
if pathStr == "" {
return ""
}
u, _ := url.Parse(pathStr)
if u.IsAbs() {
return pathStr
}
// is set as default or has already been prefixed with base path
if pathStr == defaultLogoPath(pluginType) || strings.HasPrefix(pathStr, baseURL) {
return pathStr
}
return path.Join(baseURL, pathStr)
}
func (l *Loader) PluginErrors() []*plugins.Error {
errs := make([]*plugins.Error, 0)
for _, err := range l.errs {
@ -255,41 +371,15 @@ func validatePluginJSON(data plugins.JSONData) error {
return nil
}
func (l *Loader) pluginClass(pluginDir string) plugins.Class {
isSubDir := func(base, target string) bool {
path, err := filepath.Rel(base, target)
if err != nil {
return false
}
if !strings.HasPrefix(path, "..") {
return true
}
return false
}
corePluginsDir := filepath.Join(l.cfg.StaticRootPath, "app/plugins")
if isSubDir(corePluginsDir, pluginDir) {
return plugins.Core
}
if isSubDir(l.cfg.BundledPluginsPath, pluginDir) {
return plugins.Bundled
}
return plugins.External
}
type foundPlugins map[string]plugins.JSONData
// stripDuplicates will strip duplicate plugins or plugins that already exist
func (f *foundPlugins) stripDuplicates(existingPlugins map[string]struct{}, log log.Logger) {
pluginsByID := make(map[string]struct{})
for path, scannedPlugin := range *f {
for k, scannedPlugin := range *f {
if _, existing := existingPlugins[scannedPlugin.ID]; existing {
log.Debug("Skipping plugin as it's already installed", "plugin", scannedPlugin.ID)
delete(*f, path)
delete(*f, k)
continue
}

View File

@ -1,19 +1,21 @@
package loader
import (
"context"
"errors"
"path/filepath"
"sort"
"testing"
"github.com/stretchr/testify/require"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/backendplugin/provider"
"github.com/grafana/grafana/pkg/plugins/manager/loader/finder"
"github.com/grafana/grafana/pkg/plugins/manager/loader/initializer"
"github.com/grafana/grafana/pkg/plugins/manager/signature"
@ -35,16 +37,18 @@ func TestLoader_Load(t *testing.T) {
}
tests := []struct {
name string
cfg *setting.Cfg
class plugins.Class
cfg *plugins.Cfg
pluginPaths []string
existingPlugins map[string]struct{}
want []*plugins.Plugin
pluginErrors map[string]*plugins.Error
}{
{
name: "Load a Core plugin",
cfg: &setting.Cfg{
StaticRootPath: corePluginDir,
name: "Load a Core plugin",
class: plugins.Core,
cfg: &plugins.Cfg{
PluginsPath: corePluginDir,
},
pluginPaths: []string{filepath.Join(corePluginDir, "app/plugins/datasource/cloudwatch")},
want: []*plugins.Plugin{
@ -86,15 +90,16 @@ func TestLoader_Load(t *testing.T) {
Module: "app/plugins/datasource/cloudwatch/module",
BaseURL: "public/app/plugins/datasource/cloudwatch",
PluginDir: filepath.Join(corePluginDir, "app/plugins/datasource/cloudwatch"),
Signature: "internal",
Class: "core",
Signature: plugins.SignatureInternal,
Class: plugins.Core,
},
},
},
{
name: "Load a Bundled plugin",
cfg: &setting.Cfg{
BundledPluginsPath: filepath.Join(parentDir, "testdata"),
name: "Load a Bundled plugin",
class: plugins.Bundled,
cfg: &plugins.Cfg{
PluginsPath: filepath.Join(parentDir, "testdata"),
},
pluginPaths: []string{"../testdata/valid-v2-signature"},
want: []*plugins.Plugin{
@ -129,12 +134,13 @@ func TestLoader_Load(t *testing.T) {
Signature: "valid",
SignatureType: plugins.GrafanaSignature,
SignatureOrg: "Grafana Labs",
Class: "bundled",
Class: plugins.Bundled,
},
},
}, {
name: "Load an External plugin",
cfg: &setting.Cfg{
name: "Load plugin with symbolic links",
class: plugins.External,
cfg: &plugins.Cfg{
PluginsPath: filepath.Join(parentDir),
},
pluginPaths: []string{"../testdata/symbolic-plugin-dirs"},
@ -210,10 +216,11 @@ func TestLoader_Load(t *testing.T) {
},
},
}, {
name: "Load an unsigned plugin (development)",
cfg: &setting.Cfg{
name: "Load an unsigned plugin (development)",
class: plugins.External,
cfg: &plugins.Cfg{
DevMode: true,
PluginsPath: filepath.Join(parentDir),
Env: "development",
},
pluginPaths: []string{"../testdata/unsigned-datasource"},
want: []*plugins.Plugin{
@ -248,10 +255,10 @@ func TestLoader_Load(t *testing.T) {
},
},
}, {
name: "Load an unsigned plugin (production)",
cfg: &setting.Cfg{
name: "Load an unsigned plugin (production)",
class: plugins.External,
cfg: &plugins.Cfg{
PluginsPath: filepath.Join(parentDir),
Env: "production",
},
pluginPaths: []string{"../testdata/unsigned-datasource"},
want: []*plugins.Plugin{},
@ -263,10 +270,10 @@ func TestLoader_Load(t *testing.T) {
},
},
{
name: "Load an unsigned plugin using PluginsAllowUnsigned config (production)",
cfg: &setting.Cfg{
name: "Load an unsigned plugin using PluginsAllowUnsigned config (production)",
class: plugins.External,
cfg: &plugins.Cfg{
PluginsPath: filepath.Join(parentDir),
Env: "production",
PluginsAllowUnsigned: []string{"test"},
},
pluginPaths: []string{"../testdata/unsigned-datasource"},
@ -298,15 +305,15 @@ func TestLoader_Load(t *testing.T) {
Module: "plugins/test/module",
BaseURL: "public/plugins/test",
PluginDir: filepath.Join(parentDir, "testdata/unsigned-datasource/plugin"),
Signature: "unsigned",
Signature: plugins.SignatureUnsigned,
},
},
},
{
name: "Load an unsigned plugin with modified signature (production)",
cfg: &setting.Cfg{
name: "Load an unsigned plugin with modified signature (production)",
class: plugins.External,
cfg: &plugins.Cfg{
PluginsPath: filepath.Join(parentDir),
Env: "production",
},
pluginPaths: []string{"../testdata/lacking-files"},
want: []*plugins.Plugin{},
@ -318,10 +325,10 @@ func TestLoader_Load(t *testing.T) {
},
},
{
name: "Load an unsigned plugin with modified signature using PluginsAllowUnsigned config (production) still includes a signing error",
cfg: &setting.Cfg{
name: "Load an unsigned plugin with modified signature using PluginsAllowUnsigned config (production) still includes a signing error",
class: plugins.External,
cfg: &plugins.Cfg{
PluginsPath: filepath.Join(parentDir),
Env: "production",
PluginsAllowUnsigned: []string{"test"},
},
pluginPaths: []string{"../testdata/lacking-files"},
@ -333,11 +340,61 @@ func TestLoader_Load(t *testing.T) {
},
},
},
{
name: "Load an app with includes",
class: plugins.External,
cfg: &plugins.Cfg{
PluginsPath: filepath.Join(parentDir),
PluginsAllowUnsigned: []string{"test-app"},
},
pluginPaths: []string{"../testdata/test-app-with-includes"},
want: []*plugins.Plugin{
{JSONData: plugins.JSONData{
ID: "test-app",
Type: "app",
Name: "Test App",
Info: plugins.Info{
Author: plugins.InfoLink{
Name: "Test Inc.",
URL: "http://test.com",
},
Description: "Official Grafana Test App & Dashboard bundle",
Version: "1.0.0",
Links: []plugins.InfoLink{
{Name: "Project site", URL: "http://project.com"},
{Name: "License & Terms", URL: "http://license.com"},
},
Logos: plugins.Logos{
Small: "public/img/icn-app.svg",
Large: "public/img/icn-app.svg",
},
Updated: "2015-02-10",
},
Dependencies: plugins.Dependencies{
GrafanaDependency: ">=8.0.0",
GrafanaVersion: "*",
Plugins: []plugins.Dependency{},
},
Includes: []*plugins.Includes{
{Name: "Nginx Memory", Path: "dashboards/memory.json", Type: "dashboard", Role: "Viewer", Slug: "nginx-memory", DefaultNav: true},
{Name: "Root Page (react)", Type: "page", Role: "Viewer", Path: "/a/my-simple-app", DefaultNav: true, AddToNav: true, Slug: "root-page-react"},
},
Backend: false,
},
DefaultNavURL: "/plugins/test-app/page/root-page-react",
PluginDir: filepath.Join(parentDir, "testdata/test-app-with-includes"),
Class: plugins.External,
Signature: plugins.SignatureUnsigned,
Module: "plugins/test-app/module",
BaseURL: "public/plugins/test-app",
},
},
},
}
for _, tt := range tests {
l := newLoader(tt.cfg)
t.Run(tt.name, func(t *testing.T) {
got, err := l.Load(tt.pluginPaths, tt.existingPlugins)
got, err := l.Load(context.Background(), tt.class, tt.pluginPaths, tt.existingPlugins)
require.NoError(t, err)
if !cmp.Equal(got, tt.want, compareOpts) {
t.Fatalf("Result mismatch (-want +got):\n%s", cmp.Diff(got, tt.want, compareOpts))
@ -362,7 +419,7 @@ func TestLoader_Load_MultiplePlugins(t *testing.T) {
t.Run("Load multiple", func(t *testing.T) {
tests := []struct {
name string
cfg *setting.Cfg
cfg *plugins.Cfg
pluginPaths []string
appURL string
existingPlugins map[string]struct{}
@ -371,8 +428,7 @@ func TestLoader_Load_MultiplePlugins(t *testing.T) {
}{
{
name: "Load multiple plugins (broken, valid, unsigned)",
cfg: &setting.Cfg{
Env: "production",
cfg: &plugins.Cfg{
PluginsPath: filepath.Join(parentDir),
},
appURL: "http://localhost:3000",
@ -434,7 +490,7 @@ func TestLoader_Load_MultiplePlugins(t *testing.T) {
})
setting.AppUrl = tt.appURL
got, err := l.Load(tt.pluginPaths, tt.existingPlugins)
got, err := l.Load(context.Background(), plugins.External, tt.pluginPaths, tt.existingPlugins)
require.NoError(t, err)
sort.SliceStable(got, func(i, j int) bool {
return got[i].ID < got[j].ID
@ -488,17 +544,17 @@ func TestLoader_Signature_RootURL(t *testing.T) {
Executable: "test",
},
PluginDir: filepath.Join(parentDir, "/testdata/valid-v2-pvt-signature-root-url-uri/plugin"),
Class: "external",
Signature: "valid",
SignatureType: "private",
Class: plugins.External,
Signature: plugins.SignatureValid,
SignatureType: plugins.PrivateSignature,
SignatureOrg: "Will Browne",
Module: "plugins/test/module",
BaseURL: "public/plugins/test",
},
}
l := newLoader(&setting.Cfg{PluginsPath: filepath.Join(parentDir)})
got, err := l.Load(paths, map[string]struct{}{})
l := newLoader(&plugins.Cfg{PluginsPath: filepath.Join(parentDir)})
got, err := l.Load(context.Background(), plugins.External, paths, map[string]struct{}{})
assert.NoError(t, err)
if !cmp.Equal(got, expected, compareOpts) {
@ -566,11 +622,11 @@ func TestLoader_Load_DuplicatePlugins(t *testing.T) {
},
}
l := newLoader(&setting.Cfg{
l := newLoader(&plugins.Cfg{
PluginsPath: filepath.Dir(pluginDir),
})
got, err := l.Load([]string{pluginDir, pluginDir}, map[string]struct{}{})
got, err := l.Load(context.Background(), plugins.External, []string{pluginDir, pluginDir}, map[string]struct{}{})
assert.NoError(t, err)
if !cmp.Equal(got, expected, compareOpts) {
@ -612,10 +668,10 @@ func TestLoader_loadNestedPlugins(t *testing.T) {
Module: "plugins/test-ds/module",
BaseURL: "public/plugins/test-ds",
PluginDir: filepath.Join(parentDir, "testdata/nested-plugins/parent"),
Signature: "valid",
Signature: plugins.SignatureValid,
SignatureType: plugins.GrafanaSignature,
SignatureOrg: "Grafana Labs",
Class: "external",
Class: plugins.External,
}
child := &plugins.Plugin{
@ -644,10 +700,10 @@ func TestLoader_loadNestedPlugins(t *testing.T) {
Module: "plugins/test-panel/module",
BaseURL: "public/plugins/test-panel",
PluginDir: filepath.Join(parentDir, "testdata/nested-plugins/parent/nested"),
Signature: "valid",
Signature: plugins.SignatureValid,
SignatureType: plugins.GrafanaSignature,
SignatureOrg: "Grafana Labs",
Class: "external",
Class: plugins.External,
}
parent.Children = []*plugins.Plugin{child}
@ -655,11 +711,11 @@ func TestLoader_loadNestedPlugins(t *testing.T) {
t.Run("Load nested External plugins", func(t *testing.T) {
expected := []*plugins.Plugin{parent, child}
l := newLoader(&setting.Cfg{
l := newLoader(&plugins.Cfg{
PluginsPath: parentDir,
})
got, err := l.Load([]string{"../testdata/nested-plugins"}, map[string]struct{}{})
got, err := l.Load(context.Background(), plugins.External, []string{"../testdata/nested-plugins"}, map[string]struct{}{})
assert.NoError(t, err)
// to ensure we can compare with expected
@ -677,11 +733,11 @@ func TestLoader_loadNestedPlugins(t *testing.T) {
parent.Children = nil
expected := []*plugins.Plugin{parent}
l := newLoader(&setting.Cfg{
l := newLoader(&plugins.Cfg{
PluginsPath: parentDir,
})
got, err := l.Load([]string{"../testdata/nested-plugins"}, map[string]struct{}{
got, err := l.Load(context.Background(), plugins.External, []string{"../testdata/nested-plugins"}, map[string]struct{}{
"test-panel": {},
})
assert.NoError(t, err)
@ -740,10 +796,10 @@ func TestLoader_readPluginJSON(t *testing.T) {
},
},
Includes: []*plugins.Includes{
{Name: "Nginx Connections", Path: "dashboards/connections.json", Type: "dashboard"},
{Name: "Nginx Memory", Path: "dashboards/memory.json", Type: "dashboard"},
{Name: "Nginx Panel", Type: "panel"},
{Name: "Nginx Datasource", Type: "datasource"},
{Name: "Nginx Connections", Path: "dashboards/connections.json", Type: "dashboard", Role: models.ROLE_VIEWER},
{Name: "Nginx Memory", Path: "dashboards/memory.json", Type: "dashboard", Role: models.ROLE_VIEWER},
{Name: "Nginx Panel", Type: "panel", Role: models.ROLE_VIEWER},
{Name: "Nginx Datasource", Type: "datasource", Role: models.ROLE_VIEWER},
},
Backend: false,
},
@ -822,71 +878,33 @@ func Test_validatePluginJSON(t *testing.T) {
}
}
func Test_pluginClass(t *testing.T) {
type args struct {
pluginDir string
cfg *setting.Cfg
}
tests := []struct {
name string
args args
expected plugins.Class
}{
{
name: "Core plugin class",
args: args{
pluginDir: "/root/app/plugins/test-app",
cfg: &setting.Cfg{
StaticRootPath: "/root",
},
func Test_setPathsBasedOnApp(t *testing.T) {
t.Run("When setting paths based on core plugin on Windows", func(t *testing.T) {
child := &plugins.Plugin{
PluginDir: "c:\\grafana\\public\\app\\plugins\\app\\testdata\\datasources\\datasource",
}
parent := &plugins.Plugin{
JSONData: plugins.JSONData{
ID: "testdata",
},
expected: plugins.Core,
},
{
name: "Bundled plugin class",
args: args{
pluginDir: "/test-app",
cfg: &setting.Cfg{
BundledPluginsPath: "/test-app",
},
},
expected: plugins.Bundled,
},
{
name: "External plugin class",
args: args{
pluginDir: "/test-app",
cfg: &setting.Cfg{
PluginsPath: "/test-app",
},
},
expected: plugins.External,
},
{
name: "External plugin class",
args: args{
pluginDir: "/test-app",
cfg: &setting.Cfg{
PluginsPath: "/root",
},
},
expected: plugins.External,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
l := newLoader(tt.args.cfg)
got := l.pluginClass(tt.args.pluginDir)
assert.Equal(t, tt.expected, got)
})
}
Class: plugins.Core,
PluginDir: "c:\\grafana\\public\\app\\plugins\\app\\testdata",
BaseURL: "public/app/plugins/app/testdata",
}
setChildModule(parent, child)
assert.Equal(t, "app/plugins/app/testdata/datasources/datasource/module", child.Module)
assert.Equal(t, "testdata", child.IncludedInAppID)
assert.Equal(t, "public/app/plugins/app/testdata", child.BaseURL)
})
}
func newLoader(cfg *setting.Cfg) *Loader {
func newLoader(cfg *plugins.Cfg) *Loader {
return &Loader{
cfg: cfg,
pluginFinder: finder.New(cfg),
pluginInitializer: initializer.New(cfg, &fakeLicensingService{}),
pluginFinder: finder.New(),
pluginInitializer: initializer.New(cfg, &provider.Service{}, &fakeLicensingService{}),
signatureValidator: signature.NewValidator(&signature.UnsignedPluginAuthorizer{Cfg: cfg}),
errs: make(map[string]*plugins.SignatureError),
log: &fakeLogger{},
@ -934,6 +952,10 @@ type fakeLogger struct {
log.Logger
}
func (fl fakeLogger) New(_ ...interface{}) log.MultiLoggers {
return log.MultiLoggers{}
}
func (fl fakeLogger) Info(_ string, _ ...interface{}) {
}

View File

@ -5,14 +5,12 @@ import (
"errors"
"fmt"
"net/http"
"os"
"path/filepath"
"sync"
"time"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana/pkg/infra/fs"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
@ -35,79 +33,52 @@ var _ plugins.StaticRouteResolver = (*PluginManager)(nil)
var _ plugins.RendererManager = (*PluginManager)(nil)
type PluginManager struct {
cfg *setting.Cfg
cfg *plugins.Cfg
requestValidator models.PluginRequestValidator
sqlStore *sqlstore.SQLStore
store map[string]*plugins.Plugin
pluginInstaller plugins.Installer
pluginLoader plugins.Loader
pluginsMu sync.RWMutex
pluginPaths map[plugins.Class][]string
log log.Logger
}
func ProvideService(cfg *setting.Cfg, requestValidator models.PluginRequestValidator, pluginLoader plugins.Loader,
func ProvideService(grafanaCfg *setting.Cfg, requestValidator models.PluginRequestValidator, pluginLoader plugins.Loader,
sqlStore *sqlstore.SQLStore) (*PluginManager, error) {
pm := newManager(cfg, requestValidator, pluginLoader, sqlStore)
if err := pm.init(); err != nil {
pm := New(plugins.FromGrafanaCfg(grafanaCfg), requestValidator, map[plugins.Class][]string{
plugins.Core: corePluginPaths(grafanaCfg),
plugins.Bundled: {grafanaCfg.BundledPluginsPath},
plugins.External: append([]string{grafanaCfg.PluginsPath}, pluginSettingPaths(grafanaCfg)...),
}, pluginLoader, sqlStore)
if err := pm.Init(); err != nil {
return nil, err
}
return pm, nil
}
func newManager(cfg *setting.Cfg, pluginRequestValidator models.PluginRequestValidator, pluginLoader plugins.Loader,
sqlStore *sqlstore.SQLStore) *PluginManager {
func New(cfg *plugins.Cfg, requestValidator models.PluginRequestValidator, pluginPaths map[plugins.Class][]string,
pluginLoader plugins.Loader, sqlStore *sqlstore.SQLStore) *PluginManager {
return &PluginManager{
cfg: cfg,
requestValidator: pluginRequestValidator,
sqlStore: sqlStore,
requestValidator: requestValidator,
pluginLoader: pluginLoader,
store: map[string]*plugins.Plugin{},
pluginPaths: pluginPaths,
store: make(map[string]*plugins.Plugin),
log: log.New("plugin.manager"),
pluginInstaller: installer.New(false, cfg.BuildVersion, newInstallerLogger("plugin.installer", true)),
sqlStore: sqlStore,
}
}
func (m *PluginManager) init() error {
// create external plugin's path if not exists
exists, err := fs.Exists(m.cfg.PluginsPath)
if err != nil {
return err
}
if !exists {
if err = os.MkdirAll(m.cfg.PluginsPath, os.ModePerm); err != nil {
m.log.Error("Failed to create external plugins directory", "dir", m.cfg.PluginsPath, "error", err)
} else {
m.log.Debug("External plugins directory created", "dir", m.cfg.PluginsPath)
func (m *PluginManager) Init() error {
for class, paths := range m.pluginPaths {
err := m.loadPlugins(context.Background(), class, paths...)
if err != nil {
return err
}
}
m.log.Info("Initialising plugins")
// install Core plugins
err = m.loadPlugins(m.corePluginPaths()...)
if err != nil {
return err
}
// install Bundled plugins
err = m.loadPlugins(m.cfg.BundledPluginsPath)
if err != nil {
return err
}
// install External plugins
err = m.loadPlugins(m.cfg.PluginsPath)
if err != nil {
return err
}
// install plugins from cfg.PluginSettings
err = m.loadPlugins(m.pluginSettingPaths()...)
if err != nil {
return err
}
return nil
}
@ -161,7 +132,7 @@ func (m *PluginManager) plugins() []*plugins.Plugin {
return res
}
func (m *PluginManager) loadPlugins(paths ...string) error {
func (m *PluginManager) loadPlugins(ctx context.Context, class plugins.Class, paths ...string) error {
if len(paths) == 0 {
return nil
}
@ -173,7 +144,7 @@ func (m *PluginManager) loadPlugins(paths ...string) error {
}
}
loadedPlugins, err := m.pluginLoader.Load(pluginPaths, m.registeredPlugins())
loadedPlugins, err := m.pluginLoader.Load(ctx, class, pluginPaths, m.registeredPlugins())
if err != nil {
m.log.Error("Could not load plugins", "paths", pluginPaths, "err", err)
return err
@ -499,24 +470,24 @@ func (m *PluginManager) shutdown(ctx context.Context) {
}
// corePluginPaths provides a list of the Core plugin paths which need to be scanned on init()
func (m *PluginManager) corePluginPaths() []string {
func corePluginPaths(cfg *setting.Cfg) []string {
datasourcePaths := []string{
filepath.Join(m.cfg.StaticRootPath, "app/plugins/datasource/alertmanager"),
filepath.Join(m.cfg.StaticRootPath, "app/plugins/datasource/dashboard"),
filepath.Join(m.cfg.StaticRootPath, "app/plugins/datasource/jaeger"),
filepath.Join(m.cfg.StaticRootPath, "app/plugins/datasource/mixed"),
filepath.Join(m.cfg.StaticRootPath, "app/plugins/datasource/zipkin"),
filepath.Join(cfg.StaticRootPath, "app/plugins/datasource/alertmanager"),
filepath.Join(cfg.StaticRootPath, "app/plugins/datasource/dashboard"),
filepath.Join(cfg.StaticRootPath, "app/plugins/datasource/jaeger"),
filepath.Join(cfg.StaticRootPath, "app/plugins/datasource/mixed"),
filepath.Join(cfg.StaticRootPath, "app/plugins/datasource/zipkin"),
}
panelsPath := filepath.Join(m.cfg.StaticRootPath, "app/plugins/panel")
panelsPath := filepath.Join(cfg.StaticRootPath, "app/plugins/panel")
return append(datasourcePaths, panelsPath)
}
// pluginSettingPaths provides a plugin paths defined in cfg.PluginSettings which need to be scanned on init()
func (m *PluginManager) pluginSettingPaths() []string {
func pluginSettingPaths(cfg *setting.Cfg) []string {
var pluginSettingDirs []string
for _, settings := range m.cfg.PluginSettings {
for _, settings := range cfg.PluginSettings {
path, exists := settings["path"]
if !exists || path == "" {
continue

View File

@ -7,6 +7,7 @@ import (
"testing"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/backendplugin/provider"
"github.com/grafana/grafana/pkg/plugins/manager/loader"
"github.com/grafana/grafana/pkg/plugins/manager/signature"
"github.com/grafana/grafana/pkg/services/licensing"
@ -42,9 +43,10 @@ func TestPluginManager_int_init(t *testing.T) {
license := &licensing.OSSLicensingService{
Cfg: cfg,
}
pm := newManager(cfg, nil, loader.New(license, cfg, &signature.UnsignedPluginAuthorizer{Cfg: cfg}), nil)
err = pm.init()
pmCfg := plugins.FromGrafanaCfg(cfg)
pm, err := ProvideService(cfg, nil, loader.New(pmCfg, license,
&signature.UnsignedPluginAuthorizer{Cfg: pmCfg}, &provider.Service{}), nil)
require.NoError(t, err)
verifyCorePluginCatalogue(t, pm)

View File

@ -3,56 +3,24 @@ package manager
import (
"context"
"net/http"
"os"
"path/filepath"
"sync"
"testing"
"time"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana/pkg/infra/fs"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/backendplugin"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/setting"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gopkg.in/ini.v1"
)
const (
testPluginID = "test-plugin"
)
func TestPluginManager_init(t *testing.T) {
t.Run("Plugin folder will be created if not exists", func(t *testing.T) {
testDir := "plugin-test-dir"
exists, err := fs.Exists(testDir)
require.NoError(t, err)
assert.False(t, exists)
pm := createManager(t, func(pm *PluginManager) {
pm.cfg.PluginsPath = testDir
})
err = pm.init()
require.NoError(t, err)
exists, err = fs.Exists(testDir)
require.NoError(t, err)
assert.True(t, exists)
t.Cleanup(func() {
err = os.Remove(testDir)
require.NoError(t, err)
})
})
}
func TestPluginManager_loadPlugins(t *testing.T) {
t.Run("Managed backend plugin", func(t *testing.T) {
p, pc := createPlugin(testPluginID, "", plugins.External, true, true)
@ -64,7 +32,7 @@ func TestPluginManager_loadPlugins(t *testing.T) {
pm := createManager(t, func(pm *PluginManager) {
pm.pluginLoader = loader
})
err := pm.loadPlugins("test/path")
err := pm.loadPlugins(context.Background(), plugins.External, "test/path")
require.NoError(t, err)
assert.Equal(t, 1, pc.startCount)
@ -90,7 +58,7 @@ func TestPluginManager_loadPlugins(t *testing.T) {
pm := createManager(t, func(pm *PluginManager) {
pm.pluginLoader = loader
})
err := pm.loadPlugins("test/path")
err := pm.loadPlugins(context.Background(), plugins.External, "test/path")
require.NoError(t, err)
assert.Equal(t, 0, pc.startCount)
@ -116,7 +84,7 @@ func TestPluginManager_loadPlugins(t *testing.T) {
pm := createManager(t, func(pm *PluginManager) {
pm.pluginLoader = loader
})
err := pm.loadPlugins("test/path")
err := pm.loadPlugins(context.Background(), plugins.External, "test/path")
require.NoError(t, err)
assert.Equal(t, 0, pc.startCount)
@ -142,7 +110,7 @@ func TestPluginManager_loadPlugins(t *testing.T) {
pm := createManager(t, func(pm *PluginManager) {
pm.pluginLoader = loader
})
err := pm.loadPlugins("test/path")
err := pm.loadPlugins(context.Background(), plugins.External, "test/path")
require.NoError(t, err)
assert.Equal(t, 0, pc.startCount)
@ -257,7 +225,7 @@ func TestPluginManager_Installer(t *testing.T) {
pm := createManager(t, func(pm *PluginManager) {
pm.pluginLoader = loader
})
err := pm.loadPlugins("test/path")
err := pm.loadPlugins(context.Background(), plugins.Core, "test/path")
require.NoError(t, err)
assert.Equal(t, 1, pc.startCount)
@ -291,7 +259,7 @@ func TestPluginManager_Installer(t *testing.T) {
pm := createManager(t, func(pm *PluginManager) {
pm.pluginLoader = loader
})
err := pm.loadPlugins("test/path")
err := pm.loadPlugins(context.Background(), plugins.Bundled, "test/path")
require.NoError(t, err)
assert.Equal(t, 1, pc.startCount)
@ -499,18 +467,8 @@ func TestPluginManager_lifecycle_unmanaged(t *testing.T) {
func createManager(t *testing.T, cbs ...func(*PluginManager)) *PluginManager {
t.Helper()
staticRootPath, err := filepath.Abs("../../../public/")
require.NoError(t, err)
cfg := &setting.Cfg{
Raw: ini.Empty(),
Env: setting.Prod,
StaticRootPath: staticRootPath,
}
requestValidator := &testPluginRequestValidator{}
loader := &fakeLoader{}
pm := newManager(cfg, requestValidator, loader, &sqlstore.SQLStore{})
pm := New(&plugins.Cfg{}, requestValidator, nil, &fakeLoader{}, &sqlstore.SQLStore{})
for _, cb := range cbs {
cb(pm)
@ -555,7 +513,7 @@ type managerScenarioCtx struct {
func newScenario(t *testing.T, managed bool, fn func(t *testing.T, ctx *managerScenarioCtx)) {
t.Helper()
cfg := setting.NewCfg()
cfg := &plugins.Cfg{}
cfg.AWSAllowedAuthProviders = []string{"keys", "credentials"}
cfg.AWSAssumeRoleEnabled = true
@ -563,13 +521,9 @@ func newScenario(t *testing.T, managed bool, fn func(t *testing.T, ctx *managerS
cfg.Azure.Cloud = "AzureCloud"
cfg.Azure.ManagedIdentityClientId = "client-id"
staticRootPath, err := filepath.Abs("../../../public")
require.NoError(t, err)
cfg.StaticRootPath = staticRootPath
requestValidator := &testPluginRequestValidator{}
loader := &fakeLoader{}
manager := newManager(cfg, requestValidator, loader, nil)
manager := New(cfg, requestValidator, nil, loader, nil)
manager.pluginLoader = loader
ctx := &managerScenarioCtx{
manager: manager,
@ -616,13 +570,13 @@ type fakeLoader struct {
plugins.Loader
}
func (l *fakeLoader) Load(paths []string, _ map[string]struct{}) ([]*plugins.Plugin, error) {
func (l *fakeLoader) Load(_ context.Context, _ plugins.Class, paths []string, _ map[string]struct{}) ([]*plugins.Plugin, error) {
l.loadedPaths = append(l.loadedPaths, paths...)
return l.mockedLoadedPlugins, nil
}
func (l *fakeLoader) LoadWithFactory(path string, _ backendplugin.PluginFactoryFunc) (*plugins.Plugin, error) {
func (l *fakeLoader) LoadWithFactory(_ context.Context, _ plugins.Class, path string, _ backendplugin.PluginFactoryFunc) (*plugins.Plugin, error) {
l.loadedPaths = append(l.loadedPaths, path)
return l.mockedFactoryLoadedPlugin, nil

View File

@ -7,12 +7,12 @@ import (
func ProvideService(cfg *setting.Cfg) (*UnsignedPluginAuthorizer, error) {
return &UnsignedPluginAuthorizer{
Cfg: cfg,
Cfg: plugins.FromGrafanaCfg(cfg),
}, nil
}
type UnsignedPluginAuthorizer struct {
Cfg *setting.Cfg
Cfg *plugins.Cfg
}
func (u *UnsignedPluginAuthorizer) CanLoadPlugin(p *plugins.Plugin) bool {
@ -20,7 +20,7 @@ func (u *UnsignedPluginAuthorizer) CanLoadPlugin(p *plugins.Plugin) bool {
return true
}
if u.Cfg.Env == setting.Dev {
if u.Cfg.DevMode {
return true
}

View File

@ -75,7 +75,7 @@ func (m *PluginManager) Add(ctx context.Context, pluginID, version string) error
return err
}
err = m.loadPlugins(m.cfg.PluginsPath)
err = m.loadPlugins(context.Background(), plugins.External, m.cfg.PluginsPath)
if err != nil {
return err
}
@ -83,7 +83,7 @@ func (m *PluginManager) Add(ctx context.Context, pluginID, version string) error
return nil
}
func (m *PluginManager) AddWithFactory(_ context.Context, pluginID string, factory backendplugin.PluginFactoryFunc,
func (m *PluginManager) AddWithFactory(ctx context.Context, pluginID string, factory backendplugin.PluginFactoryFunc,
pathResolver plugins.PluginPathResolver) error {
if m.isRegistered(pluginID) {
return fmt.Errorf("plugin %s is already registered", pluginID)
@ -94,7 +94,7 @@ func (m *PluginManager) AddWithFactory(_ context.Context, pluginID string, facto
return err
}
p, err := m.pluginLoader.LoadWithFactory(path, factory)
p, err := m.pluginLoader.LoadWithFactory(ctx, plugins.Core, path, factory)
if err != nil {
return err
}

View File

@ -0,0 +1,5 @@
{
"title": "Nginx Memory",
"revision": 2,
"schemaVersion": 11
}

View File

@ -0,0 +1,38 @@
{
"type": "app",
"name": "Test App",
"id": "test-app",
"info": {
"description": "Official Grafana Test App & Dashboard bundle",
"author": {
"name": "Test Inc.",
"url": "http://test.com"
},
"keywords": ["test"],
"links": [
{"name": "Project site", "url": "http://project.com"},
{"name": "License & Terms", "url": "http://license.com"}
],
"version": "1.0.0",
"updated": "2015-02-10"
},
"includes": [
{
"type": "dashboard",
"name": "Nginx Memory",
"path": "dashboards/memory.json",
"defaultNav": true
},
{
"type": "page",
"name": "Root Page (react)",
"path": "/a/my-simple-app",
"role": "Viewer",
"addToNav": true,
"defaultNav": true
}
],
"dependencies": {
"grafanaDependency": ">=8.0.0"
}
}

View File

@ -7,6 +7,7 @@ import (
"github.com/google/wire"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/backendplugin/provider"
"github.com/grafana/grafana/pkg/plugins/manager/signature"
"github.com/grafana/grafana/pkg/registry"
"github.com/grafana/grafana/pkg/server/backgroundsvcs"
@ -61,6 +62,8 @@ var wireExtsBasicSet = wire.NewSet(
wire.Bind(new(searchusers.Service), new(*searchusers.OSSService)),
signature.ProvideService,
wire.Bind(new(plugins.PluginLoaderAuthorizer), new(*signature.UnsignedPluginAuthorizer)),
provider.ProvideService,
wire.Bind(new(plugins.BackendFactoryProvider), new(*provider.Service)),
acdb.ProvideService,
wire.Bind(new(accesscontrol.ResourcePermissionsStore), new(*acdb.AccessControlStore)),
wire.Bind(new(accesscontrol.PermissionsProvider), new(*acdb.AccessControlStore)),