Plugins: Create single point of entry for adding / removing plugins (#55463)

* split out plugin manager

* remove whitespace

* fix tests

* split up tests

* updating naming conventions

* simplify manager

* tidy

* explorations

* fix build

* tidy

* fix tests

* add logger helper

* pass the tests

* tidying

* fix tests

* tidy and re-add test

* store depends on loader

* enrich tests

* fix test

* undo gomod changes
This commit is contained in:
Will Browne
2022-09-23 14:27:01 +02:00
committed by GitHub
parent 003a1cdaa0
commit d0d8544ded
21 changed files with 785 additions and 622 deletions

View File

@ -9,16 +9,12 @@ import (
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/infra/log/logtest"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/backendplugin"
"github.com/grafana/grafana/pkg/plugins/backendplugin/coreplugin"
"github.com/grafana/grafana/pkg/plugins/backendplugin/provider"
"github.com/grafana/grafana/pkg/plugins/config"
"github.com/grafana/grafana/pkg/plugins/manager/loader/finder"
"github.com/grafana/grafana/pkg/plugins/manager/fakes"
"github.com/grafana/grafana/pkg/plugins/manager/loader/initializer"
"github.com/grafana/grafana/pkg/plugins/manager/signature"
"github.com/grafana/grafana/pkg/services/org"
@ -39,13 +35,12 @@ func TestLoader_Load(t *testing.T) {
return
}
tests := []struct {
name string
class plugins.Class
cfg *config.Cfg
pluginPaths []string
existingPlugins map[string]struct{}
want []*plugins.Plugin
pluginErrors map[string]*plugins.Error
name string
class plugins.Class
cfg *config.Cfg
pluginPaths []string
want []*plugins.Plugin
pluginErrors map[string]*plugins.Error
}{
{
name: "Load a Core plugin",
@ -411,19 +406,31 @@ func TestLoader_Load(t *testing.T) {
},
}
for _, tt := range tests {
l := newLoader(tt.cfg)
reg := fakes.NewFakePluginRegistry()
storage := fakes.NewFakePluginStorage()
procPrvdr := fakes.NewFakeBackendProcessProvider()
procMgr := fakes.NewFakeProcessManager()
l := newLoader(tt.cfg, func(l *Loader) {
l.pluginRegistry = reg
l.pluginStorage = storage
l.processManager = procMgr
l.pluginInitializer = initializer.New(tt.cfg, procPrvdr, &fakes.FakeLicensingService{})
})
t.Run(tt.name, func(t *testing.T) {
got, err := l.Load(context.Background(), tt.class, tt.pluginPaths, tt.existingPlugins)
got, err := l.Load(context.Background(), 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()
assert.Equal(t, len(tt.pluginErrors), len(pluginErrs))
require.Equal(t, len(tt.pluginErrors), len(pluginErrs))
for _, pluginErr := range pluginErrs {
assert.Equal(t, tt.pluginErrors[pluginErr.PluginID], pluginErr)
require.Equal(t, tt.pluginErrors[pluginErr.PluginID], pluginErr)
}
verifyState(t, tt.want, reg, procPrvdr, storage, procMgr)
})
}
}
@ -554,7 +561,16 @@ func TestLoader_Load_MultiplePlugins(t *testing.T) {
}
for _, tt := range tests {
l := newLoader(tt.cfg)
reg := fakes.NewFakePluginRegistry()
storage := fakes.NewFakePluginStorage()
procPrvdr := fakes.NewFakeBackendProcessProvider()
procMgr := fakes.NewFakeProcessManager()
l := newLoader(tt.cfg, func(l *Loader) {
l.pluginRegistry = reg
l.pluginStorage = storage
l.processManager = procMgr
l.pluginInitializer = initializer.New(tt.cfg, procPrvdr, fakes.NewFakeLicensingService())
})
t.Run(tt.name, func(t *testing.T) {
origAppURL := setting.AppUrl
t.Cleanup(func() {
@ -562,7 +578,7 @@ func TestLoader_Load_MultiplePlugins(t *testing.T) {
})
setting.AppUrl = tt.appURL
got, err := l.Load(context.Background(), plugins.External, tt.pluginPaths, tt.existingPlugins)
got, err := l.Load(context.Background(), plugins.External, tt.pluginPaths)
require.NoError(t, err)
sort.SliceStable(got, func(i, j int) bool {
return got[i].ID < got[j].ID
@ -575,12 +591,13 @@ func TestLoader_Load_MultiplePlugins(t *testing.T) {
for _, pluginErr := range pluginErrs {
require.Equal(t, tt.pluginErrors[pluginErr.PluginID], pluginErr)
}
verifyState(t, tt.want, reg, procPrvdr, storage, procMgr)
})
}
})
}
func TestLoader_Signature_RootURL(t *testing.T) {
func TestLoader_Load_Signature_RootURL(t *testing.T) {
const defaultAppURL = "http://localhost:3000/grafana"
parentDir, err := filepath.Abs("../")
@ -630,13 +647,23 @@ func TestLoader_Signature_RootURL(t *testing.T) {
},
}
l := newLoader(&config.Cfg{})
got, err := l.Load(context.Background(), plugins.External, paths, map[string]struct{}{})
assert.NoError(t, err)
reg := fakes.NewFakePluginRegistry()
storage := fakes.NewFakePluginStorage()
procPrvdr := fakes.NewFakeBackendProcessProvider()
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(), plugins.External, 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, storage, procMgr)
})
}
@ -699,18 +726,28 @@ func TestLoader_Load_DuplicatePlugins(t *testing.T) {
},
}
l := newLoader(&config.Cfg{})
got, err := l.Load(context.Background(), plugins.External, []string{pluginDir, pluginDir}, map[string]struct{}{})
assert.NoError(t, err)
reg := fakes.NewFakePluginRegistry()
storage := fakes.NewFakePluginStorage()
procPrvdr := fakes.NewFakeBackendProcessProvider()
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(), plugins.External, []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, storage, procMgr)
})
}
func TestLoader_loadNestedPlugins(t *testing.T) {
func TestLoader_Load_NestedPlugins(t *testing.T) {
rootDir, err := filepath.Abs("../")
if err != nil {
t.Errorf("could not construct absolute path of root dir")
@ -785,42 +822,47 @@ func TestLoader_loadNestedPlugins(t *testing.T) {
child.Parent = parent
t.Run("Load nested External plugins", func(t *testing.T) {
reg := fakes.NewFakePluginRegistry()
storage := fakes.NewFakePluginStorage()
procPrvdr := fakes.NewFakeBackendProcessProvider()
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(), plugins.External, []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}
l := newLoader(&config.Cfg{})
got, err := l.Load(context.Background(), plugins.External, []string{"../testdata/nested-plugins"}, map[string]struct{}{})
assert.NoError(t, err)
// to ensure we can compare with expected
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))
}
})
t.Run("Load will exclude plugins that already exist", func(t *testing.T) {
// parent/child links will not be created when either plugins are provided in the existingPlugins map
parent.Children = nil
expected := []*plugins.Plugin{parent}
verifyState(t, expected, reg, procPrvdr, storage, procMgr)
l := newLoader(&config.Cfg{})
t.Run("Load will exclude plugins that already exist", func(t *testing.T) {
got, err := l.Load(context.Background(), plugins.External, []string{"../testdata/nested-plugins"})
require.NoError(t, err)
got, err := l.Load(context.Background(), plugins.External, []string{"../testdata/nested-plugins"}, map[string]struct{}{
"test-panel": {},
// 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, storage, procMgr)
})
assert.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))
}
})
t.Run("Plugin child field `IncludedInAppID` is set to parent app's plugin ID", func(t *testing.T) {
@ -944,12 +986,20 @@ func TestLoader_loadNestedPlugins(t *testing.T) {
parent.Children = []*plugins.Plugin{child}
child.Parent = parent
expected := []*plugins.Plugin{parent, child}
l := newLoader(&config.Cfg{})
got, err := l.Load(context.Background(), plugins.External, []string{"../testdata/app-with-child"}, map[string]struct{}{})
assert.NoError(t, err)
reg := fakes.NewFakePluginRegistry()
storage := fakes.NewFakePluginStorage()
procPrvdr := fakes.NewFakeBackendProcessProvider()
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(), plugins.External, []string{"../testdata/app-with-child"})
require.NoError(t, err)
// to ensure we can compare with expected
sort.SliceStable(got, func(i, j int) bool {
@ -960,14 +1010,24 @@ func TestLoader_loadNestedPlugins(t *testing.T) {
t.Fatalf("Result mismatch (-want +got):\n%s", cmp.Diff(got, expected, compareOpts))
}
verifyState(t, expected, reg, procPrvdr, storage, procMgr)
t.Run("order of loaded parent and child plugins gives same output", func(t *testing.T) {
parentPluginJSON := filepath.Join(rootDir, "testdata/app-with-child/dist/plugin.json")
childPluginJSON := filepath.Join(rootDir, "testdata/app-with-child/dist/child/plugin.json")
got, err := l.loadPlugins(context.Background(), plugins.External, []string{
parentPluginJSON, childPluginJSON},
map[string]struct{}{})
assert.NoError(t, err)
reg = fakes.NewFakePluginRegistry()
storage = fakes.NewFakePluginStorage()
procPrvdr = fakes.NewFakeBackendProcessProvider()
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.loadPlugins(context.Background(), plugins.External, []string{parentPluginJSON, childPluginJSON})
require.NoError(t, err)
// to ensure we can compare with expected
sort.SliceStable(got, func(i, j int) bool {
@ -978,10 +1038,20 @@ func TestLoader_loadNestedPlugins(t *testing.T) {
t.Fatalf("Result mismatch (-want +got):\n%s", cmp.Diff(got, expected, compareOpts))
}
got, err = l.loadPlugins(context.Background(), plugins.External, []string{
childPluginJSON, parentPluginJSON},
map[string]struct{}{})
assert.NoError(t, err)
verifyState(t, expected, reg, procPrvdr, storage, procMgr)
reg = fakes.NewFakePluginRegistry()
storage = fakes.NewFakePluginStorage()
procPrvdr = fakes.NewFakeBackendProcessProvider()
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.loadPlugins(context.Background(), plugins.External, []string{childPluginJSON, parentPluginJSON})
require.NoError(t, err)
// to ensure we can compare with expected
sort.SliceStable(got, func(i, j int) bool {
@ -991,6 +1061,8 @@ func TestLoader_loadNestedPlugins(t *testing.T) {
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)
})
})
}
@ -1137,55 +1209,48 @@ func Test_setPathsBasedOnApp(t *testing.T) {
configureAppChildOPlugin(parent, child)
assert.Equal(t, "app/plugins/app/testdata-app/datasources/datasource/module", child.Module)
assert.Equal(t, "testdata-app", child.IncludedInAppID)
assert.Equal(t, "public/app/plugins/app/testdata-app", child.BaseURL)
require.Equal(t, "app/plugins/app/testdata-app/datasources/datasource/module", child.Module)
require.Equal(t, "testdata-app", child.IncludedInAppID)
require.Equal(t, "public/app/plugins/app/testdata-app", child.BaseURL)
})
}
func newLoader(cfg *config.Cfg) *Loader {
return &Loader{
pluginFinder: finder.New(),
pluginInitializer: initializer.New(cfg, provider.ProvideService(coreplugin.NewRegistry(make(map[string]backendplugin.PluginFactoryFunc))), &fakeLicensingService{}),
signatureValidator: signature.NewValidator(signature.NewUnsignedAuthorizer(cfg)),
errs: make(map[string]*plugins.SignatureError),
log: &logtest.Fake{},
func newLoader(cfg *config.Cfg, cbs ...func(loader *Loader)) *Loader {
l := New(cfg, &fakes.FakeLicensingService{}, signature.NewUnsignedAuthorizer(cfg), fakes.NewFakePluginRegistry(),
fakes.NewFakeBackendProcessProvider(), fakes.NewFakeProcessManager(), fakes.NewFakePluginStorage())
for _, cb := range cbs {
cb(l)
}
return l
}
func verifyState(t *testing.T, ps []*plugins.Plugin, reg *fakes.FakePluginRegistry,
procPrvdr *fakes.FakeBackendProcessProvider, storage *fakes.FakePluginStorage, procMngr *fakes.FakeProcessManager) {
t.Helper()
for _, p := range ps {
if !cmp.Equal(p, reg.Store[p.ID], compareOpts) {
t.Fatalf("Result mismatch (-want +got):\n%s", cmp.Diff(p, reg.Store[p.ID], 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])
}
_, exists := storage.Store[p.ID]
if p.IsExternalPlugin() {
require.True(t, exists)
} else {
require.False(t, exists)
}
require.Equal(t, 1, procMngr.Started[p.ID])
require.Zero(t, procMngr.Stopped[p.ID])
}
}
type fakeLicensingService struct {
edition string
tokenRaw string
}
func (t *fakeLicensingService) Expiry() int64 {
return 0
}
func (t *fakeLicensingService) Edition() string {
return t.edition
}
func (t *fakeLicensingService) StateInfo() string {
return ""
}
func (t *fakeLicensingService) ContentDeliveryPrefix() string {
return ""
}
func (t *fakeLicensingService) LicenseURL(_ bool) string {
return ""
}
func (t *fakeLicensingService) Environment() map[string]string {
return map[string]string{"GF_ENTERPRISE_LICENSE_TEXT": t.tokenRaw}
}
func (*fakeLicensingService) EnabledFeatures() map[string]bool {
return map[string]bool{}
}
func (*fakeLicensingService) FeatureEnabled(feature string) bool {
return false
}