Plugins: Split plugin manager into smaller components (#54384)

* split out plugin manager

* remove whitespace

* fix tests

* split up tests

* updating naming conventions

* simplify manager

* tidy

* add more fakes

* testing time

* add query verif to int test

* renaming

* add process tests

* tidy up manager tests

* add extra case to int test

* add more coverage to store and process tests

* remove comment

* fix capatilization

* init on provide

* remove addfromsource from API
This commit is contained in:
Will Browne
2022-08-30 17:30:43 +02:00
committed by GitHub
parent acbbdccba9
commit 4a707e2a88
22 changed files with 1623 additions and 1319 deletions
+111
View File
@@ -0,0 +1,111 @@
package store
import (
"context"
"sort"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/manager/registry"
)
var _ plugins.Store = (*Service)(nil)
var _ plugins.RendererManager = (*Service)(nil)
var _ plugins.SecretsPluginManager = (*Service)(nil)
type Service struct {
pluginRegistry registry.Service
}
func ProvideService(pluginRegistry registry.Service) *Service {
return &Service{
pluginRegistry: pluginRegistry,
}
}
func (s *Service) Plugin(ctx context.Context, pluginID string) (plugins.PluginDTO, bool) {
p, exists := s.plugin(ctx, pluginID)
if !exists {
return plugins.PluginDTO{}, false
}
return p.ToDTO(), true
}
func (s *Service) Plugins(ctx context.Context, pluginTypes ...plugins.Type) []plugins.PluginDTO {
// if no types passed, assume all
if len(pluginTypes) == 0 {
pluginTypes = plugins.PluginTypes
}
var requestedTypes = make(map[plugins.Type]struct{})
for _, pt := range pluginTypes {
requestedTypes[pt] = struct{}{}
}
pluginsList := make([]plugins.PluginDTO, 0)
for _, p := range s.availablePlugins(ctx) {
if _, exists := requestedTypes[p.Type]; exists {
pluginsList = append(pluginsList, p.ToDTO())
}
}
return pluginsList
}
func (s *Service) Renderer() *plugins.Plugin {
for _, p := range s.availablePlugins(context.TODO()) {
if p.IsRenderer() {
return p
}
}
return nil
}
func (s *Service) SecretsManager() *plugins.Plugin {
for _, p := range s.availablePlugins(context.TODO()) {
if p.IsSecretsManager() {
return p
}
}
return nil
}
// plugin finds a plugin with `pluginID` from the registry that is not decommissioned
func (s *Service) plugin(ctx context.Context, pluginID string) (*plugins.Plugin, bool) {
p, exists := s.pluginRegistry.Plugin(ctx, pluginID)
if !exists {
return nil, false
}
if p.IsDecommissioned() {
return nil, false
}
return p, true
}
// availablePlugins returns all non-decommissioned plugins from the registry sorted by alphabetic order on `plugin.ID`
func (s *Service) availablePlugins(ctx context.Context) []*plugins.Plugin {
var res []*plugins.Plugin
for _, p := range s.pluginRegistry.Plugins(ctx) {
if !p.IsDecommissioned() {
res = append(res, p)
}
}
sort.SliceStable(res, func(i, j int) bool {
return res[i].ID < res[j].ID
})
return res
}
func (s *Service) Routes() []*plugins.StaticRoute {
staticRoutes := make([]*plugins.StaticRoute, 0)
for _, p := range s.availablePlugins(context.TODO()) {
if p.StaticRoute() != nil {
staticRoutes = append(staticRoutes, p.StaticRoute())
}
}
return staticRoutes
}
+207
View File
@@ -0,0 +1,207 @@
package store
import (
"context"
"testing"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/backendplugin"
)
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 := ProvideService(
newFakePluginRegistry(map[string]*plugins.Plugin{
p1.ID: p1,
p2.ID: p2,
}),
)
p, exists := ps.Plugin(context.Background(), p1.ID)
require.False(t, exists)
require.Equal(t, plugins.PluginDTO{}, p)
p, exists = ps.Plugin(context.Background(), p2.ID)
require.True(t, exists)
require.Equal(t, p, p2.ToDTO())
})
}
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.DataSource}}
p2 := &plugins.Plugin{JSONData: plugins.JSONData{ID: "b-test-panel", Type: plugins.Panel}}
p3 := &plugins.Plugin{JSONData: plugins.JSONData{ID: "c-test-panel", Type: plugins.Panel}}
p4 := &plugins.Plugin{JSONData: plugins.JSONData{ID: "d-test-app", Type: plugins.App}}
p5 := &plugins.Plugin{JSONData: plugins.JSONData{ID: "e-test-panel", Type: plugins.Panel}}
p5.RegisterClient(&DecommissionedPlugin{})
ps := ProvideService(
newFakePluginRegistry(map[string]*plugins.Plugin{
p1.ID: p1,
p2.ID: p2,
p3.ID: p3,
p4.ID: p4,
p5.ID: p5,
}),
)
pss := ps.Plugins(context.Background())
require.Equal(t, pss, []plugins.PluginDTO{p1.ToDTO(), p2.ToDTO(), p3.ToDTO(), p4.ToDTO()})
pss = ps.Plugins(context.Background(), plugins.App)
require.Equal(t, pss, []plugins.PluginDTO{p4.ToDTO()})
pss = ps.Plugins(context.Background(), plugins.Panel)
require.Equal(t, pss, []plugins.PluginDTO{p2.ToDTO(), p3.ToDTO()})
pss = ps.Plugins(context.Background(), plugins.DataSource)
require.Equal(t, pss, []plugins.PluginDTO{p1.ToDTO()})
pss = ps.Plugins(context.Background(), plugins.DataSource, plugins.App, plugins.Panel)
require.Equal(t, pss, []plugins.PluginDTO{p1.ToDTO(), p2.ToDTO(), p3.ToDTO(), p4.ToDTO()})
})
}
func TestStore_Renderer(t *testing.T) {
t.Run("Renderer returns a single (non-decommissioned) renderer plugin", func(t *testing.T) {
p1 := &plugins.Plugin{JSONData: plugins.JSONData{ID: "test-renderer", Type: plugins.Renderer}}
p2 := &plugins.Plugin{JSONData: plugins.JSONData{ID: "test-panel", Type: plugins.Panel}}
p3 := &plugins.Plugin{JSONData: plugins.JSONData{ID: "test-app", Type: plugins.App}}
p4 := &plugins.Plugin{JSONData: plugins.JSONData{ID: "test-datasource", Type: plugins.DataSource}}
p4.RegisterClient(&DecommissionedPlugin{})
ps := ProvideService(
newFakePluginRegistry(map[string]*plugins.Plugin{
p1.ID: p1,
p2.ID: p2,
p3.ID: p3,
p4.ID: p4,
}),
)
r := ps.Renderer()
require.Equal(t, p1, r)
})
}
func TestStore_SecretsManager(t *testing.T) {
t.Run("Renderer returns a single (non-decommissioned) secrets manager plugin", func(t *testing.T) {
p1 := &plugins.Plugin{JSONData: plugins.JSONData{ID: "test-renderer", Type: plugins.Renderer}}
p2 := &plugins.Plugin{JSONData: plugins.JSONData{ID: "test-panel", Type: plugins.Panel}}
p3 := &plugins.Plugin{JSONData: plugins.JSONData{ID: "test-secrets", Type: plugins.SecretsManager}}
p4 := &plugins.Plugin{JSONData: plugins.JSONData{ID: "test-datasource", Type: plugins.DataSource}}
ps := ProvideService(
newFakePluginRegistry(map[string]*plugins.Plugin{
p1.ID: p1,
p2.ID: p2,
p3.ID: p3,
p4.ID: p4,
}),
)
r := ps.SecretsManager()
require.Equal(t, p3, r)
})
}
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.Renderer}, PluginDir: "/some/dir"}
p2 := &plugins.Plugin{JSONData: plugins.JSONData{ID: "b-test-panel", Type: plugins.Panel}, PluginDir: "/grafana/"}
p3 := &plugins.Plugin{JSONData: plugins.JSONData{ID: "c-test-secrets", Type: plugins.SecretsManager}, PluginDir: "./secrets", Class: plugins.Core}
p4 := &plugins.Plugin{JSONData: plugins.JSONData{ID: "d-test-datasource", Type: plugins.DataSource}, PluginDir: "../test"}
p5 := &plugins.Plugin{JSONData: plugins.JSONData{ID: "e-test-app", Type: plugins.App}}
p6 := &plugins.Plugin{JSONData: plugins.JSONData{ID: "f-test-app", Type: plugins.App}}
p6.RegisterClient(&DecommissionedPlugin{})
ps := ProvideService(
newFakePluginRegistry(map[string]*plugins.Plugin{
p1.ID: p1,
p2.ID: p2,
p3.ID: p3,
p4.ID: p4,
p5.ID: p5,
p6.ID: p6,
}),
)
sr := func(p *plugins.Plugin) *plugins.StaticRoute {
return &plugins.StaticRoute{PluginID: p.ID, Directory: p.PluginDir}
}
rs := ps.Routes()
require.Equal(t, []*plugins.StaticRoute{sr(p1), sr(p2), sr(p4), sr(p5)}, rs)
})
}
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 := ProvideService(
newFakePluginRegistry(map[string]*plugins.Plugin{
p1.ID: p1,
p2.ID: p2,
}),
)
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
}
type fakePluginRegistry struct {
store map[string]*plugins.Plugin
}
func newFakePluginRegistry(m map[string]*plugins.Plugin) *fakePluginRegistry {
return &fakePluginRegistry{
store: m,
}
}
func (f *fakePluginRegistry) Plugin(_ context.Context, id string) (*plugins.Plugin, bool) {
p, exists := f.store[id]
return p, exists
}
func (f *fakePluginRegistry) Plugins(_ context.Context) []*plugins.Plugin {
var res []*plugins.Plugin
for _, p := range f.store {
res = append(res, p)
}
return res
}
func (f *fakePluginRegistry) Add(_ context.Context, p *plugins.Plugin) error {
f.store[p.ID] = p
return nil
}
func (f *fakePluginRegistry) Remove(_ context.Context, id string) error {
delete(f.store, id)
return nil
}