diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 45b4b06651c..90471eb7717 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -166,7 +166,7 @@ /pkg/services/tag/ @grafana/grafana-search-and-storage /pkg/services/team/ @grafana/access-squad /pkg/services/temp_user/ @grafana/grafana-backend-group -/pkg/services/updatechecker/ @grafana/grafana-backend-group +/pkg/services/updatemanager/ @grafana/grafana-backend-group /pkg/services/user/ @grafana/access-squad /pkg/services/validations/ @grafana/grafana-backend-group /pkg/setting/ @grafana/grafana-backend-services-squad diff --git a/apps/advisor/pkg/app/checkregistry/checkregistry.go b/apps/advisor/pkg/app/checkregistry/checkregistry.go index 3d8d8188a1b..181dff983f7 100644 --- a/apps/advisor/pkg/app/checkregistry/checkregistry.go +++ b/apps/advisor/pkg/app/checkregistry/checkregistry.go @@ -9,8 +9,8 @@ import ( "github.com/grafana/grafana/pkg/plugins/repo" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/pluginsintegration/managedplugins" + "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginchecker" "github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext" - "github.com/grafana/grafana/pkg/services/pluginsintegration/plugininstaller" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore" "github.com/grafana/grafana/pkg/services/pluginsintegration/provisionedplugins" "github.com/grafana/grafana/pkg/services/ssosettings" @@ -27,7 +27,8 @@ type Service struct { pluginContextProvider *plugincontext.Provider pluginClient plugins.Client pluginRepo repo.Service - pluginPreinstall plugininstaller.Preinstall + updateChecker pluginchecker.PluginUpdateChecker + pluginPreinstall pluginchecker.Preinstall managedPlugins managedplugins.Manager provisionedPlugins provisionedplugins.Manager ssoSettingsSvc ssosettings.Service @@ -36,9 +37,9 @@ type Service struct { func ProvideService(datasourceSvc datasources.DataSourceService, pluginStore pluginstore.Store, pluginContextProvider *plugincontext.Provider, pluginClient plugins.Client, - pluginRepo repo.Service, pluginPreinstall plugininstaller.Preinstall, managedPlugins managedplugins.Manager, + updateChecker pluginchecker.PluginUpdateChecker, + pluginRepo repo.Service, pluginPreinstall pluginchecker.Preinstall, managedPlugins managedplugins.Manager, provisionedPlugins provisionedplugins.Manager, ssoSettingsSvc ssosettings.Service, settings *setting.Cfg, - ) *Service { return &Service{ datasourceSvc: datasourceSvc, @@ -46,6 +47,7 @@ func ProvideService(datasourceSvc datasources.DataSourceService, pluginStore plu pluginContextProvider: pluginContextProvider, pluginClient: pluginClient, pluginRepo: pluginRepo, + updateChecker: updateChecker, pluginPreinstall: pluginPreinstall, managedPlugins: managedPlugins, provisionedPlugins: provisionedPlugins, @@ -67,9 +69,7 @@ func (s *Service) Checks() []checks.Check { plugincheck.New( s.pluginStore, s.pluginRepo, - s.pluginPreinstall, - s.managedPlugins, - s.provisionedPlugins, + s.updateChecker, s.GrafanaVersion, ), authchecks.New(s.ssoSettingsSvc), diff --git a/apps/advisor/pkg/app/checks/plugincheck/check.go b/apps/advisor/pkg/app/checks/plugincheck/check.go index d407b3e7f37..91e9bcbc211 100644 --- a/apps/advisor/pkg/app/checks/plugincheck/check.go +++ b/apps/advisor/pkg/app/checks/plugincheck/check.go @@ -4,17 +4,14 @@ import ( "context" "fmt" sysruntime "runtime" - "slices" "github.com/Masterminds/semver/v3" "github.com/grafana/grafana-app-sdk/logging" advisor "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1" "github.com/grafana/grafana/apps/advisor/pkg/app/checks" "github.com/grafana/grafana/pkg/plugins/repo" - "github.com/grafana/grafana/pkg/services/pluginsintegration/managedplugins" - "github.com/grafana/grafana/pkg/services/pluginsintegration/plugininstaller" + "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginchecker" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore" - "github.com/grafana/grafana/pkg/services/pluginsintegration/provisionedplugins" ) const ( @@ -26,28 +23,22 @@ const ( func New( pluginStore pluginstore.Store, pluginRepo repo.Service, - pluginPreinstall plugininstaller.Preinstall, - managedPlugins managedplugins.Manager, - provisionedPlugins provisionedplugins.Manager, + updateChecker pluginchecker.PluginUpdateChecker, grafanaVersion string, ) checks.Check { return &check{ - PluginStore: pluginStore, - PluginRepo: pluginRepo, - PluginPreinstall: pluginPreinstall, - ManagedPlugins: managedPlugins, - ProvisionedPlugins: provisionedPlugins, - GrafanaVersion: grafanaVersion, + PluginStore: pluginStore, + PluginRepo: pluginRepo, + GrafanaVersion: grafanaVersion, + updateChecker: updateChecker, } } type check struct { - PluginStore pluginstore.Store - PluginRepo repo.Service - PluginPreinstall plugininstaller.Preinstall - ManagedPlugins managedplugins.Manager - ProvisionedPlugins provisionedplugins.Manager - GrafanaVersion string + PluginStore pluginstore.Store + PluginRepo repo.Service + updateChecker pluginchecker.PluginUpdateChecker + GrafanaVersion string } func (c *check) ID() string { @@ -74,29 +65,22 @@ func (c *check) Item(ctx context.Context, id string) (any, error) { func (c *check) Steps() []checks.Step { return []checks.Step{ &deprecationStep{ - PluginRepo: c.PluginRepo, - PluginPreinstall: c.PluginPreinstall, - ManagedPlugins: c.ManagedPlugins, - ProvisionedPlugins: c.ProvisionedPlugins, - GrafanaVersion: c.GrafanaVersion, + PluginRepo: c.PluginRepo, + GrafanaVersion: c.GrafanaVersion, + updateChecker: c.updateChecker, }, &updateStep{ - PluginRepo: c.PluginRepo, - PluginPreinstall: c.PluginPreinstall, - ManagedPlugins: c.ManagedPlugins, - ProvisionedPlugins: c.ProvisionedPlugins, - GrafanaVersion: c.GrafanaVersion, + PluginRepo: c.PluginRepo, + GrafanaVersion: c.GrafanaVersion, + updateChecker: c.updateChecker, }, } } type deprecationStep struct { - PluginRepo repo.Service - PluginPreinstall plugininstaller.Preinstall - ManagedPlugins managedplugins.Manager - ProvisionedPlugins provisionedplugins.Manager - GrafanaVersion string - provisionedPlugins []string + PluginRepo repo.Service + GrafanaVersion string + updateChecker pluginchecker.PluginUpdateChecker } func (s *deprecationStep) Title() string { @@ -122,21 +106,7 @@ func (s *deprecationStep) Run(ctx context.Context, log logging.Logger, _ *adviso return nil, fmt.Errorf("invalid item type %T", it) } - // Skip if it's a core plugin - if p.IsCorePlugin() { - log.Debug("Skipping core plugin", "plugin", p.ID) - return nil, nil - } - - // Skip if it's managed or pinned - if s.isManaged(ctx, p.ID) || s.PluginPreinstall.IsPinned(p.ID) { - log.Debug("Skipping managed or pinned plugin", "plugin", p.ID) - return nil, nil - } - - // Skip if it's provisioned - if s.isProvisioned(ctx, p.ID) { - log.Debug("Skipping provisioned plugin", "plugin", p.ID) + if !s.updateChecker.IsUpdatable(ctx, p) { return nil, nil } @@ -165,12 +135,9 @@ func (s *deprecationStep) Run(ctx context.Context, log logging.Logger, _ *adviso } type updateStep struct { - PluginRepo repo.Service - PluginPreinstall plugininstaller.Preinstall - ManagedPlugins managedplugins.Manager - ProvisionedPlugins provisionedplugins.Manager - provisionedPlugins []string - GrafanaVersion string + PluginRepo repo.Service + GrafanaVersion string + updateChecker pluginchecker.PluginUpdateChecker } func (s *updateStep) Title() string { @@ -195,21 +162,7 @@ func (s *updateStep) Run(ctx context.Context, log logging.Logger, _ *advisor.Che return nil, fmt.Errorf("invalid item type %T", i) } - // Skip if it's a core plugin - if p.IsCorePlugin() { - log.Debug("Skipping core plugin", "plugin", p.ID) - return nil, nil - } - - // Skip if it's managed or pinned - if s.isManaged(ctx, p.ID) || s.PluginPreinstall.IsPinned(p.ID) { - log.Debug("Skipping managed or pinned plugin", "plugin", p.ID) - return nil, nil - } - - // Skip if it's provisioned - if s.isProvisioned(ctx, p.ID) { - log.Debug("Skipping provisioned plugin", "plugin", p.ID) + if !s.updateChecker.IsUpdatable(ctx, p) { return nil, nil } @@ -248,44 +201,3 @@ func hasUpdate(current pluginstore.Plugin, latest *repo.PluginArchiveInfo) bool // In other case, assume that a different latest version will always be newer return current.Info.Version != latest.Version } - -func (s *updateStep) isManaged(ctx context.Context, pluginID string) bool { - for _, managedPlugin := range s.ManagedPlugins.ManagedPlugins(ctx) { - if managedPlugin == pluginID { - return true - } - } - return false -} - -func (s *updateStep) isProvisioned(ctx context.Context, pluginID string) bool { - if s.provisionedPlugins == nil { - var err error - s.provisionedPlugins, err = s.ProvisionedPlugins.ProvisionedPlugins(ctx) - if err != nil { - return false - } - } - return slices.Contains(s.provisionedPlugins, pluginID) -} - -// Temporary duplicated code until there is a common IsUpdatable function -func (s *deprecationStep) isManaged(ctx context.Context, pluginID string) bool { - for _, managedPlugin := range s.ManagedPlugins.ManagedPlugins(ctx) { - if managedPlugin == pluginID { - return true - } - } - return false -} - -func (s *deprecationStep) isProvisioned(ctx context.Context, pluginID string) bool { - if s.provisionedPlugins == nil { - var err error - s.provisionedPlugins, err = s.ProvisionedPlugins.ProvisionedPlugins(ctx) - if err != nil { - return false - } - } - return slices.Contains(s.provisionedPlugins, pluginID) -} diff --git a/apps/advisor/pkg/app/checks/plugincheck/check_test.go b/apps/advisor/pkg/app/checks/plugincheck/check_test.go index 842d3197be9..b85f33db402 100644 --- a/apps/advisor/pkg/app/checks/plugincheck/check_test.go +++ b/apps/advisor/pkg/app/checks/plugincheck/check_test.go @@ -9,7 +9,7 @@ import ( "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/repo" "github.com/grafana/grafana/pkg/services/pluginsintegration/managedplugins" - "github.com/grafana/grafana/pkg/services/pluginsintegration/plugininstaller" + "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/stretchr/testify/assert" @@ -163,7 +163,8 @@ func TestRun(t *testing.T) { pluginPreinstall := &mockPluginPreinstall{pinned: tt.pluginPreinstalled} managedPlugins := &mockManagedPlugins{managed: tt.pluginManaged} provisionedPlugins := &mockProvisionedPlugins{provisioned: tt.pluginProvisioned} - check := New(pluginStore, pluginRepo, pluginPreinstall, managedPlugins, provisionedPlugins, "12.0.0") + updateChecker := pluginchecker.ProvideService(managedPlugins, provisionedPlugins, pluginPreinstall) + check := New(pluginStore, pluginRepo, updateChecker, "12.0.0") items, err := check.Items(context.Background()) assert.NoError(t, err) @@ -208,7 +209,7 @@ func (m *mockPluginRepo) GetPluginArchiveInfo(ctx context.Context, id, version s } type mockPluginPreinstall struct { - plugininstaller.Preinstall + pluginchecker.Preinstall pinned []string } diff --git a/conf/defaults.ini b/conf/defaults.ini index 600cbb9730b..97e37426cd6 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -1840,6 +1840,9 @@ preinstall = preinstall_async = true # Disables preinstall feature. It has the same effect as setting preinstall to an empty list. preinstall_disabled = false +# Update strategy for plugins. +# Available options: "latest", "minor" +update_strategy = minor #################################### Grafana Live ########################################## [live] diff --git a/pkg/api/frontendsettings_test.go b/pkg/api/frontendsettings_test.go index 62e09136af7..f7f2219a14e 100644 --- a/pkg/api/frontendsettings_test.go +++ b/pkg/api/frontendsettings_test.go @@ -38,7 +38,7 @@ import ( "github.com/grafana/grafana/pkg/services/rendering" "github.com/grafana/grafana/pkg/services/ssosettings/ssosettingstests" "github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest" - "github.com/grafana/grafana/pkg/services/updatechecker" + "github.com/grafana/grafana/pkg/services/updatemanager" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/web" @@ -93,7 +93,7 @@ func setupTestEnvironment(t *testing.T, cfg *setting.Cfg, features featuremgmt.F SQLStore: db.InitTestDB(t), SettingsProvider: setting.ProvideProvider(cfg), pluginStore: pluginStore, - grafanaUpdateChecker: &updatechecker.GrafanaService{}, + grafanaUpdateChecker: &updatemanager.GrafanaService{}, AccessControl: accesscontrolmock.New(), PluginSettings: pluginsSettings, pluginsCDNService: pluginsCDN, diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index de1c4dc810c..67a56310bc5 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -81,8 +81,8 @@ import ( "github.com/grafana/grafana/pkg/services/plugindashboards" "github.com/grafana/grafana/pkg/services/pluginsintegration/managedplugins" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginassets" + "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginchecker" "github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext" - "github.com/grafana/grafana/pkg/services/pluginsintegration/plugininstaller" pluginSettings "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore" pref "github.com/grafana/grafana/pkg/services/preference" @@ -107,7 +107,7 @@ import ( "github.com/grafana/grafana/pkg/services/tag" "github.com/grafana/grafana/pkg/services/team" tempUser "github.com/grafana/grafana/pkg/services/temp_user" - "github.com/grafana/grafana/pkg/services/updatechecker" + "github.com/grafana/grafana/pkg/services/updatemanager" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/services/validations" "github.com/grafana/grafana/pkg/setting" @@ -150,7 +150,7 @@ type HTTPServer struct { pluginStaticRouteResolver plugins.StaticRouteResolver pluginErrorResolver plugins.ErrorResolver pluginAssets *pluginassets.Service - pluginPreinstall plugininstaller.Preinstall + pluginPreinstall pluginchecker.Preinstall SearchService search.Service ShortURLService shorturls.Service QueryHistoryService queryhistory.Service @@ -175,8 +175,8 @@ type HTTPServer struct { DataSourcesService datasources.DataSourceService cleanUpService *cleanup.CleanUpService tracer tracing.Tracer - grafanaUpdateChecker *updatechecker.GrafanaService - pluginsUpdateChecker *updatechecker.PluginsService + grafanaUpdateChecker *updatemanager.GrafanaService + pluginsUpdateChecker *updatemanager.PluginsService searchUsersService searchusers.Service queryDataService query.Service serviceAccountsService serviceaccounts.Service @@ -249,8 +249,8 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi contextHandler *contexthandler.ContextHandler, loggerMiddleware loggermw.Logger, features featuremgmt.FeatureToggles, alertNG *ngalert.AlertNG, libraryPanelService librarypanels.Service, libraryElementService libraryelements.Service, quotaService quota.Service, socialService social.Service, tracer tracing.Tracer, - encryptionService encryption.Internal, grafanaUpdateChecker *updatechecker.GrafanaService, - pluginsUpdateChecker *updatechecker.PluginsService, searchUsersService searchusers.Service, + encryptionService encryption.Internal, grafanaUpdateChecker *updatemanager.GrafanaService, + pluginsUpdateChecker *updatemanager.PluginsService, searchUsersService searchusers.Service, dataSourcesService datasources.DataSourceService, queryDataService query.Service, pluginFileStore plugins.FileStore, serviceaccountsService serviceaccounts.Service, pluginAssets *pluginassets.Service, authInfoService login.AuthInfoService, storageService store.StorageService, @@ -271,7 +271,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi annotationRepo annotations.Repository, tagService tag.Service, searchv2HTTPService searchV2.SearchHTTPService, oauthTokenService oauthtoken.OAuthTokenService, statsService stats.Service, authnService authn.Service, pluginsCDNService *pluginscdn.Service, promGatherer prometheus.Gatherer, starApi *starApi.API, promRegister prometheus.Registerer, clientConfigProvider grafanaapiserver.DirectRestConfigProvider, anonService anonymous.Service, - userVerifier user.Verifier, pluginPreinstall plugininstaller.Preinstall, + userVerifier user.Verifier, pluginPreinstall pluginchecker.Preinstall, ) (*HTTPServer, error) { web.Env = cfg.Env m := web.New() diff --git a/pkg/api/plugins_test.go b/pkg/api/plugins_test.go index 219a50d5140..26decfe688f 100644 --- a/pkg/api/plugins_test.go +++ b/pkg/api/plugins_test.go @@ -44,11 +44,13 @@ import ( "github.com/grafana/grafana/pkg/services/pluginsintegration/managedplugins" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginaccesscontrol" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginassets" + "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginchecker" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginerrs" - "github.com/grafana/grafana/pkg/services/pluginsintegration/plugininstaller" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore" - "github.com/grafana/grafana/pkg/services/updatechecker" + "github.com/grafana/grafana/pkg/services/pluginsintegration/provisionedplugins" + "github.com/grafana/grafana/pkg/services/secrets/kvstore" + "github.com/grafana/grafana/pkg/services/updatemanager" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/web" @@ -112,7 +114,7 @@ func Test_PluginsInstallAndUninstall(t *testing.T) { }, }) hs.managedPluginsService = managedplugins.NewNoop() - hs.pluginPreinstall = plugininstaller.ProvidePreinstall(hs.Cfg) + hs.pluginPreinstall = pluginchecker.ProvidePreinstall(hs.Cfg) expectedIdentity := &authn.Identity{ OrgID: tc.permissionOrg, @@ -644,7 +646,14 @@ func Test_PluginsList_AccessControl(t *testing.T) { hs.pluginFileStore = filestore.ProvideService(pluginRegistry) hs.managedPluginsService = managedplugins.NewNoop() var err error - hs.pluginsUpdateChecker, err = updatechecker.ProvidePluginsService(hs.Cfg, nil, tracing.InitializeTracerForTest()) + hs.pluginsUpdateChecker, err = updatemanager.ProvidePluginsService( + hs.Cfg, + hs.pluginStore, + nil, // plugins.Installer + tracing.InitializeTracerForTest(), + kvstore.NewFakeFeatureToggles(t, true), + pluginchecker.ProvideService(hs.managedPluginsService, provisionedplugins.NewNoop(), &pluginchecker.FakePluginPreinstall{}), + ) require.NoError(t, err) }) @@ -836,7 +845,14 @@ func Test_PluginsSettings(t *testing.T) { hs.pluginAssets = pluginassets.ProvideService(pCfg, pluginCDN, sig, hs.pluginStore) hs.pluginErrorResolver = pluginerrs.ProvideStore(errTracker) var err error - hs.pluginsUpdateChecker, err = updatechecker.ProvidePluginsService(hs.Cfg, nil, tracing.InitializeTracerForTest()) + hs.pluginsUpdateChecker, err = updatemanager.ProvidePluginsService( + hs.Cfg, + hs.pluginStore, + &fakes.FakePluginInstaller{}, + tracing.InitializeTracerForTest(), + kvstore.NewFakeFeatureToggles(t, true), + pluginchecker.ProvideService(hs.managedPluginsService, provisionedplugins.NewNoop(), &pluginchecker.FakePluginPreinstall{}), + ) require.NoError(t, err) }) diff --git a/pkg/registry/backgroundsvcs/background_services.go b/pkg/registry/backgroundsvcs/background_services.go index 621c2eee751..7bb8e674957 100644 --- a/pkg/registry/backgroundsvcs/background_services.go +++ b/pkg/registry/backgroundsvcs/background_services.go @@ -47,7 +47,7 @@ import ( "github.com/grafana/grafana/pkg/services/store/sanitizer" "github.com/grafana/grafana/pkg/services/supportbundles/supportbundlesimpl" "github.com/grafana/grafana/pkg/services/team/teamapi" - "github.com/grafana/grafana/pkg/services/updatechecker" + "github.com/grafana/grafana/pkg/services/updatemanager" ) func ProvideBackgroundServiceRegistry( @@ -55,8 +55,8 @@ func ProvideBackgroundServiceRegistry( pushGateway *pushhttp.Gateway, notifications *notifications.NotificationService, pluginStore *pluginStore.Service, rendering *rendering.RenderingService, tokenService auth.UserTokenBackgroundService, tracing *tracing.TracingService, provisioning *provisioning.ProvisioningServiceImpl, usageStats *uss.UsageStats, - statsCollector *statscollector.Service, grafanaUpdateChecker *updatechecker.GrafanaService, - pluginsUpdateChecker *updatechecker.PluginsService, metrics *metrics.InternalMetricsService, + statsCollector *statscollector.Service, grafanaUpdateChecker *updatemanager.GrafanaService, + pluginsUpdateChecker *updatemanager.PluginsService, metrics *metrics.InternalMetricsService, secretsService *secretsManager.SecretsService, remoteCache *remotecache.RemoteCache, StorageService store.StorageService, searchService searchV2.SearchService, entityEventsService store.EntityEventsService, saService *samanager.ServiceAccountsService, grpcServerProvider grpcserver.Provider, secretMigrationProvider secretsMigrations.SecretMigrationProvider, loginAttemptService *loginattemptimpl.Service, diff --git a/pkg/server/wire.go b/pkg/server/wire.go index 07b09f28d19..5dd28d8a829 100644 --- a/pkg/server/wire.go +++ b/pkg/server/wire.go @@ -158,7 +158,7 @@ import ( "github.com/grafana/grafana/pkg/services/team/teamimpl" tempuser "github.com/grafana/grafana/pkg/services/temp_user" "github.com/grafana/grafana/pkg/services/temp_user/tempuserimpl" - "github.com/grafana/grafana/pkg/services/updatechecker" + "github.com/grafana/grafana/pkg/services/updatemanager" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/services/user/userimpl" "github.com/grafana/grafana/pkg/setting" @@ -215,8 +215,8 @@ var wireBasicSet = wire.NewSet( localcache.ProvideService, bundleregistry.ProvideService, wire.Bind(new(supportbundles.Service), new(*bundleregistry.Service)), - updatechecker.ProvideGrafanaService, - updatechecker.ProvidePluginsService, + updatemanager.ProvideGrafanaService, + updatemanager.ProvidePluginsService, uss.ProvideService, wire.Bind(new(usagestats.Service), new(*uss.UsageStats)), validator.ProvideService, diff --git a/pkg/services/pluginsintegration/plugininstaller/checker.go b/pkg/services/pluginsintegration/pluginchecker/checker.go similarity index 96% rename from pkg/services/pluginsintegration/plugininstaller/checker.go rename to pkg/services/pluginsintegration/pluginchecker/checker.go index 58cfde3571a..c03ba8ae333 100644 --- a/pkg/services/pluginsintegration/plugininstaller/checker.go +++ b/pkg/services/pluginsintegration/pluginchecker/checker.go @@ -1,4 +1,4 @@ -package plugininstaller +package pluginchecker import "github.com/grafana/grafana/pkg/setting" diff --git a/pkg/services/pluginsintegration/plugininstaller/checker_test.go b/pkg/services/pluginsintegration/pluginchecker/checker_test.go similarity index 97% rename from pkg/services/pluginsintegration/plugininstaller/checker_test.go rename to pkg/services/pluginsintegration/pluginchecker/checker_test.go index b099ec8138b..37beb9380e9 100644 --- a/pkg/services/pluginsintegration/plugininstaller/checker_test.go +++ b/pkg/services/pluginsintegration/pluginchecker/checker_test.go @@ -1,4 +1,4 @@ -package plugininstaller +package pluginchecker import ( "testing" diff --git a/pkg/services/pluginsintegration/pluginchecker/fake.go b/pkg/services/pluginsintegration/pluginchecker/fake.go new file mode 100644 index 00000000000..cf17d9e4eee --- /dev/null +++ b/pkg/services/pluginsintegration/pluginchecker/fake.go @@ -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 +} diff --git a/pkg/services/pluginsintegration/pluginchecker/service.go b/pkg/services/pluginsintegration/pluginchecker/service.go new file mode 100644 index 00000000000..61e519e438b --- /dev/null +++ b/pkg/services/pluginsintegration/pluginchecker/service.go @@ -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 +} diff --git a/pkg/services/pluginsintegration/plugininstaller/service.go b/pkg/services/pluginsintegration/plugininstaller/service.go index ccebdf184fe..3fd683d5adc 100644 --- a/pkg/services/pluginsintegration/plugininstaller/service.go +++ b/pkg/services/pluginsintegration/plugininstaller/service.go @@ -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 { diff --git a/pkg/services/pluginsintegration/plugininstaller/service_test.go b/pkg/services/pluginsintegration/plugininstaller/service_test.go index 1df3c8d2888..a52a0c0437e 100644 --- a/pkg/services/pluginsintegration/plugininstaller/service_test.go +++ b/pkg/services/pluginsintegration/plugininstaller/service_test.go @@ -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") diff --git a/pkg/services/pluginsintegration/pluginsintegration.go b/pkg/services/pluginsintegration/pluginsintegration.go index 8920e51551a..ecb4d32ed94 100644 --- a/pkg/services/pluginsintegration/pluginsintegration.go +++ b/pkg/services/pluginsintegration/pluginsintegration.go @@ -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 diff --git a/pkg/services/updatechecker/grafana.go b/pkg/services/updatemanager/grafana.go similarity index 99% rename from pkg/services/updatechecker/grafana.go rename to pkg/services/updatemanager/grafana.go index 3760fe06b13..e99f353cfa5 100644 --- a/pkg/services/updatechecker/grafana.go +++ b/pkg/services/updatemanager/grafana.go @@ -1,4 +1,4 @@ -package updatechecker +package updatemanager import ( "context" diff --git a/pkg/services/updatechecker/grafana_test.go b/pkg/services/updatemanager/grafana_test.go similarity index 99% rename from pkg/services/updatechecker/grafana_test.go rename to pkg/services/updatemanager/grafana_test.go index 2f0b14293f7..fb93c1a61c7 100644 --- a/pkg/services/updatechecker/grafana_test.go +++ b/pkg/services/updatemanager/grafana_test.go @@ -1,4 +1,4 @@ -package updatechecker +package updatemanager import ( "context" diff --git a/pkg/services/updatechecker/plugins.go b/pkg/services/updatemanager/plugins.go similarity index 60% rename from pkg/services/updatechecker/plugins.go rename to pkg/services/updatemanager/plugins.go index 1ff5f631c77..c6354e40c63 100644 --- a/pkg/services/updatechecker/plugins.go +++ b/pkg/services/updatemanager/plugins.go @@ -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() +} diff --git a/pkg/services/updatechecker/plugins_test.go b/pkg/services/updatemanager/plugins_test.go similarity index 63% rename from pkg/services/updatechecker/plugins_test.go rename to pkg/services/updatemanager/plugins_test.go index d0d1ff22d2f..f726b855be0 100644 --- a/pkg/services/updatechecker/plugins_test.go +++ b/pkg/services/updatemanager/plugins_test.go @@ -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 diff --git a/pkg/services/updatechecker/updatechecker.go b/pkg/services/updatemanager/updatemanager.go similarity index 82% rename from pkg/services/updatechecker/updatechecker.go rename to pkg/services/updatemanager/updatemanager.go index 9c92966d59c..4d062bd7952 100644 --- a/pkg/services/updatechecker/updatechecker.go +++ b/pkg/services/updatemanager/updatemanager.go @@ -1,4 +1,4 @@ -package updatechecker +package updatemanager import "net/http" diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index f599d3f02e6..0ad3d757354 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -206,6 +206,8 @@ type Cfg struct { PluginsCDNURLTemplate string PluginLogBackendRequests bool + PluginUpdateStrategy string + // Panels DisableSanitizeHtml bool diff --git a/pkg/setting/setting_plugins.go b/pkg/setting/setting_plugins.go index 80423e93631..9f419b10e45 100644 --- a/pkg/setting/setting_plugins.go +++ b/pkg/setting/setting_plugins.go @@ -8,6 +8,11 @@ import ( "github.com/grafana/grafana/pkg/util" ) +const ( + PluginUpdateStrategyLatest = "latest" + PluginUpdateStrategyMinor = "minor" +) + // PluginSettings maps plugin id to map of key/value settings. type PluginSettings map[string]map[string]string @@ -97,5 +102,7 @@ func (cfg *Cfg) readPluginSettings(iniFile *ini.File) error { cfg.PluginsCDNURLTemplate = strings.TrimRight(pluginsSection.Key("cdn_base_url").MustString(""), "/") cfg.PluginLogBackendRequests = pluginsSection.Key("log_backend_requests").MustBool(false) + cfg.PluginUpdateStrategy = pluginsSection.Key("update_strategy").In(PluginUpdateStrategyLatest, []string{PluginUpdateStrategyLatest, PluginUpdateStrategyMinor}) + return nil }