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 <giuseppe.guerra@grafana.com>

* inline stop func call

Co-authored-by: Giuseppe Guerra <giuseppe.guerra@grafana.com>

---------

Co-authored-by: Giuseppe Guerra <giuseppe.guerra@grafana.com>
pull/72894/head
Will Browne 2 years ago committed by GitHub
parent 7bc6d32eb9
commit 60b4a0b2a4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      pkg/api/plugin_resource_test.go
  2. 11
      pkg/plugins/manager/fakes/fakes.go
  3. 46
      pkg/plugins/manager/loader/loader.go
  4. 45
      pkg/plugins/manager/loader/loader_test.go
  5. 6
      pkg/plugins/manager/manager_integration_test.go
  6. 4
      pkg/plugins/manager/pipeline/bootstrap/bootstrap.go
  7. 2
      pkg/plugins/manager/pipeline/bootstrap/doc.go
  8. 2
      pkg/plugins/manager/pipeline/discovery/doc.go
  9. 4
      pkg/plugins/manager/pipeline/doc.go
  10. 2
      pkg/plugins/manager/pipeline/initialization/doc.go
  11. 8
      pkg/plugins/manager/pipeline/initialization/steps.go
  12. 8
      pkg/plugins/manager/pipeline/initialization/steps_test.go
  13. 5
      pkg/plugins/manager/pipeline/termination/doc.go
  14. 107
      pkg/plugins/manager/pipeline/termination/steps.go
  15. 67
      pkg/plugins/manager/pipeline/termination/termination.go
  16. 4
      pkg/services/pluginsintegration/loader/loader.go
  17. 21
      pkg/services/pluginsintegration/loader/loader_test.go
  18. 17
      pkg/services/pluginsintegration/pipeline/pipeline.go
  19. 3
      pkg/services/pluginsintegration/pluginsintegration.go

@ -25,6 +25,7 @@ import (
"github.com/grafana/grafana/pkg/plugins/manager/loader" "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/assetpath"
"github.com/grafana/grafana/pkg/plugins/manager/loader/finder" "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/registry"
"github.com/grafana/grafana/pkg/plugins/manager/signature" "github.com/grafana/grafana/pkg/plugins/manager/signature"
"github.com/grafana/grafana/pkg/plugins/manager/signature/statickey" "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) discovery := pipeline.ProvideDiscoveryStage(pCfg, finder.NewLocalFinder(pCfg.DevMode), reg)
bootstrap := pipeline.ProvideBootstrapStage(pCfg, signature.ProvideService(pCfg, statickey.New()), assetpath.ProvideService(pluginscdn.ProvideService(pCfg))) bootstrap := pipeline.ProvideBootstrapStage(pCfg, signature.ProvideService(pCfg, statickey.New()), assetpath.ProvideService(pluginscdn.ProvideService(pCfg)))
initialize := pipeline.ProvideInitializationStage(pCfg, reg, fakes.NewFakeLicensingService(), provider.ProvideService(coreRegistry)) 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), l := loader.ProvideService(pCfg, signature.NewUnsignedAuthorizer(pCfg),
reg, fakes.NewFakeRoleRegistry(), reg, fakes.NewFakeRoleRegistry(),
assetpath.ProvideService(pluginscdn.ProvideService(pCfg)), assetpath.ProvideService(pluginscdn.ProvideService(pCfg)),
angularInspector, &fakes.FakeOauthService{}, discovery, bootstrap, initialize) angularInspector, &fakes.FakeOauthService{}, discovery, bootstrap, initialize, terminate)
srcs := sources.ProvideService(cfg, pCfg) srcs := sources.ProvideService(cfg, pCfg)
ps, err := store.ProvideService(reg, srcs, l) ps, err := store.ProvideService(reg, srcs, l)
require.NoError(t, err) require.NoError(t, err)

@ -496,3 +496,14 @@ func (f *FakeInitializer) Initialize(ctx context.Context, ps []*plugins.Plugin)
} }
return []*plugins.Plugin{}, nil 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
}

