Plugins: Add plugins auto update feature (#104112)
This commit is contained in:
+1
-1
@@ -1,4 +1,4 @@
|
||||
package plugininstaller
|
||||
package pluginchecker
|
||||
|
||||
import "github.com/grafana/grafana/pkg/setting"
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package plugininstaller
|
||||
package pluginchecker
|
||||
|
||||
import (
|
||||
"testing"
|
||||
@@ -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
|
||||
}
|
||||
@@ -8,11 +8,11 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/plugins/repo"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginchecker"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
@@ -43,6 +43,7 @@ type Service struct {
|
||||
pluginRepo repo.Service
|
||||
features featuremgmt.FeatureToggles
|
||||
failOnErr bool
|
||||
updateChecker pluginchecker.PluginUpdateChecker
|
||||
}
|
||||
|
||||
func ProvideService(
|
||||
@@ -52,6 +53,7 @@ func ProvideService(
|
||||
promReg prometheus.Registerer,
|
||||
pluginRepo repo.Service,
|
||||
features featuremgmt.FeatureToggles,
|
||||
updateChecker pluginchecker.PluginUpdateChecker,
|
||||
) (*Service, error) {
|
||||
once.Do(func() {
|
||||
promReg.MustRegister(installRequestCounter)
|
||||
@@ -66,6 +68,7 @@ func ProvideService(
|
||||
failOnErr: !cfg.PreinstallPluginsAsync, // Fail on error if preinstall is synchronous
|
||||
pluginRepo: pluginRepo,
|
||||
features: features,
|
||||
updateChecker: updateChecker,
|
||||
}
|
||||
if !cfg.PreinstallPluginsAsync {
|
||||
// Block initialization process until plugins are installed
|
||||
@@ -108,34 +111,7 @@ func (s *Service) shouldUpdate(ctx context.Context, pluginID, currentVersion str
|
||||
return false
|
||||
}
|
||||
|
||||
// If we are already on the latest version, skip the installation
|
||||
if info.Version == currentVersion {
|
||||
s.log.Debug("Latest plugin already installed", "pluginId", pluginID, "version", info.Version)
|
||||
return false
|
||||
}
|
||||
|
||||
// If the latest version is a new major version, skip the installation
|
||||
parsedLatestVersion, err := semver.NewVersion(info.Version)
|
||||
if err != nil {
|
||||
s.log.Error("Failed to parse latest version, skipping potential update", "pluginId", pluginID, "version", info.Version, "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 parsedLatestVersion.Major() > parsedCurrentVersion.Major() {
|
||||
s.log.Debug("New major version available, skipping update due to possible breaking changes", "pluginId", pluginID, "version", info.Version)
|
||||
return false
|
||||
}
|
||||
if parsedCurrentVersion.Compare(parsedLatestVersion) >= 0 {
|
||||
s.log.Debug("No update available", "pluginId", pluginID, "version", info.Version)
|
||||
return false
|
||||
}
|
||||
|
||||
// We should update the plugin
|
||||
return true
|
||||
return s.updateChecker.CanUpdate(pluginID, currentVersion, info.Version, true)
|
||||
}
|
||||
|
||||
func (s *Service) installPlugins(ctx context.Context) error {
|
||||
|
||||
@@ -10,7 +10,10 @@ import (
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/registry"
|
||||
"github.com/grafana/grafana/pkg/plugins/repo"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/managedplugins"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginchecker"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/provisionedplugins"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -29,6 +32,7 @@ func TestService_IsDisabled(t *testing.T) {
|
||||
prometheus.NewRegistry(),
|
||||
&fakes.FakePluginRepo{},
|
||||
featuremgmt.WithFeatures(),
|
||||
&pluginchecker.FakePluginUpdateChecker{},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -180,6 +184,11 @@ func TestService_Run(t *testing.T) {
|
||||
},
|
||||
},
|
||||
featuremgmt.WithFeatures(featuremgmt.FlagPreinstallAutoUpdate),
|
||||
pluginchecker.ProvideService(
|
||||
managedplugins.NewNoop(),
|
||||
provisionedplugins.NewNoop(),
|
||||
&pluginchecker.FakePluginPreinstall{},
|
||||
),
|
||||
)
|
||||
if tt.blocking && !tt.shouldInstall {
|
||||
require.ErrorContains(t, err, "Failed to install plugin")
|
||||
|
||||
@@ -46,6 +46,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/managedplugins"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pipeline"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginassets"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginchecker"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginconfig"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginerrs"
|
||||
@@ -128,10 +129,12 @@ var WireSet = wire.NewSet(
|
||||
wire.Bind(new(plugincontext.BasePluginContextProvider), new(*plugincontext.BaseProvider)),
|
||||
plugininstaller.ProvideService,
|
||||
pluginassets.ProvideService,
|
||||
plugininstaller.ProvidePreinstall,
|
||||
wire.Bind(new(plugininstaller.Preinstall), new(*plugininstaller.PreinstallImpl)),
|
||||
pluginchecker.ProvidePreinstall,
|
||||
wire.Bind(new(pluginchecker.Preinstall), new(*pluginchecker.PreinstallImpl)),
|
||||
advisor.ProvideService,
|
||||
wire.Bind(new(advisor.AdvisorStats), new(*advisor.Service)),
|
||||
pluginchecker.ProvideService,
|
||||
wire.Bind(new(pluginchecker.PluginUpdateChecker), new(*pluginchecker.Service)),
|
||||
)
|
||||
|
||||
// WireExtensionSet provides a wire.ProviderSet of plugin providers that can be
|
||||
|
||||
Reference in New Issue
Block a user