From ac8f1b5b6b190494d3493d5ec0c66f2148a8be83 Mon Sep 17 00:00:00 2001 From: Will Browne Date: Mon, 31 Jan 2022 16:49:29 +0100 Subject: [PATCH] [v8.3.x] Plugins: Refactor Grafana and Plugin version update checkers (#44662) * 44529 * fix test --- pkg/api/frontendsettings.go | 4 +- pkg/api/frontendsettings_test.go | 8 +- pkg/api/http_server.go | 9 +- pkg/api/index.go | 4 +- pkg/api/metrics_test.go | 1 + pkg/api/plugins.go | 16 +- pkg/plugins/manager/manager.go | 18 -- pkg/plugins/manager/update_checker.go | 84 ------- pkg/plugins/plugins.go | 38 ++- .../backgroundsvcs/background_services.go | 6 +- pkg/server/wire.go | 3 +- pkg/services/updatechecker/grafana.go | 115 +++++++++ .../updatechecker/grafana_update_checker.go | 120 ---------- pkg/services/updatechecker/plugins.go | 167 +++++++++++++ pkg/services/updatechecker/plugins_test.go | 225 ++++++++++++++++++ 15 files changed, 554 insertions(+), 264 deletions(-) delete mode 100644 pkg/plugins/manager/update_checker.go create mode 100644 pkg/services/updatechecker/grafana.go delete mode 100644 pkg/services/updatechecker/grafana_update_checker.go create mode 100644 pkg/services/updatechecker/plugins.go create mode 100644 pkg/services/updatechecker/plugins_test.go diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index 8ef0b667bcc..e6adb286eb5 100644 --- a/pkg/api/frontendsettings.go +++ b/pkg/api/frontendsettings.go @@ -252,8 +252,8 @@ func (hs *HTTPServer) getFrontendSettingsMap(c *models.ReqContext) (map[string]i "commit": commit, "buildstamp": buildstamp, "edition": hs.License.Edition(), - "latestVersion": hs.updateChecker.LatestGrafanaVersion(), - "hasUpdate": hs.updateChecker.GrafanaUpdateAvailable(), + "latestVersion": hs.grafanaUpdateChecker.LatestVersion(), + "hasUpdate": hs.grafanaUpdateChecker.UpdateAvailable(), "env": setting.Env, "isEnterprise": hs.License.HasValidLicense(), }, diff --git a/pkg/api/frontendsettings_test.go b/pkg/api/frontendsettings_test.go index c2ffca308a5..afa94815840 100644 --- a/pkg/api/frontendsettings_test.go +++ b/pkg/api/frontendsettings_test.go @@ -44,10 +44,10 @@ func setupTestEnvironment(t *testing.T, cfg *setting.Cfg) (*web.Mux, *HTTPServer Cfg: cfg, RendererPluginManager: &fakeRendererManager{}, }, - SQLStore: sqlStore, - pluginStore: &fakePluginStore{}, - updateChecker: &updatechecker.Service{}, - AccessControl: accesscontrolmock.New().WithDisabled(), + SQLStore: sqlStore, + pluginStore: &fakePluginStore{}, + grafanaUpdateChecker: &updatechecker.GrafanaService{}, + AccessControl: accesscontrolmock.New().WithDisabled(), } m := web.New() diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index f68d7b816b7..c85fc410091 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -114,7 +114,8 @@ type HTTPServer struct { cleanUpService *cleanup.CleanUpService tracingService *tracing.TracingService internalMetricsSvc *metrics.InternalMetricsService - updateChecker *updatechecker.Service + grafanaUpdateChecker *updatechecker.GrafanaService + pluginsUpdateChecker *updatechecker.PluginsService searchUsersService searchusers.Service expressionService *expr.Service } @@ -142,7 +143,8 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi notificationService *notifications.NotificationService, tracingService *tracing.TracingService, internalMetricsSvc *metrics.InternalMetricsService, quotaService *quota.QuotaService, socialService social.Service, oauthTokenService oauthtoken.OAuthTokenService, - encryptionService encryption.Internal, updateChecker *updatechecker.Service, searchUsersService searchusers.Service, + grafanaUpdateChecker *updatechecker.GrafanaService, pluginsUpdateChecker *updatechecker.PluginsService, + encryptionService encryption.Internal, searchUsersService searchusers.Service, dataSourcesService *datasources.Service, secretsService secrets.Service, expressionService *expr.Service) (*HTTPServer, error) { web.Env = cfg.Env m := web.New() @@ -164,7 +166,8 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi pluginStaticRouteResolver: pluginStaticRouteResolver, pluginDashboardManager: pluginDashboardManager, pluginErrorResolver: pluginErrorResolver, - updateChecker: updateChecker, + grafanaUpdateChecker: grafanaUpdateChecker, + pluginsUpdateChecker: pluginsUpdateChecker, SettingsProvider: settingsProvider, DataSourceCache: dataSourceCache, AuthTokenService: userTokenService, diff --git a/pkg/api/index.go b/pkg/api/index.go index 31f7e3a5e8d..7cd348e7f18 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -589,8 +589,8 @@ func (hs *HTTPServer) setIndexViewData(c *models.ReqContext) (*dtos.IndexViewDat GoogleTagManagerId: setting.GoogleTagManagerId, BuildVersion: setting.BuildVersion, BuildCommit: setting.BuildCommit, - NewGrafanaVersion: hs.updateChecker.LatestGrafanaVersion(), - NewGrafanaVersionExists: hs.updateChecker.GrafanaUpdateAvailable(), + NewGrafanaVersion: hs.grafanaUpdateChecker.LatestVersion(), + NewGrafanaVersionExists: hs.grafanaUpdateChecker.UpdateAvailable(), AppName: setting.ApplicationName, AppNameBodyClass: getAppNameBodyClass(hs.License.HasValidLicense()), FavIcon: "public/img/fav32.png", diff --git a/pkg/api/metrics_test.go b/pkg/api/metrics_test.go index 25366d03536..8a14fced85b 100644 --- a/pkg/api/metrics_test.go +++ b/pkg/api/metrics_test.go @@ -115,6 +115,7 @@ func setup() *testContext { nil, nil, nil, + nil, sc, nil, ) diff --git a/pkg/api/plugins.go b/pkg/api/plugins.go index 7cdc4641caa..cd9369b3c90 100644 --- a/pkg/api/plugins.go +++ b/pkg/api/plugins.go @@ -69,8 +69,6 @@ func (hs *HTTPServer) GetPluginList(c *models.ReqContext) response.Response { Category: pluginDef.Category, Info: pluginDef.Info, Dependencies: pluginDef.Dependencies, - LatestVersion: pluginDef.GrafanaComVersion, - HasUpdate: pluginDef.GrafanaComHasUpdate, DefaultNavUrl: pluginDef.DefaultNavURL, State: pluginDef.State, Signature: pluginDef.Signature, @@ -78,6 +76,12 @@ func (hs *HTTPServer) GetPluginList(c *models.ReqContext) response.Response { SignatureOrg: pluginDef.SignatureOrg, } + update, exists := hs.pluginsUpdateChecker.HasUpdate(c.Req.Context(), pluginDef.ID) + if exists { + listItem.LatestVersion = update + listItem.HasUpdate = true + } + if pluginSetting, exists := pluginSettingsMap[pluginDef.ID]; exists { listItem.Enabled = pluginSetting.Enabled listItem.Pinned = pluginSetting.Pinned @@ -122,8 +126,6 @@ func (hs *HTTPServer) GetPluginSettingByID(c *models.ReqContext) response.Respon BaseUrl: plugin.BaseURL, Module: plugin.Module, DefaultNavUrl: plugin.DefaultNavURL, - LatestVersion: plugin.GrafanaComVersion, - HasUpdate: plugin.GrafanaComHasUpdate, State: plugin.State, Signature: plugin.Signature, SignatureType: plugin.SignatureType, @@ -146,6 +148,12 @@ func (hs *HTTPServer) GetPluginSettingByID(c *models.ReqContext) response.Respon dto.JsonData = query.Result.JsonData } + update, exists := hs.pluginsUpdateChecker.HasUpdate(c.Req.Context(), plugin.ID) + if exists { + dto.LatestVersion = update + dto.HasUpdate = true + } + return response.JSON(200, dto) } diff --git a/pkg/plugins/manager/manager.go b/pkg/plugins/manager/manager.go index 991b7474362..848247d3640 100644 --- a/pkg/plugins/manager/manager.go +++ b/pkg/plugins/manager/manager.go @@ -118,24 +118,6 @@ func (m *PluginManager) init() error { } func (m *PluginManager) Run(ctx context.Context) error { - if m.cfg.CheckForUpdates { - go func() { - m.checkForUpdates() - - ticker := time.NewTicker(time.Minute * 10) - run := true - - for run { - select { - case <-ticker.C: - m.checkForUpdates() - case <-ctx.Done(): - run = false - } - } - }() - } - <-ctx.Done() m.shutdown(ctx) return ctx.Err() diff --git a/pkg/plugins/manager/update_checker.go b/pkg/plugins/manager/update_checker.go deleted file mode 100644 index 4eee58a7a4a..00000000000 --- a/pkg/plugins/manager/update_checker.go +++ /dev/null @@ -1,84 +0,0 @@ -package manager - -import ( - "context" - "encoding/json" - "io/ioutil" - "net/http" - "strings" - "time" - - "github.com/hashicorp/go-version" -) - -var ( - httpClient = http.Client{Timeout: 10 * time.Second} -) - -type gcomPlugin struct { - Slug string `json:"slug"` - Version string `json:"version"` -} - -func (m *PluginManager) checkForUpdates() { - if !m.cfg.CheckForUpdates { - return - } - - m.log.Debug("Checking for updates") - - pluginIDs := m.pluginsEligibleForVersionCheck() - resp, err := httpClient.Get("https://grafana.com/api/plugins/versioncheck?slugIn=" + strings.Join(pluginIDs, ",") + "&grafanaVersion=" + m.cfg.BuildVersion) - if err != nil { - m.log.Debug("Failed to get plugins repo from grafana.com", "error", err.Error()) - return - } - defer func() { - if err := resp.Body.Close(); err != nil { - m.log.Warn("Failed to close response body", "err", err) - } - }() - - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - m.log.Debug("Update check failed, reading response from grafana.com", "error", err.Error()) - return - } - - var gcomPlugins []gcomPlugin - err = json.Unmarshal(body, &gcomPlugins) - if err != nil { - m.log.Debug("Failed to unmarshal plugin repo, reading response from grafana.com", "error", err.Error()) - return - } - - for _, localP := range m.Plugins(context.TODO()) { - for _, gcomP := range gcomPlugins { - if gcomP.Slug == localP.ID { - localP.GrafanaComVersion = gcomP.Version - - plugVersion, err1 := version.NewVersion(localP.Info.Version) - gplugVersion, err2 := version.NewVersion(gcomP.Version) - - if err1 != nil || err2 != nil { - localP.GrafanaComHasUpdate = localP.Info.Version != localP.GrafanaComVersion - } else { - localP.GrafanaComHasUpdate = plugVersion.LessThan(gplugVersion) - } - } - } - } -} - -func (m *PluginManager) pluginsEligibleForVersionCheck() []string { - var result []string - for _, p := range m.plugins() { - if p.IsCorePlugin() { - continue - } - - result = append(result, p.ID) - } - - return result -} diff --git a/pkg/plugins/plugins.go b/pkg/plugins/plugins.go index 0c5a9d7930f..2f9d5438126 100644 --- a/pkg/plugins/plugins.go +++ b/pkg/plugins/plugins.go @@ -32,10 +32,6 @@ type Plugin struct { SignedFiles PluginFiles SignatureError *SignatureError - // GCOM update checker fields - GrafanaComVersion string - GrafanaComHasUpdate bool - // SystemJS fields Module string BaseURL string @@ -63,10 +59,6 @@ type PluginDTO struct { SignedFiles PluginFiles SignatureError *SignatureError - // GCOM update checker fields - GrafanaComVersion string - GrafanaComHasUpdate bool - // SystemJS fields Module string BaseURL string @@ -315,22 +307,20 @@ func (p *Plugin) ToDTO() PluginDTO { c, _ := p.Client() return PluginDTO{ - JSONData: p.JSONData, - PluginDir: p.PluginDir, - Class: p.Class, - IncludedInAppID: p.IncludedInAppID, - DefaultNavURL: p.DefaultNavURL, - Pinned: p.Pinned, - Signature: p.Signature, - SignatureType: p.SignatureType, - SignatureOrg: p.SignatureOrg, - SignedFiles: p.SignedFiles, - SignatureError: p.SignatureError, - GrafanaComVersion: p.GrafanaComVersion, - GrafanaComHasUpdate: p.GrafanaComHasUpdate, - Module: p.Module, - BaseURL: p.BaseURL, - StreamHandler: c, + JSONData: p.JSONData, + PluginDir: p.PluginDir, + Class: p.Class, + IncludedInAppID: p.IncludedInAppID, + DefaultNavURL: p.DefaultNavURL, + Pinned: p.Pinned, + Signature: p.Signature, + SignatureType: p.SignatureType, + SignatureOrg: p.SignatureOrg, + SignedFiles: p.SignedFiles, + SignatureError: p.SignatureError, + Module: p.Module, + BaseURL: p.BaseURL, + StreamHandler: c, } } diff --git a/pkg/server/backgroundsvcs/background_services.go b/pkg/server/backgroundsvcs/background_services.go index 26679d9e583..5be0595fe72 100644 --- a/pkg/server/backgroundsvcs/background_services.go +++ b/pkg/server/backgroundsvcs/background_services.go @@ -45,8 +45,9 @@ func ProvideBackgroundServiceRegistry( live *live.GrafanaLive, pushGateway *pushhttp.Gateway, notifications *notifications.NotificationService, rendering *rendering.RenderingService, tokenService models.UserTokenBackgroundService, provisioning *provisioning.ProvisioningServiceImpl, alerting *alerting.AlertEngine, pm *manager.PluginManager, - metrics *metrics.InternalMetricsService, usageStats *uss.UsageStats, updateChecker *updatechecker.Service, + metrics *metrics.InternalMetricsService, usageStats *uss.UsageStats, tracing *tracing.TracingService, remoteCache *remotecache.RemoteCache, secretsService *secretsManager.SecretsService, + grafanaUpdateChecker *updatechecker.GrafanaService, pluginsUpdateChecker *updatechecker.PluginsService, // Need to make sure these are initialized, is there a better place to put them? _ *azuremonitor.Service, _ *cloudwatch.CloudWatchService, _ *elasticsearch.Service, _ *graphite.Service, _ *influxdb.Service, _ *loki.Service, _ *opentsdb.Service, _ *prometheus.Service, _ *tempo.Service, @@ -66,7 +67,8 @@ func ProvideBackgroundServiceRegistry( provisioning, alerting, pm, - updateChecker, + grafanaUpdateChecker, + pluginsUpdateChecker, metrics, usageStats, tracing, diff --git a/pkg/server/wire.go b/pkg/server/wire.go index 3ff8974cec9..67e9fd69be9 100644 --- a/pkg/server/wire.go +++ b/pkg/server/wire.go @@ -96,7 +96,8 @@ var wireBasicSet = wire.NewSet( hooks.ProvideService, kvstore.ProvideService, localcache.ProvideService, - updatechecker.ProvideService, + updatechecker.ProvideGrafanaService, + updatechecker.ProvidePluginsService, uss.ProvideService, wire.Bind(new(usagestats.Service), new(*uss.UsageStats)), manager.ProvideService, diff --git a/pkg/services/updatechecker/grafana.go b/pkg/services/updatechecker/grafana.go new file mode 100644 index 00000000000..777fcd07a19 --- /dev/null +++ b/pkg/services/updatechecker/grafana.go @@ -0,0 +1,115 @@ +package updatechecker + +import ( + "context" + "encoding/json" + "io/ioutil" + "net/http" + "strings" + "sync" + "time" + + "github.com/hashicorp/go-version" + + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/setting" +) + +type GrafanaService struct { + hasUpdate bool + latestVersion string + + enabled bool + grafanaVersion string + httpClient http.Client + mutex sync.RWMutex + log log.Logger +} + +func ProvideGrafanaService(cfg *setting.Cfg) *GrafanaService { + return &GrafanaService{ + enabled: cfg.CheckForUpdates, + grafanaVersion: cfg.BuildVersion, + httpClient: http.Client{Timeout: 10 * time.Second}, + log: log.New("grafana.update.checker"), + } +} + +func (s *GrafanaService) IsDisabled() bool { + return !s.enabled +} + +func (s *GrafanaService) Run(ctx context.Context) error { + s.checkForUpdates() + + ticker := time.NewTicker(time.Minute * 10) + run := true + + for run { + select { + case <-ticker.C: + s.checkForUpdates() + case <-ctx.Done(): + run = false + } + } + + return ctx.Err() +} + +func (s *GrafanaService) checkForUpdates() { + resp, err := s.httpClient.Get("https://raw.githubusercontent.com/grafana/grafana/main/latest.json") + if err != nil { + s.log.Debug("Failed to get latest.json repo from github.com", "error", err) + return + } + defer func() { + if err := resp.Body.Close(); err != nil { + s.log.Warn("Failed to close response body", "err", err) + } + }() + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + s.log.Debug("Update check failed, reading response from github.com", "error", err) + return + } + + type latestJSON struct { + Stable string `json:"stable"` + Testing string `json:"testing"` + } + var latest latestJSON + err = json.Unmarshal(body, &latest) + if err != nil { + s.log.Debug("Failed to unmarshal latest.json", "error", err) + return + } + + s.mutex.Lock() + defer s.mutex.Unlock() + if strings.Contains(s.grafanaVersion, "-") { + s.latestVersion = latest.Testing + s.hasUpdate = !strings.HasPrefix(s.grafanaVersion, latest.Testing) + } else { + s.latestVersion = latest.Stable + s.hasUpdate = latest.Stable != s.grafanaVersion + } + + currVersion, err1 := version.NewVersion(s.grafanaVersion) + latestVersion, err2 := version.NewVersion(s.latestVersion) + if err1 == nil && err2 == nil { + s.hasUpdate = currVersion.LessThan(latestVersion) + } +} + +func (s *GrafanaService) UpdateAvailable() bool { + s.mutex.RLock() + defer s.mutex.RUnlock() + return s.hasUpdate +} + +func (s *GrafanaService) LatestVersion() string { + s.mutex.RLock() + defer s.mutex.RUnlock() + return s.latestVersion +} diff --git a/pkg/services/updatechecker/grafana_update_checker.go b/pkg/services/updatechecker/grafana_update_checker.go deleted file mode 100644 index fd2fa3fac82..00000000000 --- a/pkg/services/updatechecker/grafana_update_checker.go +++ /dev/null @@ -1,120 +0,0 @@ -package updatechecker - -import ( - "context" - "encoding/json" - "io/ioutil" - "net/http" - "strings" - "sync" - "time" - - "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/setting" - "github.com/hashicorp/go-version" -) - -var ( - httpClient = http.Client{Timeout: 10 * time.Second} - logger = log.New("update.checker") -) - -type latestJSON struct { - Stable string `json:"stable"` - Testing string `json:"testing"` -} - -type Service struct { - cfg *setting.Cfg - - hasUpdate bool - latestVersion string - mutex sync.RWMutex -} - -func ProvideService(cfg *setting.Cfg) *Service { - s := newUpdateChecker(cfg) - - return s -} - -func newUpdateChecker(cfg *setting.Cfg) *Service { - return &Service{ - cfg: cfg, - } -} - -func (s *Service) IsDisabled() bool { - return !s.cfg.CheckForUpdates -} - -func (s *Service) Run(ctx context.Context) error { - s.checkForUpdates() - - ticker := time.NewTicker(time.Minute * 10) - run := true - - for run { - select { - case <-ticker.C: - s.checkForUpdates() - case <-ctx.Done(): - run = false - } - } - - return ctx.Err() -} - -func (s *Service) checkForUpdates() { - resp, err := httpClient.Get("https://raw.githubusercontent.com/grafana/grafana/main/latest.json") - if err != nil { - logger.Debug("Failed to get latest.json repo from github.com", "error", err) - return - } - defer func() { - if err := resp.Body.Close(); err != nil { - logger.Warn("Failed to close response body", "err", err) - } - }() - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - logger.Debug("Update check failed, reading response from github.com", "error", err) - return - } - - var latest latestJSON - err = json.Unmarshal(body, &latest) - if err != nil { - logger.Debug("Failed to unmarshal latest.json", "error", err) - return - } - - s.mutex.Lock() - defer s.mutex.Unlock() - if strings.Contains(s.cfg.BuildVersion, "-") { - s.latestVersion = latest.Testing - s.hasUpdate = !strings.HasPrefix(s.cfg.BuildVersion, latest.Testing) - } else { - s.latestVersion = latest.Stable - s.hasUpdate = latest.Stable != s.cfg.BuildVersion - } - - currVersion, err1 := version.NewVersion(s.cfg.BuildVersion) - latestVersion, err2 := version.NewVersion(s.latestVersion) - if err1 == nil && err2 == nil { - s.hasUpdate = currVersion.LessThan(latestVersion) - } -} - -func (s *Service) GrafanaUpdateAvailable() bool { - s.mutex.RLock() - defer s.mutex.RUnlock() - return s.hasUpdate -} - -func (s *Service) LatestGrafanaVersion() string { - s.mutex.RLock() - defer s.mutex.RUnlock() - return s.latestVersion -} diff --git a/pkg/services/updatechecker/plugins.go b/pkg/services/updatechecker/plugins.go new file mode 100644 index 00000000000..618597acda6 --- /dev/null +++ b/pkg/services/updatechecker/plugins.go @@ -0,0 +1,167 @@ +package updatechecker + +import ( + "context" + "encoding/json" + "io/ioutil" + "net/http" + "strings" + "sync" + "time" + + "github.com/hashicorp/go-version" + + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/setting" +) + +type PluginsService struct { + availableUpdates map[string]string + + enabled bool + grafanaVersion string + pluginStore plugins.Store + httpClient httpClient + mutex sync.RWMutex + log log.Logger +} + +func ProvidePluginsService(cfg *setting.Cfg, pluginStore plugins.Store) *PluginsService { + return &PluginsService{ + enabled: cfg.CheckForUpdates, + grafanaVersion: cfg.BuildVersion, + httpClient: &http.Client{Timeout: 10 * time.Second}, + log: log.New("plugins.update.checker"), + pluginStore: pluginStore, + availableUpdates: make(map[string]string), + } +} + +type httpClient interface { + Get(url string) (resp *http.Response, err error) +} + +func (s *PluginsService) IsDisabled() bool { + return !s.enabled +} + +func (s *PluginsService) Run(ctx context.Context) error { + s.checkForUpdates(ctx) + + ticker := time.NewTicker(time.Minute * 10) + run := true + + for run { + select { + case <-ticker.C: + s.checkForUpdates(ctx) + case <-ctx.Done(): + run = false + } + } + + return ctx.Err() +} + +func (s *PluginsService) HasUpdate(ctx context.Context, pluginID string) (string, bool) { + s.mutex.RLock() + updateVers, updateAvailable := s.availableUpdates[pluginID] + s.mutex.RUnlock() + if updateAvailable { + // check if plugin has already been updated since the last invocation of `checkForUpdates` + plugin, exists := s.pluginStore.Plugin(ctx, pluginID) + if !exists { + return "", false + } + + if canUpdate(plugin.Info.Version, updateVers) { + return updateVers, true + } + } + + return "", false +} + +func (s *PluginsService) checkForUpdates(ctx context.Context) { + s.log.Debug("Checking for updates") + + localPlugins := s.pluginsEligibleForVersionCheck(ctx) + resp, err := s.httpClient.Get("https://grafana.com/api/plugins/versioncheck?slugIn=" + + s.pluginIDsCSV(localPlugins) + "&grafanaVersion=" + s.grafanaVersion) + if err != nil { + s.log.Debug("Failed to get plugins repo from grafana.com", "error", err.Error()) + return + } + defer func() { + if err := resp.Body.Close(); err != nil { + s.log.Warn("Failed to close response body", "err", err) + } + }() + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + s.log.Debug("Update check failed, reading response from grafana.com", "error", err.Error()) + return + } + + type gcomPlugin struct { + Slug string `json:"slug"` + Version string `json:"version"` + } + var gcomPlugins []gcomPlugin + err = json.Unmarshal(body, &gcomPlugins) + if err != nil { + s.log.Debug("Failed to unmarshal plugin repo, reading response from grafana.com", "error", err.Error()) + return + } + + availableUpdates := map[string]string{} + for _, gcomP := range gcomPlugins { + if localP, exists := localPlugins[gcomP.Slug]; exists { + if canUpdate(localP.Info.Version, gcomP.Version) { + availableUpdates[localP.ID] = gcomP.Version + } + } + } + + if len(availableUpdates) > 0 { + s.mutex.Lock() + s.availableUpdates = availableUpdates + s.mutex.Unlock() + } +} + +func canUpdate(v1, v2 string) bool { + ver1, err1 := version.NewVersion(v1) + if err1 != nil { + return false + } + ver2, err2 := version.NewVersion(v2) + if err2 != nil { + return false + } + + return ver1.LessThan(ver2) +} + +func (s *PluginsService) pluginIDsCSV(m map[string]plugins.PluginDTO) string { + var ids []string + for pluginID := range m { + ids = append(ids, pluginID) + } + + return strings.Join(ids, ",") +} + +func (s *PluginsService) pluginsEligibleForVersionCheck(ctx context.Context) map[string]plugins.PluginDTO { + result := make(map[string]plugins.PluginDTO) + for _, p := range s.pluginStore.Plugins(ctx) { + if p.IsCorePlugin() { + continue + } + result[p.ID] = p + } + + return result +} diff --git a/pkg/services/updatechecker/plugins_test.go b/pkg/services/updatechecker/plugins_test.go new file mode 100644 index 00000000000..1f89033f083 --- /dev/null +++ b/pkg/services/updatechecker/plugins_test.go @@ -0,0 +1,225 @@ +package updatechecker + +import ( + "context" + "io/ioutil" + "net/http" + "strings" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/plugins" +) + +func TestPluginUpdateChecker_HasUpdate(t *testing.T) { + t.Run("update is available", func(t *testing.T) { + svc := PluginsService{ + availableUpdates: map[string]string{ + "test-ds": "1.0.0", + }, + pluginStore: fakePluginStore{ + plugins: map[string]plugins.PluginDTO{ + "test-ds": { + JSONData: plugins.JSONData{ + Info: plugins.Info{Version: "0.9.0"}, + }, + }, + }, + }, + } + + update, exists := svc.HasUpdate(context.Background(), "test-ds") + require.True(t, exists) + require.Equal(t, "1.0.0", update) + }) + + t.Run("update is not available", func(t *testing.T) { + svc := PluginsService{ + availableUpdates: map[string]string{ + "test-panel": "0.9.0", + "test-app": "0.0.1", + }, + pluginStore: fakePluginStore{ + plugins: map[string]plugins.PluginDTO{ + "test-ds": { + JSONData: plugins.JSONData{ + Info: plugins.Info{Version: "0.9.0"}, + }, + }, + "test-panel": { + JSONData: plugins.JSONData{ + Info: plugins.Info{Version: "0.9.0"}, + }, + }, + "test-app": { + JSONData: plugins.JSONData{ + Info: plugins.Info{Version: "0.9.0"}, + }, + }, + }, + }, + } + + update, exists := svc.HasUpdate(context.Background(), "test-ds") + require.False(t, exists) + require.Empty(t, update) + + update, exists = svc.HasUpdate(context.Background(), "test-panel") + require.False(t, exists) + require.Empty(t, update) + + update, exists = svc.HasUpdate(context.Background(), "test-app") + require.False(t, exists) + require.Empty(t, update) + }) + + t.Run("update is available but plugin is not in store", func(t *testing.T) { + svc := PluginsService{ + availableUpdates: map[string]string{ + "test-panel": "0.9.0", + }, + pluginStore: fakePluginStore{ + plugins: map[string]plugins.PluginDTO{ + "test-ds": { + JSONData: plugins.JSONData{ + Info: plugins.Info{Version: "1.0.0"}, + }, + }, + }, + }, + } + + update, exists := svc.HasUpdate(context.Background(), "test-panel") + require.False(t, exists) + require.Empty(t, update) + + update, exists = svc.HasUpdate(context.Background(), "test-ds") + require.False(t, exists) + require.Empty(t, update) + }) +} + +func TestPluginUpdateChecker_checkForUpdates(t *testing.T) { + t.Run("update is available", func(t *testing.T) { + jsonResp := `[ + { + "slug": "test-ds", + "version": "1.0.12" + }, + { + "slug": "test-panel", + "version": "2.5.7" + }, + { + "slug": "test-core-panel", + "version": "1.0.0" + } + ]` + + svc := PluginsService{ + availableUpdates: map[string]string{ + "test-app": "1.0.0", + }, + pluginStore: fakePluginStore{ + plugins: map[string]plugins.PluginDTO{ + "test-ds": { + JSONData: plugins.JSONData{ + ID: "test-ds", + Info: plugins.Info{Version: "0.9.0"}, + }, + }, + "test-app": { + JSONData: plugins.JSONData{ + ID: "test-app", + Info: plugins.Info{Version: "0.5.0"}, + }, + }, + "test-panel": { + JSONData: plugins.JSONData{ + ID: "test-panel", + Info: plugins.Info{Version: "2.5.7"}, + }, + }, + "test-core-panel": { + Class: plugins.Core, + JSONData: plugins.JSONData{ + ID: "test-core-panel", + Info: plugins.Info{Version: "0.0.1"}, + }, + }, + }, + }, + httpClient: &fakeHTTPClient{ + fakeResp: jsonResp, + }, + log: &fakeLogger{}, + } + + svc.checkForUpdates(context.Background()) + + require.Equal(t, 1, len(svc.availableUpdates)) + + require.Equal(t, "1.0.12", svc.availableUpdates["test-ds"]) + update, exists := svc.HasUpdate(context.Background(), "test-ds") + require.True(t, exists) + require.Equal(t, "1.0.12", update) + + require.Empty(t, svc.availableUpdates["test-app"]) + update, exists = svc.HasUpdate(context.Background(), "test-app") + require.False(t, exists) + require.Empty(t, update) + + require.Empty(t, svc.availableUpdates["test-panel"]) + update, exists = svc.HasUpdate(context.Background(), "test-panel") + require.False(t, exists) + require.Empty(t, update) + + require.Empty(t, svc.availableUpdates["test-core-panel"]) + }) +} + +type fakeHTTPClient struct { + fakeResp string + + requestURL string +} + +func (c *fakeHTTPClient) Get(url string) (*http.Response, error) { + c.requestURL = url + + resp := &http.Response{ + Body: ioutil.NopCloser(strings.NewReader(c.fakeResp)), + } + + return resp, nil +} + +type fakePluginStore struct { + plugins.Store + + plugins map[string]plugins.PluginDTO +} + +func (pr fakePluginStore) Plugin(_ context.Context, pluginID string) (plugins.PluginDTO, bool) { + p, exists := pr.plugins[pluginID] + + return p, exists +} + +func (pr fakePluginStore) Plugins(_ context.Context, _ ...plugins.Type) []plugins.PluginDTO { + var result []plugins.PluginDTO + for _, p := range pr.plugins { + result = append(result, p) + } + return result +} + +type fakeLogger struct { + log.Logger +} + +func (l *fakeLogger) Debug(_ string, _ ...interface{}) {} + +func (l *fakeLogger) Warn(_ string, _ ...interface{}) {}