mirror of https://github.com/grafana/grafana
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 APIpull/54468/head
parent
acbbdccba9
commit
4a707e2a88
@ -0,0 +1,35 @@ |
||||
package dashboards |
||||
|
||||
import ( |
||||
"context" |
||||
"io" |
||||
) |
||||
|
||||
// FileStore is the interface for plugin dashboard file storage.
|
||||
type FileStore interface { |
||||
// ListPluginDashboardFiles lists plugin dashboard files.
|
||||
ListPluginDashboardFiles(ctx context.Context, args *ListPluginDashboardFilesArgs) (*ListPluginDashboardFilesResult, error) |
||||
// GetPluginDashboardFileContents gets the referenced plugin dashboard file content.
|
||||
GetPluginDashboardFileContents(ctx context.Context, args *GetPluginDashboardFileContentsArgs) (*GetPluginDashboardFileContentsResult, error) |
||||
} |
||||
|
||||
// ListPluginDashboardFilesArgs list plugin dashboard files argument model.
|
||||
type ListPluginDashboardFilesArgs struct { |
||||
PluginID string |
||||
} |
||||
|
||||
// ListPluginDashboardFilesResult list plugin dashboard files result model.
|
||||
type ListPluginDashboardFilesResult struct { |
||||
FileReferences []string |
||||
} |
||||
|
||||
// GetPluginDashboardFileContentsArgs get plugin dashboard file content argument model.
|
||||
type GetPluginDashboardFileContentsArgs struct { |
||||
PluginID string |
||||
FileReference string |
||||
} |
||||
|
||||
// GetPluginDashboardFileContentsResult get plugin dashboard file content result model.
|
||||
type GetPluginDashboardFileContentsResult struct { |
||||
Content io.ReadCloser |
||||
} |
@ -0,0 +1,260 @@ |
||||
package fakes |
||||
|
||||
import ( |
||||
"archive/zip" |
||||
"context" |
||||
"sync" |
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend" |
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log" |
||||
"github.com/grafana/grafana/pkg/plugins" |
||||
"github.com/grafana/grafana/pkg/plugins/backendplugin" |
||||
"github.com/grafana/grafana/pkg/plugins/repo" |
||||
"github.com/grafana/grafana/pkg/plugins/storage" |
||||
) |
||||
|
||||
type FakeLoader struct { |
||||
LoadFunc func(_ context.Context, _ plugins.Class, paths []string, _ map[string]struct{}) ([]*plugins.Plugin, error) |
||||
|
||||
LoadedPaths []string |
||||
} |
||||
|
||||
func (l *FakeLoader) Load(ctx context.Context, class plugins.Class, paths []string, ignore map[string]struct{}) ([]*plugins.Plugin, error) { |
||||
if l.LoadFunc != nil { |
||||
return l.LoadFunc(ctx, class, paths, ignore) |
||||
} |
||||
|
||||
l.LoadedPaths = append(l.LoadedPaths, paths...) |
||||
|
||||
return nil, nil |
||||
} |
||||
|
||||
type FakePluginClient struct { |
||||
ID string |
||||
Managed bool |
||||
Log log.Logger |
||||
|
||||
startCount int |
||||
stopCount int |
||||
exited bool |
||||
decommissioned bool |
||||
backend.CollectMetricsHandlerFunc |
||||
backend.CheckHealthHandlerFunc |
||||
backend.QueryDataHandlerFunc |
||||
backend.CallResourceHandlerFunc |
||||
mutex sync.RWMutex |
||||
|
||||
backendplugin.Plugin |
||||
} |
||||
|
||||
func (pc *FakePluginClient) PluginID() string { |
||||
return pc.ID |
||||
} |
||||
|
||||
func (pc *FakePluginClient) Logger() log.Logger { |
||||
return pc.Log |
||||
} |
||||
|
||||
func (pc *FakePluginClient) Start(_ context.Context) error { |
||||
pc.mutex.Lock() |
||||
defer pc.mutex.Unlock() |
||||
pc.exited = false |
||||
pc.startCount++ |
||||
return nil |
||||
} |
||||
|
||||
func (pc *FakePluginClient) Stop(_ context.Context) error { |
||||
pc.mutex.Lock() |
||||
defer pc.mutex.Unlock() |
||||
pc.stopCount++ |
||||
pc.exited = true |
||||
return nil |
||||
} |
||||
|
||||
func (pc *FakePluginClient) IsManaged() bool { |
||||
return pc.Managed |
||||
} |
||||
|
||||
func (pc *FakePluginClient) Exited() bool { |
||||
pc.mutex.RLock() |
||||
defer pc.mutex.RUnlock() |
||||
return pc.exited |
||||
} |
||||
|
||||
func (pc *FakePluginClient) Decommission() error { |
||||
pc.mutex.Lock() |
||||
defer pc.mutex.Unlock() |
||||
pc.decommissioned = true |
||||
return nil |
||||
} |
||||
|
||||
func (pc *FakePluginClient) IsDecommissioned() bool { |
||||
pc.mutex.RLock() |
||||
defer pc.mutex.RUnlock() |
||||
return pc.decommissioned |
||||
} |
||||
|
||||
func (pc *FakePluginClient) CollectMetrics(ctx context.Context, req *backend.CollectMetricsRequest) (*backend.CollectMetricsResult, error) { |
||||
if pc.CollectMetricsHandlerFunc != nil { |
||||
return pc.CollectMetricsHandlerFunc(ctx, req) |
||||
} |
||||
|
||||
return nil, backendplugin.ErrMethodNotImplemented |
||||
} |
||||
|
||||
func (pc *FakePluginClient) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) { |
||||
if pc.CheckHealthHandlerFunc != nil { |
||||
return pc.CheckHealthHandlerFunc(ctx, req) |
||||
} |
||||
|
||||
return nil, backendplugin.ErrMethodNotImplemented |
||||
} |
||||
|
||||
func (pc *FakePluginClient) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { |
||||
if pc.QueryDataHandlerFunc != nil { |
||||
return pc.QueryDataHandlerFunc(ctx, req) |
||||
} |
||||
|
||||
return nil, backendplugin.ErrMethodNotImplemented |
||||
} |
||||
|
||||
func (pc *FakePluginClient) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { |
||||
if pc.CallResourceHandlerFunc != nil { |
||||
return pc.CallResourceHandlerFunc(ctx, req, sender) |
||||
} |
||||
|
||||
return backendplugin.ErrMethodNotImplemented |
||||
} |
||||
|
||||
func (pc *FakePluginClient) SubscribeStream(_ context.Context, _ *backend.SubscribeStreamRequest) (*backend.SubscribeStreamResponse, error) { |
||||
return nil, backendplugin.ErrMethodNotImplemented |
||||
} |
||||
|
||||
func (pc *FakePluginClient) PublishStream(_ context.Context, _ *backend.PublishStreamRequest) (*backend.PublishStreamResponse, error) { |
||||
return nil, backendplugin.ErrMethodNotImplemented |
||||
} |
||||
|
||||
func (pc *FakePluginClient) RunStream(_ context.Context, _ *backend.RunStreamRequest, _ *backend.StreamSender) error { |
||||
return backendplugin.ErrMethodNotImplemented |
||||
} |
||||
|
||||
type FakePluginRegistry struct { |
||||
Store map[string]*plugins.Plugin |
||||
} |
||||
|
||||
func NewFakePluginRegistry() *FakePluginRegistry { |
||||
return &FakePluginRegistry{ |
||||
Store: make(map[string]*plugins.Plugin), |
||||
} |
||||
} |
||||
|
||||
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 |
||||
} |
||||
|
||||
type FakePluginRepo struct { |
||||
GetPluginArchiveFunc func(_ context.Context, pluginID, version string, _ repo.CompatOpts) (*repo.PluginArchive, error) |
||||
GetPluginArchiveByURLFunc func(_ context.Context, archiveURL string, _ repo.CompatOpts) (*repo.PluginArchive, error) |
||||
GetPluginDownloadOptionsFunc func(_ context.Context, pluginID, version string, _ repo.CompatOpts) (*repo.PluginDownloadOptions, error) |
||||
} |
||||
|
||||
// GetPluginArchive fetches the requested plugin archive.
|
||||
func (r *FakePluginRepo) GetPluginArchive(ctx context.Context, pluginID, version string, opts repo.CompatOpts) (*repo.PluginArchive, error) { |
||||
if r.GetPluginArchiveFunc != nil { |
||||
return r.GetPluginArchiveFunc(ctx, pluginID, version, opts) |
||||
} |
||||
|
||||
return &repo.PluginArchive{}, nil |
||||
} |
||||
|
||||
// GetPluginArchiveByURL fetches the requested plugin from the specified URL.
|
||||
func (r *FakePluginRepo) GetPluginArchiveByURL(ctx context.Context, archiveURL string, opts repo.CompatOpts) (*repo.PluginArchive, error) { |
||||
if r.GetPluginArchiveByURLFunc != nil { |
||||
return r.GetPluginArchiveByURLFunc(ctx, archiveURL, opts) |
||||
} |
||||
|
||||
return &repo.PluginArchive{}, nil |
||||
} |
||||
|
||||
// GetPluginDownloadOptions fetches information for downloading the requested plugin.
|
||||
func (r *FakePluginRepo) GetPluginDownloadOptions(ctx context.Context, pluginID, version string, opts repo.CompatOpts) (*repo.PluginDownloadOptions, error) { |
||||
if r.GetPluginDownloadOptionsFunc != nil { |
||||
return r.GetPluginDownloadOptionsFunc(ctx, pluginID, version, opts) |
||||
} |
||||
return &repo.PluginDownloadOptions{}, nil |
||||
} |
||||
|
||||
type FakePluginStorage struct { |
||||
AddFunc func(_ context.Context, pluginID string, z *zip.ReadCloser) (*storage.ExtractedPluginArchive, error) |
||||
RemoveFunc func(_ context.Context, pluginID string) error |
||||
Added map[string]string |
||||
Removed map[string]int |
||||
} |
||||
|
||||
func (s *FakePluginStorage) Add(ctx context.Context, pluginID string, z *zip.ReadCloser) (*storage.ExtractedPluginArchive, error) { |
||||
s.Added[pluginID] = z.File[0].Name |
||||
if s.AddFunc != nil { |
||||
return s.AddFunc(ctx, pluginID, z) |
||||
} |
||||
return &storage.ExtractedPluginArchive{}, nil |
||||
} |
||||
|
||||
func (s *FakePluginStorage) Remove(ctx context.Context, pluginID string) error { |
||||
s.Removed[pluginID]++ |
||||
if s.RemoveFunc != nil { |
||||
return s.RemoveFunc(ctx, pluginID) |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
type FakeProcessManager struct { |
||||
StartFunc func(_ context.Context, pluginID string) error |
||||
StopFunc func(_ context.Context, pluginID string) error |
||||
Started map[string]int |
||||
Stopped map[string]int |
||||
} |
||||
|
||||
func NewFakeProcessManager() *FakeProcessManager { |
||||
return &FakeProcessManager{ |
||||
Started: make(map[string]int), |
||||
Stopped: make(map[string]int), |
||||
} |
||||
} |
||||
|
||||
func (m *FakeProcessManager) Start(ctx context.Context, pluginID string) error { |
||||
m.Started[pluginID]++ |
||||
if m.StartFunc != nil { |
||||
return m.StartFunc(ctx, pluginID) |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
func (m *FakeProcessManager) Stop(ctx context.Context, pluginID string) error { |
||||
m.Stopped[pluginID]++ |
||||
if m.StopFunc != nil { |
||||
return m.StopFunc(ctx, pluginID) |
||||
} |
||||
return nil |
||||
} |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,10 @@ |
||||
package process |
||||
|
||||
import "context" |
||||
|
||||
type Service interface { |
||||
// Start executes a backend plugin process.
|
||||
Start(ctx context.Context, pluginID string) error |
||||
// Stop terminates a backend plugin process.
|
||||
Stop(ctx context.Context, pluginID string) error |
||||
} |
@ -0,0 +1,145 @@ |
||||
package process |
||||
|
||||
import ( |
||||
"context" |
||||
"errors" |
||||
"sync" |
||||
"time" |
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log" |
||||
"github.com/grafana/grafana/pkg/plugins" |
||||
"github.com/grafana/grafana/pkg/plugins/backendplugin" |
||||
"github.com/grafana/grafana/pkg/plugins/manager/registry" |
||||
) |
||||
|
||||
var _ Service = (*Manager)(nil) |
||||
|
||||
type Manager struct { |
||||
pluginRegistry registry.Service |
||||
|
||||
mu sync.Mutex |
||||
log log.Logger |
||||
} |
||||
|
||||
func ProvideService(pluginRegistry registry.Service) *Manager { |
||||
return NewManager(pluginRegistry) |
||||
} |
||||
|
||||
func NewManager(pluginRegistry registry.Service) *Manager { |
||||
return &Manager{ |
||||
pluginRegistry: pluginRegistry, |
||||
log: log.New("plugin.process.manager"), |
||||
} |
||||
} |
||||
|
||||
func (m *Manager) Run(ctx context.Context) error { |
||||
<-ctx.Done() |
||||
m.shutdown(ctx) |
||||
return ctx.Err() |
||||
} |
||||
|
||||
func (m *Manager) Start(ctx context.Context, pluginID string) error { |
||||
p, exists := m.pluginRegistry.Plugin(ctx, pluginID) |
||||
if !exists { |
||||
return backendplugin.ErrPluginNotRegistered |
||||
} |
||||
|
||||
if !p.IsManaged() || !p.Backend || p.SignatureError != nil { |
||||
return nil |
||||
} |
||||
|
||||
if p.IsCorePlugin() { |
||||
return nil |
||||
} |
||||
|
||||
m.log.Info("Plugin registered", "pluginID", p.ID) |
||||
m.mu.Lock() |
||||
if err := startPluginAndRestartKilledProcesses(ctx, p); err != nil { |
||||
return err |
||||
} |
||||
|
||||
p.Logger().Debug("Successfully started backend plugin process") |
||||
m.mu.Unlock() |
||||
return nil |
||||
} |
||||
|
||||
func (m *Manager) Stop(ctx context.Context, pluginID string) error { |
||||
p, exists := m.pluginRegistry.Plugin(ctx, pluginID) |
||||
if !exists { |
||||
return backendplugin.ErrPluginNotRegistered |
||||
} |
||||
m.log.Debug("Stopping plugin process", "pluginID", p.ID) |
||||
m.mu.Lock() |
||||
defer m.mu.Unlock() |
||||
|
||||
if err := p.Decommission(); err != nil { |
||||
return err |
||||
} |
||||
|
||||
if err := p.Stop(ctx); err != nil { |
||||
return err |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
// shutdown stops all backend plugin processes
|
||||
func (m *Manager) shutdown(ctx context.Context) { |
||||
var wg sync.WaitGroup |
||||
for _, p := range m.pluginRegistry.Plugins(ctx) { |
||||
wg.Add(1) |
||||
go func(p backendplugin.Plugin, ctx context.Context) { |
||||
defer wg.Done() |
||||
p.Logger().Debug("Stopping plugin") |
||||
if err := p.Stop(ctx); err != nil { |
||||
p.Logger().Error("Failed to stop plugin", "error", err) |
||||
} |
||||
p.Logger().Debug("Plugin stopped") |
||||
}(p, ctx) |
||||
} |
||||
wg.Wait() |
||||
} |
||||
|
||||
func startPluginAndRestartKilledProcesses(ctx context.Context, p *plugins.Plugin) error { |
||||
if err := p.Start(ctx); err != nil { |
||||
return err |
||||
} |
||||
|
||||
go func(ctx context.Context, p *plugins.Plugin) { |
||||
if err := restartKilledProcess(ctx, p); err != nil { |
||||
p.Logger().Error("Attempt to restart killed plugin process failed", "error", err) |
||||
} |
||||
}(ctx, p) |
||||
|
||||
return nil |
||||
} |
||||
|
||||
func restartKilledProcess(ctx context.Context, p *plugins.Plugin) error { |
||||
ticker := time.NewTicker(time.Second * 1) |
||||
|
||||
for { |
||||
select { |
||||
case <-ctx.Done(): |
||||
if err := ctx.Err(); err != nil && !errors.Is(err, context.Canceled) { |
||||
return err |
||||
} |
||||
return nil |
||||
case <-ticker.C: |
||||
if p.IsDecommissioned() { |
||||
p.Logger().Debug("Plugin decommissioned") |
||||
return nil |
||||
} |
||||
|
||||
if !p.Exited() { |
||||
continue |
||||
} |
||||
|
||||
p.Logger().Debug("Restarting plugin") |
||||
if err := p.Start(ctx); err != nil { |
||||
p.Logger().Error("Failed to restart plugin", "error", err) |
||||
continue |
||||
} |
||||
p.Logger().Debug("Plugin restarted") |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,300 @@ |
||||
package process |
||||
|
||||
import ( |
||||
"context" |
||||
"sync" |
||||
"testing" |
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log" |
||||
"github.com/grafana/grafana/pkg/plugins" |
||||
"github.com/grafana/grafana/pkg/plugins/backendplugin" |
||||
"github.com/stretchr/testify/require" |
||||
) |
||||
|
||||
func TestProcessManager_Start(t *testing.T) { |
||||
t.Run("Plugin not found in registry", func(t *testing.T) { |
||||
m := NewManager(newFakePluginRegistry(map[string]*plugins.Plugin{})) |
||||
err := m.Start(context.Background(), "non-existing-datasource") |
||||
require.ErrorIs(t, err, backendplugin.ErrPluginNotRegistered) |
||||
}) |
||||
|
||||
t.Run("Cannot start a core plugin", func(t *testing.T) { |
||||
pluginID := "core-datasource" |
||||
|
||||
bp := newFakeBackendPlugin(true) |
||||
p := createPlugin(t, bp, func(plugin *plugins.Plugin) { |
||||
plugin.ID = pluginID |
||||
plugin.Class = plugins.Core |
||||
plugin.Backend = true |
||||
}) |
||||
|
||||
m := NewManager(newFakePluginRegistry(map[string]*plugins.Plugin{ |
||||
pluginID: p, |
||||
})) |
||||
err := m.Start(context.Background(), pluginID) |
||||
require.NoError(t, err) |
||||
require.True(t, p.Exited()) |
||||
require.Zero(t, bp.startCount) |
||||
}) |
||||
|
||||
t.Run("Plugin state determines process start", func(t *testing.T) { |
||||
tcs := []struct { |
||||
name string |
||||
managed bool |
||||
backend bool |
||||
signatureError *plugins.SignatureError |
||||
expectedStartCount int |
||||
}{ |
||||
{ |
||||
name: "Unmanaged backend plugin will not be started", |
||||
managed: false, |
||||
backend: true, |
||||
expectedStartCount: 0, |
||||
}, |
||||
{ |
||||
name: "Managed non-backend plugin will not be started", |
||||
managed: false, |
||||
backend: true, |
||||
expectedStartCount: 0, |
||||
}, |
||||
{ |
||||
name: "Managed backend plugin with signature error will not be started", |
||||
managed: true, |
||||
backend: true, |
||||
signatureError: &plugins.SignatureError{ |
||||
SignatureStatus: plugins.SignatureUnsigned, |
||||
}, |
||||
expectedStartCount: 0, |
||||
}, |
||||
{ |
||||
name: "Managed backend plugin with no signature errors will be started", |
||||
managed: true, |
||||
backend: true, |
||||
expectedStartCount: 1, |
||||
}, |
||||
} |
||||
for _, tc := range tcs { |
||||
t.Run(tc.name, func(t *testing.T) { |
||||
bp := newFakeBackendPlugin(tc.managed) |
||||
p := createPlugin(t, bp, func(plugin *plugins.Plugin) { |
||||
plugin.Backend = tc.backend |
||||
plugin.SignatureError = tc.signatureError |
||||
}) |
||||
|
||||
m := NewManager(newFakePluginRegistry(map[string]*plugins.Plugin{ |
||||
p.ID: p, |
||||
})) |
||||
|
||||
err := m.Start(context.Background(), p.ID) |
||||
require.NoError(t, err) |
||||
require.Equal(t, tc.expectedStartCount, bp.startCount) |
||||
|
||||
if tc.expectedStartCount > 0 { |
||||
require.True(t, !p.Exited()) |
||||
} else { |
||||
require.True(t, p.Exited()) |
||||
} |
||||
}) |
||||
} |
||||
}) |
||||
} |
||||
|
||||
func TestProcessManager_Stop(t *testing.T) { |
||||
t.Run("Plugin not found in registry", func(t *testing.T) { |
||||
m := NewManager(newFakePluginRegistry(map[string]*plugins.Plugin{})) |
||||
err := m.Stop(context.Background(), "non-existing-datasource") |
||||
require.ErrorIs(t, err, backendplugin.ErrPluginNotRegistered) |
||||
}) |
||||
|
||||
t.Run("Can stop a running plugin", func(t *testing.T) { |
||||
pluginID := "test-datasource" |
||||
|
||||
bp := newFakeBackendPlugin(true) |
||||
p := createPlugin(t, bp, func(plugin *plugins.Plugin) { |
||||
plugin.ID = pluginID |
||||
plugin.Backend = true |
||||
}) |
||||
|
||||
m := NewManager(newFakePluginRegistry(map[string]*plugins.Plugin{ |
||||
pluginID: p, |
||||
})) |
||||
err := m.Stop(context.Background(), pluginID) |
||||
require.NoError(t, err) |
||||
|
||||
require.True(t, p.IsDecommissioned()) |
||||
require.True(t, bp.decommissioned) |
||||
require.True(t, p.Exited()) |
||||
require.Equal(t, 1, bp.stopCount) |
||||
}) |
||||
} |
||||
|
||||
func TestProcessManager_ManagedBackendPluginLifecycle(t *testing.T) { |
||||
bp := newFakeBackendPlugin(true) |
||||
p := createPlugin(t, bp, func(plugin *plugins.Plugin) { |
||||
plugin.Backend = true |
||||
}) |
||||
|
||||
m := NewManager(newFakePluginRegistry(map[string]*plugins.Plugin{ |
||||
p.ID: p, |
||||
})) |
||||
|
||||
err := m.Start(context.Background(), p.ID) |
||||
require.NoError(t, err) |
||||
require.Equal(t, 1, bp.startCount) |
||||
|
||||
t.Run("When plugin process is killed, the process is restarted", func(t *testing.T) { |
||||
pCtx := context.Background() |
||||
cCtx, cancel := context.WithCancel(pCtx) |
||||
var wgRun sync.WaitGroup |
||||
wgRun.Add(1) |
||||
var runErr error |
||||
go func() { |
||||
runErr = m.Run(cCtx) |
||||
wgRun.Done() |
||||
}() |
||||
|
||||
var wgKill sync.WaitGroup |
||||
wgKill.Add(1) |
||||
go func() { |
||||
bp.kill() // manually kill process
|
||||
for { |
||||
if !bp.Exited() { |
||||
break |
||||
} |
||||
} |
||||
wgKill.Done() |
||||
}() |
||||
wgKill.Wait() |
||||
require.True(t, !p.Exited()) |
||||
require.Equal(t, 2, bp.startCount) |
||||
require.Equal(t, 0, bp.stopCount) |
||||
|
||||
t.Run("When context is cancelled the plugin is stopped", func(t *testing.T) { |
||||
cancel() |
||||
wgRun.Wait() |
||||
require.ErrorIs(t, runErr, context.Canceled) |
||||
require.True(t, p.Exited()) |
||||
require.Equal(t, 2, bp.startCount) |
||||
require.Equal(t, 1, bp.stopCount) |
||||
}) |
||||
}) |
||||
} |
||||
|
||||
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 |
||||
} |
||||
|
||||
type fakeBackendPlugin struct { |
||||
managed bool |
||||
|
||||
startCount int |
||||
stopCount int |
||||
decommissioned bool |
||||
running bool |
||||
|
||||
mutex sync.RWMutex |
||||
backendplugin.Plugin |
||||
} |
||||
|
||||
func newFakeBackendPlugin(managed bool) *fakeBackendPlugin { |
||||
return &fakeBackendPlugin{ |
||||
managed: managed, |
||||
} |
||||
} |
||||
|
||||
func (p *fakeBackendPlugin) Start(_ context.Context) error { |
||||
p.mutex.Lock() |
||||
defer p.mutex.Unlock() |
||||
p.running = true |
||||
p.startCount++ |
||||
return nil |
||||
} |
||||
|
||||
func (p *fakeBackendPlugin) Stop(_ context.Context) error { |
||||
p.mutex.Lock() |
||||
defer p.mutex.Unlock() |
||||
p.running = false |
||||
p.stopCount++ |
||||
return nil |
||||
} |
||||
|
||||
func (p *fakeBackendPlugin) Decommission() error { |
||||
p.mutex.Lock() |
||||
defer p.mutex.Unlock() |
||||
p.decommissioned = true |
||||
return nil |
||||
} |
||||
|
||||
func (p *fakeBackendPlugin) IsDecommissioned() bool { |
||||
p.mutex.RLock() |
||||
defer p.mutex.RUnlock() |
||||
return p.decommissioned |
||||
} |
||||
|
||||
func (p *fakeBackendPlugin) IsManaged() bool { |
||||
p.mutex.RLock() |
||||
defer p.mutex.RUnlock() |
||||
return p.managed |
||||
} |
||||
|
||||
func (p *fakeBackendPlugin) Exited() bool { |
||||
p.mutex.RLock() |
||||
defer p.mutex.RUnlock() |
||||
return !p.running |
||||
} |
||||
|
||||
func (p *fakeBackendPlugin) kill() { |
||||
p.mutex.Lock() |
||||
defer p.mutex.Unlock() |
||||
p.running = false |
||||
} |
||||
|
||||
func createPlugin(t *testing.T, bp backendplugin.Plugin, cbs ...func(p *plugins.Plugin)) *plugins.Plugin { |
||||
t.Helper() |
||||
|
||||
p := &plugins.Plugin{ |
||||
Class: plugins.External, |
||||
JSONData: plugins.JSONData{ |
||||
ID: "test-datasource", |
||||
}, |
||||
} |
||||
|
||||
p.SetLogger(log.NewNopLogger()) |
||||
p.RegisterClient(bp) |
||||
|
||||
for _, cb := range cbs { |
||||
cb(p) |
||||
} |
||||
|
||||
return p |
||||
} |
@ -1,176 +0,0 @@ |
||||
package manager |
||||
|
||||
import ( |
||||
"context" |
||||
"fmt" |
||||
|
||||
"github.com/grafana/grafana/pkg/plugins" |
||||
"github.com/grafana/grafana/pkg/plugins/repo" |
||||
) |
||||
|
||||
func (m *PluginManager) Plugin(ctx context.Context, pluginID string) (plugins.PluginDTO, bool) { |
||||
p, exists := m.plugin(ctx, pluginID) |
||||
if !exists { |
||||
return plugins.PluginDTO{}, false |
||||
} |
||||
|
||||
return p.ToDTO(), true |
||||
} |
||||
|
||||
func (m *PluginManager) 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 m.availablePlugins(ctx) { |
||||
if _, exists := requestedTypes[p.Type]; exists { |
||||
pluginsList = append(pluginsList, p.ToDTO()) |
||||
} |
||||
} |
||||
return pluginsList |
||||
} |
||||
|
||||
// plugin finds a plugin with `pluginID` from the registry that is not decommissioned
|
||||
func (m *PluginManager) plugin(ctx context.Context, pluginID string) (*plugins.Plugin, bool) { |
||||
p, exists := m.pluginRegistry.Plugin(ctx, pluginID) |
||||
if !exists || p.IsDecommissioned() { |
||||
return nil, false |
||||
} |
||||
|
||||
return p, true |
||||
} |
||||
|
||||
// availablePlugins returns all non-decommissioned plugins from the registry
|
||||
func (m *PluginManager) availablePlugins(ctx context.Context) []*plugins.Plugin { |
||||
var res []*plugins.Plugin |
||||
for _, p := range m.pluginRegistry.Plugins(ctx) { |
||||
if !p.IsDecommissioned() { |
||||
res = append(res, p) |
||||
} |
||||
} |
||||
return res |
||||
} |
||||
|
||||
// registeredPlugins returns all registered plugins from the registry
|
||||
func (m *PluginManager) registeredPlugins(ctx context.Context) map[string]struct{} { |
||||
pluginsByID := make(map[string]struct{}) |
||||
for _, p := range m.pluginRegistry.Plugins(ctx) { |
||||
pluginsByID[p.ID] = struct{}{} |
||||
} |
||||
|
||||
return pluginsByID |
||||
} |
||||
|
||||
func (m *PluginManager) Add(ctx context.Context, pluginID, version string, opts plugins.CompatOpts) error { |
||||
compatOpts := repo.NewCompatOpts(opts.GrafanaVersion, opts.OS, opts.Arch) |
||||
|
||||
var pluginArchive *repo.PluginArchive |
||||
if plugin, exists := m.plugin(ctx, pluginID); exists { |
||||
if !plugin.IsExternalPlugin() { |
||||
return plugins.ErrInstallCorePlugin |
||||
} |
||||
|
||||
if plugin.Info.Version == version { |
||||
return plugins.DuplicateError{ |
||||
PluginID: plugin.ID, |
||||
ExistingPluginDir: plugin.PluginDir, |
||||
} |
||||
} |
||||
|
||||
// get plugin update information to confirm if target update is possible
|
||||
dlOpts, err := m.pluginRepo.GetPluginDownloadOptions(ctx, pluginID, version, compatOpts) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
// if existing plugin version is the same as the target update version
|
||||
if dlOpts.Version == plugin.Info.Version { |
||||
return plugins.DuplicateError{ |
||||
PluginID: plugin.ID, |
||||
ExistingPluginDir: plugin.PluginDir, |
||||
} |
||||
} |
||||
|
||||
if dlOpts.PluginZipURL == "" && dlOpts.Version == "" { |
||||
return fmt.Errorf("could not determine update options for %s", pluginID) |
||||
} |
||||
|
||||
// remove existing installation of plugin
|
||||
err = m.Remove(ctx, plugin.ID) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
if dlOpts.PluginZipURL != "" { |
||||
pluginArchive, err = m.pluginRepo.GetPluginArchiveByURL(ctx, dlOpts.PluginZipURL, compatOpts) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
} else { |
||||
pluginArchive, err = m.pluginRepo.GetPluginArchive(ctx, pluginID, dlOpts.Version, compatOpts) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
} |
||||
} else { |
||||
var err error |
||||
pluginArchive, err = m.pluginRepo.GetPluginArchive(ctx, pluginID, version, compatOpts) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
} |
||||
|
||||
extractedArchive, err := m.pluginStorage.Add(ctx, pluginID, pluginArchive.File) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
// download dependency plugins
|
||||
pathsToScan := []string{extractedArchive.Path} |
||||
for _, dep := range extractedArchive.Dependencies { |
||||
m.log.Info("Fetching %s dependencies...", dep.ID) |
||||
d, err := m.pluginRepo.GetPluginArchive(ctx, dep.ID, dep.Version, compatOpts) |
||||
if err != nil { |
||||
return fmt.Errorf("%v: %w", fmt.Sprintf("failed to download plugin %s from repository", dep.ID), err) |
||||
} |
||||
|
||||
depArchive, err := m.pluginStorage.Add(ctx, dep.ID, d.File) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
pathsToScan = append(pathsToScan, depArchive.Path) |
||||
} |
||||
|
||||
err = m.loadPlugins(context.Background(), plugins.External, pathsToScan...) |
||||
if err != nil { |
||||
m.log.Error("Could not load plugins", "paths", pathsToScan, "err", err) |
||||
return err |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
func (m *PluginManager) Remove(ctx context.Context, pluginID string) error { |
||||
plugin, exists := m.plugin(ctx, pluginID) |
||||
if !exists { |
||||
return plugins.ErrPluginNotInstalled |
||||
} |
||||
|
||||
if !plugin.IsExternalPlugin() { |
||||
return plugins.ErrUninstallCorePlugin |
||||
} |
||||
|
||||
if err := m.unregisterAndStop(ctx, plugin); err != nil { |
||||
return err |
||||
} |
||||
|
||||
return m.pluginStorage.Remove(ctx, plugin.ID) |
||||
} |
@ -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 |
||||
} |
@ -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 |
||||
} |
Loading…
Reference in new issue