Plugins: Enable plugin runtime install/uninstall capabilities (#33836)

* add uninstall flow

* add install flow

* small cleanup

* smaller-footprint solution

* cleanup + make bp start auto

* fix interface contract

* improve naming

* accept version arg

* ensure use of shared logger

* make installer a field

* add plugin decommissioning

* add basic error checking

* fix api docs

* making initialization idempotent

* add mutex

* fix comment

* fix test

* add test for decommission

* improve existing test

* add more test coverage

* more tests

* change test func to use read lock

* refactoring + adding test asserts

* improve purging old install flow

* improve dupe checking

* change log name

* skip over dupe scanned

* make test assertion more flexible

* remove trailing line

* fix pointer receiver name

* update comment

* add context to API

* add config flag

* add base http api test + fix update functionality

* simplify existing check

* clean up test

* refactor tests based on feedback

* add single quotes to errs

* use gcmp in tests + fix logo issue

* make plugin list testing more flexible

* address feedback

* fix API test

* fix linter

* undo preallocate

* Update docs/sources/administration/configuration.md

Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com>

* Update docs/sources/administration/configuration.md

Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com>

* Update docs/sources/administration/configuration.md

Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com>

* fix linting issue in test

* add docs placeholder

* update install notes

* Update docs/sources/plugins/marketplace.md

Co-authored-by: Marcus Olsson <marcus.olsson@hey.com>

* update access wording

* add more placeholder docs

* add link to more info

* PR feedback - improved errors, refactor, lock fix

* improve err details

* propagate plugin version errors

* don't autostart renderer

* add H1

* fix imports

Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com>
Co-authored-by: Marcus Olsson <marcus.olsson@hey.com>
This commit is contained in:
Will Browne
2021-05-12 20:05:16 +02:00
committed by GitHub
co-authored by achatterjee-grafana Marcus Olsson
parent 1fbadab600
commit c39d6ad97d
35 changed files with 1020 additions and 126 deletions
+68 -16
View File
@@ -47,7 +47,6 @@ func (m *manager) Init() error {
}
func (m *manager) Run(ctx context.Context) error {
m.start(ctx)
<-ctx.Done()
m.stop(ctx)
return ctx.Err()
@@ -96,8 +95,60 @@ func (m *manager) Register(pluginID string, factory backendplugin.PluginFactoryF
return nil
}
// RegisterAndStart registers and starts a backend plugin
func (m *manager) RegisterAndStart(ctx context.Context, pluginID string, factory backendplugin.PluginFactoryFunc) error {
err := m.Register(pluginID, factory)
if err != nil {
return err
}
p, exists := m.Get(pluginID)
if !exists {
return fmt.Errorf("backend plugin %s is not registered", pluginID)
}
m.start(ctx, p)
return nil
}
// UnregisterAndStop unregisters and stops a backend plugin
func (m *manager) UnregisterAndStop(ctx context.Context, pluginID string) error {
m.logger.Debug("Unregistering backend plugin", "pluginId", pluginID)
m.pluginsMu.Lock()
defer m.pluginsMu.Unlock()
p, exists := m.plugins[pluginID]
if !exists {
return fmt.Errorf("backend plugin %s is not registered", pluginID)
}
m.logger.Debug("Stopping backend plugin process", "pluginId", pluginID)
if err := p.Decommission(); err != nil {
return err
}
if err := p.Stop(ctx); err != nil {
return err
}
delete(m.plugins, pluginID)
m.logger.Debug("Backend plugin unregistered", "pluginId", pluginID)
return nil
}
func (m *manager) IsRegistered(pluginID string) bool {
p, _ := m.Get(pluginID)
return p != nil && !p.IsDecommissioned()
}
func (m *manager) Get(pluginID string) (backendplugin.Plugin, bool) {
m.pluginsMu.RLock()
p, ok := m.plugins[pluginID]
m.pluginsMu.RUnlock()
return p, ok
}
@@ -115,31 +166,27 @@ func (m *manager) getAWSEnvironmentVariables() []string {
//nolint: staticcheck // plugins.DataPlugin deprecated
func (m *manager) GetDataPlugin(pluginID string) interface{} {
plugin := m.plugins[pluginID]
if plugin == nil {
p, _ := m.Get(pluginID)
if p == nil {
return nil
}
if dataPlugin, ok := plugin.(plugins.DataPlugin); ok {
if dataPlugin, ok := p.(plugins.DataPlugin); ok {
return dataPlugin
}
return nil
}
// start starts all managed backend plugins
func (m *manager) start(ctx context.Context) {
m.pluginsMu.RLock()
defer m.pluginsMu.RUnlock()
for _, p := range m.plugins {
if !p.IsManaged() {
continue
}
// start starts a managed backend plugin
func (m *manager) start(ctx context.Context, p backendplugin.Plugin) {
if !p.IsManaged() {
return
}
if err := startPluginAndRestartKilledProcesses(ctx, p); err != nil {
p.Logger().Error("Failed to start plugin", "error", err)
continue
}
if err := startPluginAndRestartKilledProcesses(ctx, p); err != nil {
p.Logger().Error("Failed to start plugin", "error", err)
}
}
@@ -435,6 +482,11 @@ func restartKilledProcess(ctx context.Context, p backendplugin.Plugin) error {
}
return nil
case <-ticker.C:
if p.IsDecommissioned() {
p.Logger().Debug("Plugin decommissioned")
return nil
}
if !p.Exited() {
continue
}
@@ -48,14 +48,17 @@ func TestManager(t *testing.T) {
ctx.cfg.BuildVersion = "7.0.0"
t.Run("Should be able to register plugin", func(t *testing.T) {
err := ctx.manager.Register(testPluginID, ctx.factory)
err := ctx.manager.RegisterAndStart(context.Background(), testPluginID, ctx.factory)
require.NoError(t, err)
require.NotNil(t, ctx.plugin)
require.Equal(t, testPluginID, ctx.plugin.pluginID)
require.NotNil(t, ctx.plugin.logger)
require.Equal(t, 1, ctx.plugin.startCount)
require.True(t, ctx.manager.IsRegistered(testPluginID))
t.Run("Should not be able to register an already registered plugin", func(t *testing.T) {
err := ctx.manager.Register(testPluginID, ctx.factory)
err := ctx.manager.RegisterAndStart(context.Background(), testPluginID, ctx.factory)
require.Equal(t, 1, ctx.plugin.startCount)
require.Error(t, err)
})
@@ -113,7 +116,7 @@ func TestManager(t *testing.T) {
wgRun.Wait()
require.Equal(t, context.Canceled, runErr)
require.Equal(t, 1, ctx.plugin.stopCount)
require.Equal(t, 2, ctx.plugin.startCount)
require.Equal(t, 1, ctx.plugin.startCount)
})
t.Run("Shouldn't be able to start managed plugin", func(t *testing.T) {
@@ -191,6 +194,21 @@ func TestManager(t *testing.T) {
require.Equal(t, http.StatusOK, w.Code)
})
})
t.Run("Should be able to decommission a running plugin", func(t *testing.T) {
require.True(t, ctx.manager.IsRegistered(testPluginID))
err := ctx.manager.UnregisterAndStop(context.Background(), testPluginID)
require.NoError(t, err)
require.Equal(t, 2, ctx.plugin.stopCount)
require.False(t, ctx.manager.IsRegistered(testPluginID))
p := ctx.manager.plugins[testPluginID]
require.Nil(t, p)
err = ctx.manager.StartPlugin(context.Background(), testPluginID)
require.Equal(t, backendplugin.ErrPluginNotRegistered, err)
})
})
})
})
@@ -202,8 +220,9 @@ func TestManager(t *testing.T) {
ctx.cfg.BuildVersion = "7.0.0"
t.Run("Should be able to register plugin", func(t *testing.T) {
err := ctx.manager.Register(testPluginID, ctx.factory)
err := ctx.manager.RegisterAndStart(context.Background(), testPluginID, ctx.factory)
require.NoError(t, err)
require.True(t, ctx.manager.IsRegistered(testPluginID))
require.False(t, ctx.plugin.managed)
t.Run("When manager runs should not start plugin", func(t *testing.T) {
@@ -259,7 +278,7 @@ func TestManager(t *testing.T) {
ctx.cfg.BuildVersion = "7.0.0"
ctx.cfg.EnterpriseLicensePath = "/license.txt"
err := ctx.manager.Register(testPluginID, ctx.factory)
err := ctx.manager.RegisterAndStart(context.Background(), testPluginID, ctx.factory)
require.NoError(t, err)
t.Run("Should provide expected host environment variables", func(t *testing.T) {
@@ -317,12 +336,13 @@ func newManagerScenario(t *testing.T, managed bool, fn func(t *testing.T, ctx *m
}
type testPlugin struct {
pluginID string
logger log.Logger
startCount int
stopCount int
managed bool
exited bool
pluginID string
logger log.Logger
startCount int
stopCount int
managed bool
exited bool
decommissioned bool
backend.CollectMetricsHandlerFunc
backend.CheckHealthHandlerFunc
backend.CallResourceHandlerFunc
@@ -362,6 +382,21 @@ func (tp *testPlugin) Exited() bool {
return tp.exited
}
func (tp *testPlugin) Decommission() error {
tp.mutex.Lock()
defer tp.mutex.Unlock()
tp.decommissioned = true
return nil
}
func (tp *testPlugin) IsDecommissioned() bool {
tp.mutex.RLock()
defer tp.mutex.RUnlock()
return tp.decommissioned
}
func (tp *testPlugin) kill() {
tp.mutex.Lock()
defer tp.mutex.Unlock()