diff --git a/pkg/api/plugin_resource_test.go b/pkg/api/plugin_resource_test.go index 68353159a5e..459f3e84352 100644 --- a/pkg/api/plugin_resource_test.go +++ b/pkg/api/plugin_resource_test.go @@ -73,11 +73,12 @@ func TestCallResource(t *testing.T) { discovery := pipeline.ProvideDiscoveryStage(pCfg, finder.NewLocalFinder(pCfg.DevMode), reg) bootstrap := pipeline.ProvideBootstrapStage(pCfg, signature.ProvideService(pCfg, statickey.New()), assetpath.ProvideService(pluginscdn.ProvideService(pCfg))) + initialize := pipeline.ProvideInitializationStage(pCfg, reg, fakes.NewFakeLicensingService(), provider.ProvideService(coreRegistry)) - l := loader.ProvideService(pCfg, fakes.NewFakeLicensingService(), signature.NewUnsignedAuthorizer(pCfg), - reg, provider.ProvideService(coreRegistry), fakes.NewFakeRoleRegistry(), + l := loader.ProvideService(pCfg, signature.NewUnsignedAuthorizer(pCfg), + reg, fakes.NewFakeRoleRegistry(), assetpath.ProvideService(pluginscdn.ProvideService(pCfg)), - angularInspector, &fakes.FakeOauthService{}, discovery, bootstrap) + angularInspector, &fakes.FakeOauthService{}, discovery, bootstrap, initialize) srcs := sources.ProvideService(cfg, pCfg) ps, err := store.ProvideService(reg, srcs, l) require.NoError(t, err) diff --git a/pkg/plugins/manager/fakes/fakes.go b/pkg/plugins/manager/fakes/fakes.go index dffd4372a64..247a8167aaf 100644 --- a/pkg/plugins/manager/fakes/fakes.go +++ b/pkg/plugins/manager/fakes/fakes.go @@ -463,3 +463,36 @@ func (pr *FakePluginStore) Plugins(_ context.Context, pluginTypes ...plugins.Typ return result } + +type FakeDiscoverer struct { + DiscoverFunc func(ctx context.Context, src plugins.PluginSource) ([]*plugins.FoundBundle, error) +} + +func (f *FakeDiscoverer) Discover(ctx context.Context, src plugins.PluginSource) ([]*plugins.FoundBundle, error) { + if f.DiscoverFunc != nil { + return f.DiscoverFunc(ctx, src) + } + return []*plugins.FoundBundle{}, nil +} + +type FakeBootstrapper struct { + BootstrapFunc func(ctx context.Context, src plugins.PluginSource, bundles []*plugins.FoundBundle) ([]*plugins.Plugin, error) +} + +func (f *FakeBootstrapper) Bootstrap(ctx context.Context, src plugins.PluginSource, bundles []*plugins.FoundBundle) ([]*plugins.Plugin, error) { + if f.BootstrapFunc != nil { + return f.BootstrapFunc(ctx, src, bundles) + } + return []*plugins.Plugin{}, nil +} + +type FakeInitializer struct { + IntializeFunc func(ctx context.Context, ps []*plugins.Plugin) ([]*plugins.Plugin, error) +} + +func (f *FakeInitializer) Initialize(ctx context.Context, ps []*plugins.Plugin) ([]*plugins.Plugin, error) { + if f.IntializeFunc != nil { + return f.IntializeFunc(ctx, ps) + } + return []*plugins.Plugin{}, nil +} diff --git a/pkg/plugins/manager/loader/initializer/initializer.go b/pkg/plugins/manager/loader/initializer/initializer.go deleted file mode 100644 index 50937826e5f..00000000000 --- a/pkg/plugins/manager/loader/initializer/initializer.go +++ /dev/null @@ -1,43 +0,0 @@ -package initializer - -import ( - "context" - "errors" - - "github.com/grafana/grafana/pkg/plugins" - "github.com/grafana/grafana/pkg/plugins/config" - "github.com/grafana/grafana/pkg/plugins/envvars" -) - -type Initializer struct { - envVarProvider envvars.Provider - backendProvider plugins.BackendFactoryProvider -} - -func New(cfg *config.Cfg, backendProvider plugins.BackendFactoryProvider, license plugins.Licensing) Initializer { - return Initializer{ - envVarProvider: envvars.NewProvider(cfg, license), - backendProvider: backendProvider, - } -} - -func (i *Initializer) Initialize(ctx context.Context, p *plugins.Plugin) error { - if p.Backend { - backendFactory := i.backendProvider.BackendFactory(ctx, p) - if backendFactory == nil { - return errors.New("could not find backend factory for plugin") - } - - env, err := i.envVarProvider.Get(ctx, p) - if err != nil { - return err - } - if backendClient, err := backendFactory(p.ID, p.Logger(), env); err != nil { - return err - } else { - p.RegisterClient(backendClient) - } - } - - return nil -} diff --git a/pkg/plugins/manager/loader/loader.go b/pkg/plugins/manager/loader/loader.go index 88b7e722e8e..caa53556edd 100644 --- a/pkg/plugins/manager/loader/loader.go +++ b/pkg/plugins/manager/loader/loader.go @@ -11,9 +11,9 @@ import ( "github.com/grafana/grafana/pkg/plugins/log" "github.com/grafana/grafana/pkg/plugins/manager/loader/angular/angularinspector" "github.com/grafana/grafana/pkg/plugins/manager/loader/assetpath" - "github.com/grafana/grafana/pkg/plugins/manager/loader/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/pipeline/initialization" "github.com/grafana/grafana/pkg/plugins/manager/process" "github.com/grafana/grafana/pkg/plugins/manager/registry" "github.com/grafana/grafana/pkg/plugins/manager/signature" @@ -24,13 +24,13 @@ import ( var _ plugins.ErrorResolver = (*Loader)(nil) type Loader struct { - discovery discovery.Discoverer - bootstrap bootstrap.Bootstrapper + discovery discovery.Discoverer + bootstrap bootstrap.Bootstrapper + initializer initialization.Initializer processManager process.Service pluginRegistry registry.Service roleRegistry plugins.RoleRegistry - pluginInitializer initializer.Initializer signatureValidator signature.Validator externalServiceRegistry oauth.ExternalServiceRegistry assetPath *assetpath.Service @@ -42,23 +42,20 @@ type Loader struct { errs map[string]*plugins.SignatureError } -func ProvideService(cfg *config.Cfg, license plugins.Licensing, authorizer plugins.PluginLoaderAuthorizer, - pluginRegistry registry.Service, backendProvider plugins.BackendFactoryProvider, - roleRegistry plugins.RoleRegistry, assetPath *assetpath.Service, +func ProvideService(cfg *config.Cfg, authorizer plugins.PluginLoaderAuthorizer, + pluginRegistry registry.Service, roleRegistry plugins.RoleRegistry, assetPath *assetpath.Service, angularInspector angularinspector.Inspector, externalServiceRegistry oauth.ExternalServiceRegistry, - discovery discovery.Discoverer, bootstrap bootstrap.Bootstrapper) *Loader { - return New(cfg, license, authorizer, pluginRegistry, backendProvider, process.NewManager(pluginRegistry), - roleRegistry, assetPath, angularInspector, externalServiceRegistry, discovery, bootstrap) + discovery discovery.Discoverer, bootstrap bootstrap.Bootstrapper, initializer initialization.Initializer) *Loader { + return New(cfg, authorizer, pluginRegistry, process.NewManager(pluginRegistry), roleRegistry, assetPath, + angularInspector, externalServiceRegistry, discovery, bootstrap, initializer) } -func New(cfg *config.Cfg, license plugins.Licensing, authorizer plugins.PluginLoaderAuthorizer, - pluginRegistry registry.Service, backendProvider plugins.BackendFactoryProvider, +func New(cfg *config.Cfg, authorizer plugins.PluginLoaderAuthorizer, pluginRegistry registry.Service, processManager process.Service, roleRegistry plugins.RoleRegistry, assetPath *assetpath.Service, angularInspector angularinspector.Inspector, externalServiceRegistry oauth.ExternalServiceRegistry, - discovery discovery.Discoverer, bootstrap bootstrap.Bootstrapper) *Loader { + discovery discovery.Discoverer, bootstrap bootstrap.Bootstrapper, initializer initialization.Initializer) *Loader { return &Loader{ pluginRegistry: pluginRegistry, - pluginInitializer: initializer.New(cfg, backendProvider, license), signatureValidator: signature.NewValidator(authorizer), processManager: processManager, errs: make(map[string]*plugins.SignatureError), @@ -70,6 +67,7 @@ func New(cfg *config.Cfg, license plugins.Licensing, authorizer plugins.PluginLo externalServiceRegistry: externalServiceRegistry, discovery: discovery, bootstrap: bootstrap, + initializer: initializer, } } @@ -144,24 +142,9 @@ func (l *Loader) Load(ctx context.Context, src plugins.PluginSource) ([]*plugins // // - initializedPlugins := make([]*plugins.Plugin, 0, len(verifiedPlugins)) - for _, p := range verifiedPlugins { - err = l.pluginInitializer.Initialize(ctx, p) - if err != nil { - l.log.Error("Could not initialize plugin", "pluginId", p.ID, "err", err) - 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) + initializedPlugins, err := l.initializer.Initialize(ctx, verifiedPlugins) + if err != nil { + return nil, err } // diff --git a/pkg/plugins/manager/loader/loader_test.go b/pkg/plugins/manager/loader/loader_test.go index a1cfcc11205..e8357a4c729 100644 --- a/pkg/plugins/manager/loader/loader_test.go +++ b/pkg/plugins/manager/loader/loader_test.go @@ -2,7 +2,6 @@ package loader import ( "context" - "errors" "path/filepath" "sort" "testing" @@ -12,21 +11,17 @@ import ( "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/plugins" - "github.com/grafana/grafana/pkg/plugins/backendplugin" "github.com/grafana/grafana/pkg/plugins/config" - "github.com/grafana/grafana/pkg/plugins/log" "github.com/grafana/grafana/pkg/plugins/manager/fakes" "github.com/grafana/grafana/pkg/plugins/manager/loader/angular/angularinspector" "github.com/grafana/grafana/pkg/plugins/manager/loader/assetpath" - "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/pipeline/initialization" "github.com/grafana/grafana/pkg/plugins/manager/signature" "github.com/grafana/grafana/pkg/plugins/manager/sources" "github.com/grafana/grafana/pkg/plugins/pluginscdn" "github.com/grafana/grafana/pkg/services/org" - "github.com/grafana/grafana/pkg/setting" ) var compareOpts = []cmp.Option{cmpopts.IgnoreFields(plugins.Plugin{}, "client", "log"), fsComparer} @@ -393,38 +388,39 @@ func TestLoader_Load(t *testing.T) { }, pluginPaths: []string{"../testdata/test-app-with-includes"}, want: []*plugins.Plugin{ - {JSONData: plugins.JSONData{ - ID: "test-app", - Type: plugins.TypeApp, - Name: "Test App", - Info: plugins.Info{ - Author: plugins.InfoLink{ - Name: "Test Inc.", - URL: "http://test.com", + { + JSONData: plugins.JSONData{ + ID: "test-app", + Type: plugins.TypeApp, + Name: "Test App", + Info: plugins.Info{ + Author: plugins.InfoLink{ + Name: "Test Inc.", + URL: "http://test.com", + }, + Description: "Official Grafana Test App & Dashboard bundle", + Version: "1.0.0", + Links: []plugins.InfoLink{ + {Name: "Project site", URL: "http://project.com"}, + {Name: "License & Terms", URL: "http://license.com"}, + }, + Logos: plugins.Logos{ + Small: "public/img/icn-app.svg", + Large: "public/img/icn-app.svg", + }, + Updated: "2015-02-10", }, - 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"}, + Dependencies: plugins.Dependencies{ + GrafanaDependency: ">=8.0.0", + GrafanaVersion: "*", + Plugins: []plugins.Dependency{}, }, - Logos: plugins.Logos{ - Small: "public/img/icn-app.svg", - Large: "public/img/icn-app.svg", + Includes: []*plugins.Includes{ + {Name: "Nginx Memory", Path: "dashboards/memory.json", Type: "dashboard", Role: org.RoleViewer, Slug: "nginx-memory"}, + {Name: "Root Page (react)", Type: "page", Role: org.RoleViewer, Path: "/a/my-simple-app", DefaultNav: true, AddToNav: true, Slug: "root-page-react"}, }, - Updated: "2015-02-10", + Backend: false, }, - Dependencies: plugins.Dependencies{ - GrafanaDependency: ">=8.0.0", - GrafanaVersion: "*", - Plugins: []plugins.Dependency{}, - }, - Includes: []*plugins.Includes{ - {Name: "Nginx Memory", Path: "dashboards/memory.json", Type: "dashboard", Role: org.RoleViewer, Slug: "nginx-memory"}, - {Name: "Root Page (react)", Type: "page", Role: org.RoleViewer, Path: "/a/my-simple-app", DefaultNav: true, AddToNav: true, Slug: "root-page-react"}, - }, - Backend: false, - }, DefaultNavURL: "/plugins/test-app/page/root-page-react", FS: mustNewStaticFSForTests(t, filepath.Join(parentDir, "testdata/test-app-with-includes")), Class: plugins.ClassExternal, @@ -436,14 +432,14 @@ func TestLoader_Load(t *testing.T) { }, } for _, tt := range tests { - reg := fakes.NewFakePluginRegistry() - procPrvdr := fakes.NewFakeBackendProcessProvider() - procMgr := fakes.NewFakeProcessManager() - l := newLoader(t, tt.cfg, func(l *Loader) { - l.pluginRegistry = reg - l.processManager = procMgr - l.pluginInitializer = initializer.New(tt.cfg, procPrvdr, &fakes.FakeLicensingService{}) - }) + angularInspector, err := angularinspector.NewStaticInspector() + require.NoError(t, err) + + l := New(tt.cfg, signature.NewUnsignedAuthorizer(tt.cfg), fakes.NewFakePluginRegistry(), + fakes.NewFakeProcessManager(), fakes.NewFakeRoleRegistry(), + assetpath.ProvideService(pluginscdn.ProvideService(tt.cfg)), angularInspector, &fakes.FakeOauthService{}, + discovery.New(tt.cfg, discovery.Opts{}), bootstrap.New(tt.cfg, bootstrap.Opts{}), + initialization.New(tt.cfg, initialization.Opts{})) t.Run(tt.name, func(t *testing.T) { got, err := l.Load(context.Background(), sources.NewLocalSource(tt.class, tt.pluginPaths)) @@ -457,989 +453,69 @@ func TestLoader_Load(t *testing.T) { for _, pluginErr := range pluginErrs { require.Equal(t, tt.pluginErrors[pluginErr.PluginID], pluginErr) } - - verifyState(t, tt.want, reg, procPrvdr, procMgr) }) } -} -func TestLoader_Load_CustomSource(t *testing.T) { - t.Run("Load a plugin", func(t *testing.T) { - parentDir, err := filepath.Abs("../") - if err != nil { - t.Errorf("could not construct absolute path of current dir") - return - } - - cfg := &config.Cfg{ - PluginsCDNURLTemplate: "https://cdn.example.com", - PluginSettings: setting.PluginSettings{ - "grafana-worldmap-panel": {"cdn": "true"}, - }, - } - - pluginPaths := []string{"../testdata/cdn"} - expected := []*plugins.Plugin{{ - JSONData: plugins.JSONData{ - ID: "grafana-worldmap-panel", - Type: plugins.TypePanel, - Name: "Worldmap Panel", - Info: plugins.Info{ - Version: "0.3.3", - Links: []plugins.InfoLink{ - {Name: "Project site", URL: "https://github.com/grafana/worldmap-panel"}, - {Name: "MIT License", URL: "https://github.com/grafana/worldmap-panel/blob/master/LICENSE"}, - }, - Logos: plugins.Logos{ - // Path substitution - Small: "https://cdn.example.com/grafana-worldmap-panel/0.3.3/public/plugins/grafana-worldmap-panel/images/worldmap_logo.svg", - Large: "https://cdn.example.com/grafana-worldmap-panel/0.3.3/public/plugins/grafana-worldmap-panel/images/worldmap_logo.svg", - }, - Screenshots: []plugins.Screenshots{ - { - Name: "World", - Path: "https://cdn.example.com/grafana-worldmap-panel/0.3.3/public/plugins/grafana-worldmap-panel/images/worldmap-world.png", - }, - { - Name: "USA", - Path: "https://cdn.example.com/grafana-worldmap-panel/0.3.3/public/plugins/grafana-worldmap-panel/images/worldmap-usa.png", - }, - { - Name: "Light Theme", - Path: "https://cdn.example.com/grafana-worldmap-panel/0.3.3/public/plugins/grafana-worldmap-panel/images/worldmap-light-theme.png", - }, - }, - }, - Dependencies: plugins.Dependencies{ - GrafanaVersion: "3.x.x", - Plugins: []plugins.Dependency{}, - }, - }, - FS: mustNewStaticFSForTests(t, filepath.Join(parentDir, "testdata/cdn/plugin")), - Class: plugins.ClassBundled, - Signature: plugins.SignatureStatusValid, - BaseURL: "plugin-cdn/grafana-worldmap-panel/0.3.3/public/plugins/grafana-worldmap-panel", - Module: "plugin-cdn/grafana-worldmap-panel/0.3.3/public/plugins/grafana-worldmap-panel/module", - }} - - l := newLoader(t, cfg) - got, err := l.Load(context.Background(), &fakes.FakePluginSource{ + t.Run("Simple", func(t *testing.T) { + src := &fakes.FakePluginSource{ PluginClassFunc: func(ctx context.Context) plugins.Class { - return plugins.ClassBundled + return plugins.ClassExternal }, PluginURIsFunc: func(ctx context.Context) []string { - return pluginPaths + return []string{"http://example.com"} }, DefaultSignatureFunc: func(ctx context.Context) (plugins.Signature, bool) { - return plugins.Signature{ - Status: plugins.SignatureStatusValid, - }, true - }, - }) - - require.NoError(t, err) - if !cmp.Equal(got, expected, compareOpts...) { - t.Fatalf("Result mismatch (-want +got):\n%s", cmp.Diff(got, expected, compareOpts...)) - } - }) -} - -func TestLoader_Load_MultiplePlugins(t *testing.T) { - parentDir, err := filepath.Abs("../") - if err != nil { - t.Errorf("could not construct absolute path of current dir") - return - } - - t.Run("Load multiple", func(t *testing.T) { - tests := []struct { - name string - cfg *config.Cfg - pluginPaths []string - existingPlugins map[string]struct{} - want []*plugins.Plugin - pluginErrors map[string]*plugins.Error - }{ - { - name: "Load multiple plugins (broken, valid, unsigned)", - cfg: &config.Cfg{ - GrafanaAppURL: "http://localhost:3000", - }, - pluginPaths: []string{ - "../testdata/invalid-plugin-json", // test-app - "../testdata/valid-v2-pvt-signature", // test - "../testdata/unsigned-panel", // test-panel - }, - want: []*plugins.Plugin{ - { - JSONData: plugins.JSONData{ - ID: "test-datasource", - Type: plugins.TypeDataSource, - Name: "Test", - Info: plugins.Info{ - Author: plugins.InfoLink{ - Name: "Will Browne", - URL: "https://willbrowne.com", - }, - Logos: plugins.Logos{ - Small: "public/img/icn-datasource.svg", - Large: "public/img/icn-datasource.svg", - }, - Description: "Test", - Version: "1.0.0", - }, - Dependencies: plugins.Dependencies{ - GrafanaVersion: "*", - Plugins: []plugins.Dependency{}, - }, - Backend: true, - Executable: "test", - State: plugins.ReleaseStateAlpha, - }, - Class: plugins.ClassExternal, - Module: "plugins/test-datasource/module", - BaseURL: "public/plugins/test-datasource", - FS: mustNewStaticFSForTests(t, filepath.Join(parentDir, "testdata/valid-v2-pvt-signature/plugin")), - Signature: "valid", - SignatureType: plugins.SignatureTypePrivate, - SignatureOrg: "Will Browne", - }, - }, - pluginErrors: map[string]*plugins.Error{ - "test-panel": { - PluginID: "test-panel", - ErrorCode: "signatureMissing", - }, - }, + return plugins.Signature{}, false }, } - - for _, tt := range tests { - reg := fakes.NewFakePluginRegistry() - procPrvdr := fakes.NewFakeBackendProcessProvider() - procMgr := fakes.NewFakeProcessManager() - l := newLoader(t, tt.cfg, func(l *Loader) { - l.pluginRegistry = reg - l.processManager = procMgr - l.pluginInitializer = initializer.New(tt.cfg, procPrvdr, fakes.NewFakeLicensingService()) - }) - t.Run(tt.name, func(t *testing.T) { - got, err := l.Load(context.Background(), &fakes.FakePluginSource{ - PluginClassFunc: func(ctx context.Context) plugins.Class { - return plugins.ClassExternal - }, - PluginURIsFunc: func(ctx context.Context) []string { - return tt.pluginPaths - }, - }) - require.NoError(t, err) - sort.SliceStable(got, func(i, j int) bool { - return got[i].ID < got[j].ID - }) - if !cmp.Equal(got, tt.want, compareOpts...) { - t.Fatalf("Result mismatch (-want +got):\n%s", cmp.Diff(got, tt.want, compareOpts...)) - } - pluginErrs := l.PluginErrors() - require.Equal(t, len(tt.pluginErrors), len(pluginErrs)) - for _, pluginErr := range pluginErrs { - require.Equal(t, tt.pluginErrors[pluginErr.PluginID], pluginErr) - } - verifyState(t, tt.want, reg, procPrvdr, procMgr) - }) - } - }) -} - -func TestLoader_Load_RBACReady(t *testing.T) { - pluginDir, err := filepath.Abs("../testdata/test-app-with-roles") - if err != nil { - t.Errorf("could not construct absolute path of current dir") - return - } - - tests := []struct { - name string - cfg *config.Cfg - pluginPaths []string - existingPlugins map[string]struct{} - want []*plugins.Plugin - }{ - { - name: "Load plugin defining one RBAC role", - cfg: &config.Cfg{ - GrafanaAppURL: "http://localhost:3000", - }, - pluginPaths: []string{"../testdata/test-app-with-roles"}, - want: []*plugins.Plugin{ - { - JSONData: plugins.JSONData{ - ID: "test-app", - Type: plugins.TypeApp, - Name: "Test App", - Info: plugins.Info{ - Author: plugins.InfoLink{ - Name: "Test Inc.", - URL: "http://test.com", - }, - Description: "Test App", - Version: "1.0.0", - Links: []plugins.InfoLink{}, - Logos: plugins.Logos{ - Small: "public/img/icn-app.svg", - Large: "public/img/icn-app.svg", - }, - Updated: "2015-02-10", - }, - Dependencies: plugins.Dependencies{ - GrafanaVersion: "*", - GrafanaDependency: ">=8.0.0", - Plugins: []plugins.Dependency{}, - }, - Includes: []*plugins.Includes{}, - Roles: []plugins.RoleRegistration{ - { - Role: plugins.Role{ - Name: "Reader", - Description: "View everything in the test-app plugin", - Permissions: []plugins.Permission{ - {Action: "plugins.app:access", Scope: "plugins.app:id:test-app"}, - {Action: "test-app.resource:read", Scope: "resources:*"}, - {Action: "test-app.otherresource:toggle"}, - }, - }, - Grants: []string{"Admin"}, - }, - }, - Backend: false, - }, - FS: mustNewStaticFSForTests(t, pluginDir), - Class: plugins.ClassExternal, - Signature: plugins.SignatureStatusValid, - SignatureType: plugins.SignatureTypePrivate, - SignatureOrg: "gabrielmabille", - Module: "plugins/test-app/module", - BaseURL: "public/plugins/test-app", - }, - }, - }, - } - - for _, tt := range tests { - reg := fakes.NewFakePluginRegistry() - procPrvdr := fakes.NewFakeBackendProcessProvider() - procMgr := fakes.NewFakeProcessManager() - l := newLoader(t, tt.cfg, func(l *Loader) { - l.pluginRegistry = reg - l.processManager = procMgr - l.pluginInitializer = initializer.New(tt.cfg, procPrvdr, fakes.NewFakeLicensingService()) - }) - - got, err := l.Load(context.Background(), &fakes.FakePluginSource{ - PluginClassFunc: func(ctx context.Context) plugins.Class { - return plugins.ClassExternal - }, - PluginURIsFunc: func(ctx context.Context) []string { - return tt.pluginPaths - }, - }) - 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...)) - } - pluginErrs := l.PluginErrors() - require.Len(t, pluginErrs, 0) - - verifyState(t, tt.want, reg, procPrvdr, procMgr) - } -} - -func TestLoader_Load_Signature_RootURL(t *testing.T) { - const defaultAppURL = "http://localhost:3000/grafana" - - parentDir, err := filepath.Abs("../") - if err != nil { - t.Errorf("could not construct absolute path of current dir") - return - } - - t.Run("Private signature verification ignores trailing slash in root URL", func(t *testing.T) { - paths := []string{"../testdata/valid-v2-pvt-signature-root-url-uri"} - - expected := []*plugins.Plugin{ - { - JSONData: plugins.JSONData{ - ID: "test-datasource", - Type: plugins.TypeDataSource, - Name: "Test", - Info: plugins.Info{ - Author: plugins.InfoLink{Name: "Will Browne", URL: "https://willbrowne.com"}, - Description: "Test", - Logos: plugins.Logos{ - Small: "public/img/icn-datasource.svg", - Large: "public/img/icn-datasource.svg", - }, - Version: "1.0.0", - }, - State: plugins.ReleaseStateAlpha, - Dependencies: plugins.Dependencies{GrafanaVersion: "*", Plugins: []plugins.Dependency{}}, - Backend: true, - Executable: "test", - }, - FS: mustNewStaticFSForTests(t, filepath.Join(parentDir, "/testdata/valid-v2-pvt-signature-root-url-uri/plugin")), - Class: plugins.ClassExternal, - Signature: plugins.SignatureStatusValid, - SignatureType: plugins.SignatureTypePrivate, - SignatureOrg: "Will Browne", - Module: "plugins/test-datasource/module", - BaseURL: "public/plugins/test-datasource", - }, - } - - reg := fakes.NewFakePluginRegistry() - procPrvdr := fakes.NewFakeBackendProcessProvider() - procMgr := fakes.NewFakeProcessManager() - l := newLoader(t, &config.Cfg{GrafanaAppURL: defaultAppURL}, func(l *Loader) { - l.pluginRegistry = reg - l.processManager = procMgr - l.pluginInitializer = initializer.New(&config.Cfg{}, procPrvdr, fakes.NewFakeLicensingService()) - }) - got, err := l.Load(context.Background(), &fakes.FakePluginSource{ - PluginClassFunc: func(ctx context.Context) plugins.Class { - return plugins.ClassExternal - }, - PluginURIsFunc: func(ctx context.Context) []string { - return paths - }, - }) - require.NoError(t, err) - - if !cmp.Equal(got, expected, compareOpts...) { - t.Fatalf("Result mismatch (-want +got):\n%s", cmp.Diff(got, expected, compareOpts...)) - } - verifyState(t, expected, reg, procPrvdr, procMgr) - }) -} - -func TestLoader_Load_DuplicatePlugins(t *testing.T) { - t.Run("Load duplicate plugin folders", func(t *testing.T) { - pluginDir, err := filepath.Abs("../testdata/test-app") - if err != nil { - t.Errorf("could not construct absolute path of plugin dir") - return - } - expected := []*plugins.Plugin{ - { - JSONData: plugins.JSONData{ - ID: "test-app", - Type: plugins.TypeApp, - Name: "Test App", - Info: plugins.Info{ - Author: plugins.InfoLink{ - Name: "Test Inc.", - URL: "http://test.com", - }, - Description: "Official Grafana Test App & Dashboard bundle", - Version: "1.0.0", - Links: []plugins.InfoLink{ - {Name: "Project site", URL: "http://project.com"}, - {Name: "License & Terms", URL: "http://license.com"}, - }, - Logos: plugins.Logos{ - Small: "public/plugins/test-app/img/logo_small.png", - Large: "public/plugins/test-app/img/logo_large.png", - }, - Screenshots: []plugins.Screenshots{ - {Path: "public/plugins/test-app/img/screenshot1.png", Name: "img1"}, - {Path: "public/plugins/test-app/img/screenshot2.png", Name: "img2"}, - }, - Updated: "2015-02-10", - }, - Dependencies: plugins.Dependencies{ - GrafanaVersion: "3.x.x", - Plugins: []plugins.Dependency{ - {Type: "datasource", ID: "graphite", Name: "Graphite", Version: "1.0.0"}, - {Type: "panel", ID: "graph", Name: "Graph", Version: "1.0.0"}, - }, - }, - Includes: []*plugins.Includes{ - {Name: "Nginx Connections", Path: "dashboards/connections.json", Type: "dashboard", Role: org.RoleViewer, Slug: "nginx-connections"}, - {Name: "Nginx Memory", Path: "dashboards/memory.json", Type: "dashboard", Role: org.RoleViewer, Slug: "nginx-memory"}, - {Name: "Nginx Panel", Type: "panel", Role: org.RoleViewer, Slug: "nginx-panel"}, - {Name: "Nginx Datasource", Type: "datasource", Role: org.RoleViewer, Slug: "nginx-datasource"}, - }, - Backend: false, - }, - FS: mustNewStaticFSForTests(t, pluginDir), - Class: plugins.ClassExternal, - Signature: plugins.SignatureStatusValid, - SignatureType: plugins.SignatureTypeGrafana, - SignatureOrg: "Grafana Labs", - Module: "plugins/test-app/module", - BaseURL: "public/plugins/test-app", - }, - } - - reg := fakes.NewFakePluginRegistry() - procPrvdr := fakes.NewFakeBackendProcessProvider() - procMgr := fakes.NewFakeProcessManager() - l := newLoader(t, &config.Cfg{}, func(l *Loader) { - l.pluginRegistry = reg - l.processManager = procMgr - l.pluginInitializer = initializer.New(&config.Cfg{}, procPrvdr, fakes.NewFakeLicensingService()) - }) - got, err := l.Load(context.Background(), &fakes.FakePluginSource{ - PluginClassFunc: func(ctx context.Context) plugins.Class { - return plugins.ClassExternal - }, - PluginURIsFunc: func(ctx context.Context) []string { - return []string{pluginDir, pluginDir} - }, - }) - require.NoError(t, err) - - if !cmp.Equal(got, expected, compareOpts...) { - t.Fatalf("Result mismatch (-want +got):\n%s", cmp.Diff(got, expected, compareOpts...)) - } - - verifyState(t, expected, reg, procPrvdr, procMgr) - }) -} - -func TestLoader_Load_SkipUninitializedPlugins(t *testing.T) { - t.Run("Load duplicate plugin folders", func(t *testing.T) { - pluginDir1, err := filepath.Abs("../testdata/test-app") - if err != nil { - t.Errorf("could not construct absolute path of plugin dir") - return - } - pluginDir2, err := filepath.Abs("../testdata/valid-v2-signature") - if err != nil { - t.Errorf("could not construct absolute path of plugin dir") - return - } - expected := []*plugins.Plugin{ - { - JSONData: plugins.JSONData{ - ID: "test-app", - Type: plugins.TypeApp, - Name: "Test App", - Info: plugins.Info{ - Author: plugins.InfoLink{ - Name: "Test Inc.", - URL: "http://test.com", - }, - Description: "Official Grafana Test App & Dashboard bundle", - Version: "1.0.0", - Links: []plugins.InfoLink{ - {Name: "Project site", URL: "http://project.com"}, - {Name: "License & Terms", URL: "http://license.com"}, - }, - Logos: plugins.Logos{ - Small: "public/plugins/test-app/img/logo_small.png", - Large: "public/plugins/test-app/img/logo_large.png", - }, - Screenshots: []plugins.Screenshots{ - {Path: "public/plugins/test-app/img/screenshot1.png", Name: "img1"}, - {Path: "public/plugins/test-app/img/screenshot2.png", Name: "img2"}, - }, - Updated: "2015-02-10", - }, - Dependencies: plugins.Dependencies{ - GrafanaVersion: "3.x.x", - Plugins: []plugins.Dependency{ - {Type: "datasource", ID: "graphite", Name: "Graphite", Version: "1.0.0"}, - {Type: "panel", ID: "graph", Name: "Graph", Version: "1.0.0"}, - }, - }, - Includes: []*plugins.Includes{ - {Name: "Nginx Connections", Path: "dashboards/connections.json", Type: "dashboard", Role: org.RoleViewer, Slug: "nginx-connections"}, - {Name: "Nginx Memory", Path: "dashboards/memory.json", Type: "dashboard", Role: org.RoleViewer, Slug: "nginx-memory"}, - {Name: "Nginx Panel", Type: "panel", Role: org.RoleViewer, Slug: "nginx-panel"}, - {Name: "Nginx Datasource", Type: "datasource", Role: org.RoleViewer, Slug: "nginx-datasource"}, - }, - Backend: false, - }, - FS: mustNewStaticFSForTests(t, pluginDir1), - Class: plugins.ClassExternal, - Signature: plugins.SignatureStatusValid, - SignatureType: plugins.SignatureTypeGrafana, - SignatureOrg: "Grafana Labs", - Module: "plugins/test-app/module", - BaseURL: "public/plugins/test-app", - }, - } - - reg := fakes.NewFakePluginRegistry() - procPrvdr := fakes.NewFakeBackendProcessProvider() - // Cause an initialization error - procPrvdr.BackendFactoryFunc = func(ctx context.Context, p *plugins.Plugin) backendplugin.PluginFactoryFunc { - return func(pluginID string, _ log.Logger, _ []string) (backendplugin.Plugin, error) { - if pluginID == "test-datasource" { - return nil, errors.New("failed to initialize") - } - return &fakes.FakePluginClient{}, nil - } - } - procMgr := fakes.NewFakeProcessManager() - l := newLoader(t, &config.Cfg{}, func(l *Loader) { - l.pluginRegistry = reg - l.processManager = procMgr - l.pluginInitializer = initializer.New(&config.Cfg{}, procPrvdr, fakes.NewFakeLicensingService()) - }) - got, err := l.Load(context.Background(), &fakes.FakePluginSource{ - PluginClassFunc: func(ctx context.Context) plugins.Class { - return plugins.ClassExternal - }, - PluginURIsFunc: func(ctx context.Context) []string { - return []string{pluginDir1, pluginDir2} - }, - }) - require.NoError(t, err) - - if !cmp.Equal(got, expected, compareOpts...) { - t.Fatalf("Result mismatch (-want +got):\n%s", cmp.Diff(got, expected, compareOpts...)) - } - - verifyState(t, expected, reg, procPrvdr, procMgr) - }) -} - -func TestLoader_AngularClass(t *testing.T) { - for _, tc := range []struct { - name string - class plugins.Class - expAngularDetectionRun bool - }{ - { - name: "core plugin should skip angular detection", - class: plugins.ClassCore, - expAngularDetectionRun: false, - }, - { - name: "bundled plugin should skip angular detection", - class: plugins.ClassBundled, - expAngularDetectionRun: false, - }, - { - name: "external plugin should run angular detection", - class: plugins.ClassExternal, - expAngularDetectionRun: true, - }, - { - name: "other-class plugin should run angular detection", - class: "CDN", // (enterprise-only class) - expAngularDetectionRun: true, - }, - } { - t.Run(tc.name, func(t *testing.T) { - fakePluginSource := &fakes.FakePluginSource{ - PluginClassFunc: func(ctx context.Context) plugins.Class { - return tc.class - }, - PluginURIsFunc: func(ctx context.Context) []string { - return []string{"../testdata/valid-v2-signature"} - }, - } - l := newLoader(t, &config.Cfg{AngularSupportEnabled: true}, func(l *Loader) { - // So if angularDetected = true, it means that the detection has run - l.angularInspector = angularinspector.AlwaysAngularFakeInspector - }) - p, err := l.Load(context.Background(), fakePluginSource) - require.NoError(t, err) - require.Len(t, p, 1, "should load 1 plugin") - if tc.expAngularDetectionRun { - require.True(t, p[0].AngularDetected, "angular detection should run") - } else { - require.False(t, p[0].AngularDetected, "angular detection should not run") - } - }) - } -} - -func TestLoader_Load_Angular(t *testing.T) { - fakePluginSource := &fakes.FakePluginSource{ - PluginClassFunc: func(ctx context.Context) plugins.Class { - return plugins.ClassExternal - }, - PluginURIsFunc: func(ctx context.Context) []string { - return []string{"../testdata/valid-v2-signature"} - }, - } - for _, cfgTc := range []struct { - name string - cfg *config.Cfg - }{ - {name: "angular support enabled", cfg: &config.Cfg{AngularSupportEnabled: true}}, - {name: "angular support disabled", cfg: &config.Cfg{AngularSupportEnabled: false}}, - } { - t.Run(cfgTc.name, func(t *testing.T) { - for _, tc := range []struct { - name string - angularInspector angularinspector.Inspector - shouldLoad bool - }{ - { - name: "angular plugin", - angularInspector: angularinspector.AlwaysAngularFakeInspector, - // angular plugins should load only if allowed by the cfg - shouldLoad: cfgTc.cfg.AngularSupportEnabled, - }, - { - name: "non angular plugin", - angularInspector: angularinspector.NeverAngularFakeInspector, - // non-angular plugins should always load - shouldLoad: true, - }, - } { - t.Run(tc.name, func(t *testing.T) { - l := newLoader(t, cfgTc.cfg, func(l *Loader) { - l.angularInspector = tc.angularInspector - }) - p, err := l.Load(context.Background(), fakePluginSource) - require.NoError(t, err) - if tc.shouldLoad { - require.Len(t, p, 1, "plugin should have been loaded") - } else { - require.Empty(t, p, "plugin shouldn't have been loaded") - } - }) - } - }) - } -} - -func TestLoader_Load_NestedPlugins(t *testing.T) { - rootDir, err := filepath.Abs("../") - if err != nil { - t.Errorf("could not construct absolute path of root dir") - return - } - parent := &plugins.Plugin{ - JSONData: plugins.JSONData{ - ID: "test-datasource", - Type: plugins.TypeDataSource, - Name: "Parent", - Info: plugins.Info{ - Author: plugins.InfoLink{ - Name: "Grafana Labs", - URL: "http://grafana.com", - }, - Logos: plugins.Logos{ - Small: "public/img/icn-datasource.svg", - Large: "public/img/icn-datasource.svg", - }, - Description: "Parent plugin", - Version: "1.0.0", - Updated: "2020-10-20", - }, - Dependencies: plugins.Dependencies{ - GrafanaVersion: "*", - Plugins: []plugins.Dependency{}, - }, - Backend: true, - }, - Module: "plugins/test-datasource/module", - BaseURL: "public/plugins/test-datasource", - FS: mustNewStaticFSForTests(t, filepath.Join(rootDir, "testdata/nested-plugins/parent")), - Signature: plugins.SignatureStatusValid, - SignatureType: plugins.SignatureTypeGrafana, - SignatureOrg: "Grafana Labs", - Class: plugins.ClassExternal, - } - - child := &plugins.Plugin{ - JSONData: plugins.JSONData{ - ID: "test-panel", - Type: plugins.TypePanel, - Name: "Child", - Info: plugins.Info{ - Author: plugins.InfoLink{ - Name: "Grafana Labs", - URL: "http://grafana.com", - }, - Logos: plugins.Logos{ - Small: "public/img/icn-panel.svg", - Large: "public/img/icn-panel.svg", - }, - Description: "Child plugin", - Version: "1.0.1", - Updated: "2020-10-30", - }, - Dependencies: plugins.Dependencies{ - GrafanaVersion: "*", - Plugins: []plugins.Dependency{}, - }, - }, - Module: "plugins/test-panel/module", - BaseURL: "public/plugins/test-panel", - FS: mustNewStaticFSForTests(t, filepath.Join(rootDir, "testdata/nested-plugins/parent/nested")), - Signature: plugins.SignatureStatusValid, - SignatureType: plugins.SignatureTypeGrafana, - SignatureOrg: "Grafana Labs", - Class: plugins.ClassExternal, - } - - parent.Children = []*plugins.Plugin{child} - child.Parent = parent - - t.Run("Load nested External plugins", func(t *testing.T) { - procPrvdr := fakes.NewFakeBackendProcessProvider() - procMgr := fakes.NewFakeProcessManager() - l := newLoader(t, &config.Cfg{}, func(l *Loader) { - l.processManager = procMgr - l.pluginInitializer = initializer.New(&config.Cfg{}, procPrvdr, fakes.NewFakeLicensingService()) - l.discovery = discovery.New(l.cfg, discovery.Opts{ - FindFilterFuncs: []discovery.FindFilterFunc{ - func(ctx context.Context, class plugins.Class, bundles []*plugins.FoundBundle) ([]*plugins.FoundBundle, error) { - return discovery.NewDuplicatePluginFilterStep(l.pluginRegistry).Filter(ctx, bundles) - }, - }, - }, - ) - }) - - got, err := l.Load(context.Background(), &fakes.FakePluginSource{ - PluginClassFunc: func(ctx context.Context) plugins.Class { - return plugins.ClassExternal - }, - PluginURIsFunc: func(ctx context.Context) []string { - return []string{"../testdata/nested-plugins"} - }, - }) - require.NoError(t, err) - - // to ensure we can compare with expected - sort.SliceStable(got, func(i, j int) bool { - return got[i].ID < got[j].ID - }) - - expected := []*plugins.Plugin{parent, child} - if !cmp.Equal(got, expected, compareOpts...) { - t.Fatalf("Result mismatch (-want +got):\n%s", cmp.Diff(got, expected, compareOpts...)) - } - - verifyState(t, expected, l.pluginRegistry, procPrvdr, procMgr) - - t.Run("Load will exclude plugins that already exist", func(t *testing.T) { - got, err := l.Load(context.Background(), &fakes.FakePluginSource{ - PluginClassFunc: func(ctx context.Context) plugins.Class { - return plugins.ClassExternal - }, - PluginURIsFunc: func(ctx context.Context) []string { - return []string{"../testdata/nested-plugins"} - }, - }) - require.NoError(t, err) - - // to ensure we can compare with expected - sort.SliceStable(got, func(i, j int) bool { - return got[i].ID < got[j].ID - }) - - if !cmp.Equal(got, []*plugins.Plugin{}, compareOpts...) { - t.Fatalf("Result mismatch (-want +got):\n%s", cmp.Diff(got, expected, compareOpts...)) - } - - verifyState(t, expected, l.pluginRegistry, procPrvdr, procMgr) - }) - }) - - t.Run("Plugin child field `IncludedInAppID` is set to parent app's plugin ID", func(t *testing.T) { - parent := &plugins.Plugin{ - JSONData: plugins.JSONData{ - ID: "myorgid-simple-app", - Type: plugins.TypeApp, - Name: "Simple App", - Info: plugins.Info{ - Author: plugins.InfoLink{ - Name: "Your Name", - }, - Links: []plugins.InfoLink{ - {Name: "Website", URL: "https://github.com/grafana/grafana-starter-app"}, - {Name: "License", URL: "https://github.com/grafana/grafana-starter-app/blob/master/LICENSE"}, - }, - Logos: plugins.Logos{ - Small: "public/plugins/myorgid-simple-app/img/logo.svg", - Large: "public/plugins/myorgid-simple-app/img/logo.svg", - }, - Screenshots: []plugins.Screenshots{}, - Description: "Grafana App Plugin Template", - Version: "%VERSION%", - Updated: "%TODAY%", - }, - Dependencies: plugins.Dependencies{ - GrafanaVersion: "7.0.0", - GrafanaDependency: ">=7.0.0", - Plugins: []plugins.Dependency{}, - }, - Includes: []*plugins.Includes{ - { - Name: "Root Page (react)", - Path: "/a/myorgid-simple-app", - Type: "page", - Role: org.RoleViewer, - AddToNav: true, - DefaultNav: true, - Slug: "root-page-react", - }, - { - Name: "Root Page (Tab B)", - Path: "/a/myorgid-simple-app/?tab=b", - Type: "page", - Role: org.RoleViewer, - AddToNav: true, - Slug: "root-page-tab-b", - }, - { - Name: "React Config", - Path: "/plugins/myorgid-simple-app/?page=page2", - Type: "page", - Role: org.RoleAdmin, - AddToNav: true, - Slug: "react-config", - }, - { - Name: "Streaming Example", - Path: "dashboards/streaming.json", - Type: "dashboard", - Role: org.RoleViewer, - Slug: "streaming-example", - }, - { - Name: "Lots of Stats", - Path: "dashboards/stats.json", - Type: "dashboard", - Role: org.RoleViewer, - Slug: "lots-of-stats", - }, - }, - Backend: false, - }, - Module: "plugins/myorgid-simple-app/module", - BaseURL: "public/plugins/myorgid-simple-app", - FS: mustNewStaticFSForTests(t, filepath.Join(rootDir, "testdata/app-with-child/dist")), - DefaultNavURL: "/plugins/myorgid-simple-app/page/root-page-react", + pJSON := plugins.JSONData{ID: "test-datasource", Type: plugins.TypeDataSource, Info: plugins.Info{Version: "1.0.0"}} + p := &plugins.Plugin{ + JSONData: pJSON, Signature: plugins.SignatureStatusValid, - SignatureType: plugins.SignatureTypeGrafana, - SignatureOrg: "Grafana Labs", - Class: plugins.ClassExternal, + SignatureType: plugins.SignatureTypeCommunity, + FS: plugins.NewFakeFS(), } - child := &plugins.Plugin{ - JSONData: plugins.JSONData{ - ID: "myorgid-simple-panel", - Type: plugins.TypePanel, - Name: "Grafana Panel Plugin Template", - Info: plugins.Info{ - Author: plugins.InfoLink{ - Name: "Your Name", - }, - Links: []plugins.InfoLink{ - {Name: "Website", URL: "https://github.com/grafana/grafana-starter-panel"}, - {Name: "License", URL: "https://github.com/grafana/grafana-starter-panel/blob/master/LICENSE"}, - }, - Logos: plugins.Logos{ - Small: "public/plugins/myorgid-simple-panel/img/logo.svg", - Large: "public/plugins/myorgid-simple-panel/img/logo.svg", - }, - Screenshots: []plugins.Screenshots{}, - Description: "Grafana Panel Plugin Template", - Version: "%VERSION%", - Updated: "%TODAY%", - }, - Dependencies: plugins.Dependencies{ - GrafanaDependency: ">=7.0.0", - GrafanaVersion: "*", - Plugins: []plugins.Dependency{}, - }, - }, - Module: "plugins/myorgid-simple-app/child/module", - BaseURL: "public/plugins/myorgid-simple-app", - FS: mustNewStaticFSForTests(t, filepath.Join(rootDir, "testdata/app-with-child/dist/child")), - IncludedInAppID: parent.ID, - Signature: plugins.SignatureStatusValid, - SignatureType: plugins.SignatureTypeGrafana, - SignatureOrg: "Grafana Labs", - Class: plugins.ClassExternal, - } - - parent.Children = []*plugins.Plugin{child} - child.Parent = parent - expected := []*plugins.Plugin{parent, child} - - reg := fakes.NewFakePluginRegistry() - procPrvdr := fakes.NewFakeBackendProcessProvider() - procMgr := fakes.NewFakeProcessManager() - l := newLoader(t, &config.Cfg{}, func(l *Loader) { - l.pluginRegistry = reg - l.processManager = procMgr - l.pluginInitializer = initializer.New(&config.Cfg{}, procPrvdr, fakes.NewFakeLicensingService()) - }) - got, err := l.Load(context.Background(), &fakes.FakePluginSource{ - PluginClassFunc: func(ctx context.Context) plugins.Class { - return plugins.ClassExternal - }, - PluginURIsFunc: func(ctx context.Context) []string { - return []string{"../testdata/app-with-child"} - }, - }) + cfg := &config.Cfg{} + angularInspector, err := angularinspector.NewStaticInspector() require.NoError(t, err) - // to ensure we can compare with expected - sort.SliceStable(got, func(i, j int) bool { - return got[i].ID < got[j].ID - }) + var steps []string + l := New(cfg, signature.NewUnsignedAuthorizer(cfg), fakes.NewFakePluginRegistry(), fakes.NewFakeProcessManager(), + fakes.NewFakeRoleRegistry(), assetpath.ProvideService(pluginscdn.ProvideService(cfg)), + angularInspector, &fakes.FakeOauthService{}, + &fakes.FakeDiscoverer{ + DiscoverFunc: func(ctx context.Context, s plugins.PluginSource) ([]*plugins.FoundBundle, error) { + require.Equal(t, src, s) + steps = append(steps, "discover") + return []*plugins.FoundBundle{{Primary: plugins.FoundPlugin{JSONData: pJSON}}}, nil + }, + }, &fakes.FakeBootstrapper{ + BootstrapFunc: func(ctx context.Context, s plugins.PluginSource, b []*plugins.FoundBundle) ([]*plugins.Plugin, error) { + require.True(t, len(b) == 1) + require.Equal(t, b[0].Primary.JSONData, pJSON) + require.Equal(t, src, s) - if !cmp.Equal(got, expected, compareOpts...) { - t.Fatalf("Result mismatch (-want +got):\n%s", cmp.Diff(got, expected, compareOpts...)) - } + steps = append(steps, "bootstrap") + return []*plugins.Plugin{p}, nil + }, + }, &fakes.FakeInitializer{ + IntializeFunc: func(ctx context.Context, ps []*plugins.Plugin) ([]*plugins.Plugin, error) { + require.True(t, len(ps) == 1) + require.Equal(t, ps[0].JSONData, pJSON) + steps = append(steps, "initialize") + return ps, nil + }, + }) - verifyState(t, expected, reg, procPrvdr, procMgr) + got, err := l.Load(context.Background(), src) + require.NoError(t, err) + require.Equal(t, []*plugins.Plugin{p}, got) + require.Equal(t, []string{"discover", "bootstrap", "initialize"}, steps) + require.Zero(t, len(l.PluginErrors())) }) } -func newLoader(t *testing.T, cfg *config.Cfg, cbs ...func(loader *Loader)) *Loader { - angularInspector, err := angularinspector.NewStaticInspector() - reg := fakes.NewFakePluginRegistry() - assets := assetpath.ProvideService(pluginscdn.ProvideService(cfg)) - require.NoError(t, err) - l := New(cfg, &fakes.FakeLicensingService{}, signature.NewUnsignedAuthorizer(cfg), reg, - fakes.NewFakeBackendProcessProvider(), fakes.NewFakeProcessManager(), fakes.NewFakeRoleRegistry(), - assets, angularInspector, &fakes.FakeOauthService{}, - discovery.New(cfg, discovery.Opts{}), bootstrap.New(cfg, bootstrap.Opts{})) - - for _, cb := range cbs { - cb(l) - } - - return l -} - -func verifyState(t *testing.T, ps []*plugins.Plugin, reg registry.Service, - procPrvdr *fakes.FakeBackendProcessProvider, procMngr *fakes.FakeProcessManager) { - t.Helper() - - for _, p := range ps { - regP, exists := reg.Plugin(context.Background(), p.ID) - require.True(t, exists) - if !cmp.Equal(p, regP, compareOpts...) { - t.Fatalf("Result mismatch (-want +got):\n%s", cmp.Diff(p, regP, compareOpts...)) - } - - if p.Backend { - require.Equal(t, 1, procPrvdr.Requested[p.ID]) - require.Equal(t, 1, procPrvdr.Invoked[p.ID]) - } else { - require.Zero(t, procPrvdr.Requested[p.ID]) - require.Zero(t, procPrvdr.Invoked[p.ID]) - } - - require.Equal(t, 1, procMngr.Started[p.ID]) - require.Zero(t, procMngr.Stopped[p.ID]) - } -} - func mustNewStaticFSForTests(t *testing.T, dir string) plugins.FS { sfs, err := plugins.NewStaticFS(plugins.NewLocalFS(dir)) require.NoError(t, err) diff --git a/pkg/plugins/manager/manager_integration_test.go b/pkg/plugins/manager/manager_integration_test.go index 17fd7789919..65d0a8a28a1 100644 --- a/pkg/plugins/manager/manager_integration_test.go +++ b/pkg/plugins/manager/manager_integration_test.go @@ -124,11 +124,11 @@ func TestIntegrationPluginManager(t *testing.T) { discovery := pipeline.ProvideDiscoveryStage(pCfg, finder.NewLocalFinder(pCfg.DevMode), reg) bootstrap := pipeline.ProvideBootstrapStage(pCfg, signature.ProvideService(pCfg, statickey.New()), assetpath.ProvideService(pluginscdn.ProvideService(pCfg))) - - l := loader.ProvideService(pCfg, lic, signature.NewUnsignedAuthorizer(pCfg), - reg, provider.ProvideService(coreRegistry), fakes.NewFakeRoleRegistry(), + initialize := pipeline.ProvideInitializationStage(pCfg, reg, lic, provider.ProvideService(coreRegistry)) + l := loader.ProvideService(pCfg, signature.NewUnsignedAuthorizer(pCfg), + reg, fakes.NewFakeRoleRegistry(), assetpath.ProvideService(pluginscdn.ProvideService(pCfg)), - angularInspector, &fakes.FakeOauthService{}, discovery, bootstrap) + angularInspector, &fakes.FakeOauthService{}, discovery, bootstrap, initialize) srcs := sources.ProvideService(cfg, pCfg) ps, err := store.ProvideService(reg, srcs, l) require.NoError(t, err) diff --git a/pkg/plugins/manager/pipeline/bootstrap/bootstrap.go b/pkg/plugins/manager/pipeline/bootstrap/bootstrap.go index 67708a8a513..1be4c7fa2a5 100644 --- a/pkg/plugins/manager/pipeline/bootstrap/bootstrap.go +++ b/pkg/plugins/manager/pipeline/bootstrap/bootstrap.go @@ -47,7 +47,7 @@ func New(cfg *config.Cfg, opts Opts) *Bootstrap { opts.ConstructFunc = DefaultConstructFunc(signature.DefaultCalculator(cfg), assetpath.DefaultService(cfg)) } - if len(opts.DecorateFuncs) == 0 { + if opts.DecorateFuncs == nil { opts.DecorateFuncs = DefaultDecorateFuncs } diff --git a/pkg/plugins/manager/pipeline/discovery/discovery.go b/pkg/plugins/manager/pipeline/discovery/discovery.go index f24b2b5bc75..c187f25509d 100644 --- a/pkg/plugins/manager/pipeline/discovery/discovery.go +++ b/pkg/plugins/manager/pipeline/discovery/discovery.go @@ -45,7 +45,7 @@ func New(cfg *config.Cfg, opts Opts) *Discovery { opts.FindFunc = DefaultFindFunc(cfg) } - if len(opts.FindFilterFuncs) == 0 { + if opts.FindFilterFuncs == nil { opts.FindFilterFuncs = []FindFilterFunc{} // no filters by default } diff --git a/pkg/plugins/manager/pipeline/initialization/doc.go b/pkg/plugins/manager/pipeline/initialization/doc.go new file mode 100644 index 00000000000..8ed60725e99 --- /dev/null +++ b/pkg/plugins/manager/pipeline/initialization/doc.go @@ -0,0 +1,6 @@ +// Package initialization defines the fourth stage of the plugin loader pipeline. +// +// The Initialization stage must implement the Initializer interface. +// - Initialize(ctx context.Context, ps []*plugins.Plugin) ([]*plugins.Plugin, error) + +package initialization diff --git a/pkg/plugins/manager/pipeline/initialization/initialization.go b/pkg/plugins/manager/pipeline/initialization/initialization.go new file mode 100644 index 00000000000..5ca993e27c7 --- /dev/null +++ b/pkg/plugins/manager/pipeline/initialization/initialization.go @@ -0,0 +1,67 @@ +package initialization + +import ( + "context" + + "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/plugins/config" + "github.com/grafana/grafana/pkg/plugins/log" +) + +// Initializer is responsible for the Initialization stage of the plugin loader pipeline. +type Initializer interface { + Initialize(ctx context.Context, ps []*plugins.Plugin) ([]*plugins.Plugin, error) +} + +// InitializeFunc is the function used for the Initialize step of the Initialization stage. +type InitializeFunc func(ctx context.Context, p *plugins.Plugin) (*plugins.Plugin, error) + +type Initialize struct { + cfg *config.Cfg + initializeSteps []InitializeFunc + log log.Logger +} + +type Opts struct { + InitializeFuncs []InitializeFunc +} + +// New returns a new Initialization stage. +func New(cfg *config.Cfg, opts Opts) *Initialize { + if opts.InitializeFuncs == nil { + opts.InitializeFuncs = []InitializeFunc{} + } + + return &Initialize{ + cfg: cfg, + initializeSteps: opts.InitializeFuncs, + log: log.New("plugins.initialization"), + } +} + +// Initialize will execute the Initialize steps of the Initialization stage. +func (i *Initialize) Initialize(ctx context.Context, ps []*plugins.Plugin) ([]*plugins.Plugin, error) { + if len(i.initializeSteps) == 0 { + return ps, nil + } + + var err error + initializedPlugins := make([]*plugins.Plugin, 0, len(ps)) + for _, p := range ps { + var ip *plugins.Plugin + stepFailed := false + for _, init := range i.initializeSteps { + ip, err = init(ctx, p) + if err != nil { + stepFailed = true + i.log.Error("Could not initialize plugin", "pluginId", p.ID, "err", err) + break + } + } + if !stepFailed { + initializedPlugins = append(initializedPlugins, ip) + } + } + + return initializedPlugins, nil +} diff --git a/pkg/plugins/manager/pipeline/initialization/steps.go b/pkg/plugins/manager/pipeline/initialization/steps.go new file mode 100644 index 00000000000..3a683c47fbb --- /dev/null +++ b/pkg/plugins/manager/pipeline/initialization/steps.go @@ -0,0 +1,90 @@ +package initialization + +import ( + "context" + "errors" + + "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/plugins/envvars" + "github.com/grafana/grafana/pkg/plugins/log" + "github.com/grafana/grafana/pkg/plugins/manager/registry" +) + +// BackendClientInit implements an InitializeFunc for initializing a backend plugin process. +// +// It uses the envvars.Provider to retrieve the environment variables required for the plugin and the plugins.BackendFactoryProvider +// to get fetch backend factory, which is used to form a connection to the backend plugin process. +// +// Note: This step does not start the backend plugin process. +type BackendClientInit struct { + envVarProvider envvars.Provider + backendProvider plugins.BackendFactoryProvider + log log.Logger +} + +// NewBackendClientInitStep returns a new InitializeFunc for registering a backend plugin process. +func NewBackendClientInitStep(envVarProvider envvars.Provider, + backendProvider plugins.BackendFactoryProvider) InitializeFunc { + return newBackendProcessRegistration(envVarProvider, backendProvider).Initialize +} + +func newBackendProcessRegistration(envVarProvider envvars.Provider, + backendProvider plugins.BackendFactoryProvider) *BackendClientInit { + return &BackendClientInit{ + backendProvider: backendProvider, + envVarProvider: envVarProvider, + log: log.New("plugins.backend.registration"), + } +} + +// Initialize will initialize a backend plugin client, if the plugin is a backend plugin. +func (b *BackendClientInit) Initialize(ctx context.Context, p *plugins.Plugin) (*plugins.Plugin, error) { + if p.Backend { + backendFactory := b.backendProvider.BackendFactory(ctx, p) + if backendFactory == nil { + return nil, errors.New("could not find backend factory for plugin") + } + + env, err := b.envVarProvider.Get(ctx, p) + if err != nil { + return nil, err + } + if backendClient, err := backendFactory(p.ID, p.Logger(), env); err != nil { + return nil, err + } else { + p.RegisterClient(backendClient) + } + } + return p, nil +} + +// PluginRegistration implements an InitializeFunc for registering a plugin with the plugin registry. +type PluginRegistration struct { + pluginRegistry registry.Service + log log.Logger +} + +// NewPluginRegistrationStep returns a new InitializeFunc for registering a plugin with the plugin registry. +func NewPluginRegistrationStep(pluginRegistry registry.Service) InitializeFunc { + return newPluginRegistration(pluginRegistry).Initialize +} + +func newPluginRegistration(pluginRegistry registry.Service) *PluginRegistration { + return &PluginRegistration{ + pluginRegistry: pluginRegistry, + log: log.New("plugins.registration"), + } +} + +// Initialize registers the plugin with the plugin registry. +func (r *PluginRegistration) Initialize(ctx context.Context, p *plugins.Plugin) (*plugins.Plugin, error) { + if err := r.pluginRegistry.Add(ctx, p); err != nil { + r.log.Error("Could not register plugin", "pluginID", p.ID, "err", err) + return nil, errors.New("could not register plugin") + } + if !p.IsCorePlugin() { + r.log.Info("Plugin registered", "pluginID", p.ID) + } + + return p, nil +} diff --git a/pkg/plugins/manager/loader/initializer/initializer_test.go b/pkg/plugins/manager/pipeline/initialization/steps_test.go similarity index 65% rename from pkg/plugins/manager/loader/initializer/initializer_test.go rename to pkg/plugins/manager/pipeline/initialization/steps_test.go index f553d93949d..d20efdbe411 100644 --- a/pkg/plugins/manager/loader/initializer/initializer_test.go +++ b/pkg/plugins/manager/pipeline/initialization/steps_test.go @@ -1,10 +1,10 @@ -package initializer +package initialization import ( "context" "testing" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/backendplugin" @@ -28,19 +28,15 @@ func TestInitializer_Initialize(t *testing.T) { Class: plugins.ClassCore, } - i := &Initializer{ - backendProvider: &fakeBackendProvider{ - plugin: p, - }, - envVarProvider: &fakeEnvVarsProvider{}, - } + stepFunc := NewBackendClientInitStep(&fakeEnvVarsProvider{}, &fakeBackendProvider{plugin: p}) - err := i.Initialize(context.Background(), p) - assert.NoError(t, err) + var err error + p, err = stepFunc(context.Background(), p) + require.NoError(t, err) c, exists := p.Client() - assert.True(t, exists) - assert.NotNil(t, c) + require.True(t, exists) + require.NotNil(t, c) }) t.Run("renderer", func(t *testing.T) { @@ -56,19 +52,15 @@ func TestInitializer_Initialize(t *testing.T) { Class: plugins.ClassExternal, } - i := &Initializer{ - backendProvider: &fakeBackendProvider{ - plugin: p, - }, - envVarProvider: &fakeEnvVarsProvider{}, - } + stepFunc := NewBackendClientInitStep(&fakeEnvVarsProvider{}, &fakeBackendProvider{plugin: p}) - err := i.Initialize(context.Background(), p) - assert.NoError(t, err) + var err error + p, err = stepFunc(context.Background(), p) + require.NoError(t, err) c, exists := p.Client() - assert.True(t, exists) - assert.NotNil(t, c) + require.True(t, exists) + require.NotNil(t, c) }) t.Run("secretsmanager", func(t *testing.T) { @@ -84,19 +76,15 @@ func TestInitializer_Initialize(t *testing.T) { Class: plugins.ClassExternal, } - i := &Initializer{ - backendProvider: &fakeBackendProvider{ - plugin: p, - }, - envVarProvider: &fakeEnvVarsProvider{}, - } + stepFunc := NewBackendClientInitStep(&fakeEnvVarsProvider{}, &fakeBackendProvider{plugin: p}) - err := i.Initialize(context.Background(), p) - assert.NoError(t, err) + var err error + p, err = stepFunc(context.Background(), p) + require.NoError(t, err) c, exists := p.Client() - assert.True(t, exists) - assert.NotNil(t, c) + require.True(t, exists) + require.NotNil(t, c) }) t.Run("non backend plugin app", func(t *testing.T) { @@ -106,19 +94,17 @@ func TestInitializer_Initialize(t *testing.T) { }, } - i := &Initializer{ - backendProvider: &fakeBackendProvider{ - plugin: p, - }, - envVarProvider: &fakeEnvVarsProvider{}, - } + i := NewBackendClientInitStep(&fakeEnvVarsProvider{}, &fakeBackendProvider{ + plugin: p, + }) - err := i.Initialize(context.Background(), p) - assert.NoError(t, err) + var err error + p, err = i(context.Background(), p) + require.NoError(t, err) c, exists := p.Client() - assert.False(t, exists) - assert.Nil(t, c) + require.False(t, exists) + require.Nil(t, c) }) } diff --git a/pkg/services/pluginsintegration/loader/loader.go b/pkg/services/pluginsintegration/loader/loader.go new file mode 100644 index 00000000000..dd1bfdaa2d4 --- /dev/null +++ b/pkg/services/pluginsintegration/loader/loader.go @@ -0,0 +1,47 @@ +package loader + +import ( + "context" + + "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/plugins/config" + pluginsLoader "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/pipeline/bootstrap" + "github.com/grafana/grafana/pkg/plugins/manager/pipeline/discovery" + "github.com/grafana/grafana/pkg/plugins/manager/pipeline/initialization" + "github.com/grafana/grafana/pkg/plugins/manager/process" + "github.com/grafana/grafana/pkg/plugins/manager/registry" + "github.com/grafana/grafana/pkg/plugins/oauth" +) + +var _ plugins.ErrorResolver = (*Loader)(nil) +var _ pluginsLoader.Service = (*Loader)(nil) + +type Loader struct { + loader *pluginsLoader.Loader +} + +func ProvideService(cfg *config.Cfg, authorizer plugins.PluginLoaderAuthorizer, processManager process.Service, + pluginRegistry registry.Service, roleRegistry plugins.RoleRegistry, assetPath *assetpath.Service, + angularInspector angularinspector.Inspector, externalServiceRegistry oauth.ExternalServiceRegistry, + discovery discovery.Discoverer, bootstrap bootstrap.Bootstrapper, initializer initialization.Initializer, +) *Loader { + return &Loader{ + loader: pluginsLoader.New(cfg, authorizer, pluginRegistry, processManager, roleRegistry, assetPath, + angularInspector, externalServiceRegistry, discovery, bootstrap, initializer), + } +} + +func (l *Loader) Load(ctx context.Context, src plugins.PluginSource) ([]*plugins.Plugin, error) { + return l.loader.Load(ctx, src) +} + +func (l *Loader) Unload(ctx context.Context, pluginID string) error { + return l.loader.Unload(ctx, pluginID) +} + +func (l *Loader) PluginErrors() []*plugins.Error { + return l.loader.PluginErrors() +} diff --git a/pkg/services/pluginsintegration/loader/loader_test.go b/pkg/services/pluginsintegration/loader/loader_test.go new file mode 100644 index 00000000000..cad919a9939 --- /dev/null +++ b/pkg/services/pluginsintegration/loader/loader_test.go @@ -0,0 +1,1382 @@ +package loader + +import ( + "context" + "errors" + "path/filepath" + "sort" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/plugins/backendplugin" + "github.com/grafana/grafana/pkg/plugins/config" + "github.com/grafana/grafana/pkg/plugins/log" + "github.com/grafana/grafana/pkg/plugins/manager/fakes" + "github.com/grafana/grafana/pkg/plugins/manager/loader/angular/angularinspector" + "github.com/grafana/grafana/pkg/plugins/manager/loader/assetpath" + "github.com/grafana/grafana/pkg/plugins/manager/loader/finder" + "github.com/grafana/grafana/pkg/plugins/manager/pipeline/bootstrap" + "github.com/grafana/grafana/pkg/plugins/manager/pipeline/discovery" + "github.com/grafana/grafana/pkg/plugins/manager/pipeline/initialization" + "github.com/grafana/grafana/pkg/plugins/manager/process" + "github.com/grafana/grafana/pkg/plugins/manager/registry" + "github.com/grafana/grafana/pkg/plugins/manager/signature" + "github.com/grafana/grafana/pkg/plugins/manager/sources" + "github.com/grafana/grafana/pkg/plugins/pluginscdn" + "github.com/grafana/grafana/pkg/services/org" + "github.com/grafana/grafana/pkg/services/pluginsintegration/pipeline" + "github.com/grafana/grafana/pkg/setting" +) + +var compareOpts = []cmp.Option{cmpopts.IgnoreFields(plugins.Plugin{}, "client", "log"), fsComparer} + +var fsComparer = cmp.Comparer(func(fs1 plugins.FS, fs2 plugins.FS) bool { + fs1Files, err := fs1.Files() + if err != nil { + panic(err) + } + fs2Files, err := fs2.Files() + if err != nil { + panic(err) + } + + sort.SliceStable(fs1Files, func(i, j int) bool { + return fs1Files[i] < fs1Files[j] + }) + + sort.SliceStable(fs2Files, func(i, j int) bool { + return fs2Files[i] < fs2Files[j] + }) + + return cmp.Equal(fs1Files, fs2Files) && fs1.Base() == fs2.Base() +}) + +func TestLoader_Load(t *testing.T) { + tests := []struct { + name string + class plugins.Class + cfg *config.Cfg + pluginPaths []string + want []*plugins.Plugin + pluginErrors map[string]*plugins.Error + }{ + { + name: "Load a Core plugin", + class: plugins.ClassCore, + cfg: &config.Cfg{}, + pluginPaths: []string{filepath.Join(corePluginDir(t), "app/plugins/datasource/cloudwatch")}, + want: []*plugins.Plugin{ + { + JSONData: plugins.JSONData{ + ID: "cloudwatch", + Type: plugins.TypeDataSource, + Name: "CloudWatch", + Info: plugins.Info{ + Author: plugins.InfoLink{ + Name: "Grafana Labs", + URL: "https://grafana.com", + }, + Description: "Data source for Amazon AWS monitoring service", + Logos: plugins.Logos{ + Small: "public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png", + Large: "public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png", + }, + }, + Includes: []*plugins.Includes{ + {Name: "EC2", Path: "dashboards/ec2.json", Type: "dashboard", Role: "Viewer"}, + {Name: "EBS", Path: "dashboards/EBS.json", Type: "dashboard", Role: "Viewer"}, + {Name: "Lambda", Path: "dashboards/Lambda.json", Type: "dashboard", Role: "Viewer"}, + {Name: "Logs", Path: "dashboards/Logs.json", Type: "dashboard", Role: "Viewer"}, + {Name: "RDS", Path: "dashboards/RDS.json", Type: "dashboard", Role: "Viewer"}, + }, + Dependencies: plugins.Dependencies{ + GrafanaVersion: "*", + Plugins: []plugins.Dependency{}, + }, + Category: "cloud", + Annotations: true, + Metrics: true, + Alerting: true, + Logs: true, + Backend: true, + QueryOptions: map[string]bool{"minInterval": true}, + }, + Module: "app/plugins/datasource/cloudwatch/module", + BaseURL: "public/app/plugins/datasource/cloudwatch", + + FS: mustNewStaticFSForTests(t, filepath.Join(corePluginDir(t), "app/plugins/datasource/cloudwatch")), + Signature: plugins.SignatureStatusInternal, + Class: plugins.ClassCore, + }, + }, + }, + { + name: "Load a Bundled plugin", + class: plugins.ClassBundled, + cfg: &config.Cfg{}, + pluginPaths: []string{filepath.Join(testDataDir(t), "valid-v2-signature")}, + want: []*plugins.Plugin{ + { + JSONData: plugins.JSONData{ + ID: "test-datasource", + Type: plugins.TypeDataSource, + Name: "Test", + Info: plugins.Info{ + Author: plugins.InfoLink{ + Name: "Will Browne", + URL: "https://willbrowne.com", + }, + Version: "1.0.0", + Logos: plugins.Logos{ + Small: "public/img/icn-datasource.svg", + Large: "public/img/icn-datasource.svg", + }, + Description: "Test", + }, + Dependencies: plugins.Dependencies{ + GrafanaVersion: "*", + Plugins: []plugins.Dependency{}, + }, + Executable: "test", + Backend: true, + State: "alpha", + }, + Module: "plugins/test-datasource/module", + BaseURL: "public/plugins/test-datasource", + FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "valid-v2-signature/plugin/")), + Signature: "valid", + SignatureType: plugins.SignatureTypeGrafana, + SignatureOrg: "Grafana Labs", + Class: plugins.ClassBundled, + }, + }, + }, { + name: "Load plugin with symbolic links", + class: plugins.ClassExternal, + cfg: &config.Cfg{}, + pluginPaths: []string{filepath.Join(testDataDir(t), "symbolic-plugin-dirs")}, + 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", + }, + Logos: plugins.Logos{ + Small: "public/plugins/test-app/img/logo_small.png", + Large: "public/plugins/test-app/img/logo_large.png", + }, + Links: []plugins.InfoLink{ + {Name: "Project site", URL: "http://project.com"}, + {Name: "License & Terms", URL: "http://license.com"}, + }, + Description: "Official Grafana Test App & Dashboard bundle", + Screenshots: []plugins.Screenshots{ + {Path: "public/plugins/test-app/img/screenshot1.png", Name: "img1"}, + {Path: "public/plugins/test-app/img/screenshot2.png", Name: "img2"}, + }, + Version: "1.0.0", + Updated: "2015-02-10", + }, + Dependencies: plugins.Dependencies{ + GrafanaVersion: "3.x.x", + Plugins: []plugins.Dependency{ + {Type: "datasource", ID: "graphite", Name: "Graphite", Version: "1.0.0"}, + {Type: "panel", ID: "graph", Name: "Graph", Version: "1.0.0"}, + }, + }, + Includes: []*plugins.Includes{ + { + Name: "Nginx Connections", + Path: "dashboards/connections.json", + Type: "dashboard", + Role: org.RoleViewer, + Slug: "nginx-connections", + }, + { + Name: "Nginx Memory", + Path: "dashboards/memory.json", + Type: "dashboard", + Role: org.RoleViewer, + Slug: "nginx-memory", + }, + { + Name: "Nginx Panel", + Type: string(plugins.TypePanel), + Role: org.RoleViewer, + Slug: "nginx-panel"}, + { + Name: "Nginx Datasource", + Type: string(plugins.TypeDataSource), + Role: org.RoleViewer, + Slug: "nginx-datasource", + }, + }, + }, + Class: plugins.ClassExternal, + Module: "plugins/test-app/module", + BaseURL: "public/plugins/test-app", + FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "includes-symlinks")), + Signature: "valid", + SignatureType: plugins.SignatureTypeGrafana, + SignatureOrg: "Grafana Labs", + }, + }, + }, { + name: "Load an unsigned plugin (development)", + class: plugins.ClassExternal, + cfg: &config.Cfg{ + DevMode: true, + }, + pluginPaths: []string{filepath.Join(testDataDir(t), "unsigned-datasource")}, + want: []*plugins.Plugin{ + { + JSONData: plugins.JSONData{ + ID: "test-datasource", + Type: plugins.TypeDataSource, + Name: "Test", + Info: plugins.Info{ + Author: plugins.InfoLink{ + Name: "Grafana Labs", + URL: "https://grafana.com", + }, + Logos: plugins.Logos{ + Small: "public/img/icn-datasource.svg", + Large: "public/img/icn-datasource.svg", + }, + Description: "Test", + }, + Dependencies: plugins.Dependencies{ + GrafanaVersion: "*", + Plugins: []plugins.Dependency{}, + }, + Backend: true, + State: plugins.ReleaseStateAlpha, + }, + Class: plugins.ClassExternal, + Module: "plugins/test-datasource/module", + BaseURL: "public/plugins/test-datasource", + FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "unsigned-datasource/plugin")), + Signature: "unsigned", + }, + }, + }, { + name: "Load an unsigned plugin (production)", + class: plugins.ClassExternal, + cfg: &config.Cfg{}, + pluginPaths: []string{filepath.Join(testDataDir(t), "unsigned-datasource")}, + want: []*plugins.Plugin{}, + pluginErrors: map[string]*plugins.Error{ + "test-datasource": { + PluginID: "test-datasource", + ErrorCode: "signatureMissing", + }, + }, + }, + { + name: "Load an unsigned plugin using PluginsAllowUnsigned config (production)", + class: plugins.ClassExternal, + cfg: &config.Cfg{ + PluginsAllowUnsigned: []string{"test-datasource"}, + }, + pluginPaths: []string{filepath.Join(testDataDir(t), "unsigned-datasource")}, + want: []*plugins.Plugin{ + { + JSONData: plugins.JSONData{ + ID: "test-datasource", + Type: plugins.TypeDataSource, + Name: "Test", + Info: plugins.Info{ + Author: plugins.InfoLink{ + Name: "Grafana Labs", + URL: "https://grafana.com", + }, + Logos: plugins.Logos{ + Small: "public/img/icn-datasource.svg", + Large: "public/img/icn-datasource.svg", + }, + Description: "Test", + }, + Dependencies: plugins.Dependencies{ + GrafanaVersion: "*", + Plugins: []plugins.Dependency{}, + }, + Backend: true, + State: plugins.ReleaseStateAlpha, + }, + Class: plugins.ClassExternal, + Module: "plugins/test-datasource/module", + BaseURL: "public/plugins/test-datasource", + FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "unsigned-datasource/plugin")), + Signature: plugins.SignatureStatusUnsigned, + }, + }, + }, + { + name: "Load a plugin with v1 manifest should return signatureInvalid", + class: plugins.ClassExternal, + cfg: &config.Cfg{}, + pluginPaths: []string{filepath.Join(testDataDir(t), "lacking-files")}, + want: []*plugins.Plugin{}, + pluginErrors: map[string]*plugins.Error{ + "test-datasource": { + PluginID: "test-datasource", + ErrorCode: "signatureInvalid", + }, + }, + }, + { + name: "Load a plugin with v1 manifest using PluginsAllowUnsigned config (production) should return signatureInvalid", + class: plugins.ClassExternal, + cfg: &config.Cfg{ + PluginsAllowUnsigned: []string{"test-datasource"}, + }, + pluginPaths: []string{filepath.Join(testDataDir(t), "lacking-files")}, + want: []*plugins.Plugin{}, + pluginErrors: map[string]*plugins.Error{ + "test-datasource": { + PluginID: "test-datasource", + ErrorCode: "signatureInvalid", + }, + }, + }, + { + name: "Load a plugin with manifest which has a file not found in plugin folder", + class: plugins.ClassExternal, + cfg: &config.Cfg{ + PluginsAllowUnsigned: []string{"test-datasource"}, + }, + pluginPaths: []string{filepath.Join(testDataDir(t), "invalid-v2-missing-file")}, + want: []*plugins.Plugin{}, + pluginErrors: map[string]*plugins.Error{ + "test-datasource": { + PluginID: "test-datasource", + ErrorCode: "signatureModified", + }, + }, + }, + { + name: "Load a plugin with file which is missing from the manifest", + class: plugins.ClassExternal, + cfg: &config.Cfg{ + PluginsAllowUnsigned: []string{"test-datasource"}, + }, + pluginPaths: []string{filepath.Join(testDataDir(t), "invalid-v2-extra-file")}, + want: []*plugins.Plugin{}, + pluginErrors: map[string]*plugins.Error{ + "test-datasource": { + PluginID: "test-datasource", + ErrorCode: "signatureModified", + }, + }, + }, + { + name: "Load an app with includes", + class: plugins.ClassExternal, + cfg: &config.Cfg{ + PluginsAllowUnsigned: []string{"test-app"}, + }, + pluginPaths: []string{filepath.Join(testDataDir(t), "test-app-with-includes")}, + want: []*plugins.Plugin{ + {JSONData: plugins.JSONData{ + ID: "test-app", + Type: plugins.TypeApp, + Name: "Test App", + Info: plugins.Info{ + Author: plugins.InfoLink{ + Name: "Test Inc.", + URL: "http://test.com", + }, + Description: "Official Grafana Test App & Dashboard bundle", + Version: "1.0.0", + Links: []plugins.InfoLink{ + {Name: "Project site", URL: "http://project.com"}, + {Name: "License & Terms", URL: "http://license.com"}, + }, + 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: org.RoleViewer, Slug: "nginx-memory"}, + {Name: "Root Page (react)", Type: "page", Role: org.RoleViewer, Path: "/a/my-simple-app", DefaultNav: true, AddToNav: true, Slug: "root-page-react"}, + }, + Backend: false, + }, + DefaultNavURL: "/plugins/test-app/page/root-page-react", + FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "test-app-with-includes")), + Class: plugins.ClassExternal, + Signature: plugins.SignatureStatusUnsigned, + Module: "plugins/test-app/module", + BaseURL: "public/plugins/test-app", + }, + }, + }, + } + for _, tt := range tests { + reg := fakes.NewFakePluginRegistry() + procPrvdr := fakes.NewFakeBackendProcessProvider() + procMgr := fakes.NewFakeProcessManager() + l := newLoader(t, tt.cfg, reg, procMgr, procPrvdr) + + t.Run(tt.name, func(t *testing.T) { + got, err := l.Load(context.Background(), sources.NewLocalSource(tt.class, tt.pluginPaths)) + 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...)) + } + + pluginErrs := l.PluginErrors() + require.Equal(t, len(tt.pluginErrors), len(pluginErrs)) + for _, pluginErr := range pluginErrs { + require.Equal(t, tt.pluginErrors[pluginErr.PluginID], pluginErr) + } + + verifyState(t, tt.want, reg, procPrvdr, procMgr) + }) + } +} + +func TestLoader_Load_CustomSource(t *testing.T) { + t.Run("Load a plugin", func(t *testing.T) { + cfg := &config.Cfg{ + PluginsCDNURLTemplate: "https://cdn.example.com", + PluginSettings: setting.PluginSettings{ + "grafana-worldmap-panel": {"cdn": "true"}, + }, + } + + pluginPaths := []string{filepath.Join(testDataDir(t), "cdn")} + expected := []*plugins.Plugin{{ + JSONData: plugins.JSONData{ + ID: "grafana-worldmap-panel", + Type: plugins.TypePanel, + Name: "Worldmap Panel", + Info: plugins.Info{ + Version: "0.3.3", + Links: []plugins.InfoLink{ + {Name: "Project site", URL: "https://github.com/grafana/worldmap-panel"}, + {Name: "MIT License", URL: "https://github.com/grafana/worldmap-panel/blob/master/LICENSE"}, + }, + Logos: plugins.Logos{ + // Path substitution + Small: "https://cdn.example.com/grafana-worldmap-panel/0.3.3/public/plugins/grafana-worldmap-panel/images/worldmap_logo.svg", + Large: "https://cdn.example.com/grafana-worldmap-panel/0.3.3/public/plugins/grafana-worldmap-panel/images/worldmap_logo.svg", + }, + Screenshots: []plugins.Screenshots{ + { + Name: "World", + Path: "https://cdn.example.com/grafana-worldmap-panel/0.3.3/public/plugins/grafana-worldmap-panel/images/worldmap-world.png", + }, + { + Name: "USA", + Path: "https://cdn.example.com/grafana-worldmap-panel/0.3.3/public/plugins/grafana-worldmap-panel/images/worldmap-usa.png", + }, + { + Name: "Light Theme", + Path: "https://cdn.example.com/grafana-worldmap-panel/0.3.3/public/plugins/grafana-worldmap-panel/images/worldmap-light-theme.png", + }, + }, + }, + Dependencies: plugins.Dependencies{ + GrafanaVersion: "3.x.x", + Plugins: []plugins.Dependency{}, + }, + }, + FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "cdn/plugin")), + Class: plugins.ClassBundled, + Signature: plugins.SignatureStatusValid, + BaseURL: "plugin-cdn/grafana-worldmap-panel/0.3.3/public/plugins/grafana-worldmap-panel", + Module: "plugin-cdn/grafana-worldmap-panel/0.3.3/public/plugins/grafana-worldmap-panel/module", + }} + + l := newLoader(t, cfg, fakes.NewFakePluginRegistry(), fakes.NewFakeProcessManager(), fakes.NewFakeBackendProcessProvider()) + got, err := l.Load(context.Background(), &fakes.FakePluginSource{ + PluginClassFunc: func(ctx context.Context) plugins.Class { + return plugins.ClassBundled + }, + PluginURIsFunc: func(ctx context.Context) []string { + return pluginPaths + }, + DefaultSignatureFunc: func(ctx context.Context) (plugins.Signature, bool) { + return plugins.Signature{ + Status: plugins.SignatureStatusValid, + }, true + }, + }) + + require.NoError(t, err) + if !cmp.Equal(got, expected, compareOpts...) { + t.Fatalf("Result mismatch (-want +got):\n%s", cmp.Diff(got, expected, compareOpts...)) + } + }) +} + +func TestLoader_Load_MultiplePlugins(t *testing.T) { + t.Run("Load multiple", func(t *testing.T) { + tests := []struct { + name string + cfg *config.Cfg + pluginPaths []string + existingPlugins map[string]struct{} + want []*plugins.Plugin + pluginErrors map[string]*plugins.Error + }{ + { + name: "Load multiple plugins (broken, valid, unsigned)", + cfg: &config.Cfg{ + GrafanaAppURL: "http://localhost:3000", + }, + pluginPaths: []string{ + filepath.Join(testDataDir(t), "invalid-plugin-json"), // test-app + filepath.Join(testDataDir(t), "valid-v2-pvt-signature"), // test + filepath.Join(testDataDir(t), "unsigned-panel"), // test-panel + }, + want: []*plugins.Plugin{ + { + JSONData: plugins.JSONData{ + ID: "test-datasource", + Type: plugins.TypeDataSource, + Name: "Test", + Info: plugins.Info{ + Author: plugins.InfoLink{ + Name: "Will Browne", + URL: "https://willbrowne.com", + }, + Logos: plugins.Logos{ + Small: "public/img/icn-datasource.svg", + Large: "public/img/icn-datasource.svg", + }, + Description: "Test", + Version: "1.0.0", + }, + Dependencies: plugins.Dependencies{ + GrafanaVersion: "*", + Plugins: []plugins.Dependency{}, + }, + Backend: true, + Executable: "test", + State: plugins.ReleaseStateAlpha, + }, + Class: plugins.ClassExternal, + Module: "plugins/test-datasource/module", + BaseURL: "public/plugins/test-datasource", + FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "valid-v2-pvt-signature/plugin")), + Signature: "valid", + SignatureType: plugins.SignatureTypePrivate, + SignatureOrg: "Will Browne", + }, + }, + pluginErrors: map[string]*plugins.Error{ + "test-panel": { + PluginID: "test-panel", + ErrorCode: "signatureMissing", + }, + }, + }, + } + + for _, tt := range tests { + reg := fakes.NewFakePluginRegistry() + procPrvdr := fakes.NewFakeBackendProcessProvider() + procMgr := fakes.NewFakeProcessManager() + l := newLoader(t, tt.cfg, reg, procMgr, procPrvdr) + t.Run(tt.name, func(t *testing.T) { + got, err := l.Load(context.Background(), &fakes.FakePluginSource{ + PluginClassFunc: func(ctx context.Context) plugins.Class { + return plugins.ClassExternal + }, + PluginURIsFunc: func(ctx context.Context) []string { + return tt.pluginPaths + }, + }) + require.NoError(t, err) + sort.SliceStable(got, func(i, j int) bool { + return got[i].ID < got[j].ID + }) + if !cmp.Equal(got, tt.want, compareOpts...) { + t.Fatalf("Result mismatch (-want +got):\n%s", cmp.Diff(got, tt.want, compareOpts...)) + } + pluginErrs := l.PluginErrors() + require.Equal(t, len(tt.pluginErrors), len(pluginErrs)) + for _, pluginErr := range pluginErrs { + require.Equal(t, tt.pluginErrors[pluginErr.PluginID], pluginErr) + } + verifyState(t, tt.want, reg, procPrvdr, procMgr) + }) + } + }) +} + +func TestLoader_Load_RBACReady(t *testing.T) { + tests := []struct { + name string + cfg *config.Cfg + pluginPaths []string + existingPlugins map[string]struct{} + want []*plugins.Plugin + }{ + { + name: "Load plugin defining one RBAC role", + cfg: &config.Cfg{ + GrafanaAppURL: "http://localhost:3000", + }, + pluginPaths: []string{filepath.Join(testDataDir(t), "test-app-with-roles")}, + want: []*plugins.Plugin{ + { + JSONData: plugins.JSONData{ + ID: "test-app", + Type: plugins.TypeApp, + Name: "Test App", + Info: plugins.Info{ + Author: plugins.InfoLink{ + Name: "Test Inc.", + URL: "http://test.com", + }, + Description: "Test App", + Version: "1.0.0", + Links: []plugins.InfoLink{}, + Logos: plugins.Logos{ + Small: "public/img/icn-app.svg", + Large: "public/img/icn-app.svg", + }, + Updated: "2015-02-10", + }, + Dependencies: plugins.Dependencies{ + GrafanaVersion: "*", + GrafanaDependency: ">=8.0.0", + Plugins: []plugins.Dependency{}, + }, + Includes: []*plugins.Includes{}, + Roles: []plugins.RoleRegistration{ + { + Role: plugins.Role{ + Name: "Reader", + Description: "View everything in the test-app plugin", + Permissions: []plugins.Permission{ + {Action: "plugins.app:access", Scope: "plugins.app:id:test-app"}, + {Action: "test-app.resource:read", Scope: "resources:*"}, + {Action: "test-app.otherresource:toggle"}, + }, + }, + Grants: []string{"Admin"}, + }, + }, + Backend: false, + }, + FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "test-app-with-roles")), + Class: plugins.ClassExternal, + Signature: plugins.SignatureStatusValid, + SignatureType: plugins.SignatureTypePrivate, + SignatureOrg: "gabrielmabille", + Module: "plugins/test-app/module", + BaseURL: "public/plugins/test-app", + }, + }, + }, + } + + for _, tt := range tests { + reg := fakes.NewFakePluginRegistry() + procPrvdr := fakes.NewFakeBackendProcessProvider() + procMgr := fakes.NewFakeProcessManager() + l := newLoader(t, tt.cfg, reg, procMgr, procPrvdr) + + got, err := l.Load(context.Background(), &fakes.FakePluginSource{ + PluginClassFunc: func(ctx context.Context) plugins.Class { + return plugins.ClassExternal + }, + PluginURIsFunc: func(ctx context.Context) []string { + return tt.pluginPaths + }, + }) + 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...)) + } + pluginErrs := l.PluginErrors() + require.Len(t, pluginErrs, 0) + + verifyState(t, tt.want, reg, procPrvdr, procMgr) + } +} + +func TestLoader_Load_Signature_RootURL(t *testing.T) { + t.Run("Private signature verification ignores trailing slash in root URL", func(t *testing.T) { + const defaultAppURL = "http://localhost:3000/grafana" + + expected := []*plugins.Plugin{ + { + JSONData: plugins.JSONData{ + ID: "test-datasource", + Type: plugins.TypeDataSource, + Name: "Test", + Info: plugins.Info{ + Author: plugins.InfoLink{Name: "Will Browne", URL: "https://willbrowne.com"}, + Description: "Test", + Logos: plugins.Logos{ + Small: "public/img/icn-datasource.svg", + Large: "public/img/icn-datasource.svg", + }, + Version: "1.0.0", + }, + State: plugins.ReleaseStateAlpha, + Dependencies: plugins.Dependencies{GrafanaVersion: "*", Plugins: []plugins.Dependency{}}, + Backend: true, + Executable: "test", + }, + FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "valid-v2-pvt-signature-root-url-uri/plugin")), + Class: plugins.ClassExternal, + Signature: plugins.SignatureStatusValid, + SignatureType: plugins.SignatureTypePrivate, + SignatureOrg: "Will Browne", + Module: "plugins/test-datasource/module", + BaseURL: "public/plugins/test-datasource", + }, + } + + reg := fakes.NewFakePluginRegistry() + procPrvdr := fakes.NewFakeBackendProcessProvider() + procMgr := fakes.NewFakeProcessManager() + cfg := &config.Cfg{GrafanaAppURL: defaultAppURL} + l := newLoader(t, cfg, reg, procMgr, procPrvdr) + got, err := l.Load(context.Background(), &fakes.FakePluginSource{ + PluginClassFunc: func(ctx context.Context) plugins.Class { + return plugins.ClassExternal + }, + PluginURIsFunc: func(ctx context.Context) []string { + return []string{filepath.Join(testDataDir(t), "valid-v2-pvt-signature-root-url-uri")} + }, + }) + require.NoError(t, err) + + if !cmp.Equal(got, expected, compareOpts...) { + t.Fatalf("Result mismatch (-want +got):\n%s", cmp.Diff(got, expected, compareOpts...)) + } + verifyState(t, expected, reg, procPrvdr, procMgr) + }) +} + +func TestLoader_Load_DuplicatePlugins(t *testing.T) { + t.Run("Load duplicate plugin folders", func(t *testing.T) { + expected := []*plugins.Plugin{ + { + JSONData: plugins.JSONData{ + ID: "test-app", + Type: plugins.TypeApp, + Name: "Test App", + Info: plugins.Info{ + Author: plugins.InfoLink{ + Name: "Test Inc.", + URL: "http://test.com", + }, + Description: "Official Grafana Test App & Dashboard bundle", + Version: "1.0.0", + Links: []plugins.InfoLink{ + {Name: "Project site", URL: "http://project.com"}, + {Name: "License & Terms", URL: "http://license.com"}, + }, + Logos: plugins.Logos{ + Small: "public/plugins/test-app/img/logo_small.png", + Large: "public/plugins/test-app/img/logo_large.png", + }, + Screenshots: []plugins.Screenshots{ + {Path: "public/plugins/test-app/img/screenshot1.png", Name: "img1"}, + {Path: "public/plugins/test-app/img/screenshot2.png", Name: "img2"}, + }, + Updated: "2015-02-10", + }, + Dependencies: plugins.Dependencies{ + GrafanaVersion: "3.x.x", + Plugins: []plugins.Dependency{ + {Type: "datasource", ID: "graphite", Name: "Graphite", Version: "1.0.0"}, + {Type: "panel", ID: "graph", Name: "Graph", Version: "1.0.0"}, + }, + }, + Includes: []*plugins.Includes{ + {Name: "Nginx Connections", Path: "dashboards/connections.json", Type: "dashboard", Role: org.RoleViewer, Slug: "nginx-connections"}, + {Name: "Nginx Memory", Path: "dashboards/memory.json", Type: "dashboard", Role: org.RoleViewer, Slug: "nginx-memory"}, + {Name: "Nginx Panel", Type: "panel", Role: org.RoleViewer, Slug: "nginx-panel"}, + {Name: "Nginx Datasource", Type: "datasource", Role: org.RoleViewer, Slug: "nginx-datasource"}, + }, + Backend: false, + }, + FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "test-app")), + Class: plugins.ClassExternal, + Signature: plugins.SignatureStatusValid, + SignatureType: plugins.SignatureTypeGrafana, + SignatureOrg: "Grafana Labs", + Module: "plugins/test-app/module", + BaseURL: "public/plugins/test-app", + }, + } + + reg := fakes.NewFakePluginRegistry() + procPrvdr := fakes.NewFakeBackendProcessProvider() + procMgr := fakes.NewFakeProcessManager() + cfg := &config.Cfg{} + l := newLoader(t, cfg, reg, procMgr, procPrvdr) + got, err := l.Load(context.Background(), &fakes.FakePluginSource{ + PluginClassFunc: func(ctx context.Context) plugins.Class { + return plugins.ClassExternal + }, + PluginURIsFunc: func(ctx context.Context) []string { + return []string{filepath.Join(testDataDir(t), "test-app"), filepath.Join(testDataDir(t), "test-app")} + }, + }) + require.NoError(t, err) + + if !cmp.Equal(got, expected, compareOpts...) { + t.Fatalf("Result mismatch (-want +got):\n%s", cmp.Diff(got, expected, compareOpts...)) + } + + verifyState(t, expected, reg, procPrvdr, procMgr) + }) +} + +func TestLoader_Load_SkipUninitializedPlugins(t *testing.T) { + t.Run("Load duplicate plugin folders", func(t *testing.T) { + pluginDir1 := filepath.Join(testDataDir(t), "test-app") + pluginDir2 := filepath.Join(testDataDir(t), "valid-v2-signature") + + expected := []*plugins.Plugin{ + { + JSONData: plugins.JSONData{ + ID: "test-app", + Type: plugins.TypeApp, + Name: "Test App", + Info: plugins.Info{ + Author: plugins.InfoLink{ + Name: "Test Inc.", + URL: "http://test.com", + }, + Description: "Official Grafana Test App & Dashboard bundle", + Version: "1.0.0", + Links: []plugins.InfoLink{ + {Name: "Project site", URL: "http://project.com"}, + {Name: "License & Terms", URL: "http://license.com"}, + }, + Logos: plugins.Logos{ + Small: "public/plugins/test-app/img/logo_small.png", + Large: "public/plugins/test-app/img/logo_large.png", + }, + Screenshots: []plugins.Screenshots{ + {Path: "public/plugins/test-app/img/screenshot1.png", Name: "img1"}, + {Path: "public/plugins/test-app/img/screenshot2.png", Name: "img2"}, + }, + Updated: "2015-02-10", + }, + Dependencies: plugins.Dependencies{ + GrafanaVersion: "3.x.x", + Plugins: []plugins.Dependency{ + {Type: "datasource", ID: "graphite", Name: "Graphite", Version: "1.0.0"}, + {Type: "panel", ID: "graph", Name: "Graph", Version: "1.0.0"}, + }, + }, + Includes: []*plugins.Includes{ + {Name: "Nginx Connections", Path: "dashboards/connections.json", Type: "dashboard", Role: org.RoleViewer, Slug: "nginx-connections"}, + {Name: "Nginx Memory", Path: "dashboards/memory.json", Type: "dashboard", Role: org.RoleViewer, Slug: "nginx-memory"}, + {Name: "Nginx Panel", Type: "panel", Role: org.RoleViewer, Slug: "nginx-panel"}, + {Name: "Nginx Datasource", Type: "datasource", Role: org.RoleViewer, Slug: "nginx-datasource"}, + }, + Backend: false, + }, + FS: mustNewStaticFSForTests(t, pluginDir1), + Class: plugins.ClassExternal, + Signature: plugins.SignatureStatusValid, + SignatureType: plugins.SignatureTypeGrafana, + SignatureOrg: "Grafana Labs", + Module: "plugins/test-app/module", + BaseURL: "public/plugins/test-app", + }, + } + + reg := fakes.NewFakePluginRegistry() + procPrvdr := fakes.NewFakeBackendProcessProvider() + // Cause an initialization error + procPrvdr.BackendFactoryFunc = func(ctx context.Context, p *plugins.Plugin) backendplugin.PluginFactoryFunc { + return func(pluginID string, _ log.Logger, _ []string) (backendplugin.Plugin, error) { + if pluginID == "test-datasource" { + return nil, errors.New("failed to initialize") + } + return &fakes.FakePluginClient{}, nil + } + } + procMgr := fakes.NewFakeProcessManager() + cfg := &config.Cfg{} + l := newLoader(t, cfg, reg, procMgr, procPrvdr) + got, err := l.Load(context.Background(), &fakes.FakePluginSource{ + PluginClassFunc: func(ctx context.Context) plugins.Class { + return plugins.ClassExternal + }, + PluginURIsFunc: func(ctx context.Context) []string { + return []string{pluginDir1, pluginDir2} + }, + }) + require.NoError(t, err) + + if !cmp.Equal(got, expected, compareOpts...) { + t.Fatalf("Result mismatch (-want +got):\n%s", cmp.Diff(got, expected, compareOpts...)) + } + + verifyState(t, expected, reg, procPrvdr, procMgr) + }) +} + +func TestLoader_AngularClass(t *testing.T) { + for _, tc := range []struct { + name string + class plugins.Class + expAngularDetectionRun bool + }{ + { + name: "core plugin should skip angular detection", + class: plugins.ClassCore, + expAngularDetectionRun: false, + }, + { + name: "bundled plugin should skip angular detection", + class: plugins.ClassBundled, + expAngularDetectionRun: false, + }, + { + name: "external plugin should run angular detection", + class: plugins.ClassExternal, + expAngularDetectionRun: true, + }, + { + name: "other-class plugin should run angular detection", + class: "CDN", // (enterprise-only class) + expAngularDetectionRun: true, + }, + } { + t.Run(tc.name, func(t *testing.T) { + fakePluginSource := &fakes.FakePluginSource{ + PluginClassFunc: func(ctx context.Context) plugins.Class { + return tc.class + }, + PluginURIsFunc: func(ctx context.Context) []string { + return []string{filepath.Join(testDataDir(t), "valid-v2-signature")} + }, + } + // if angularDetected = true, it means that the detection has run + l := newLoaderWithAngularInspector(&config.Cfg{AngularSupportEnabled: true}, angularinspector.AlwaysAngularFakeInspector) + p, err := l.Load(context.Background(), fakePluginSource) + require.NoError(t, err) + require.Len(t, p, 1, "should load 1 plugin") + if tc.expAngularDetectionRun { + require.True(t, p[0].AngularDetected, "angular detection should run") + } else { + require.False(t, p[0].AngularDetected, "angular detection should not run") + } + }) + } +} + +func TestLoader_Load_Angular(t *testing.T) { + fakePluginSource := &fakes.FakePluginSource{ + PluginClassFunc: func(ctx context.Context) plugins.Class { + return plugins.ClassExternal + }, + PluginURIsFunc: func(ctx context.Context) []string { + return []string{filepath.Join(testDataDir(t), "valid-v2-signature")} + }, + } + for _, cfgTc := range []struct { + name string + cfg *config.Cfg + }{ + {name: "angular support enabled", cfg: &config.Cfg{AngularSupportEnabled: true}}, + {name: "angular support disabled", cfg: &config.Cfg{AngularSupportEnabled: false}}, + } { + t.Run(cfgTc.name, func(t *testing.T) { + for _, tc := range []struct { + name string + angularInspector angularinspector.Inspector + shouldLoad bool + }{ + { + name: "angular plugin", + angularInspector: angularinspector.AlwaysAngularFakeInspector, + // angular plugins should load only if allowed by the cfg + shouldLoad: cfgTc.cfg.AngularSupportEnabled, + }, + { + name: "non angular plugin", + angularInspector: angularinspector.NeverAngularFakeInspector, + // non-angular plugins should always load + shouldLoad: true, + }, + } { + t.Run(tc.name, func(t *testing.T) { + l := newLoaderWithAngularInspector(cfgTc.cfg, tc.angularInspector) + p, err := l.Load(context.Background(), fakePluginSource) + require.NoError(t, err) + if tc.shouldLoad { + require.Len(t, p, 1, "plugin should have been loaded") + } else { + require.Empty(t, p, "plugin shouldn't have been loaded") + } + }) + } + }) + } +} + +func TestLoader_Load_NestedPlugins(t *testing.T) { + parent := &plugins.Plugin{ + JSONData: plugins.JSONData{ + ID: "test-datasource", + Type: plugins.TypeDataSource, + Name: "Parent", + Info: plugins.Info{ + Author: plugins.InfoLink{ + Name: "Grafana Labs", + URL: "http://grafana.com", + }, + Logos: plugins.Logos{ + Small: "public/img/icn-datasource.svg", + Large: "public/img/icn-datasource.svg", + }, + Description: "Parent plugin", + Version: "1.0.0", + Updated: "2020-10-20", + }, + Dependencies: plugins.Dependencies{ + GrafanaVersion: "*", + Plugins: []plugins.Dependency{}, + }, + Backend: true, + }, + Module: "plugins/test-datasource/module", + BaseURL: "public/plugins/test-datasource", + FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "nested-plugins/parent")), + Signature: plugins.SignatureStatusValid, + SignatureType: plugins.SignatureTypeGrafana, + SignatureOrg: "Grafana Labs", + Class: plugins.ClassExternal, + } + + child := &plugins.Plugin{ + JSONData: plugins.JSONData{ + ID: "test-panel", + Type: plugins.TypePanel, + Name: "Child", + Info: plugins.Info{ + Author: plugins.InfoLink{ + Name: "Grafana Labs", + URL: "http://grafana.com", + }, + Logos: plugins.Logos{ + Small: "public/img/icn-panel.svg", + Large: "public/img/icn-panel.svg", + }, + Description: "Child plugin", + Version: "1.0.1", + Updated: "2020-10-30", + }, + Dependencies: plugins.Dependencies{ + GrafanaVersion: "*", + Plugins: []plugins.Dependency{}, + }, + }, + Module: "plugins/test-panel/module", + BaseURL: "public/plugins/test-panel", + FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "nested-plugins/parent/nested")), + Signature: plugins.SignatureStatusValid, + SignatureType: plugins.SignatureTypeGrafana, + SignatureOrg: "Grafana Labs", + Class: plugins.ClassExternal, + } + + parent.Children = []*plugins.Plugin{child} + child.Parent = parent + + t.Run("Load nested External plugins", func(t *testing.T) { + procPrvdr := fakes.NewFakeBackendProcessProvider() + procMgr := fakes.NewFakeProcessManager() + reg := fakes.NewFakePluginRegistry() + cfg := &config.Cfg{} + l := newLoader(t, cfg, reg, procMgr, procPrvdr) + + got, err := l.Load(context.Background(), &fakes.FakePluginSource{ + PluginClassFunc: func(ctx context.Context) plugins.Class { + return plugins.ClassExternal + }, + PluginURIsFunc: func(ctx context.Context) []string { + return []string{filepath.Join(testDataDir(t), "nested-plugins")} + }, + }) + require.NoError(t, err) + + // to ensure we can compare with expected + sort.SliceStable(got, func(i, j int) bool { + return got[i].ID < got[j].ID + }) + + expected := []*plugins.Plugin{parent, child} + if !cmp.Equal(got, expected, compareOpts...) { + t.Fatalf("Result mismatch (-want +got):\n%s", cmp.Diff(got, expected, compareOpts...)) + } + + verifyState(t, expected, reg, procPrvdr, procMgr) + + t.Run("Load will exclude plugins that already exist", func(t *testing.T) { + got, err := l.Load(context.Background(), &fakes.FakePluginSource{ + PluginClassFunc: func(ctx context.Context) plugins.Class { + return plugins.ClassExternal + }, + PluginURIsFunc: func(ctx context.Context) []string { + return []string{filepath.Join(testDataDir(t), "nested-plugins")} + }, + }) + require.NoError(t, err) + + // to ensure we can compare with expected + sort.SliceStable(got, func(i, j int) bool { + return got[i].ID < got[j].ID + }) + + if !cmp.Equal(got, []*plugins.Plugin{}, compareOpts...) { + t.Fatalf("Result mismatch (-want +got):\n%s", cmp.Diff(got, expected, compareOpts...)) + } + + verifyState(t, expected, reg, procPrvdr, procMgr) + }) + }) + + t.Run("Plugin child field `IncludedInAppID` is set to parent app's plugin ID", func(t *testing.T) { + parent := &plugins.Plugin{ + JSONData: plugins.JSONData{ + ID: "myorgid-simple-app", + Type: plugins.TypeApp, + Name: "Simple App", + Info: plugins.Info{ + Author: plugins.InfoLink{ + Name: "Your Name", + }, + Links: []plugins.InfoLink{ + {Name: "Website", URL: "https://github.com/grafana/grafana-starter-app"}, + {Name: "License", URL: "https://github.com/grafana/grafana-starter-app/blob/master/LICENSE"}, + }, + Logos: plugins.Logos{ + Small: "public/plugins/myorgid-simple-app/img/logo.svg", + Large: "public/plugins/myorgid-simple-app/img/logo.svg", + }, + Screenshots: []plugins.Screenshots{}, + Description: "Grafana App Plugin Template", + Version: "%VERSION%", + Updated: "%TODAY%", + }, + Dependencies: plugins.Dependencies{ + GrafanaVersion: "7.0.0", + GrafanaDependency: ">=7.0.0", + Plugins: []plugins.Dependency{}, + }, + Includes: []*plugins.Includes{ + { + Name: "Root Page (react)", + Path: "/a/myorgid-simple-app", + Type: "page", + Role: org.RoleViewer, + AddToNav: true, + DefaultNav: true, + Slug: "root-page-react", + }, + { + Name: "Root Page (Tab B)", + Path: "/a/myorgid-simple-app/?tab=b", + Type: "page", + Role: org.RoleViewer, + AddToNav: true, + Slug: "root-page-tab-b", + }, + { + Name: "React Config", + Path: "/plugins/myorgid-simple-app/?page=page2", + Type: "page", + Role: org.RoleAdmin, + AddToNav: true, + Slug: "react-config", + }, + { + Name: "Streaming Example", + Path: "dashboards/streaming.json", + Type: "dashboard", + Role: org.RoleViewer, + Slug: "streaming-example", + }, + { + Name: "Lots of Stats", + Path: "dashboards/stats.json", + Type: "dashboard", + Role: org.RoleViewer, + Slug: "lots-of-stats", + }, + }, + Backend: false, + }, + Module: "plugins/myorgid-simple-app/module", + BaseURL: "public/plugins/myorgid-simple-app", + FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "app-with-child/dist")), + DefaultNavURL: "/plugins/myorgid-simple-app/page/root-page-react", + Signature: plugins.SignatureStatusValid, + SignatureType: plugins.SignatureTypeGrafana, + SignatureOrg: "Grafana Labs", + Class: plugins.ClassExternal, + } + + child := &plugins.Plugin{ + JSONData: plugins.JSONData{ + ID: "myorgid-simple-panel", + Type: plugins.TypePanel, + Name: "Grafana Panel Plugin Template", + Info: plugins.Info{ + Author: plugins.InfoLink{ + Name: "Your Name", + }, + Links: []plugins.InfoLink{ + {Name: "Website", URL: "https://github.com/grafana/grafana-starter-panel"}, + {Name: "License", URL: "https://github.com/grafana/grafana-starter-panel/blob/master/LICENSE"}, + }, + Logos: plugins.Logos{ + Small: "public/plugins/myorgid-simple-panel/img/logo.svg", + Large: "public/plugins/myorgid-simple-panel/img/logo.svg", + }, + Screenshots: []plugins.Screenshots{}, + Description: "Grafana Panel Plugin Template", + Version: "%VERSION%", + Updated: "%TODAY%", + }, + Dependencies: plugins.Dependencies{ + GrafanaDependency: ">=7.0.0", + GrafanaVersion: "*", + Plugins: []plugins.Dependency{}, + }, + }, + Module: "plugins/myorgid-simple-app/child/module", + BaseURL: "public/plugins/myorgid-simple-app", + FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "app-with-child/dist/child")), + IncludedInAppID: parent.ID, + Signature: plugins.SignatureStatusValid, + SignatureType: plugins.SignatureTypeGrafana, + SignatureOrg: "Grafana Labs", + Class: plugins.ClassExternal, + } + + parent.Children = []*plugins.Plugin{child} + child.Parent = parent + expected := []*plugins.Plugin{parent, child} + + reg := fakes.NewFakePluginRegistry() + procPrvdr := fakes.NewFakeBackendProcessProvider() + procMgr := fakes.NewFakeProcessManager() + cfg := &config.Cfg{} + l := newLoader(t, cfg, reg, procMgr, procPrvdr) + got, err := l.Load(context.Background(), &fakes.FakePluginSource{ + PluginClassFunc: func(ctx context.Context) plugins.Class { + return plugins.ClassExternal + }, + PluginURIsFunc: func(ctx context.Context) []string { + return []string{filepath.Join(testDataDir(t), "app-with-child")} + }, + }) + require.NoError(t, err) + + // to ensure we can compare with expected + sort.SliceStable(got, func(i, j int) bool { + return got[i].ID < got[j].ID + }) + + if !cmp.Equal(got, expected, compareOpts...) { + t.Fatalf("Result mismatch (-want +got):\n%s", cmp.Diff(got, expected, compareOpts...)) + } + + verifyState(t, expected, reg, procPrvdr, procMgr) + }) +} + +func newLoader(t *testing.T, cfg *config.Cfg, reg registry.Service, proc process.Service, + backendFactory plugins.BackendFactoryProvider) *Loader { + assets := assetpath.ProvideService(pluginscdn.ProvideService(cfg)) + lic := fakes.NewFakeLicensingService() + angularInspector, err := angularinspector.NewStaticInspector() + require.NoError(t, err) + return ProvideService(cfg, signature.NewUnsignedAuthorizer(cfg), proc, reg, fakes.NewFakeRoleRegistry(), assets, + angularInspector, &fakes.FakeOauthService{}, + pipeline.ProvideDiscoveryStage(cfg, finder.NewLocalFinder(false), reg), + pipeline.ProvideBootstrapStage(cfg, signature.DefaultCalculator(cfg), assets), + pipeline.ProvideInitializationStage(cfg, reg, lic, backendFactory)) +} + +func newLoaderWithAngularInspector(cfg *config.Cfg, angularInspector angularinspector.Inspector) *Loader { + reg := fakes.NewFakePluginRegistry() + return ProvideService(cfg, signature.NewUnsignedAuthorizer(cfg), process.ProvideService(reg), reg, + fakes.NewFakeRoleRegistry(), assetpath.ProvideService(pluginscdn.ProvideService(cfg)), + angularInspector, &fakes.FakeOauthService{}, + discovery.New(cfg, discovery.Opts{}), bootstrap.New(cfg, bootstrap.Opts{}), initialization.New(cfg, initialization.Opts{})) +} + +func verifyState(t *testing.T, ps []*plugins.Plugin, reg registry.Service, + procPrvdr *fakes.FakeBackendProcessProvider, procMngr *fakes.FakeProcessManager) { + t.Helper() + + for _, p := range ps { + regP, exists := reg.Plugin(context.Background(), p.ID) + require.True(t, exists) + if !cmp.Equal(p, regP, compareOpts...) { + t.Fatalf("Result mismatch (-want +got):\n%s", cmp.Diff(p, regP, compareOpts...)) + } + + if p.Backend { + require.Equal(t, 1, procPrvdr.Requested[p.ID]) + require.Equal(t, 1, procPrvdr.Invoked[p.ID]) + } else { + require.Zero(t, procPrvdr.Requested[p.ID]) + require.Zero(t, procPrvdr.Invoked[p.ID]) + } + + require.Equal(t, 1, procMngr.Started[p.ID]) + require.Zero(t, procMngr.Stopped[p.ID]) + } +} + +func mustNewStaticFSForTests(t *testing.T, dir string) plugins.FS { + sfs, err := plugins.NewStaticFS(plugins.NewLocalFS(dir)) + require.NoError(t, err) + return sfs +} + +func testDataDir(t *testing.T) string { + dir, err := filepath.Abs("../../../plugins/manager/testdata") + if err != nil { + t.Errorf("could not construct absolute path of current dir") + return "" + } + return dir +} + +func corePluginDir(t *testing.T) string { + dir, err := filepath.Abs("./../../../../public") + if err != nil { + t.Errorf("could not construct absolute path of core plugins dir") + return "" + } + return dir +} diff --git a/pkg/services/pluginsintegration/pipeline/discovery.go b/pkg/services/pluginsintegration/pipeline/discovery.go deleted file mode 100644 index c4e29033a7b..00000000000 --- a/pkg/services/pluginsintegration/pipeline/discovery.go +++ /dev/null @@ -1,33 +0,0 @@ -package pipeline - -import ( - "context" - - "github.com/grafana/grafana/pkg/plugins" - "github.com/grafana/grafana/pkg/plugins/config" - "github.com/grafana/grafana/pkg/plugins/manager/loader/assetpath" - "github.com/grafana/grafana/pkg/plugins/manager/loader/finder" - "github.com/grafana/grafana/pkg/plugins/manager/pipeline/bootstrap" - "github.com/grafana/grafana/pkg/plugins/manager/pipeline/discovery" - "github.com/grafana/grafana/pkg/plugins/manager/registry" -) - -func ProvideDiscoveryStage(cfg *config.Cfg, pluginFinder finder.Finder, pluginRegistry registry.Service) *discovery.Discovery { - return discovery.New(cfg, discovery.Opts{ - FindFunc: func(ctx context.Context, src plugins.PluginSource) ([]*plugins.FoundBundle, error) { - return pluginFinder.Find(ctx, src) - }, - FindFilterFuncs: []discovery.FindFilterFunc{ - func(ctx context.Context, _ plugins.Class, bundles []*plugins.FoundBundle) ([]*plugins.FoundBundle, error) { - return discovery.NewDuplicatePluginFilterStep(pluginRegistry).Filter(ctx, bundles) - }, - }, - }) -} - -func ProvideBootstrapStage(cfg *config.Cfg, signatureCalculator plugins.SignatureCalculator, assetPath *assetpath.Service) *bootstrap.Bootstrap { - return bootstrap.New(cfg, bootstrap.Opts{ - ConstructFunc: bootstrap.DefaultConstructFunc(signatureCalculator, assetPath), - DecorateFuncs: bootstrap.DefaultDecorateFuncs, - }) -} diff --git a/pkg/services/pluginsintegration/pipeline/pipeline.go b/pkg/services/pluginsintegration/pipeline/pipeline.go new file mode 100644 index 00000000000..8224e9dcea5 --- /dev/null +++ b/pkg/services/pluginsintegration/pipeline/pipeline.go @@ -0,0 +1,44 @@ +package pipeline + +import ( + "context" + + "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/plugins/config" + "github.com/grafana/grafana/pkg/plugins/envvars" + "github.com/grafana/grafana/pkg/plugins/manager/loader/assetpath" + "github.com/grafana/grafana/pkg/plugins/manager/loader/finder" + "github.com/grafana/grafana/pkg/plugins/manager/pipeline/bootstrap" + "github.com/grafana/grafana/pkg/plugins/manager/pipeline/discovery" + "github.com/grafana/grafana/pkg/plugins/manager/pipeline/initialization" + "github.com/grafana/grafana/pkg/plugins/manager/registry" +) + +func ProvideDiscoveryStage(cfg *config.Cfg, pf finder.Finder, pr registry.Service) *discovery.Discovery { + return discovery.New(cfg, discovery.Opts{ + FindFunc: func(ctx context.Context, src plugins.PluginSource) ([]*plugins.FoundBundle, error) { + return pf.Find(ctx, src) + }, + FindFilterFuncs: []discovery.FindFilterFunc{ + func(ctx context.Context, _ plugins.Class, b []*plugins.FoundBundle) ([]*plugins.FoundBundle, error) { + return discovery.NewDuplicatePluginFilterStep(pr).Filter(ctx, b) + }, + }, + }) +} + +func ProvideBootstrapStage(cfg *config.Cfg, sc plugins.SignatureCalculator, a *assetpath.Service) *bootstrap.Bootstrap { + return bootstrap.New(cfg, bootstrap.Opts{ + ConstructFunc: bootstrap.DefaultConstructFunc(sc, a), + DecorateFuncs: bootstrap.DefaultDecorateFuncs, + }) +} + +func ProvideInitializationStage(cfg *config.Cfg, pr registry.Service, l plugins.Licensing, bp plugins.BackendFactoryProvider) *initialization.Initialize { + return initialization.New(cfg, initialization.Opts{ + InitializeFuncs: []initialization.InitializeFunc{ + initialization.NewBackendClientInitStep(envvars.NewProvider(cfg, l), bp), + initialization.NewPluginRegistrationStep(pr), + }, + }) +} diff --git a/pkg/services/pluginsintegration/pluginsintegration.go b/pkg/services/pluginsintegration/pluginsintegration.go index 9630b8290b6..cd6a0407d25 100644 --- a/pkg/services/pluginsintegration/pluginsintegration.go +++ b/pkg/services/pluginsintegration/pluginsintegration.go @@ -11,12 +11,13 @@ import ( "github.com/grafana/grafana/pkg/plugins/manager" "github.com/grafana/grafana/pkg/plugins/manager/client" "github.com/grafana/grafana/pkg/plugins/manager/filestore" - "github.com/grafana/grafana/pkg/plugins/manager/loader" + pluginLoader "github.com/grafana/grafana/pkg/plugins/manager/loader" pAngularInspector "github.com/grafana/grafana/pkg/plugins/manager/loader/angular/angularinspector" "github.com/grafana/grafana/pkg/plugins/manager/loader/assetpath" "github.com/grafana/grafana/pkg/plugins/manager/loader/finder" "github.com/grafana/grafana/pkg/plugins/manager/pipeline/bootstrap" "github.com/grafana/grafana/pkg/plugins/manager/pipeline/discovery" + "github.com/grafana/grafana/pkg/plugins/manager/pipeline/initialization" "github.com/grafana/grafana/pkg/plugins/manager/process" "github.com/grafana/grafana/pkg/plugins/manager/registry" "github.com/grafana/grafana/pkg/plugins/manager/signature" @@ -37,6 +38,7 @@ import ( "github.com/grafana/grafana/pkg/services/pluginsintegration/keyretriever/dynamic" "github.com/grafana/grafana/pkg/services/pluginsintegration/keystore" "github.com/grafana/grafana/pkg/services/pluginsintegration/licensing" + "github.com/grafana/grafana/pkg/services/pluginsintegration/loader" "github.com/grafana/grafana/pkg/services/pluginsintegration/pipeline" "github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings" @@ -65,6 +67,8 @@ var WireSet = wire.NewSet( wire.Bind(new(discovery.Discoverer), new(*discovery.Discovery)), pipeline.ProvideBootstrapStage, wire.Bind(new(bootstrap.Bootstrapper), new(*bootstrap.Bootstrap)), + pipeline.ProvideInitializationStage, + wire.Bind(new(initialization.Initializer), new(*initialization.Initialize)), angularpatternsstore.ProvideService, angulardetectorsprovider.ProvideDynamic, @@ -72,7 +76,7 @@ var WireSet = wire.NewSet( wire.Bind(new(pAngularInspector.Inspector), new(*angularinspector.Service)), loader.ProvideService, - wire.Bind(new(loader.Service), new(*loader.Loader)), + wire.Bind(new(pluginLoader.Service), new(*loader.Loader)), wire.Bind(new(plugins.ErrorResolver), new(*loader.Loader)), manager.ProvideInstaller, wire.Bind(new(plugins.Installer), new(*manager.PluginInstaller)),