@ -14,6 +14,7 @@ import (
"github.com/grafana/grafana/pkg/plugins/manager/pipeline/bootstrap" "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/discovery"
"github.com/grafana/grafana/pkg/plugins/manager/pipeline/initialization" "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/process"
"github.com/grafana/grafana/pkg/plugins/manager/registry" "github.com/grafana/grafana/pkg/plugins/manager/registry"
"github.com/grafana/grafana/pkg/plugins/manager/signature" "github.com/grafana/grafana/pkg/plugins/manager/signature"
@ -27,6 +28,7 @@ type Loader struct {
discovery discovery.Discoverer discovery discovery.Discoverer
bootstrap bootstrap.Bootstrapper bootstrap bootstrap.Bootstrapper
initializer initialization.Initializer initializer initialization.Initializer
termination termination.Terminator
processManager process.Service processManager process.Service
pluginRegistry registry.Service pluginRegistry registry.Service
@ -45,15 +47,17 @@ type Loader struct {
func ProvideService(cfg *config.Cfg, authorizer plugins.PluginLoaderAuthorizer, func ProvideService(cfg *config.Cfg, authorizer plugins.PluginLoaderAuthorizer,
pluginRegistry registry.Service, roleRegistry plugins.RoleRegistry, assetPath *assetpath.Service, pluginRegistry registry.Service, roleRegistry plugins.RoleRegistry, assetPath *assetpath.Service,
angularInspector angularinspector.Inspector, externalServiceRegistry oauth.ExternalServiceRegistry, 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, 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, func New(cfg *config.Cfg, authorizer plugins.PluginLoaderAuthorizer, pluginRegistry registry.Service,
processManager process.Service, roleRegistry plugins.RoleRegistry, assetPath *assetpath.Service, processManager process.Service, roleRegistry plugins.RoleRegistry, assetPath *assetpath.Service,
angularInspector angularinspector.Inspector, externalServiceRegistry oauth.ExternalServiceRegistry, 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{ return &Loader{
pluginRegistry: pluginRegistry, pluginRegistry: pluginRegistry,
signatureValidator: signature.NewValidator(authorizer), signatureValidator: signature.NewValidator(authorizer),
@ -68,6 +72,7 @@ func New(cfg *config.Cfg, authorizer plugins.PluginLoaderAuthorizer, pluginRegis
discovery: discovery, discovery: discovery,
bootstrap: bootstrap, bootstrap: bootstrap,
initializer: initializer, 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 { func (l *Loader) Unload(ctx context.Context, pluginID string) error {
plugin, exists := l.pluginRegistry.Plugin(ctx, pluginID) return l.termination.Terminate(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
} }
func (l *Loader) PluginErrors() []*plugins.Error { func (l *Loader) PluginErrors() []*plugins.Error {

@ -2,6 +2,7 @@ package loader
import ( import (
"context" "context"
"errors"
"path/filepath" "path/filepath"
"sort" "sort"
"testing" "testing"
@ -18,6 +19,7 @@ import (
"github.com/grafana/grafana/pkg/plugins/manager/pipeline/bootstrap" "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/discovery"
"github.com/grafana/grafana/pkg/plugins/manager/pipeline/initialization" "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/signature"
"github.com/grafana/grafana/pkg/plugins/manager/sources" "github.com/grafana/grafana/pkg/plugins/manager/sources"
"github.com/grafana/grafana/pkg/plugins/pluginscdn" "github.com/grafana/grafana/pkg/plugins/pluginscdn"
@ -435,11 +437,15 @@ func TestLoader_Load(t *testing.T) {
angularInspector, err := angularinspector.NewStaticInspector() angularInspector, err := angularinspector.NewStaticInspector()
require.NoError(t, err) 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(), l := New(tt.cfg, signature.NewUnsignedAuthorizer(tt.cfg), fakes.NewFakePluginRegistry(),
fakes.NewFakeProcessManager(), fakes.NewFakeRoleRegistry(), fakes.NewFakeProcessManager(), fakes.NewFakeRoleRegistry(),
assetpath.ProvideService(pluginscdn.ProvideService(tt.cfg)), angularInspector, &fakes.FakeOauthService{}, assetpath.ProvideService(pluginscdn.ProvideService(tt.cfg)), angularInspector, &fakes.FakeOauthService{},
discovery.New(tt.cfg, discovery.Opts{}), bootstrap.New(tt.cfg, bootstrap.Opts{}), 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) { t.Run(tt.name, func(t *testing.T) {
got, err := l.Load(context.Background(), sources.NewLocalSource(tt.class, tt.pluginPaths)) 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") steps = append(steps, "initialize")
return ps, nil return ps, nil
}, },
}) }, &fakes.FakeTerminator{})
got, err := l.Load(context.Background(), src) got, err := l.Load(context.Background(), src)
require.NoError(t, err) 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 { func mustNewStaticFSForTests(t *testing.T, dir string) plugins.FS {
sfs, err := plugins.NewStaticFS(plugins.NewLocalFS(dir)) sfs, err := plugins.NewStaticFS(plugins.NewLocalFS(dir))
require.NoError(t, err) require.NoError(t, err)

@ -24,6 +24,7 @@ import (
"github.com/grafana/grafana/pkg/plugins/manager/loader/angular/angularinspector" "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/assetpath"
"github.com/grafana/grafana/pkg/plugins/manager/loader/finder" "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/registry"
"github.com/grafana/grafana/pkg/plugins/manager/signature" "github.com/grafana/grafana/pkg/plugins/manager/signature"
"github.com/grafana/grafana/pkg/plugins/manager/signature/statickey" "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) discovery := pipeline.ProvideDiscoveryStage(pCfg, finder.NewLocalFinder(pCfg.DevMode), reg)
bootstrap := pipeline.ProvideBootstrapStage(pCfg, signature.ProvideService(pCfg, statickey.New()), assetpath.ProvideService(pluginscdn.ProvideService(pCfg))) bootstrap := pipeline.ProvideBootstrapStage(pCfg, signature.ProvideService(pCfg, statickey.New()), assetpath.ProvideService(pluginscdn.ProvideService(pCfg)))
initialize := pipeline.ProvideInitializationStage(pCfg, reg, lic, provider.ProvideService(coreRegistry)) 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), l := loader.ProvideService(pCfg, signature.NewUnsignedAuthorizer(pCfg),
reg, fakes.NewFakeRoleRegistry(), reg, fakes.NewFakeRoleRegistry(),
assetpath.ProvideService(pluginscdn.ProvideService(pCfg)), assetpath.ProvideService(pluginscdn.ProvideService(pCfg)),
angularInspector, &fakes.FakeOauthService{}, discovery, bootstrap, initialize) angularInspector, &fakes.FakeOauthService{}, discovery, bootstrap, initialize, terminate)
srcs := sources.ProvideService(cfg, pCfg) srcs := sources.ProvideService(cfg, pCfg)
ps, err := store.ProvideService(reg, srcs, l) ps, err := store.ProvideService(reg, srcs, l)
require.NoError(t, err) require.NoError(t, err)

@ -65,6 +65,10 @@ func (b *Bootstrap) Bootstrap(ctx context.Context, src plugins.PluginSource, fou
return nil, err return nil, err
} }
if len(b.decorateSteps) == 0 {
return ps, nil
}
for _, p := range ps { for _, p := range ps {
for _, decorator := range b.decorateSteps { for _, decorator := range b.decorateSteps {
p, err = decorator(ctx, p) p, err = decorator(ctx, p)

@ -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. // The Bootstrap stage must implement the Bootstrapper interface.
// - Bootstrap(ctx context.Context, src plugins.PluginSource, bundles []*plugins.FoundBundle) ([]*plugins.Plugin, error) // - Bootstrap(ctx context.Context, src plugins.PluginSource, bundles []*plugins.FoundBundle) ([]*plugins.Plugin, error)

@ -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. // The Discovery stage must implement the Discoverer interface.
// - Discover(ctx context.Context, src plugins.PluginSource) ([]*plugins.FoundBundle, error) // - Discover(ctx context.Context, src plugins.PluginSource) ([]*plugins.FoundBundle, error)

@ -5,7 +5,7 @@
// Discovery: Find plugins (e.g. from disk, remote, etc.), and [optionally] filter the results based on some criteria. // 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. // 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.) // 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.) // Initialization: Initialize the plugin for use (e.g. register with Grafana, start the backend process, declare RBAC roles etc.)
// Post-Initialization: Perform any post-initialization tasks (e.g. start the backend process, declare RBAC roles etc.) // - Termination: Terminate the plugin (e.g. stop the backend process, cleanup, etc.)
package pipeline package pipeline

@ -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. // The Initialization stage must implement the Initializer interface.
// - Initialize(ctx context.Context, ps []*plugins.Plugin) ([]*plugins.Plugin, error) // - Initialize(ctx context.Context, ps []*plugins.Plugin) ([]*plugins.Plugin, error)

@ -22,8 +22,8 @@ type BackendClientInit struct {
log log.Logger log log.Logger
} }
// NewBackendClientInitStep returns a new InitializeFunc for registering a backend plugin process. // BackendClientInitStep returns a new InitializeFunc for registering a backend plugin process.
func NewBackendClientInitStep(envVarProvider envvars.Provider, func BackendClientInitStep(envVarProvider envvars.Provider,
backendProvider plugins.BackendFactoryProvider) InitializeFunc { backendProvider plugins.BackendFactoryProvider) InitializeFunc {
return newBackendProcessRegistration(envVarProvider, backendProvider).Initialize return newBackendProcessRegistration(envVarProvider, backendProvider).Initialize
} }
@ -64,8 +64,8 @@ type PluginRegistration struct {
log log.Logger log log.Logger
} }
// NewPluginRegistrationStep returns a new InitializeFunc for registering a plugin with the plugin registry. // PluginRegistrationStep returns a new InitializeFunc for registering a plugin with the plugin registry.
func NewPluginRegistrationStep(pluginRegistry registry.Service) InitializeFunc { func PluginRegistrationStep(pluginRegistry registry.Service) InitializeFunc {
return newPluginRegistration(pluginRegistry).Initialize return newPluginRegistration(pluginRegistry).Initialize
} }

@ -28,7 +28,7 @@ func TestInitializer_Initialize(t *testing.T) {
Class: plugins.ClassCore, Class: plugins.ClassCore,
} }
stepFunc := NewBackendClientInitStep(&fakeEnvVarsProvider{}, &fakeBackendProvider{plugin: p}) stepFunc := BackendClientInitStep(&fakeEnvVarsProvider{}, &fakeBackendProvider{plugin: p})
var err error var err error
p, err = stepFunc(context.Background(), p) p, err = stepFunc(context.Background(), p)
@ -52,7 +52,7 @@ func TestInitializer_Initialize(t *testing.T) {
Class: plugins.ClassExternal, Class: plugins.ClassExternal,
} }
stepFunc := NewBackendClientInitStep(&fakeEnvVarsProvider{}, &fakeBackendProvider{plugin: p}) stepFunc := BackendClientInitStep(&fakeEnvVarsProvider{}, &fakeBackendProvider{plugin: p})
var err error var err error
p, err = stepFunc(context.Background(), p) p, err = stepFunc(context.Background(), p)
@ -76,7 +76,7 @@ func TestInitializer_Initialize(t *testing.T) {
Class: plugins.ClassExternal, Class: plugins.ClassExternal,
} }
stepFunc := NewBackendClientInitStep(&fakeEnvVarsProvider{}, &fakeBackendProvider{plugin: p}) stepFunc := BackendClientInitStep(&fakeEnvVarsProvider{}, &fakeBackendProvider{plugin: p})
var err error var err error
p, err = stepFunc(context.Background(), p) 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, plugin: p,
}) })

@ -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

@ -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
}

@ -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
}

@ -11,6 +11,7 @@ import (
"github.com/grafana/grafana/pkg/plugins/manager/pipeline/bootstrap" "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/discovery"
"github.com/grafana/grafana/pkg/plugins/manager/pipeline/initialization" "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/process"
"github.com/grafana/grafana/pkg/plugins/manager/registry" "github.com/grafana/grafana/pkg/plugins/manager/registry"
"github.com/grafana/grafana/pkg/plugins/oauth" "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, pluginRegistry registry.Service, roleRegistry plugins.RoleRegistry, assetPath *assetpath.Service,
angularInspector angularinspector.Inspector, externalServiceRegistry oauth.ExternalServiceRegistry, angularInspector angularinspector.Inspector, externalServiceRegistry oauth.ExternalServiceRegistry,
discovery discovery.Discoverer, bootstrap bootstrap.Bootstrapper, initializer initialization.Initializer, discovery discovery.Discoverer, bootstrap bootstrap.Bootstrapper, initializer initialization.Initializer,
termination termination.Terminator,
) *Loader { ) *Loader {
return &Loader{ return &Loader{
loader: pluginsLoader.New(cfg, authorizer, pluginRegistry, processManager, roleRegistry, assetPath, loader: pluginsLoader.New(cfg, authorizer, pluginRegistry, processManager, roleRegistry, assetPath,
angularInspector, externalServiceRegistry, discovery, bootstrap, initializer), angularInspector, externalServiceRegistry, discovery, bootstrap, initializer, termination),
} }
} }

@ -22,6 +22,7 @@ import (
"github.com/grafana/grafana/pkg/plugins/manager/pipeline/bootstrap" "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/discovery"
"github.com/grafana/grafana/pkg/plugins/manager/pipeline/initialization" "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/process"
"github.com/grafana/grafana/pkg/plugins/manager/registry" "github.com/grafana/grafana/pkg/plugins/manager/registry"
"github.com/grafana/grafana/pkg/plugins/manager/signature" "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 // 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) p, err := l.Load(context.Background(), fakePluginSource)
require.NoError(t, err) require.NoError(t, err)
require.Len(t, p, 1, "should load 1 plugin") 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) { 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) p, err := l.Load(context.Background(), fakePluginSource)
require.NoError(t, err) require.NoError(t, err)
if tc.shouldLoad { if tc.shouldLoad {
@ -1318,19 +1319,29 @@ func newLoader(t *testing.T, cfg *config.Cfg, reg registry.Service, proc process
lic := fakes.NewFakeLicensingService() lic := fakes.NewFakeLicensingService()
angularInspector, err := angularinspector.NewStaticInspector() angularInspector, err := angularinspector.NewStaticInspector()
require.NoError(t, err) 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, return ProvideService(cfg, signature.NewUnsignedAuthorizer(cfg), proc, reg, fakes.NewFakeRoleRegistry(), assets,
angularInspector, &fakes.FakeOauthService{}, angularInspector, &fakes.FakeOauthService{},
pipeline.ProvideDiscoveryStage(cfg, finder.NewLocalFinder(false), reg), pipeline.ProvideDiscoveryStage(cfg, finder.NewLocalFinder(false), reg),
pipeline.ProvideBootstrapStage(cfg, signature.DefaultCalculator(cfg), assets), 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() reg := fakes.NewFakePluginRegistry()
terminationStage, err := termination.New(cfg, termination.Opts{})
require.NoError(t, err)
return ProvideService(cfg, signature.NewUnsignedAuthorizer(cfg), process.ProvideService(reg), reg, return ProvideService(cfg, signature.NewUnsignedAuthorizer(cfg), process.ProvideService(reg), reg,
fakes.NewFakeRoleRegistry(), assetpath.ProvideService(pluginscdn.ProvideService(cfg)), fakes.NewFakeRoleRegistry(), assetpath.ProvideService(pluginscdn.ProvideService(cfg)),
angularInspector, &fakes.FakeOauthService{}, 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, func verifyState(t *testing.T, ps []*plugins.Plugin, reg registry.Service,

@ -11,6 +11,8 @@ import (
"github.com/grafana/grafana/pkg/plugins/manager/pipeline/bootstrap" "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/discovery"
"github.com/grafana/grafana/pkg/plugins/manager/pipeline/initialization" "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/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 { func ProvideInitializationStage(cfg *config.Cfg, pr registry.Service, l plugins.Licensing, bp plugins.BackendFactoryProvider) *initialization.Initialize {
return initialization.New(cfg, initialization.Opts{ return initialization.New(cfg, initialization.Opts{
InitializeFuncs: []initialization.InitializeFunc{ InitializeFuncs: []initialization.InitializeFunc{
initialization.NewBackendClientInitStep(envvars.NewProvider(cfg, l), bp), initialization.BackendClientInitStep(envvars.NewProvider(cfg, l), bp),
initialization.NewPluginRegistrationStep(pr), 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,
}, },
}) })
} }

@ -18,6 +18,7 @@ import (
"github.com/grafana/grafana/pkg/plugins/manager/pipeline/bootstrap" "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/discovery"
"github.com/grafana/grafana/pkg/plugins/manager/pipeline/initialization" "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/process"
"github.com/grafana/grafana/pkg/plugins/manager/registry" "github.com/grafana/grafana/pkg/plugins/manager/registry"
"github.com/grafana/grafana/pkg/plugins/manager/signature" "github.com/grafana/grafana/pkg/plugins/manager/signature"
@ -69,6 +70,8 @@ var WireSet = wire.NewSet(
wire.Bind(new(bootstrap.Bootstrapper), new(*bootstrap.Bootstrap)), wire.Bind(new(bootstrap.Bootstrapper), new(*bootstrap.Bootstrap)),
pipeline.ProvideInitializationStage, pipeline.ProvideInitializationStage,
wire.Bind(new(initialization.Initializer), new(*initialization.Initialize)), wire.Bind(new(initialization.Initializer), new(*initialization.Initialize)),
pipeline.ProvideTerminationStage,
wire.Bind(new(termination.Terminator), new(*termination.Terminate)),
angularpatternsstore.ProvideService, angularpatternsstore.ProvideService,
angulardetectorsprovider.ProvideDynamic, angulardetectorsprovider.ProvideDynamic,

Loading…
Cancel
Save