package pluginstore import ( "context" "errors" "testing" "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/backendplugin" "github.com/grafana/grafana/pkg/plugins/log" "github.com/grafana/grafana/pkg/plugins/manager/pluginfakes" "github.com/grafana/grafana/pkg/services/featuremgmt" ) func TestStore_ProvideService(t *testing.T) { t.Run("Plugin sources are added in order", func(t *testing.T) { tests := []struct { name string featureEnabled bool expectedLoadOnStartup bool expectedBeforeStart []plugins.Class expectedAfterStart []plugins.Class }{ { name: "with FlagPluginStoreServiceLoading disabled", featureEnabled: false, expectedLoadOnStartup: false, expectedBeforeStart: []plugins.Class{"1", "2", "3"}, expectedAfterStart: []plugins.Class{"1", "2", "3"}, }, { name: "with FlagPluginStoreServiceLoading enabled", featureEnabled: true, expectedLoadOnStartup: true, expectedBeforeStart: nil, expectedAfterStart: []plugins.Class{"1", "2", "3"}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var loadedSrcs []plugins.Class l := &pluginfakes.FakeLoader{ LoadFunc: func(ctx context.Context, src plugins.PluginSource) ([]*plugins.Plugin, error) { loadedSrcs = append(loadedSrcs, src.PluginClass(ctx)) return nil, nil }, } srcs := &pluginfakes.FakeSourceRegistry{ListFunc: func(_ context.Context) []plugins.PluginSource { return []plugins.PluginSource{ &pluginfakes.FakePluginSource{ PluginClassFunc: func(ctx context.Context) plugins.Class { return "1" }, }, &pluginfakes.FakePluginSource{ PluginClassFunc: func(ctx context.Context) plugins.Class { return "2" }, }, &pluginfakes.FakePluginSource{ PluginClassFunc: func(ctx context.Context) plugins.Class { return "3" }, }, } }} var features featuremgmt.FeatureToggles if tt.featureEnabled { features = featuremgmt.WithFeatures(featuremgmt.FlagPluginStoreServiceLoading) } else { features = featuremgmt.WithFeatures() } service, err := ProvideService(pluginfakes.NewFakePluginRegistry(), srcs, l, features) require.Equal(t, tt.expectedLoadOnStartup, service.loadOnStartup) require.Equal(t, tt.expectedBeforeStart, loadedSrcs) require.NoError(t, err) ctx := context.Background() err = service.StartAsync(ctx) require.NoError(t, err) err = service.AwaitRunning(ctx) require.NoError(t, err) require.Equal(t, tt.expectedAfterStart, loadedSrcs) }) } }) } func TestStore_Plugin(t *testing.T) { t.Run("Plugin returns all non-decommissioned plugins", func(t *testing.T) { p1 := &plugins.Plugin{JSONData: plugins.JSONData{ID: "test-datasource"}} p1.RegisterClient(&DecommissionedPlugin{}) p2 := &plugins.Plugin{JSONData: plugins.JSONData{ID: "test-panel"}} ps, err := NewPluginStoreForTest(&pluginfakes.FakePluginRegistry{ Store: map[string]*plugins.Plugin{ p1.ID: p1, p2.ID: p2, }, }, &pluginfakes.FakeLoader{}, &pluginfakes.FakeSourceRegistry{}) require.NoError(t, err) p, exists := ps.Plugin(context.Background(), p1.ID) require.False(t, exists) require.Equal(t, Plugin{}, p) p, exists = ps.Plugin(context.Background(), p2.ID) require.True(t, exists) require.Equal(t, p, ToGrafanaDTO(p2)) }) } func TestStore_Plugins(t *testing.T) { t.Run("Plugin returns all non-decommissioned plugins by type", func(t *testing.T) { p1 := &plugins.Plugin{JSONData: plugins.JSONData{ID: "a-test-datasource", Type: plugins.TypeDataSource}} p2 := &plugins.Plugin{JSONData: plugins.JSONData{ID: "b-test-panel", Type: plugins.TypePanel}} p3 := &plugins.Plugin{JSONData: plugins.JSONData{ID: "c-test-panel", Type: plugins.TypePanel}} p4 := &plugins.Plugin{JSONData: plugins.JSONData{ID: "d-test-app", Type: plugins.TypeApp}} p5 := &plugins.Plugin{JSONData: plugins.JSONData{ID: "e-test-panel", Type: plugins.TypePanel}} p5.RegisterClient(&DecommissionedPlugin{}) ps, err := NewPluginStoreForTest(&pluginfakes.FakePluginRegistry{ Store: map[string]*plugins.Plugin{ p1.ID: p1, p2.ID: p2, p3.ID: p3, p4.ID: p4, p5.ID: p5, }, }, &pluginfakes.FakeLoader{}, &pluginfakes.FakeSourceRegistry{}) require.NoError(t, err) ToGrafanaDTO(p1) pss := ps.Plugins(context.Background()) require.Equal(t, pss, []Plugin{ ToGrafanaDTO(p1), ToGrafanaDTO(p2), ToGrafanaDTO(p3), ToGrafanaDTO(p4), }) pss = ps.Plugins(context.Background(), plugins.TypeApp) require.Equal(t, pss, []Plugin{ToGrafanaDTO(p4)}) pss = ps.Plugins(context.Background(), plugins.TypePanel) require.Equal(t, pss, []Plugin{ToGrafanaDTO(p2), ToGrafanaDTO(p3)}) pss = ps.Plugins(context.Background(), plugins.TypeDataSource) require.Equal(t, pss, []Plugin{ToGrafanaDTO(p1)}) pss = ps.Plugins(context.Background(), plugins.TypeDataSource, plugins.TypeApp, plugins.TypePanel) require.Equal(t, pss, []Plugin{ ToGrafanaDTO(p1), ToGrafanaDTO(p2), ToGrafanaDTO(p3), ToGrafanaDTO(p4), }) }) } func TestStore_Routes(t *testing.T) { t.Run("Routes returns all static routes for non-decommissioned plugins", func(t *testing.T) { p1 := &plugins.Plugin{JSONData: plugins.JSONData{ID: "a-test-renderer", Type: plugins.TypeRenderer}, FS: pluginfakes.NewFakePluginFS("/some/dir")} p2 := &plugins.Plugin{JSONData: plugins.JSONData{ID: "b-test-panel", Type: plugins.TypePanel}, FS: pluginfakes.NewFakePluginFS("/grafana/")} p4 := &plugins.Plugin{JSONData: plugins.JSONData{ID: "d-test-datasource", Type: plugins.TypeDataSource}, FS: pluginfakes.NewFakePluginFS("../test")} p5 := &plugins.Plugin{JSONData: plugins.JSONData{ID: "e-test-app", Type: plugins.TypeApp}, FS: pluginfakes.NewFakePluginFS("any/path")} p6 := &plugins.Plugin{JSONData: plugins.JSONData{ID: "f-test-app", Type: plugins.TypeApp}} p6.RegisterClient(&DecommissionedPlugin{}) ps, err := NewPluginStoreForTest(&pluginfakes.FakePluginRegistry{ Store: map[string]*plugins.Plugin{ p1.ID: p1, p2.ID: p2, p4.ID: p4, p5.ID: p5, p6.ID: p6, }, }, &pluginfakes.FakeLoader{}, &pluginfakes.FakeSourceRegistry{}) require.NoError(t, err) sr := func(p *plugins.Plugin) *plugins.StaticRoute { return &plugins.StaticRoute{PluginID: p.ID, Directory: p.FS.Base()} } rs := ps.Routes(context.Background()) require.Equal(t, []*plugins.StaticRoute{sr(p1), sr(p2), sr(p4), sr(p5)}, rs) }) } func TestProcessManager_shutdown(t *testing.T) { t.Run("When context is cancelled the plugin is stopped", func(t *testing.T) { p := &plugins.Plugin{JSONData: plugins.JSONData{ID: "test-datasource", Type: plugins.TypeDataSource}} // Backend: true backend := &pluginfakes.FakeBackendPlugin{} p.RegisterClient(backend) p.SetLogger(log.NewTestLogger()) unloaded := false ps := New(&pluginfakes.FakePluginRegistry{ Store: map[string]*plugins.Plugin{ p.ID: p, }, }, &pluginfakes.FakeLoader{ UnloadFunc: func(_ context.Context, plugin *plugins.Plugin) (*plugins.Plugin, error) { require.Equal(t, p, plugin) unloaded = true return nil, nil }, }, &pluginfakes.FakeSourceRegistry{}) ctx, cancel := context.WithCancel(context.Background()) err := ps.StartAsync(ctx) require.NoError(t, err) err = ps.AwaitRunning(ctx) require.NoError(t, err) // Cancel context to trigger shutdown cancel() // Wait for service to be fully terminated err = ps.AwaitTerminated(context.Background()) require.NoError(t, err) require.True(t, unloaded) }) t.Run("When shutdown fails, stopping method returns error", func(t *testing.T) { p := &plugins.Plugin{JSONData: plugins.JSONData{ID: "test-datasource", Type: plugins.TypeDataSource}} backend := &pluginfakes.FakeBackendPlugin{} p.RegisterClient(backend) p.SetLogger(log.NewTestLogger()) expectedErr := errors.New("unload failed") ps, err := NewPluginStoreForTest(&pluginfakes.FakePluginRegistry{ Store: map[string]*plugins.Plugin{ p.ID: p, }, }, &pluginfakes.FakeLoader{ UnloadFunc: func(_ context.Context, plugin *plugins.Plugin) (*plugins.Plugin, error) { return nil, expectedErr }, }, &pluginfakes.FakeSourceRegistry{}) require.NoError(t, err) err = ps.stopping(nil) require.Error(t, err) require.ErrorIs(t, err, expectedErr) }) } func TestStore_availablePlugins(t *testing.T) { t.Run("Decommissioned plugins are excluded from availablePlugins", func(t *testing.T) { p1 := &plugins.Plugin{JSONData: plugins.JSONData{ID: "test-datasource"}} p1.RegisterClient(&DecommissionedPlugin{}) p2 := &plugins.Plugin{JSONData: plugins.JSONData{ID: "test-app"}} ps, err := NewPluginStoreForTest(&pluginfakes.FakePluginRegistry{ Store: map[string]*plugins.Plugin{ p1.ID: p1, p2.ID: p2, }, }, &pluginfakes.FakeLoader{}, &pluginfakes.FakeSourceRegistry{}) require.NoError(t, err) aps := ps.availablePlugins(context.Background()) require.Len(t, aps, 1) require.Equal(t, p2, aps[0]) }) } type DecommissionedPlugin struct { backendplugin.Plugin } func (p *DecommissionedPlugin) Decommission() error { return nil } func (p *DecommissionedPlugin) IsDecommissioned() bool { return true }