mirror of
https://github.com/grafana/grafana.git
synced 2024-11-25 10:20:29 -06:00
Feature: Allow to install plugins through configuration (#91790)
This commit is contained in:
parent
b03a709500
commit
9067797eb4
@ -21,7 +21,7 @@ import (
|
|||||||
type FakePluginInstaller struct {
|
type FakePluginInstaller struct {
|
||||||
AddFunc func(ctx context.Context, pluginID, version string, opts plugins.CompatOpts) error
|
AddFunc func(ctx context.Context, pluginID, version string, opts plugins.CompatOpts) error
|
||||||
// Remove removes a plugin from the store.
|
// Remove removes a plugin from the store.
|
||||||
RemoveFunc func(ctx context.Context, pluginID string) error
|
RemoveFunc func(ctx context.Context, pluginID, version string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *FakePluginInstaller) Add(ctx context.Context, pluginID, version string, opts plugins.CompatOpts) error {
|
func (i *FakePluginInstaller) Add(ctx context.Context, pluginID, version string, opts plugins.CompatOpts) error {
|
||||||
@ -31,9 +31,9 @@ func (i *FakePluginInstaller) Add(ctx context.Context, pluginID, version string,
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *FakePluginInstaller) Remove(ctx context.Context, pluginID string) error {
|
func (i *FakePluginInstaller) Remove(ctx context.Context, pluginID, version string) error {
|
||||||
if i.RemoveFunc != nil {
|
if i.RemoveFunc != nil {
|
||||||
return i.RemoveFunc(ctx, pluginID)
|
return i.RemoveFunc(ctx, pluginID, version)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -2,31 +2,68 @@ package plugininstaller
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
|
"runtime"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/infra/log"
|
"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/featuremgmt"
|
||||||
|
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Service struct {
|
type Service struct {
|
||||||
features featuremgmt.FeatureToggles
|
cfg *setting.Cfg
|
||||||
log log.Logger
|
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{
|
s := &Service{
|
||||||
features: features,
|
features: features,
|
||||||
log: log.New("plugin.installer"),
|
log: log.New("plugin.backgroundinstaller"),
|
||||||
|
cfg: cfg,
|
||||||
|
pluginInstaller: pluginInstaller,
|
||||||
|
pluginStore: pluginStore,
|
||||||
}
|
}
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsDisabled disables background installation of plugins.
|
// IsDisabled disables background installation of plugins.
|
||||||
func (s *Service) IsDisabled() bool {
|
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 {
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -1,20 +1,179 @@
|
|||||||
package plugininstaller
|
package plugininstaller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"testing"
|
"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/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
|
// Test if the service is disabled
|
||||||
func TestService_IsDisabled(t *testing.T) {
|
func TestService_IsDisabled(t *testing.T) {
|
||||||
// Create a new service
|
// Create a new service
|
||||||
s := &Service{
|
s := ProvideService(
|
||||||
features: featuremgmt.WithFeatures(featuremgmt.FlagBackgroundPluginInstaller),
|
&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
|
// Check if the service is disabled
|
||||||
if s.IsDisabled() {
|
if s.IsDisabled() {
|
||||||
t.Error("Service should be enabled")
|
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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@ -198,6 +198,7 @@ type Cfg struct {
|
|||||||
HideAngularDeprecation []string
|
HideAngularDeprecation []string
|
||||||
PluginInstallToken string
|
PluginInstallToken string
|
||||||
ForwardHostEnvVars []string
|
ForwardHostEnvVars []string
|
||||||
|
InstallPlugins []InstallPlugin
|
||||||
|
|
||||||
PluginsCDNURLTemplate string
|
PluginsCDNURLTemplate string
|
||||||
PluginLogBackendRequests bool
|
PluginLogBackendRequests bool
|
||||||
@ -526,6 +527,11 @@ type Cfg struct {
|
|||||||
UnifiedStorage map[string]grafanarest.DualWriterMode
|
UnifiedStorage map[string]grafanarest.DualWriterMode
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type InstallPlugin struct {
|
||||||
|
ID string
|
||||||
|
Version string
|
||||||
|
}
|
||||||
|
|
||||||
// AddChangePasswordLink returns if login form is disabled or not since
|
// AddChangePasswordLink returns if login form is disabled or not since
|
||||||
// the same intention can be used to hide both features.
|
// the same intention can be used to hide both features.
|
||||||
func (cfg *Cfg) AddChangePasswordLink() bool {
|
func (cfg *Cfg) AddChangePasswordLink() bool {
|
||||||
|
@ -39,6 +39,17 @@ func (cfg *Cfg) readPluginSettings(iniFile *ini.File) error {
|
|||||||
cfg.DisablePlugins = util.SplitString(pluginsSection.Key("disable_plugins").MustString(""))
|
cfg.DisablePlugins = util.SplitString(pluginsSection.Key("disable_plugins").MustString(""))
|
||||||
cfg.HideAngularDeprecation = util.SplitString(pluginsSection.Key("hide_angular_deprecation").MustString(""))
|
cfg.HideAngularDeprecation = util.SplitString(pluginsSection.Key("hide_angular_deprecation").MustString(""))
|
||||||
cfg.ForwardHostEnvVars = util.SplitString(pluginsSection.Key("forward_host_env_vars").MustString(""))
|
cfg.ForwardHostEnvVars = util.SplitString(pluginsSection.Key("forward_host_env_vars").MustString(""))
|
||||||
|
rawInstallPlugins := util.SplitString(pluginsSection.Key("install").MustString(""))
|
||||||
|
cfg.InstallPlugins = make([]InstallPlugin, len(rawInstallPlugins))
|
||||||
|
for i, plugin := range rawInstallPlugins {
|
||||||
|
parts := strings.Split(plugin, "@")
|
||||||
|
id := parts[0]
|
||||||
|
v := ""
|
||||||
|
if len(parts) == 2 {
|
||||||
|
v = parts[1]
|
||||||
|
}
|
||||||
|
cfg.InstallPlugins[i] = InstallPlugin{id, v}
|
||||||
|
}
|
||||||
|
|
||||||
cfg.PluginCatalogURL = pluginsSection.Key("plugin_catalog_url").MustString("https://grafana.com/grafana/plugins/")
|
cfg.PluginCatalogURL = pluginsSection.Key("plugin_catalog_url").MustString("https://grafana.com/grafana/plugins/")
|
||||||
cfg.PluginAdminEnabled = pluginsSection.Key("plugin_admin_enabled").MustBool(true)
|
cfg.PluginAdminEnabled = pluginsSection.Key("plugin_admin_enabled").MustBool(true)
|
||||||
|
Loading…
Reference in New Issue
Block a user