Plugins: Add plugins auto update feature (#104112)

pull/105181/head
Hugo Kiyodi Oshiro 2 months ago committed by GitHub
parent 42028a1b03
commit 43748e43bb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 2
      .github/CODEOWNERS
  2. 14
      apps/advisor/pkg/app/checkregistry/checkregistry.go
  3. 136
      apps/advisor/pkg/app/checks/plugincheck/check.go
  4. 7
      apps/advisor/pkg/app/checks/plugincheck/check_test.go
  5. 3
      conf/defaults.ini
  6. 4
      pkg/api/frontendsettings_test.go
  7. 16
      pkg/api/http_server.go
  8. 26
      pkg/api/plugins_test.go
  9. 6
      pkg/registry/backgroundsvcs/background_services.go
  10. 6
      pkg/server/wire.go
  11. 2
      pkg/services/pluginsintegration/pluginchecker/checker.go
  12. 2
      pkg/services/pluginsintegration/pluginchecker/checker_test.go
  13. 45
      pkg/services/pluginsintegration/pluginchecker/fake.go
  14. 118
      pkg/services/pluginsintegration/pluginchecker/service.go
  15. 34
      pkg/services/pluginsintegration/plugininstaller/service.go
  16. 9
      pkg/services/pluginsintegration/plugininstaller/service_test.go
  17. 7
      pkg/services/pluginsintegration/pluginsintegration.go
  18. 2
      pkg/services/updatemanager/grafana.go
  19. 2
      pkg/services/updatemanager/grafana_test.go
  20. 109
      pkg/services/updatemanager/plugins.go
  21. 99
      pkg/services/updatemanager/plugins_test.go
  22. 2
      pkg/services/updatemanager/updatemanager.go
  23. 2
      pkg/setting/setting.go
  24. 7
      pkg/setting/setting_plugins.go

@ -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

@ -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),

@ -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)
}

@ -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
}

@ -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]

@ -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,

@ -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()

@ -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)
})

@ -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,

@ -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,

@ -1,4 +1,4 @@
package plugininstaller
package pluginchecker
import "github.com/grafana/grafana/pkg/setting"

@ -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 availableUpdate struct {
localVersion string
availableVersion string
}
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
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, tracer tracing.Tracer) (*PluginsService, error) {
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 {
func (s *PluginsService) canUpdate(ctx context.Context, plugin pluginstore.Plugin, gcomVersion string) bool {
if !s.updateChecker.IsUpdatable(ctx, plugin) {
return false
}
ver2, err2 := version.NewVersion(v2)
if err2 != nil {
if plugin.Info.Version == gcomVersion {
return false
}
return ver1.LessThan(ver2)
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"

@ -206,6 +206,8 @@ type Cfg struct {
PluginsCDNURLTemplate string
PluginLogBackendRequests bool
PluginUpdateStrategy string
// Panels
DisableSanitizeHtml bool

@ -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
}

Loading…
Cancel
Save