Add support for synchronous plugin installation (#92129)

This commit is contained in:
Andres Martinez Gotor
2024-08-21 16:11:55 +02:00
committed by GitHub
parent 801f2ba728
commit 21bf013a8e
7 changed files with 125 additions and 30 deletions
@@ -3,8 +3,10 @@ package plugininstaller
import (
"context"
"errors"
"fmt"
"runtime"
"cuelang.org/go/pkg/time"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/services/featuremgmt"
@@ -18,29 +20,57 @@ type Service struct {
log log.Logger
pluginInstaller plugins.Installer
pluginStore pluginstore.Store
failOnErr bool
}
func ProvideService(cfg *setting.Cfg, features featuremgmt.FeatureToggles, pluginStore pluginstore.Store, pluginInstaller plugins.Installer) *Service {
func ProvideService(cfg *setting.Cfg, features featuremgmt.FeatureToggles, pluginStore pluginstore.Store, pluginInstaller plugins.Installer) (*Service, error) {
s := &Service{
features: features,
log: log.New("plugin.backgroundinstaller"),
cfg: cfg,
pluginInstaller: pluginInstaller,
pluginStore: pluginStore,
failOnErr: !cfg.PreinstallPluginsAsync, // Fail on error if preinstall is synchronous
}
return s
if !cfg.PreinstallPluginsAsync {
// Block initialization process until plugins are installed
err := s.installPluginsWithTimeout()
if err != nil {
return nil, err
}
}
return s, nil
}
// IsDisabled disables background installation of plugins.
func (s *Service) IsDisabled() bool {
return !s.features.IsEnabled(context.Background(), featuremgmt.FlagBackgroundPluginInstaller) ||
len(s.cfg.InstallPlugins) == 0
len(s.cfg.PreinstallPlugins) == 0 ||
!s.cfg.PreinstallPluginsAsync
}
func (s *Service) Run(ctx context.Context) error {
func (s *Service) installPluginsWithTimeout() error {
// Installation process does not timeout by default nor reuses the context
// passed to the request so we need to handle the timeout here.
// We could make this timeout configurable in the future.
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer cancel()
done := make(chan struct{ err error })
go func() {
done <- struct{ err error }{err: s.installPlugins(ctx)}
}()
select {
case <-ctx.Done():
return fmt.Errorf("failed to install plugins: %w", ctx.Err())
case d := <-done:
return d.err
}
}
func (s *Service) installPlugins(ctx context.Context) error {
compatOpts := plugins.NewCompatOpts(s.cfg.BuildVersion, runtime.GOOS, runtime.GOARCH)
for _, installPlugin := range s.cfg.InstallPlugins {
for _, installPlugin := range s.cfg.PreinstallPlugins {
// Check if the plugin is already installed
p, exists := s.pluginStore.Plugin(ctx, installPlugin.ID)
if exists {
@@ -59,6 +89,10 @@ func (s *Service) Run(ctx context.Context) error {
s.log.Debug("Plugin already installed", "pluginId", installPlugin.ID, "version", installPlugin.Version)
continue
}
if s.failOnErr {
// Halt execution in the synchronous scenario
return fmt.Errorf("failed to install plugin %s@%s: %w", installPlugin.ID, installPlugin.Version, err)
}
s.log.Error("Failed to install plugin", "pluginId", installPlugin.ID, "version", installPlugin.Version, "error", err)
continue
}
@@ -67,3 +101,12 @@ func (s *Service) Run(ctx context.Context) error {
return nil
}
func (s *Service) Run(ctx context.Context) error {
err := s.installPlugins(ctx)
if err != nil {
// Unexpected error, asynchronous installation should not return errors
s.log.Error("Failed to install plugins", "error", err)
}
return nil
}
@@ -16,14 +16,16 @@ import (
// Test if the service is disabled
func TestService_IsDisabled(t *testing.T) {
// Create a new service
s := ProvideService(
s, err := ProvideService(
&setting.Cfg{
InstallPlugins: []setting.InstallPlugin{{ID: "myplugin"}},
PreinstallPlugins: []setting.InstallPlugin{{ID: "myplugin"}},
PreinstallPluginsAsync: true,
},
featuremgmt.WithFeatures(featuremgmt.FlagBackgroundPluginInstaller),
pluginstore.New(registry.NewInMemory(), &fakes.FakeLoader{}),
&fakes.FakePluginInstaller{},
)
require.NoError(t, err)
// Check if the service is disabled
if s.IsDisabled() {
@@ -34,9 +36,9 @@ func TestService_IsDisabled(t *testing.T) {
func TestService_Run(t *testing.T) {
t.Run("Installs a plugin", func(t *testing.T) {
installed := false
s := ProvideService(
s, err := ProvideService(
&setting.Cfg{
InstallPlugins: []setting.InstallPlugin{{ID: "myplugin"}},
PreinstallPlugins: []setting.InstallPlugin{{ID: "myplugin"}},
},
featuremgmt.WithFeatures(),
pluginstore.New(registry.NewInMemory(), &fakes.FakeLoader{}),
@@ -47,17 +49,19 @@ func TestService_Run(t *testing.T) {
},
},
)
require.NoError(t, err)
err := s.Run(context.Background())
err = s.Run(context.Background())
require.NoError(t, err)
require.True(t, installed)
})
t.Run("Install a plugin with version", func(t *testing.T) {
installed := false
s := ProvideService(
s, err := ProvideService(
&setting.Cfg{
InstallPlugins: []setting.InstallPlugin{{ID: "myplugin", Version: "1.0.0"}},
PreinstallPlugins: []setting.InstallPlugin{{ID: "myplugin", Version: "1.0.0"}},
PreinstallPluginsAsync: true,
},
featuremgmt.WithFeatures(),
pluginstore.New(registry.NewInMemory(), &fakes.FakeLoader{}),
@@ -70,8 +74,9 @@ func TestService_Run(t *testing.T) {
},
},
)
require.NoError(t, err)
err := s.Run(context.Background())
err = s.Run(context.Background())
require.NoError(t, err)
require.True(t, installed)
})
@@ -84,9 +89,10 @@ func TestService_Run(t *testing.T) {
},
})
require.NoError(t, err)
s := ProvideService(
s, err := ProvideService(
&setting.Cfg{
InstallPlugins: []setting.InstallPlugin{{ID: "myplugin"}},
PreinstallPlugins: []setting.InstallPlugin{{ID: "myplugin"}},
PreinstallPluginsAsync: true,
},
featuremgmt.WithFeatures(),
pluginstore.New(preg, &fakes.FakeLoader{}),
@@ -97,6 +103,7 @@ func TestService_Run(t *testing.T) {
},
},
)
require.NoError(t, err)
err = s.Run(context.Background())
require.NoError(t, err)
@@ -114,9 +121,10 @@ func TestService_Run(t *testing.T) {
},
})
require.NoError(t, err)
s := ProvideService(
s, err := ProvideService(
&setting.Cfg{
InstallPlugins: []setting.InstallPlugin{{ID: "myplugin", Version: "2.0.0"}},
PreinstallPlugins: []setting.InstallPlugin{{ID: "myplugin", Version: "2.0.0"}},
PreinstallPluginsAsync: true,
},
featuremgmt.WithFeatures(),
pluginstore.New(preg, &fakes.FakeLoader{}),
@@ -127,6 +135,7 @@ func TestService_Run(t *testing.T) {
},
},
)
require.NoError(t, err)
err = s.Run(context.Background())
require.NoError(t, err)
@@ -135,9 +144,10 @@ func TestService_Run(t *testing.T) {
t.Run("Install multiple plugins", func(t *testing.T) {
installed := 0
s := ProvideService(
s, err := ProvideService(
&setting.Cfg{
InstallPlugins: []setting.InstallPlugin{{ID: "myplugin1"}, {ID: "myplugin2"}},
PreinstallPlugins: []setting.InstallPlugin{{ID: "myplugin1"}, {ID: "myplugin2"}},
PreinstallPluginsAsync: true,
},
featuremgmt.WithFeatures(),
pluginstore.New(registry.NewInMemory(), &fakes.FakeLoader{}),
@@ -148,17 +158,19 @@ func TestService_Run(t *testing.T) {
},
},
)
require.NoError(t, err)
err := s.Run(context.Background())
err = s.Run(context.Background())
require.NoError(t, err)
require.Equal(t, 2, installed)
})
t.Run("Fails to install a plugin but install the rest", func(t *testing.T) {
installed := 0
s := ProvideService(
s, err := ProvideService(
&setting.Cfg{
InstallPlugins: []setting.InstallPlugin{{ID: "myplugin1"}, {ID: "myplugin2"}},
PreinstallPlugins: []setting.InstallPlugin{{ID: "myplugin1"}, {ID: "myplugin2"}},
PreinstallPluginsAsync: true,
},
featuremgmt.WithFeatures(),
pluginstore.New(registry.NewInMemory(), &fakes.FakeLoader{}),
@@ -172,8 +184,46 @@ func TestService_Run(t *testing.T) {
},
},
)
err := s.Run(context.Background())
require.NoError(t, err)
err = s.Run(context.Background())
require.NoError(t, err)
require.Equal(t, 1, installed)
})
t.Run("Install a blocking plugin", func(t *testing.T) {
installed := false
_, err := ProvideService(
&setting.Cfg{
PreinstallPlugins: []setting.InstallPlugin{{ID: "myplugin"}},
PreinstallPluginsAsync: false,
},
featuremgmt.WithFeatures(),
pluginstore.New(registry.NewInMemory(), &fakes.FakeLoader{}),
&fakes.FakePluginInstaller{
AddFunc: func(ctx context.Context, pluginID string, version string, opts plugins.CompatOpts) error {
installed = true
return nil
},
},
)
require.NoError(t, err)
require.True(t, installed)
})
t.Run("Fails to install a blocking plugin", func(t *testing.T) {
_, err := ProvideService(
&setting.Cfg{
PreinstallPlugins: []setting.InstallPlugin{{ID: "myplugin"}},
PreinstallPluginsAsync: false,
},
featuremgmt.WithFeatures(),
pluginstore.New(registry.NewInMemory(), &fakes.FakeLoader{}),
&fakes.FakePluginInstaller{
AddFunc: func(ctx context.Context, pluginID string, version string, opts plugins.CompatOpts) error {
return plugins.NotFoundError{}
},
},
)
require.ErrorAs(t, err, &plugins.NotFoundError{})
})
}