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
@@ -1,4 +1,4 @@
package plugininstaller
package pluginchecker
import "github.com/grafana/grafana/pkg/setting"
@@ -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
@@ -1,4 +1,4 @@
package updatechecker
package updatemanager
import (
"context"
@@ -1,4 +1,4 @@
package updatechecker
package updatemanager
import (
"context"
@@ -1,4 +1,4 @@
package updatechecker
package updatemanager
import (
"context"
@@ -7,35 +7,54 @@ import (
"io"
"net/http"
"net/url"
"runtime"
"strings"
"sync"
"time"
"github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
"github.com/hashicorp/go-version"
"go.opentelemetry.io/otel/codes"
"github.com/grafana/grafana/pkg/infra/httpclient/httpclientprovider"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/plugins"
"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"
)
type PluginsService struct {
availableUpdates map[string]string
enabled bool
grafanaVersion string
pluginStore pluginstore.Store
httpClient httpClient
mutex sync.RWMutex
log log.Logger
tracer tracing.Tracer
updateCheckURL *url.URL
type availableUpdate struct {
localVersion string
availableVersion string
}
func ProvidePluginsService(cfg *setting.Cfg, pluginStore pluginstore.Store, tracer tracing.Tracer) (*PluginsService, error) {
type PluginsService struct {
availableUpdates map[string]availableUpdate
enabled bool
grafanaVersion string
pluginStore pluginstore.Store
httpClient httpClient
mutex sync.RWMutex
log log.Logger
tracer tracing.Tracer
updateCheckURL *url.URL
pluginInstaller plugins.Installer
updateChecker *pluginchecker.Service
updateStrategy string
features featuremgmt.FeatureToggles
}
func ProvidePluginsService(cfg *setting.Cfg,
pluginStore pluginstore.Store,
pluginInstaller plugins.Installer,
tracer tracing.Tracer,
features featuremgmt.FeatureToggles,
updateChecker *pluginchecker.Service,
) (*PluginsService, error) {
logger := log.New("plugins.update.checker")
cl, err := httpclient.New(httpclient.Options{
Middlewares: []httpclient.Middleware{
@@ -63,8 +82,12 @@ func ProvidePluginsService(cfg *setting.Cfg, pluginStore pluginstore.Store, trac
log: logger,
tracer: tracer,
pluginStore: pluginStore,
availableUpdates: make(map[string]string),
availableUpdates: make(map[string]availableUpdate),
updateCheckURL: parsedUpdateCheckURL,
pluginInstaller: pluginInstaller,
features: features,
updateChecker: updateChecker,
updateStrategy: cfg.PluginUpdateStrategy,
}, nil
}
@@ -74,6 +97,9 @@ func (s *PluginsService) IsDisabled() bool {
func (s *PluginsService) Run(ctx context.Context) error {
s.instrumentedCheckForUpdates(ctx)
if s.features.IsEnabledGlobally(featuremgmt.FlagPluginsAutoUpdate) {
s.updateAll(ctx)
}
ticker := time.NewTicker(time.Minute * 10)
run := true
@@ -82,6 +108,9 @@ func (s *PluginsService) Run(ctx context.Context) error {
select {
case <-ticker.C:
s.instrumentedCheckForUpdates(ctx)
if s.features.IsEnabledGlobally(featuremgmt.FlagPluginsAutoUpdate) {
s.updateAll(ctx)
}
case <-ctx.Done():
run = false
}
@@ -92,7 +121,7 @@ func (s *PluginsService) Run(ctx context.Context) error {
func (s *PluginsService) HasUpdate(ctx context.Context, pluginID string) (string, bool) {
s.mutex.RLock()
updateVers, updateAvailable := s.availableUpdates[pluginID]
update, updateAvailable := s.availableUpdates[pluginID]
s.mutex.RUnlock()
if updateAvailable {
// check if plugin has already been updated since the last invocation of `checkForUpdates`
@@ -101,8 +130,8 @@ func (s *PluginsService) HasUpdate(ctx context.Context, pluginID string) (string
return "", false
}
if canUpdate(plugin.Info.Version, updateVers) {
return updateVers, true
if s.canUpdate(ctx, plugin, update.availableVersion) {
return update.availableVersion, true
}
}
@@ -164,11 +193,15 @@ func (s *PluginsService) checkForUpdates(ctx context.Context) error {
return fmt.Errorf("failed to unmarshal plugin repo, reading response from grafana.com: %w", err)
}
availableUpdates := map[string]string{}
availableUpdates := make(map[string]availableUpdate)
for _, gcomP := range gcomPlugins {
if localP, exists := localPlugins[gcomP.Slug]; exists {
if canUpdate(localP.Info.Version, gcomP.Version) {
availableUpdates[localP.ID] = gcomP.Version
if s.canUpdate(ctx, localP, gcomP.Version) {
availableUpdates[localP.ID] = availableUpdate{
localVersion: localP.Info.Version,
availableVersion: gcomP.Version,
}
}
}
}
@@ -182,17 +215,20 @@ func (s *PluginsService) checkForUpdates(ctx context.Context) error {
return nil
}
func canUpdate(v1, v2 string) bool {
ver1, err1 := version.NewVersion(v1)
if err1 != nil {
return false
}
ver2, err2 := version.NewVersion(v2)
if err2 != nil {
func (s *PluginsService) canUpdate(ctx context.Context, plugin pluginstore.Plugin, gcomVersion string) bool {
if !s.updateChecker.IsUpdatable(ctx, plugin) {
return false
}
return ver1.LessThan(ver2)
if plugin.Info.Version == gcomVersion {
return false
}
if s.features.IsEnabledGlobally(featuremgmt.FlagPluginsAutoUpdate) {
return s.updateChecker.CanUpdate(plugin.ID, plugin.Info.Version, gcomVersion, s.updateStrategy == setting.PluginUpdateStrategyMinor)
}
return s.updateChecker.CanUpdate(plugin.ID, plugin.Info.Version, gcomVersion, false)
}
func (s *PluginsService) pluginIDsCSV(m map[string]pluginstore.Plugin) string {
@@ -215,3 +251,24 @@ func (s *PluginsService) pluginsEligibleForVersionCheck(ctx context.Context) map
return result
}
func (s *PluginsService) updateAll(ctx context.Context) {
ctxLogger := s.log.FromContext(ctx)
failedUpdates := make(map[string]availableUpdate)
for pluginID, availableUpdate := range s.availableUpdates {
compatOpts := plugins.NewAddOpts(s.grafanaVersion, runtime.GOOS, runtime.GOARCH, "")
ctxLogger.Info("Auto updating plugin", "pluginID", pluginID, "from", availableUpdate.localVersion, "to", availableUpdate.availableVersion)
err := s.pluginInstaller.Add(ctx, pluginID, availableUpdate.availableVersion, compatOpts)
if err != nil {
ctxLogger.Error("Failed to auto update plugin", "pluginID", pluginID, "from", availableUpdate.localVersion, "to", availableUpdate.availableVersion, "error", err)
failedUpdates[pluginID] = availableUpdate
}
}
s.mutex.Lock()
s.availableUpdates = failedUpdates
s.mutex.Unlock()
}
@@ -1,4 +1,4 @@
package updatechecker
package updatemanager
import (
"context"
@@ -13,16 +13,32 @@ import (
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/manager/fakes"
"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"
)
type mockPluginPreinstall struct {
pluginchecker.Preinstall
}
func (m *mockPluginPreinstall) IsPinned(pluginID string) bool {
return false
}
func TestPluginUpdateChecker_HasUpdate(t *testing.T) {
t.Run("update is available", func(t *testing.T) {
updateCheckURL, _ := url.Parse("https://grafana.com/api/plugins/versioncheck")
svc := PluginsService{
availableUpdates: map[string]string{
"test-ds": "1.0.0",
availableUpdates: map[string]availableUpdate{
"test-ds": {
localVersion: "0.9.0",
availableVersion: "1.0.0",
},
},
pluginStore: &pluginstore.FakePluginStore{
PluginList: []pluginstore.Plugin{
@@ -35,6 +51,8 @@ func TestPluginUpdateChecker_HasUpdate(t *testing.T) {
},
},
updateCheckURL: updateCheckURL,
updateChecker: pluginchecker.ProvideService(managedplugins.NewNoop(), provisionedplugins.NewNoop(), &mockPluginPreinstall{}),
features: &featuremgmt.FeatureManager{},
}
update, exists := svc.HasUpdate(context.Background(), "test-ds")
@@ -46,9 +64,15 @@ func TestPluginUpdateChecker_HasUpdate(t *testing.T) {
updateCheckURL, _ := url.Parse("https://grafana.com/api/plugins/versioncheck")
svc := PluginsService{
availableUpdates: map[string]string{
"test-panel": "0.9.0",
"test-app": "0.0.1",
availableUpdates: map[string]availableUpdate{
"test-panel": {
localVersion: "0.9.0",
availableVersion: "0.9.0",
},
"test-app": {
localVersion: "0.9.0",
availableVersion: "0.9.0",
},
},
pluginStore: &pluginstore.FakePluginStore{
PluginList: []pluginstore.Plugin{
@@ -73,6 +97,7 @@ func TestPluginUpdateChecker_HasUpdate(t *testing.T) {
},
},
updateCheckURL: updateCheckURL,
updateChecker: pluginchecker.ProvideService(managedplugins.NewNoop(), provisionedplugins.NewNoop(), &mockPluginPreinstall{}),
}
update, exists := svc.HasUpdate(context.Background(), "test-ds")
@@ -92,8 +117,11 @@ func TestPluginUpdateChecker_HasUpdate(t *testing.T) {
updateCheckURL, _ := url.Parse("https://grafana.com/api/plugins/versioncheck")
svc := PluginsService{
availableUpdates: map[string]string{
"test-panel": "0.9.0",
availableUpdates: map[string]availableUpdate{
"test-panel": {
localVersion: "0.9.0",
availableVersion: "0.9.0",
},
},
pluginStore: &pluginstore.FakePluginStore{
PluginList: []pluginstore.Plugin{
@@ -138,8 +166,11 @@ func TestPluginUpdateChecker_checkForUpdates(t *testing.T) {
updateCheckURL, _ := url.Parse("https://grafana.com/api/plugins/versioncheck")
svc := PluginsService{
availableUpdates: map[string]string{
"test-app": "1.0.0",
availableUpdates: map[string]availableUpdate{
"test-app": {
localVersion: "0.5.0",
availableVersion: "1.0.0",
},
},
pluginStore: &pluginstore.FakePluginStore{
PluginList: []pluginstore.Plugin{
@@ -183,13 +214,15 @@ func TestPluginUpdateChecker_checkForUpdates(t *testing.T) {
log: log.NewNopLogger(),
tracer: tracing.InitializeTracerForTest(),
updateCheckURL: updateCheckURL,
updateChecker: pluginchecker.ProvideService(managedplugins.NewNoop(), provisionedplugins.NewNoop(), &mockPluginPreinstall{}),
features: &featuremgmt.FeatureManager{},
}
svc.instrumentedCheckForUpdates(context.Background())
require.Equal(t, 1, len(svc.availableUpdates))
require.Equal(t, "1.0.12", svc.availableUpdates["test-ds"])
require.Equal(t, "1.0.12", svc.availableUpdates["test-ds"].availableVersion)
update, exists := svc.HasUpdate(context.Background(), "test-ds")
require.True(t, exists)
require.Equal(t, "1.0.12", update)
@@ -207,6 +240,50 @@ func TestPluginUpdateChecker_checkForUpdates(t *testing.T) {
require.Empty(t, svc.availableUpdates["test-core-panel"])
})
}
func TestPluginUpdateChecker_updateAll(t *testing.T) {
t.Run("update is available", func(t *testing.T) {
pluginsFakeStore := map[string]string{}
availableUpdates := map[string]availableUpdate{
"test-app-0": {
localVersion: "0.9.0",
availableVersion: "1.0.0",
},
"test-app-1": {
localVersion: "0.9.0",
availableVersion: "1.0.0",
},
"test-app-2": {
localVersion: "0.9.0",
availableVersion: "1.0.0",
},
}
svc := PluginsService{
availableUpdates: availableUpdates,
log: log.NewNopLogger(),
tracer: tracing.InitializeTracerForTest(),
pluginInstaller: &fakes.FakePluginInstaller{
AddFunc: func(ctx context.Context, pluginID, version string, opts plugins.AddOpts) error {
pluginsFakeStore[pluginID] = version
return nil
},
RemoveFunc: func(ctx context.Context, pluginID, version string) error {
delete(pluginsFakeStore, pluginID)
return nil
},
},
}
svc.updateAll(context.Background())
require.Equal(t, 0, len(svc.availableUpdates))
require.Equal(t, len(availableUpdates), len(pluginsFakeStore))
for pluginID, availableUpdate := range availableUpdates {
require.Equal(t, availableUpdate.availableVersion, pluginsFakeStore[pluginID])
}
})
}
type fakeHTTPClient struct {
fakeResp string
@@ -1,4 +1,4 @@
package updatechecker
package updatemanager
import "net/http"