From 60b4a0b2a4293f8bf21eac36a5925c60255d7289 Mon Sep 17 00:00:00 2001 From: Will Browne Date: Fri, 4 Aug 2023 11:57:49 +0200 Subject: [PATCH] Plugins: Add termination stage to plugin loader pipeline (#72822) * add termination stage * uid -> pluginID (for now) * also fix fakes * add simple test * Fix logger name Co-authored-by: Giuseppe Guerra * inline stop func call Co-authored-by: Giuseppe Guerra --------- Co-authored-by: Giuseppe Guerra --- pkg/api/plugin_resource_test.go | 5 +- pkg/plugins/manager/fakes/fakes.go | 11 ++ pkg/plugins/manager/loader/loader.go | 46 ++------ pkg/plugins/manager/loader/loader_test.go | 45 +++++++- .../manager/manager_integration_test.go | 6 +- .../manager/pipeline/bootstrap/bootstrap.go | 4 + pkg/plugins/manager/pipeline/bootstrap/doc.go | 2 +- pkg/plugins/manager/pipeline/discovery/doc.go | 2 +- pkg/plugins/manager/pipeline/doc.go | 4 +- .../manager/pipeline/initialization/doc.go | 2 +- .../manager/pipeline/initialization/steps.go | 8 +- .../pipeline/initialization/steps_test.go | 8 +- .../manager/pipeline/termination/doc.go | 5 + .../manager/pipeline/termination/steps.go | 107 ++++++++++++++++++ .../pipeline/termination/termination.go | 67 +++++++++++ .../pluginsintegration/loader/loader.go | 4 +- .../pluginsintegration/loader/loader_test.go | 21 +++- .../pluginsintegration/pipeline/pipeline.go | 17 ++- .../pluginsintegration/pluginsintegration.go | 3 + 19 files changed, 305 insertions(+), 62 deletions(-) create mode 100644 pkg/plugins/manager/pipeline/termination/doc.go create mode 100644 pkg/plugins/manager/pipeline/termination/steps.go create mode 100644 pkg/plugins/manager/pipeline/termination/termination.go diff --git a/pkg/api/plugin_resource_test.go b/pkg/api/plugin_resource_test.go index 459f3e84352..6cc39d0bd06 100644 --- a/pkg/api/plugin_resource_test.go +++ b/pkg/api/plugin_resource_test.go @@ -25,6 +25,7 @@ import ( "github.com/grafana/grafana/pkg/plugins/manager/loader" "github.com/grafana/grafana/pkg/plugins/manager/loader/assetpath" "github.com/grafana/grafana/pkg/plugins/manager/loader/finder" + "github.com/grafana/grafana/pkg/plugins/manager/process" "github.com/grafana/grafana/pkg/plugins/manager/registry" "github.com/grafana/grafana/pkg/plugins/manager/signature" "github.com/grafana/grafana/pkg/plugins/manager/signature/statickey" @@ -74,11 +75,13 @@ func TestCallResource(t *testing.T) { discovery := pipeline.ProvideDiscoveryStage(pCfg, finder.NewLocalFinder(pCfg.DevMode), reg) bootstrap := pipeline.ProvideBootstrapStage(pCfg, signature.ProvideService(pCfg, statickey.New()), assetpath.ProvideService(pluginscdn.ProvideService(pCfg))) initialize := pipeline.ProvideInitializationStage(pCfg, reg, fakes.NewFakeLicensingService(), provider.ProvideService(coreRegistry)) + terminate, err := pipeline.ProvideTerminationStage(pCfg, reg, process.NewManager(reg)) + require.NoError(t, err) l := loader.ProvideService(pCfg, signature.NewUnsignedAuthorizer(pCfg), reg, fakes.NewFakeRoleRegistry(), assetpath.ProvideService(pluginscdn.ProvideService(pCfg)), - angularInspector, &fakes.FakeOauthService{}, discovery, bootstrap, initialize) + angularInspector, &fakes.FakeOauthService{}, discovery, bootstrap, initialize, terminate) srcs := sources.ProvideService(cfg, pCfg) ps, err := store.ProvideService(reg, srcs, l) require.NoError(t, err) diff --git a/pkg/plugins/manager/fakes/fakes.go b/pkg/plugins/manager/fakes/fakes.go index 247a8167aaf..3a2958bc0ec 100644 --- a/pkg/plugins/manager/fakes/fakes.go +++ b/pkg/plugins/manager/fakes/fakes.go @@ -496,3 +496,14 @@ func (f *FakeInitializer) Initialize(ctx context.Context, ps []*plugins.Plugin) } return []*plugins.Plugin{}, nil } + +type FakeTerminator struct { + TerminateFunc func(ctx context.Context, pluginID string) error +} + +func (f *FakeTerminator) Terminate(ctx context.Context, pluginID string) error { + if f.TerminateFunc != nil { + return f.TerminateFunc(ctx, pluginID) + } + return nil +} diff --git a/pkg/plugins/manager/loader/loader.go b/pkg/plugins/manager/loader/loader.go index caa53556edd..9daa2438473 100644 --- a/pkg/plugins/manager/loader/loader.go +++ b/pkg/plugins/manager/loader/loader.go @@ -14,6 +14,7 @@ import ( "github.com/grafana/grafana/pkg/plugins/manager/pipeline/bootstrap" "github.com/grafana/grafana/pkg/plugins/manager/pipeline/discovery" "github.com/grafana/grafana/pkg/plugins/manager/pipeline/initialization" + "github.com/grafana/grafana/pkg/plugins/manager/pipeline/termination" "github.com/grafana/grafana/pkg/plugins/manager/process" "github.com/grafana/grafana/pkg/plugins/manager/registry" "github.com/grafana/grafana/pkg/plugins/manager/signature" @@ -27,6 +28,7 @@ type Loader struct { discovery discovery.Discoverer bootstrap bootstrap.Bootstrapper initializer initialization.Initializer + termination termination.Terminator processManager process.Service pluginRegistry registry.Service @@ -45,15 +47,17 @@ type Loader struct { func ProvideService(cfg *config.Cfg, authorizer plugins.PluginLoaderAuthorizer, pluginRegistry registry.Service, roleRegistry plugins.RoleRegistry, assetPath *assetpath.Service, angularInspector angularinspector.Inspector, externalServiceRegistry oauth.ExternalServiceRegistry, - discovery discovery.Discoverer, bootstrap bootstrap.Bootstrapper, initializer initialization.Initializer) *Loader { + discovery discovery.Discoverer, bootstrap bootstrap.Bootstrapper, initializer initialization.Initializer, + termination termination.Terminator) *Loader { return New(cfg, authorizer, pluginRegistry, process.NewManager(pluginRegistry), roleRegistry, assetPath, - angularInspector, externalServiceRegistry, discovery, bootstrap, initializer) + angularInspector, externalServiceRegistry, discovery, bootstrap, initializer, termination) } func New(cfg *config.Cfg, authorizer plugins.PluginLoaderAuthorizer, pluginRegistry registry.Service, processManager process.Service, roleRegistry plugins.RoleRegistry, assetPath *assetpath.Service, angularInspector angularinspector.Inspector, externalServiceRegistry oauth.ExternalServiceRegistry, - discovery discovery.Discoverer, bootstrap bootstrap.Bootstrapper, initializer initialization.Initializer) *Loader { + discovery discovery.Discoverer, bootstrap bootstrap.Bootstrapper, initializer initialization.Initializer, + termination termination.Terminator) *Loader { return &Loader{ pluginRegistry: pluginRegistry, signatureValidator: signature.NewValidator(authorizer), @@ -68,6 +72,7 @@ func New(cfg *config.Cfg, authorizer plugins.PluginLoaderAuthorizer, pluginRegis discovery: discovery, bootstrap: bootstrap, initializer: initializer, + termination: termination, } } @@ -178,40 +183,7 @@ func (l *Loader) Load(ctx context.Context, src plugins.PluginSource) ([]*plugins } func (l *Loader) Unload(ctx context.Context, pluginID string) error { - plugin, exists := l.pluginRegistry.Plugin(ctx, pluginID) - if !exists { - return plugins.ErrPluginNotInstalled - } - - if plugin.IsCorePlugin() || plugin.IsBundledPlugin() { - return plugins.ErrUninstallCorePlugin - } - - if err := l.unload(ctx, plugin); err != nil { - return err - } - return nil -} - -func (l *Loader) unload(ctx context.Context, p *plugins.Plugin) error { - l.log.Debug("Stopping plugin process", "pluginId", p.ID) - - if err := l.processManager.Stop(ctx, p.ID); err != nil { - return err - } - - if err := l.pluginRegistry.Remove(ctx, p.ID); err != nil { - return err - } - l.log.Debug("Plugin unregistered", "pluginId", p.ID) - - if remover, ok := p.FS.(plugins.FSRemover); ok { - if err := remover.Remove(); err != nil { - return err - } - } - - return nil + return l.termination.Terminate(ctx, pluginID) } func (l *Loader) PluginErrors() []*plugins.Error { diff --git a/pkg/plugins/manager/loader/loader_test.go b/pkg/plugins/manager/loader/loader_test.go index e8357a4c729..992b25eeb6d 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" + "errors" "path/filepath" "sort" "testing" @@ -18,6 +19,7 @@ import ( "github.com/grafana/grafana/pkg/plugins/manager/pipeline/bootstrap" "github.com/grafana/grafana/pkg/plugins/manager/pipeline/discovery" "github.com/grafana/grafana/pkg/plugins/manager/pipeline/initialization" + "github.com/grafana/grafana/pkg/plugins/manager/pipeline/termination" "github.com/grafana/grafana/pkg/plugins/manager/signature" "github.com/grafana/grafana/pkg/plugins/manager/sources" "github.com/grafana/grafana/pkg/plugins/pluginscdn" @@ -435,11 +437,15 @@ func TestLoader_Load(t *testing.T) { angularInspector, err := angularinspector.NewStaticInspector() require.NoError(t, err) + terminationStage, err := termination.New(tt.cfg, termination.Opts{}) + require.NoError(t, err) + l := New(tt.cfg, signature.NewUnsignedAuthorizer(tt.cfg), fakes.NewFakePluginRegistry(), fakes.NewFakeProcessManager(), fakes.NewFakeRoleRegistry(), assetpath.ProvideService(pluginscdn.ProvideService(tt.cfg)), angularInspector, &fakes.FakeOauthService{}, discovery.New(tt.cfg, discovery.Opts{}), bootstrap.New(tt.cfg, bootstrap.Opts{}), - initialization.New(tt.cfg, initialization.Opts{})) + initialization.New(tt.cfg, initialization.Opts{}), + terminationStage) t.Run(tt.name, func(t *testing.T) { got, err := l.Load(context.Background(), sources.NewLocalSource(tt.class, tt.pluginPaths)) @@ -506,7 +512,7 @@ func TestLoader_Load(t *testing.T) { steps = append(steps, "initialize") return ps, nil }, - }) + }, &fakes.FakeTerminator{}) got, err := l.Load(context.Background(), src) require.NoError(t, err) @@ -516,6 +522,41 @@ func TestLoader_Load(t *testing.T) { }) } +func TestLoader_Unload(t *testing.T) { + t.Run("Termination stage error is returned from Unload", func(t *testing.T) { + pluginID := "grafana-test-panel" + cfg := &config.Cfg{} + angularInspector, err := angularinspector.NewStaticInspector() + require.NoError(t, err) + + tcs := []struct { + expectedErr error + }{ + { + expectedErr: errors.New("plugin not found"), + }, + { + expectedErr: nil, + }, + } + + for _, tc := range tcs { + l := New(cfg, signature.NewUnsignedAuthorizer(cfg), fakes.NewFakePluginRegistry(), fakes.NewFakeProcessManager(), + fakes.NewFakeRoleRegistry(), assetpath.ProvideService(pluginscdn.ProvideService(cfg)), angularInspector, + &fakes.FakeOauthService{}, &fakes.FakeDiscoverer{}, &fakes.FakeBootstrapper{}, &fakes.FakeInitializer{}, + &fakes.FakeTerminator{ + TerminateFunc: func(ctx context.Context, pID string) error { + require.Equal(t, pluginID, pID) + return tc.expectedErr + }, + }) + + err = l.Unload(context.Background(), pluginID) + require.ErrorIs(t, err, tc.expectedErr) + } + }) +} + func mustNewStaticFSForTests(t *testing.T, dir string) plugins.FS { sfs, err := plugins.NewStaticFS(plugins.NewLocalFS(dir)) require.NoError(t, err) diff --git a/pkg/plugins/manager/manager_integration_test.go b/pkg/plugins/manager/manager_integration_test.go index 65d0a8a28a1..1c965c64edf 100644 --- a/pkg/plugins/manager/manager_integration_test.go +++ b/pkg/plugins/manager/manager_integration_test.go @@ -24,6 +24,7 @@ import ( "github.com/grafana/grafana/pkg/plugins/manager/loader/angular/angularinspector" "github.com/grafana/grafana/pkg/plugins/manager/loader/assetpath" "github.com/grafana/grafana/pkg/plugins/manager/loader/finder" + "github.com/grafana/grafana/pkg/plugins/manager/process" "github.com/grafana/grafana/pkg/plugins/manager/registry" "github.com/grafana/grafana/pkg/plugins/manager/signature" "github.com/grafana/grafana/pkg/plugins/manager/signature/statickey" @@ -125,10 +126,13 @@ func TestIntegrationPluginManager(t *testing.T) { discovery := pipeline.ProvideDiscoveryStage(pCfg, finder.NewLocalFinder(pCfg.DevMode), reg) bootstrap := pipeline.ProvideBootstrapStage(pCfg, signature.ProvideService(pCfg, statickey.New()), assetpath.ProvideService(pluginscdn.ProvideService(pCfg))) initialize := pipeline.ProvideInitializationStage(pCfg, reg, lic, provider.ProvideService(coreRegistry)) + terminate, err := pipeline.ProvideTerminationStage(pCfg, reg, process.NewManager(reg)) + require.NoError(t, err) + l := loader.ProvideService(pCfg, signature.NewUnsignedAuthorizer(pCfg), reg, fakes.NewFakeRoleRegistry(), assetpath.ProvideService(pluginscdn.ProvideService(pCfg)), - angularInspector, &fakes.FakeOauthService{}, discovery, bootstrap, initialize) + angularInspector, &fakes.FakeOauthService{}, discovery, bootstrap, initialize, terminate) srcs := sources.ProvideService(cfg, pCfg) ps, err := store.ProvideService(reg, srcs, l) require.NoError(t, err) diff --git a/pkg/plugins/manager/pipeline/bootstrap/bootstrap.go b/pkg/plugins/manager/pipeline/bootstrap/bootstrap.go index 1be4c7fa2a5..94fb7bc7c97 100644 --- a/pkg/plugins/manager/pipeline/bootstrap/bootstrap.go +++ b/pkg/plugins/manager/pipeline/bootstrap/bootstrap.go @@ -65,6 +65,10 @@ func (b *Bootstrap) Bootstrap(ctx context.Context, src plugins.PluginSource, fou return nil, err } + if len(b.decorateSteps) == 0 { + return ps, nil + } + for _, p := range ps { for _, decorator := range b.decorateSteps { p, err = decorator(ctx, p) diff --git a/pkg/plugins/manager/pipeline/bootstrap/doc.go b/pkg/plugins/manager/pipeline/bootstrap/doc.go index d601296c698..49f337bd072 100644 --- a/pkg/plugins/manager/pipeline/bootstrap/doc.go +++ b/pkg/plugins/manager/pipeline/bootstrap/doc.go @@ -1,4 +1,4 @@ -// Package bootstrap defines the second stage of the plugin loader pipeline. +// Package bootstrap defines the Bootstrap stage of the plugin loader pipeline. // // The Bootstrap stage must implement the Bootstrapper interface. // - Bootstrap(ctx context.Context, src plugins.PluginSource, bundles []*plugins.FoundBundle) ([]*plugins.Plugin, error) diff --git a/pkg/plugins/manager/pipeline/discovery/doc.go b/pkg/plugins/manager/pipeline/discovery/doc.go index 37d91ce8592..6db3e26f858 100644 --- a/pkg/plugins/manager/pipeline/discovery/doc.go +++ b/pkg/plugins/manager/pipeline/discovery/doc.go @@ -1,4 +1,4 @@ -// Package discovery defines the first stage of the plugin loader pipeline. +// Package discovery defines the Discovery stage of the plugin loader pipeline. // The Discovery stage must implement the Discoverer interface. // - Discover(ctx context.Context, src plugins.PluginSource) ([]*plugins.FoundBundle, error) diff --git a/pkg/plugins/manager/pipeline/doc.go b/pkg/plugins/manager/pipeline/doc.go index 9b04096e158..8b32f3cb60d 100644 --- a/pkg/plugins/manager/pipeline/doc.go +++ b/pkg/plugins/manager/pipeline/doc.go @@ -5,7 +5,7 @@ // Discovery: Find plugins (e.g. from disk, remote, etc.), and [optionally] filter the results based on some criteria. // Bootstrap: Create the plugins found in the discovery stage and enrich them with metadata. // Verification: Verify the plugins based on some criteria (e.g. signature validation, angular detection, etc.) -// Initialization: Initialize the plugin for use (e.g. register with Grafana, etc.) -// Post-Initialization: Perform any post-initialization tasks (e.g. start the backend process, declare RBAC roles etc.) +// Initialization: Initialize the plugin for use (e.g. register with Grafana, start the backend process, declare RBAC roles etc.) +// - Termination: Terminate the plugin (e.g. stop the backend process, cleanup, etc.) package pipeline diff --git a/pkg/plugins/manager/pipeline/initialization/doc.go b/pkg/plugins/manager/pipeline/initialization/doc.go index 8ed60725e99..adf364c2c5a 100644 --- a/pkg/plugins/manager/pipeline/initialization/doc.go +++ b/pkg/plugins/manager/pipeline/initialization/doc.go @@ -1,4 +1,4 @@ -// Package initialization defines the fourth stage of the plugin loader pipeline. +// Package initialization defines the Initialization stage of the plugin loader pipeline. // // The Initialization stage must implement the Initializer interface. // - Initialize(ctx context.Context, ps []*plugins.Plugin) ([]*plugins.Plugin, error) diff --git a/pkg/plugins/manager/pipeline/initialization/steps.go b/pkg/plugins/manager/pipeline/initialization/steps.go index 3a683c47fbb..72b9e4bc14b 100644 --- a/pkg/plugins/manager/pipeline/initialization/steps.go +++ b/pkg/plugins/manager/pipeline/initialization/steps.go @@ -22,8 +22,8 @@ type BackendClientInit struct { log log.Logger } -// NewBackendClientInitStep returns a new InitializeFunc for registering a backend plugin process. -func NewBackendClientInitStep(envVarProvider envvars.Provider, +// BackendClientInitStep returns a new InitializeFunc for registering a backend plugin process. +func BackendClientInitStep(envVarProvider envvars.Provider, backendProvider plugins.BackendFactoryProvider) InitializeFunc { return newBackendProcessRegistration(envVarProvider, backendProvider).Initialize } @@ -64,8 +64,8 @@ type PluginRegistration struct { log log.Logger } -// NewPluginRegistrationStep returns a new InitializeFunc for registering a plugin with the plugin registry. -func NewPluginRegistrationStep(pluginRegistry registry.Service) InitializeFunc { +// PluginRegistrationStep returns a new InitializeFunc for registering a plugin with the plugin registry. +func PluginRegistrationStep(pluginRegistry registry.Service) InitializeFunc { return newPluginRegistration(pluginRegistry).Initialize } diff --git a/pkg/plugins/manager/pipeline/initialization/steps_test.go b/pkg/plugins/manager/pipeline/initialization/steps_test.go index d20efdbe411..439efeaecb6 100644 --- a/pkg/plugins/manager/pipeline/initialization/steps_test.go +++ b/pkg/plugins/manager/pipeline/initialization/steps_test.go @@ -28,7 +28,7 @@ func TestInitializer_Initialize(t *testing.T) { Class: plugins.ClassCore, } - stepFunc := NewBackendClientInitStep(&fakeEnvVarsProvider{}, &fakeBackendProvider{plugin: p}) + stepFunc := BackendClientInitStep(&fakeEnvVarsProvider{}, &fakeBackendProvider{plugin: p}) var err error p, err = stepFunc(context.Background(), p) @@ -52,7 +52,7 @@ func TestInitializer_Initialize(t *testing.T) { Class: plugins.ClassExternal, } - stepFunc := NewBackendClientInitStep(&fakeEnvVarsProvider{}, &fakeBackendProvider{plugin: p}) + stepFunc := BackendClientInitStep(&fakeEnvVarsProvider{}, &fakeBackendProvider{plugin: p}) var err error p, err = stepFunc(context.Background(), p) @@ -76,7 +76,7 @@ func TestInitializer_Initialize(t *testing.T) { Class: plugins.ClassExternal, } - stepFunc := NewBackendClientInitStep(&fakeEnvVarsProvider{}, &fakeBackendProvider{plugin: p}) + stepFunc := BackendClientInitStep(&fakeEnvVarsProvider{}, &fakeBackendProvider{plugin: p}) var err error p, err = stepFunc(context.Background(), p) @@ -94,7 +94,7 @@ func TestInitializer_Initialize(t *testing.T) { }, } - i := NewBackendClientInitStep(&fakeEnvVarsProvider{}, &fakeBackendProvider{ + i := BackendClientInitStep(&fakeEnvVarsProvider{}, &fakeBackendProvider{ plugin: p, }) diff --git a/pkg/plugins/manager/pipeline/termination/doc.go b/pkg/plugins/manager/pipeline/termination/doc.go new file mode 100644 index 00000000000..4e4e934770c --- /dev/null +++ b/pkg/plugins/manager/pipeline/termination/doc.go @@ -0,0 +1,5 @@ +// Package termination defines the Termination stage of the plugin loader pipeline. +// +// The Termination stage must implement the Terminator interface. +// - Terminate(ctx context.Context, pluginID string) error +package termination diff --git a/pkg/plugins/manager/pipeline/termination/steps.go b/pkg/plugins/manager/pipeline/termination/steps.go new file mode 100644 index 00000000000..a0073b4d460 --- /dev/null +++ b/pkg/plugins/manager/pipeline/termination/steps.go @@ -0,0 +1,107 @@ +package termination + +import ( + "context" + + "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/plugins/log" + "github.com/grafana/grafana/pkg/plugins/manager/process" + "github.com/grafana/grafana/pkg/plugins/manager/registry" +) + +// TerminablePluginResolver implements a ResolveFunc for resolving a plugin that can be terminated. +type TerminablePluginResolver struct { + pluginRegistry registry.Service + log log.Logger +} + +// TerminablePluginResolverStep returns a new ResolveFunc for resolving a plugin that can be terminated. +func TerminablePluginResolverStep(pluginRegistry registry.Service) ResolveFunc { + return newTerminablePluginResolver(pluginRegistry).Resolve +} + +func newTerminablePluginResolver(pluginRegistry registry.Service) *TerminablePluginResolver { + return &TerminablePluginResolver{ + pluginRegistry: pluginRegistry, + log: log.New("plugins.resolver"), + } +} + +// Resolve returns a plugin that can be terminated. +func (r *TerminablePluginResolver) Resolve(ctx context.Context, pluginID string) (*plugins.Plugin, error) { + p, exists := r.pluginRegistry.Plugin(ctx, pluginID) + if !exists { + return nil, plugins.ErrPluginNotInstalled + } + + // core plugins and bundled plugins cannot be terminated + if p.IsCorePlugin() || p.IsBundledPlugin() { + return nil, plugins.ErrUninstallCorePlugin + } + + return p, nil +} + +// BackendProcessTerminator implements a TerminateFunc for stopping a backend plugin process. +// +// It uses the process.Service to stop the backend plugin process. +type BackendProcessTerminator struct { + processManager process.Service + log log.Logger +} + +// BackendProcessTerminatorStep returns a new TerminateFunc for stopping a backend plugin process. +func BackendProcessTerminatorStep(processManager process.Service) TerminateFunc { + return newBackendProcessTerminator(processManager).Terminate +} + +func newBackendProcessTerminator(processManager process.Service) *BackendProcessTerminator { + return &BackendProcessTerminator{ + processManager: processManager, + log: log.New("plugins.backend.termination"), + } +} + +// Terminate stops a backend plugin process. +func (t *BackendProcessTerminator) Terminate(ctx context.Context, p *plugins.Plugin) error { + t.log.Debug("Stopping plugin process", "pluginId", p.ID) + + return t.processManager.Stop(ctx, p.ID) +} + +// Deregister implements a TerminateFunc for removing a plugin from the plugin registry. +type Deregister struct { + pluginRegistry registry.Service + log log.Logger +} + +// DeregisterStep returns a new TerminateFunc for removing a plugin from the plugin registry. +func DeregisterStep(pluginRegistry registry.Service) TerminateFunc { + return newDeregister(pluginRegistry).Deregister +} + +func newDeregister(pluginRegistry registry.Service) *Deregister { + return &Deregister{ + pluginRegistry: pluginRegistry, + log: log.New("plugins.deregister"), + } +} + +// Deregister removes a plugin from the plugin registry. +func (d *Deregister) Deregister(ctx context.Context, p *plugins.Plugin) error { + if err := d.pluginRegistry.Remove(ctx, p.ID); err != nil { + return err + } + d.log.Debug("Plugin unregistered", "pluginId", p.ID) + return nil +} + +// FSRemoval implements a TerminateFunc for removing plugin files from the filesystem. +func FSRemoval(_ context.Context, p *plugins.Plugin) error { + if remover, ok := p.FS.(plugins.FSRemover); ok { + if err := remover.Remove(); err != nil { + return err + } + } + return nil +} diff --git a/pkg/plugins/manager/pipeline/termination/termination.go b/pkg/plugins/manager/pipeline/termination/termination.go new file mode 100644 index 00000000000..84a8cdbf8f3 --- /dev/null +++ b/pkg/plugins/manager/pipeline/termination/termination.go @@ -0,0 +1,67 @@ +package termination + +import ( + "context" + "errors" + + "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/plugins/config" + "github.com/grafana/grafana/pkg/plugins/log" +) + +// Terminator is responsible for the Termination stage of the plugin loader pipeline. +type Terminator interface { + Terminate(ctx context.Context, pluginID string) error +} + +// ResolveFunc is the function used for the Resolve step of the Termination stage. +type ResolveFunc func(ctx context.Context, pluginID string) (*plugins.Plugin, error) + +// TerminateFunc is the function used for the Terminate step of the Termination stage. +type TerminateFunc func(ctx context.Context, p *plugins.Plugin) error + +type Terminate struct { + cfg *config.Cfg + resolveStep ResolveFunc + terminateSteps []TerminateFunc + log log.Logger +} + +type Opts struct { + ResolveFunc ResolveFunc + TerminateFuncs []TerminateFunc +} + +// New returns a new Termination stage. +func New(cfg *config.Cfg, opts Opts) (*Terminate, error) { + // without a resolve function, we can't do anything so return an error + if opts.ResolveFunc == nil && opts.TerminateFuncs != nil { + return nil, errors.New("resolve function is required") + } + + if opts.TerminateFuncs == nil { + opts.TerminateFuncs = []TerminateFunc{} + } + + return &Terminate{ + cfg: cfg, + resolveStep: opts.ResolveFunc, + terminateSteps: opts.TerminateFuncs, + log: log.New("plugins.termination"), + }, nil +} + +// Terminate will execute the Terminate steps of the Termination stage. +func (t *Terminate) Terminate(ctx context.Context, pluginID string) error { + p, err := t.resolveStep(ctx, pluginID) + if err != nil { + return err + } + + for _, terminate := range t.terminateSteps { + if err = terminate(ctx, p); err != nil { + return err + } + } + return nil +} diff --git a/pkg/services/pluginsintegration/loader/loader.go b/pkg/services/pluginsintegration/loader/loader.go index dd1bfdaa2d4..e75a6173292 100644 --- a/pkg/services/pluginsintegration/loader/loader.go +++ b/pkg/services/pluginsintegration/loader/loader.go @@ -11,6 +11,7 @@ import ( "github.com/grafana/grafana/pkg/plugins/manager/pipeline/bootstrap" "github.com/grafana/grafana/pkg/plugins/manager/pipeline/discovery" "github.com/grafana/grafana/pkg/plugins/manager/pipeline/initialization" + "github.com/grafana/grafana/pkg/plugins/manager/pipeline/termination" "github.com/grafana/grafana/pkg/plugins/manager/process" "github.com/grafana/grafana/pkg/plugins/manager/registry" "github.com/grafana/grafana/pkg/plugins/oauth" @@ -27,10 +28,11 @@ func ProvideService(cfg *config.Cfg, authorizer plugins.PluginLoaderAuthorizer, pluginRegistry registry.Service, roleRegistry plugins.RoleRegistry, assetPath *assetpath.Service, angularInspector angularinspector.Inspector, externalServiceRegistry oauth.ExternalServiceRegistry, discovery discovery.Discoverer, bootstrap bootstrap.Bootstrapper, initializer initialization.Initializer, + termination termination.Terminator, ) *Loader { return &Loader{ loader: pluginsLoader.New(cfg, authorizer, pluginRegistry, processManager, roleRegistry, assetPath, - angularInspector, externalServiceRegistry, discovery, bootstrap, initializer), + angularInspector, externalServiceRegistry, discovery, bootstrap, initializer, termination), } } diff --git a/pkg/services/pluginsintegration/loader/loader_test.go b/pkg/services/pluginsintegration/loader/loader_test.go index cad919a9939..90b72e12a57 100644 --- a/pkg/services/pluginsintegration/loader/loader_test.go +++ b/pkg/services/pluginsintegration/loader/loader_test.go @@ -22,6 +22,7 @@ import ( "github.com/grafana/grafana/pkg/plugins/manager/pipeline/bootstrap" "github.com/grafana/grafana/pkg/plugins/manager/pipeline/discovery" "github.com/grafana/grafana/pkg/plugins/manager/pipeline/initialization" + "github.com/grafana/grafana/pkg/plugins/manager/pipeline/termination" "github.com/grafana/grafana/pkg/plugins/manager/process" "github.com/grafana/grafana/pkg/plugins/manager/registry" "github.com/grafana/grafana/pkg/plugins/manager/signature" @@ -976,7 +977,7 @@ func TestLoader_AngularClass(t *testing.T) { }, } // if angularDetected = true, it means that the detection has run - l := newLoaderWithAngularInspector(&config.Cfg{AngularSupportEnabled: true}, angularinspector.AlwaysAngularFakeInspector) + l := newLoaderWithAngularInspector(t, &config.Cfg{AngularSupportEnabled: true}, angularinspector.AlwaysAngularFakeInspector) p, err := l.Load(context.Background(), fakePluginSource) require.NoError(t, err) require.Len(t, p, 1, "should load 1 plugin") @@ -1025,7 +1026,7 @@ func TestLoader_Load_Angular(t *testing.T) { }, } { t.Run(tc.name, func(t *testing.T) { - l := newLoaderWithAngularInspector(cfgTc.cfg, tc.angularInspector) + l := newLoaderWithAngularInspector(t, cfgTc.cfg, tc.angularInspector) p, err := l.Load(context.Background(), fakePluginSource) require.NoError(t, err) if tc.shouldLoad { @@ -1318,19 +1319,29 @@ func newLoader(t *testing.T, cfg *config.Cfg, reg registry.Service, proc process lic := fakes.NewFakeLicensingService() angularInspector, err := angularinspector.NewStaticInspector() require.NoError(t, err) + + terminate, err := pipeline.ProvideTerminationStage(cfg, reg, proc) + require.NoError(t, err) + return ProvideService(cfg, signature.NewUnsignedAuthorizer(cfg), proc, reg, fakes.NewFakeRoleRegistry(), assets, angularInspector, &fakes.FakeOauthService{}, pipeline.ProvideDiscoveryStage(cfg, finder.NewLocalFinder(false), reg), pipeline.ProvideBootstrapStage(cfg, signature.DefaultCalculator(cfg), assets), - pipeline.ProvideInitializationStage(cfg, reg, lic, backendFactory)) + pipeline.ProvideInitializationStage(cfg, reg, lic, backendFactory), + terminate) } -func newLoaderWithAngularInspector(cfg *config.Cfg, angularInspector angularinspector.Inspector) *Loader { +func newLoaderWithAngularInspector(t *testing.T, cfg *config.Cfg, angularInspector angularinspector.Inspector) *Loader { reg := fakes.NewFakePluginRegistry() + + terminationStage, err := termination.New(cfg, termination.Opts{}) + require.NoError(t, err) + return ProvideService(cfg, signature.NewUnsignedAuthorizer(cfg), process.ProvideService(reg), reg, fakes.NewFakeRoleRegistry(), assetpath.ProvideService(pluginscdn.ProvideService(cfg)), angularInspector, &fakes.FakeOauthService{}, - discovery.New(cfg, discovery.Opts{}), bootstrap.New(cfg, bootstrap.Opts{}), initialization.New(cfg, initialization.Opts{})) + discovery.New(cfg, discovery.Opts{}), bootstrap.New(cfg, bootstrap.Opts{}), + initialization.New(cfg, initialization.Opts{}), terminationStage) } func verifyState(t *testing.T, ps []*plugins.Plugin, reg registry.Service, diff --git a/pkg/services/pluginsintegration/pipeline/pipeline.go b/pkg/services/pluginsintegration/pipeline/pipeline.go index 8224e9dcea5..bd37c47998d 100644 --- a/pkg/services/pluginsintegration/pipeline/pipeline.go +++ b/pkg/services/pluginsintegration/pipeline/pipeline.go @@ -11,6 +11,8 @@ import ( "github.com/grafana/grafana/pkg/plugins/manager/pipeline/bootstrap" "github.com/grafana/grafana/pkg/plugins/manager/pipeline/discovery" "github.com/grafana/grafana/pkg/plugins/manager/pipeline/initialization" + "github.com/grafana/grafana/pkg/plugins/manager/pipeline/termination" + "github.com/grafana/grafana/pkg/plugins/manager/process" "github.com/grafana/grafana/pkg/plugins/manager/registry" ) @@ -37,8 +39,19 @@ func ProvideBootstrapStage(cfg *config.Cfg, sc plugins.SignatureCalculator, a *a func ProvideInitializationStage(cfg *config.Cfg, pr registry.Service, l plugins.Licensing, bp plugins.BackendFactoryProvider) *initialization.Initialize { return initialization.New(cfg, initialization.Opts{ InitializeFuncs: []initialization.InitializeFunc{ - initialization.NewBackendClientInitStep(envvars.NewProvider(cfg, l), bp), - initialization.NewPluginRegistrationStep(pr), + initialization.BackendClientInitStep(envvars.NewProvider(cfg, l), bp), + initialization.PluginRegistrationStep(pr), + }, + }) +} + +func ProvideTerminationStage(cfg *config.Cfg, pr registry.Service, pm process.Service) (*termination.Terminate, error) { + return termination.New(cfg, termination.Opts{ + ResolveFunc: termination.TerminablePluginResolverStep(pr), + TerminateFuncs: []termination.TerminateFunc{ + termination.BackendProcessTerminatorStep(pm), + termination.DeregisterStep(pr), + termination.FSRemoval, }, }) } diff --git a/pkg/services/pluginsintegration/pluginsintegration.go b/pkg/services/pluginsintegration/pluginsintegration.go index cd6a0407d25..720f749be41 100644 --- a/pkg/services/pluginsintegration/pluginsintegration.go +++ b/pkg/services/pluginsintegration/pluginsintegration.go @@ -18,6 +18,7 @@ import ( "github.com/grafana/grafana/pkg/plugins/manager/pipeline/bootstrap" "github.com/grafana/grafana/pkg/plugins/manager/pipeline/discovery" "github.com/grafana/grafana/pkg/plugins/manager/pipeline/initialization" + "github.com/grafana/grafana/pkg/plugins/manager/pipeline/termination" "github.com/grafana/grafana/pkg/plugins/manager/process" "github.com/grafana/grafana/pkg/plugins/manager/registry" "github.com/grafana/grafana/pkg/plugins/manager/signature" @@ -69,6 +70,8 @@ var WireSet = wire.NewSet( wire.Bind(new(bootstrap.Bootstrapper), new(*bootstrap.Bootstrap)), pipeline.ProvideInitializationStage, wire.Bind(new(initialization.Initializer), new(*initialization.Initialize)), + pipeline.ProvideTerminationStage, + wire.Bind(new(termination.Terminator), new(*termination.Terminate)), angularpatternsstore.ProvideService, angulardetectorsprovider.ProvideDynamic,