Plugins: Add plugins auto update feature (#104112)

This commit is contained in:
Hugo Kiyodi Oshiro
2025-05-09 15:58:04 +02:00
committed by GitHub
parent 42028a1b03
commit 43748e43bb
24 changed files with 446 additions and 220 deletions
@@ -0,0 +1,36 @@
package pluginchecker
import "github.com/grafana/grafana/pkg/setting"
type Preinstall interface {
IsPreinstalled(pluginID string) bool
IsPinned(pluginID string) bool
}
func ProvidePreinstall(
cfg *setting.Cfg,
) *PreinstallImpl {
plugins := make(map[string]*setting.InstallPlugin)
for _, p := range cfg.PreinstallPlugins {
plugins[p.ID] = &p
}
return &PreinstallImpl{
plugins: plugins,
}
}
type PreinstallImpl struct {
plugins map[string]*setting.InstallPlugin
}
func (c *PreinstallImpl) IsPreinstalled(pluginID string) bool {
_, ok := c.plugins[pluginID]
return ok
}
func (c *PreinstallImpl) IsPinned(pluginID string) bool {
if p, ok := c.plugins[pluginID]; ok {
return p.Version != ""
}
return false
}
@@ -0,0 +1,36 @@
package pluginchecker
import (
"testing"
"github.com/grafana/grafana/pkg/setting"
"github.com/stretchr/testify/assert"
)
func TestIsPreinstalled(t *testing.T) {
cfg := &setting.Cfg{
PreinstallPlugins: []setting.InstallPlugin{
{ID: "plugin1"},
{ID: "plugin2"},
},
}
preinstall := ProvidePreinstall(cfg)
assert.True(t, preinstall.IsPreinstalled("plugin1"))
assert.True(t, preinstall.IsPreinstalled("plugin2"))
assert.False(t, preinstall.IsPreinstalled("plugin3"))
}
func TestIsPinned(t *testing.T) {
cfg := &setting.Cfg{
PreinstallPlugins: []setting.InstallPlugin{
{ID: "plugin1", Version: "1.0.0"},
{ID: "plugin2"},
},
}
preinstall := ProvidePreinstall(cfg)
assert.True(t, preinstall.IsPinned("plugin1"))
assert.False(t, preinstall.IsPinned("plugin2"))
assert.False(t, preinstall.IsPinned("plugin3"))
}
@@ -0,0 +1,45 @@
package pluginchecker
import (
"context"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
)
type FakePluginUpdateChecker struct {
IsUpdatableFunc func(ctx context.Context, plugin pluginstore.Plugin) bool
CanUpdateFunc func(pluginId string, currentVersion string, targetVersion string, onlyMinor bool) bool
}
func (f *FakePluginUpdateChecker) IsUpdatable(ctx context.Context, plugin pluginstore.Plugin) bool {
if f.IsUpdatableFunc != nil {
return f.IsUpdatableFunc(ctx, plugin)
}
return true
}
func (f *FakePluginUpdateChecker) CanUpdate(pluginId string, currentVersion string, targetVersion string, onlyMinor bool) bool {
if f.CanUpdateFunc != nil {
return f.CanUpdateFunc(pluginId, currentVersion, targetVersion, onlyMinor)
}
return true
}
type FakePluginPreinstall struct {
IsPinnedFunc func(pluginID string) bool
IsPreinstalledFunc func(pluginID string) bool
}
func (f *FakePluginPreinstall) IsPinned(pluginID string) bool {
if f.IsPinnedFunc != nil {
return f.IsPinnedFunc(pluginID)
}
return false
}
func (f *FakePluginPreinstall) IsPreinstalled(pluginID string) bool {
if f.IsPreinstalledFunc != nil {
return f.IsPreinstalledFunc(pluginID)
}
return false
}
@@ -0,0 +1,118 @@
package pluginchecker
import (
"context"
"slices"
"github.com/Masterminds/semver"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/pluginsintegration/managedplugins"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
"github.com/grafana/grafana/pkg/services/pluginsintegration/provisionedplugins"
)
type PluginUpdateChecker interface {
IsUpdatable(ctx context.Context, plugin pluginstore.Plugin) bool
CanUpdate(pluginId string, currentVersion string, targetVersion string, onlyMinor bool) bool
}
var _ PluginUpdateChecker = (*Service)(nil)
type Service struct {
managedPluginsManager managedplugins.Manager
provisionedPluginsManager provisionedplugins.Manager
pluginPreinstall Preinstall
provisionedPlugins []string
log log.Logger
}
func ProvideService(
managedPluginsManager managedplugins.Manager,
provisionedPluginsManager provisionedplugins.Manager,
pluginPreinstall Preinstall,
) *Service {
return &Service{
managedPluginsManager: managedPluginsManager,
provisionedPluginsManager: provisionedPluginsManager,
pluginPreinstall: pluginPreinstall,
log: log.New("plugin.updatechecker"),
}
}
func (s *Service) isManaged(ctx context.Context, pluginID string) bool {
for _, managedPlugin := range s.managedPluginsManager.ManagedPlugins(ctx) {
if managedPlugin == pluginID {
return true
}
}
return false
}
func (s *Service) isProvisioned(ctx context.Context, pluginID string) bool {
if s.provisionedPlugins == nil {
var err error
s.provisionedPlugins, err = s.provisionedPluginsManager.ProvisionedPlugins(ctx)
if err != nil {
return false
}
}
return slices.Contains(s.provisionedPlugins, pluginID)
}
func (s *Service) IsUpdatable(ctx context.Context, plugin pluginstore.Plugin) bool {
if plugin.IsCorePlugin() {
s.log.Debug("Skipping core plugin", "plugin", plugin.ID)
return false
}
if s.isManaged(ctx, plugin.ID) {
s.log.Debug("Skipping managed plugin", "plugin", plugin.ID)
return false
}
if s.pluginPreinstall.IsPinned(plugin.ID) {
s.log.Debug("Skipping pinned plugin", "plugin", plugin.ID)
return false
}
if s.isProvisioned(ctx, plugin.ID) {
s.log.Debug("Skipping provisioned plugin", "plugin", plugin.ID)
return false
}
return true
}
func (s *Service) CanUpdate(pluginId string, currentVersion string, targetVersion string, onlyMinor bool) bool {
// If we are already on the latest version, skip the installation
if currentVersion == targetVersion {
s.log.Debug("Latest plugin already installed", "pluginId", pluginId, "version", targetVersion)
return false
}
// If the latest version is a new major version, skip the installation
parsedLatestVersion, err := semver.NewVersion(targetVersion)
if err != nil {
s.log.Error("Failed to parse latest version, skipping potential update", "pluginId", pluginId, "version", targetVersion, "error", err)
return false
}
parsedCurrentVersion, err := semver.NewVersion(currentVersion)
if err != nil {
s.log.Error("Failed to parse current version, skipping potential update", "pluginId", pluginId, "version", currentVersion, "error", err)
return false
}
if onlyMinor && (parsedLatestVersion.Major() > parsedCurrentVersion.Major()) {
s.log.Debug("New major version available, skipping update due to possible breaking changes", "pluginId", pluginId, "version", targetVersion)
return false
}
if parsedCurrentVersion.Compare(parsedLatestVersion) >= 0 {
s.log.Debug("No update available", "pluginId", pluginId, "version", targetVersion)
return false
}
// We should update the plugin
return true
}