diff --git a/pkg/plugins/manager/fakes/fakes.go b/pkg/plugins/manager/fakes/fakes.go index 55c8f8d595a..b94a40c7e78 100644 --- a/pkg/plugins/manager/fakes/fakes.go +++ b/pkg/plugins/manager/fakes/fakes.go @@ -299,23 +299,28 @@ func (m *FakeProcessManager) Stop(ctx context.Context, pluginID string) error { } type FakeBackendProcessProvider struct { - Requested map[string]int - Invoked map[string]int + Requested map[string]int + Invoked map[string]int + BackendFactoryFunc func(context.Context, *plugins.Plugin) backendplugin.PluginFactoryFunc } func NewFakeBackendProcessProvider() *FakeBackendProcessProvider { - return &FakeBackendProcessProvider{ + f := &FakeBackendProcessProvider{ Requested: make(map[string]int), Invoked: make(map[string]int), } + f.BackendFactoryFunc = func(ctx context.Context, p *plugins.Plugin) backendplugin.PluginFactoryFunc { + f.Requested[p.ID]++ + return func(pluginID string, _ log.Logger, _ []string) (backendplugin.Plugin, error) { + f.Invoked[pluginID]++ + return &FakePluginClient{}, nil + } + } + return f } -func (pr *FakeBackendProcessProvider) BackendFactory(_ context.Context, p *plugins.Plugin) backendplugin.PluginFactoryFunc { - pr.Requested[p.ID]++ - return func(pluginID string, _ log.Logger, _ []string) (backendplugin.Plugin, error) { - pr.Invoked[pluginID]++ - return &FakePluginClient{}, nil - } +func (pr *FakeBackendProcessProvider) BackendFactory(ctx context.Context, p *plugins.Plugin) backendplugin.PluginFactoryFunc { + return pr.BackendFactoryFunc(ctx, p) } type FakeLicensingService struct { diff --git a/pkg/plugins/manager/loader/loader.go b/pkg/plugins/manager/loader/loader.go index 2349b4d255e..0cc65c06468 100644 --- a/pkg/plugins/manager/loader/loader.go +++ b/pkg/plugins/manager/loader/loader.go @@ -161,17 +161,22 @@ func (l *Loader) loadPlugins(ctx context.Context, src plugins.PluginSource, foun verifiedPlugins = append(verifiedPlugins, plugin) } + // initialize plugins + initializedPlugins := make([]*plugins.Plugin, 0) for _, p := range verifiedPlugins { err := l.pluginInitializer.Initialize(ctx, p) if err != nil { - return nil, err + l.log.Error("Could not initialize plugin", "pluginId", p.ID, "err", err) + continue } if errDeclareRoles := l.roleRegistry.DeclarePluginRoles(ctx, p.ID, p.Name, p.Roles); errDeclareRoles != nil { l.log.Warn("Declare plugin roles failed.", "pluginID", p.ID, "err", errDeclareRoles) } + + initializedPlugins = append(initializedPlugins, p) } - for _, p := range verifiedPlugins { + for _, p := range initializedPlugins { if err := l.load(ctx, p); err != nil { l.log.Error("Could not start plugin", "pluginId", p.ID, "err", err) } @@ -181,7 +186,7 @@ func (l *Loader) loadPlugins(ctx context.Context, src plugins.PluginSource, foun } } - return verifiedPlugins, nil + return initializedPlugins, nil } func (l *Loader) Unload(ctx context.Context, pluginID string) error { diff --git a/pkg/plugins/manager/loader/loader_test.go b/pkg/plugins/manager/loader/loader_test.go index 81e09f76813..d922f27fb3c 100644 --- a/pkg/plugins/manager/loader/loader_test.go +++ b/pkg/plugins/manager/loader/loader_test.go @@ -2,6 +2,7 @@ package loader import ( "context" + "fmt" "os" "path/filepath" "sort" @@ -12,6 +13,7 @@ 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" @@ -1014,6 +1016,107 @@ func TestLoader_Load_DuplicatePlugins(t *testing.T) { }) } +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: "app", + Name: "Test App", + Info: plugins.Info{ + Author: plugins.InfoLink{ + Name: "Test Inc.", + URL: "http://test.com", + }, + Description: "Official Grafana Test App & Dashboard bundle", + Version: "1.0.0", + Links: []plugins.InfoLink{ + {Name: "Project site", URL: "http://project.com"}, + {Name: "License & Terms", URL: "http://license.com"}, + }, + Logos: plugins.Logos{ + Small: "public/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: "Viewer", Slug: "nginx-connections"}, + {Name: "Nginx Memory", Path: "dashboards/memory.json", Type: "dashboard", Role: "Viewer", Slug: "nginx-memory"}, + {Name: "Nginx Panel", Type: "panel", Role: "Viewer", Slug: "nginx-panel"}, + {Name: "Nginx Datasource", Type: "datasource", Role: "Viewer", Slug: "nginx-datasource"}, + }, + Backend: false, + }, + FS: plugins.NewLocalFS(filesInDir(t, pluginDir1), pluginDir1), + Class: plugins.External, + Signature: plugins.SignatureValid, + SignatureType: plugins.GrafanaSignature, + SignatureOrg: "Grafana Labs", + Module: "plugins/test-app/module", + BaseURL: "public/plugins/test-app", + }, + } + + reg := fakes.NewFakePluginRegistry() + storage := fakes.NewFakePluginStorage() + 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, fmt.Errorf("failed to initialize") + } + return &fakes.FakePluginClient{}, nil + } + } + procMgr := fakes.NewFakeProcessManager() + l := newLoader(&config.Cfg{}, func(l *Loader) { + l.pluginRegistry = reg + l.pluginStorage = storage + 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.External + }, + 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, storage, procMgr) + }) +} + func TestLoader_Load_NestedPlugins(t *testing.T) { rootDir, err := filepath.Abs("../") if err != nil {