Feature: Allow to install plugins through configuration (#91790)

This commit is contained in:
Andres Martinez Gotor
2024-08-13 16:57:55 +02:00
committed by GitHub
parent b03a709500
commit 9067797eb4
5 changed files with 226 additions and 13 deletions
@@ -2,31 +2,68 @@ package plugininstaller
import (
"context"
"errors"
"runtime"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
"github.com/grafana/grafana/pkg/setting"
)
type Service struct {
features featuremgmt.FeatureToggles
log log.Logger
cfg *setting.Cfg
features featuremgmt.FeatureToggles
log log.Logger
pluginInstaller plugins.Installer
pluginStore pluginstore.Store
}
func ProvideService(cfg *setting.Cfg, features featuremgmt.FeatureToggles) *Service {
func ProvideService(cfg *setting.Cfg, features featuremgmt.FeatureToggles, pluginStore pluginstore.Store, pluginInstaller plugins.Installer) *Service {
s := &Service{
features: features,
log: log.New("plugin.installer"),
features: features,
log: log.New("plugin.backgroundinstaller"),
cfg: cfg,
pluginInstaller: pluginInstaller,
pluginStore: pluginStore,
}
return s
}
// IsDisabled disables background installation of plugins.
func (s *Service) IsDisabled() bool {
return !s.features.IsEnabled(context.Background(), featuremgmt.FlagBackgroundPluginInstaller)
return !s.features.IsEnabled(context.Background(), featuremgmt.FlagBackgroundPluginInstaller) ||
len(s.cfg.InstallPlugins) == 0
}
func (s *Service) Run(ctx context.Context) error {
s.log.Debug("PluginInstaller.Run not implemented")
compatOpts := plugins.NewCompatOpts(s.cfg.BuildVersion, runtime.GOOS, runtime.GOARCH)
for _, installPlugin := range s.cfg.InstallPlugins {
// Check if the plugin is already installed
p, exists := s.pluginStore.Plugin(ctx, installPlugin.ID)
if exists {
// If it's installed, check if we are looking for a specific version
if installPlugin.Version == "" || p.Info.Version == installPlugin.Version {
s.log.Debug("Plugin already installed", "pluginId", installPlugin.ID, "version", installPlugin.Version)
continue
}
}
s.log.Info("Installing plugin", "pluginId", installPlugin.ID, "version", installPlugin.Version)
err := s.pluginInstaller.Add(ctx, installPlugin.ID, installPlugin.Version, compatOpts)
if err != nil {
var dupeErr plugins.DuplicateError
if errors.As(err, &dupeErr) {
s.log.Debug("Plugin already installed", "pluginId", installPlugin.ID, "version", installPlugin.Version)
continue
}
s.log.Error("Failed to install plugin", "pluginId", installPlugin.ID, "version", installPlugin.Version, "error", err)
continue
}
s.log.Info("Plugin successfully installed", "pluginId", installPlugin.ID, "version", installPlugin.Version)
}
return nil
}
@@ -1,20 +1,179 @@
package plugininstaller
import (
"context"
"testing"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/manager/fakes"
"github.com/grafana/grafana/pkg/plugins/manager/registry"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
"github.com/grafana/grafana/pkg/setting"
"github.com/stretchr/testify/require"
)
// Test if the service is disabled
func TestService_IsDisabled(t *testing.T) {
// Create a new service
s := &Service{
features: featuremgmt.WithFeatures(featuremgmt.FlagBackgroundPluginInstaller),
}
s := ProvideService(
&setting.Cfg{
InstallPlugins: []setting.InstallPlugin{{ID: "myplugin"}},
},
featuremgmt.WithFeatures(featuremgmt.FlagBackgroundPluginInstaller),
pluginstore.New(registry.NewInMemory(), &fakes.FakeLoader{}),
&fakes.FakePluginInstaller{},
)
// Check if the service is disabled
if s.IsDisabled() {
t.Error("Service should be enabled")
}
}
func TestService_Run(t *testing.T) {
t.Run("Installs a plugin", func(t *testing.T) {
installed := false
s := ProvideService(
&setting.Cfg{
InstallPlugins: []setting.InstallPlugin{{ID: "myplugin"}},
},
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
},
},
)
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(
&setting.Cfg{
InstallPlugins: []setting.InstallPlugin{{ID: "myplugin", Version: "1.0.0"}},
},
featuremgmt.WithFeatures(),
pluginstore.New(registry.NewInMemory(), &fakes.FakeLoader{}),
&fakes.FakePluginInstaller{
AddFunc: func(ctx context.Context, pluginID string, version string, opts plugins.CompatOpts) error {
if pluginID == "myplugin" && version == "1.0.0" {
installed = true
}
return nil
},
},
)
err := s.Run(context.Background())
require.NoError(t, err)
require.True(t, installed)
})
t.Run("Skips already installed plugin", func(t *testing.T) {
preg := registry.NewInMemory()
err := preg.Add(context.Background(), &plugins.Plugin{
JSONData: plugins.JSONData{
ID: "myplugin",
},
})
require.NoError(t, err)
s := ProvideService(
&setting.Cfg{
InstallPlugins: []setting.InstallPlugin{{ID: "myplugin"}},
},
featuremgmt.WithFeatures(),
pluginstore.New(preg, &fakes.FakeLoader{}),
&fakes.FakePluginInstaller{
AddFunc: func(ctx context.Context, pluginID string, version string, opts plugins.CompatOpts) error {
t.Fatal("Should not install plugin")
return plugins.DuplicateError{}
},
},
)
err = s.Run(context.Background())
require.NoError(t, err)
})
t.Run("Still installs a plugin if the plugin version does not match", func(t *testing.T) {
installed := false
preg := registry.NewInMemory()
err := preg.Add(context.Background(), &plugins.Plugin{
JSONData: plugins.JSONData{
ID: "myplugin",
Info: plugins.Info{
Version: "1.0.0",
},
},
})
require.NoError(t, err)
s := ProvideService(
&setting.Cfg{
InstallPlugins: []setting.InstallPlugin{{ID: "myplugin", Version: "2.0.0"}},
},
featuremgmt.WithFeatures(),
pluginstore.New(preg, &fakes.FakeLoader{}),
&fakes.FakePluginInstaller{
AddFunc: func(ctx context.Context, pluginID string, version string, opts plugins.CompatOpts) error {
installed = true
return nil
},
},
)
err = s.Run(context.Background())
require.NoError(t, err)
require.True(t, installed)
})
t.Run("Install multiple plugins", func(t *testing.T) {
installed := 0
s := ProvideService(
&setting.Cfg{
InstallPlugins: []setting.InstallPlugin{{ID: "myplugin1"}, {ID: "myplugin2"}},
},
featuremgmt.WithFeatures(),
pluginstore.New(registry.NewInMemory(), &fakes.FakeLoader{}),
&fakes.FakePluginInstaller{
AddFunc: func(ctx context.Context, pluginID string, version string, opts plugins.CompatOpts) error {
installed++
return nil
},
},
)
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(
&setting.Cfg{
InstallPlugins: []setting.InstallPlugin{{ID: "myplugin1"}, {ID: "myplugin2"}},
},
featuremgmt.WithFeatures(),
pluginstore.New(registry.NewInMemory(), &fakes.FakeLoader{}),
&fakes.FakePluginInstaller{
AddFunc: func(ctx context.Context, pluginID string, version string, opts plugins.CompatOpts) error {
if pluginID == "myplugin1" {
return plugins.NotFoundError{}
}
installed++
return nil
},
},
)
err := s.Run(context.Background())
require.NoError(t, err)
require.Equal(t, 1, installed)
})
}