Plugins: Add preinstall_sync config - allow plugins to be preinstalled synchronously (#105292)
* feat: preinstall_sync config - process and installation logic * ref: add preinstall_sync list to preinstalled plugins of frontendsettings * fix: conf blank line for sections * ref: remove plugins async flag, and rename PreinstallPlugins * docs: default installed plugin list
This commit is contained in:
@@ -42,7 +42,6 @@ type Service struct {
|
||||
pluginStore pluginstore.Store
|
||||
pluginRepo repo.Service
|
||||
features featuremgmt.FeatureToggles
|
||||
failOnErr bool
|
||||
updateChecker pluginchecker.PluginUpdateChecker
|
||||
}
|
||||
|
||||
@@ -65,14 +64,13 @@ func ProvideService(
|
||||
cfg: cfg,
|
||||
pluginInstaller: pluginInstaller,
|
||||
pluginStore: pluginStore,
|
||||
failOnErr: !cfg.PreinstallPluginsAsync, // Fail on error if preinstall is synchronous
|
||||
pluginRepo: pluginRepo,
|
||||
features: features,
|
||||
updateChecker: updateChecker,
|
||||
}
|
||||
if !cfg.PreinstallPluginsAsync {
|
||||
if len(cfg.PreinstallPluginsSync) > 0 {
|
||||
// Block initialization process until plugins are installed
|
||||
err := s.installPluginsWithTimeout()
|
||||
err := s.installPluginsWithTimeout(cfg.PreinstallPluginsSync)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -82,11 +80,10 @@ func ProvideService(
|
||||
|
||||
// IsDisabled disables background installation of plugins.
|
||||
func (s *Service) IsDisabled() bool {
|
||||
return len(s.cfg.PreinstallPlugins) == 0 ||
|
||||
!s.cfg.PreinstallPluginsAsync
|
||||
return len(s.cfg.PreinstallPluginsAsync) == 0
|
||||
}
|
||||
|
||||
func (s *Service) installPluginsWithTimeout() error {
|
||||
func (s *Service) installPluginsWithTimeout(pluginsToInstall []setting.InstallPlugin) 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.
|
||||
@@ -94,7 +91,7 @@ func (s *Service) installPluginsWithTimeout() error {
|
||||
defer cancel()
|
||||
done := make(chan struct{ err error })
|
||||
go func() {
|
||||
done <- struct{ err error }{err: s.installPlugins(ctx)}
|
||||
done <- struct{ err error }{err: s.installPlugins(ctx, pluginsToInstall, true)}
|
||||
}()
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
@@ -114,8 +111,8 @@ func (s *Service) shouldUpdate(ctx context.Context, pluginID, currentVersion str
|
||||
return s.updateChecker.CanUpdate(pluginID, currentVersion, info.Version, true)
|
||||
}
|
||||
|
||||
func (s *Service) installPlugins(ctx context.Context) error {
|
||||
for _, installPlugin := range s.cfg.PreinstallPlugins {
|
||||
func (s *Service) installPlugins(ctx context.Context, pluginsToInstall []setting.InstallPlugin, failOnErr bool) error {
|
||||
for _, installPlugin := range pluginsToInstall {
|
||||
// Check if the plugin is already installed
|
||||
p, exists := s.pluginStore.Plugin(ctx, installPlugin.ID)
|
||||
if exists {
|
||||
@@ -148,7 +145,7 @@ func (s *Service) installPlugins(ctx context.Context) error {
|
||||
s.log.Debug("Plugin already installed", "pluginId", installPlugin.ID, "version", installPlugin.Version)
|
||||
continue
|
||||
}
|
||||
if s.failOnErr {
|
||||
if failOnErr {
|
||||
// Halt execution in the synchronous scenario
|
||||
return fmt.Errorf("failed to install plugin %s@%s: %w", installPlugin.ID, installPlugin.Version, err)
|
||||
}
|
||||
@@ -165,7 +162,7 @@ func (s *Service) installPlugins(ctx context.Context) error {
|
||||
}
|
||||
|
||||
func (s *Service) Run(ctx context.Context) error {
|
||||
err := s.installPlugins(ctx)
|
||||
err := s.installPlugins(ctx, s.cfg.PreinstallPluginsAsync, false)
|
||||
if err != nil {
|
||||
// Unexpected error, asynchronous installation should not return errors
|
||||
s.log.Error("Failed to install plugins", "error", err)
|
||||
|
||||
@@ -24,8 +24,7 @@ func TestService_IsDisabled(t *testing.T) {
|
||||
// Create a new service
|
||||
s, err := ProvideService(
|
||||
&setting.Cfg{
|
||||
PreinstallPlugins: []setting.InstallPlugin{{ID: "myplugin"}},
|
||||
PreinstallPluginsAsync: true,
|
||||
PreinstallPluginsAsync: []setting.InstallPlugin{{ID: "myplugin"}},
|
||||
},
|
||||
pluginstore.New(registry.NewInMemory(), &fakes.FakeLoader{}),
|
||||
&fakes.FakePluginInstaller{},
|
||||
@@ -44,13 +43,14 @@ func TestService_IsDisabled(t *testing.T) {
|
||||
|
||||
func TestService_Run(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
shouldInstall bool
|
||||
pluginsToInstall []setting.InstallPlugin
|
||||
existingPlugins []*plugins.Plugin
|
||||
pluginsToFail []string
|
||||
blocking bool
|
||||
latestPlugin *repo.PluginArchiveInfo
|
||||
name string
|
||||
shouldInstall bool
|
||||
shouldThrowError bool
|
||||
pluginsToInstall []setting.InstallPlugin
|
||||
pluginsToInstallSync []setting.InstallPlugin
|
||||
existingPlugins []*plugins.Plugin
|
||||
pluginsToFail []string
|
||||
latestPlugin *repo.PluginArchiveInfo
|
||||
}{
|
||||
{
|
||||
name: "Installs a plugin",
|
||||
@@ -64,6 +64,7 @@ func TestService_Run(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "Skips already installed plugin",
|
||||
shouldThrowError: false,
|
||||
shouldInstall: false,
|
||||
pluginsToInstall: []setting.InstallPlugin{{ID: "myplugin"}},
|
||||
existingPlugins: []*plugins.Plugin{{JSONData: plugins.JSONData{ID: "myplugin"}}},
|
||||
@@ -86,17 +87,16 @@ func TestService_Run(t *testing.T) {
|
||||
pluginsToFail: []string{"myplugin1"},
|
||||
},
|
||||
{
|
||||
name: "Install a blocking plugin",
|
||||
shouldInstall: true,
|
||||
pluginsToInstall: []setting.InstallPlugin{{ID: "myplugin"}},
|
||||
blocking: true,
|
||||
name: "Install a plugin from sync list",
|
||||
shouldInstall: true,
|
||||
pluginsToInstallSync: []setting.InstallPlugin{{ID: "myplugin"}},
|
||||
},
|
||||
{
|
||||
name: "Fails to install a blocking plugin",
|
||||
shouldInstall: false,
|
||||
pluginsToInstall: []setting.InstallPlugin{{ID: "myplugin"}},
|
||||
blocking: true,
|
||||
pluginsToFail: []string{"myplugin"},
|
||||
name: "when installation fails in sync mode, it should throw an error",
|
||||
shouldInstall: false,
|
||||
shouldThrowError: true,
|
||||
pluginsToInstallSync: []setting.InstallPlugin{{ID: "myplugin"}},
|
||||
pluginsToFail: []string{"myplugin"},
|
||||
},
|
||||
{
|
||||
name: "Updates a plugin",
|
||||
@@ -138,6 +138,12 @@ func TestService_Run(t *testing.T) {
|
||||
existingPlugins: []*plugins.Plugin{{JSONData: plugins.JSONData{ID: "myplugin", Info: plugins.Info{Version: "1.0.0"}}}},
|
||||
latestPlugin: &repo.PluginArchiveInfo{Version: "1.0.0-rc.1"},
|
||||
},
|
||||
{
|
||||
name: "should install all plugins - sync and async",
|
||||
shouldInstall: true,
|
||||
pluginsToInstallSync: []setting.InstallPlugin{{ID: "myplugin"}},
|
||||
pluginsToInstall: []setting.InstallPlugin{{ID: "myplugin2"}},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
@@ -150,8 +156,8 @@ func TestService_Run(t *testing.T) {
|
||||
installedFromURL := 0
|
||||
s, err := ProvideService(
|
||||
&setting.Cfg{
|
||||
PreinstallPlugins: tt.pluginsToInstall,
|
||||
PreinstallPluginsAsync: !tt.blocking,
|
||||
PreinstallPluginsAsync: tt.pluginsToInstall,
|
||||
PreinstallPluginsSync: tt.pluginsToInstallSync,
|
||||
},
|
||||
pluginstore.New(preg, &fakes.FakeLoader{}),
|
||||
&fakes.FakePluginInstaller{
|
||||
@@ -165,7 +171,8 @@ func TestService_Run(t *testing.T) {
|
||||
t.Fatal("Should not install plugin")
|
||||
return errors.New("Should not install plugin")
|
||||
}
|
||||
for _, plugin := range tt.pluginsToInstall {
|
||||
allPluginsToInstall := append(tt.pluginsToInstallSync, tt.pluginsToInstall...)
|
||||
for _, plugin := range allPluginsToInstall {
|
||||
if plugin.ID == pluginID && plugin.Version == version {
|
||||
if opts.URL() != "" {
|
||||
installedFromURL++
|
||||
@@ -190,20 +197,19 @@ func TestService_Run(t *testing.T) {
|
||||
&pluginchecker.FakePluginPreinstall{},
|
||||
),
|
||||
)
|
||||
if tt.blocking && !tt.shouldInstall {
|
||||
if tt.shouldThrowError {
|
||||
require.ErrorContains(t, err, "Failed to install plugin")
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
err = s.Run(context.Background())
|
||||
require.NoError(t, err)
|
||||
|
||||
if !tt.blocking {
|
||||
err = s.Run(context.Background())
|
||||
require.NoError(t, err)
|
||||
}
|
||||
if tt.shouldInstall {
|
||||
expectedInstalled := 0
|
||||
expectedInstalledFromURL := 0
|
||||
for _, plugin := range tt.pluginsToInstall {
|
||||
allPluginsToInstall := append(tt.pluginsToInstallSync, tt.pluginsToInstall...)
|
||||
for _, plugin := range allPluginsToInstall {
|
||||
expectedFailed := false
|
||||
for _, pluginFail := range tt.pluginsToFail {
|
||||
if plugin.ID == pluginFail {
|
||||
|
||||
Reference in New Issue
Block a user