mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Plugins: Refactor Grafana and Plugin version update checkers (#44529)
* refactor * rework plugin update checking * make smarter * simplify * fix linter issue * make use of mutex * apply feedback to simplify * format imports * fix tests
This commit is contained in:
parent
bf8694e709
commit
76603b93d6
@ -239,8 +239,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,
|
||||
},
|
||||
"licenseInfo": map[string]interface{}{
|
||||
|
@ -47,11 +47,11 @@ func setupTestEnvironment(t *testing.T, cfg *setting.Cfg, features *featuremgmt.
|
||||
Cfg: cfg,
|
||||
RendererPluginManager: &fakeRendererManager{},
|
||||
},
|
||||
SQLStore: sqlStore,
|
||||
SettingsProvider: setting.ProvideProvider(cfg),
|
||||
pluginStore: &fakePluginStore{},
|
||||
updateChecker: &updatechecker.Service{},
|
||||
AccessControl: accesscontrolmock.New().WithDisabled(),
|
||||
SQLStore: sqlStore,
|
||||
SettingsProvider: setting.ProvideProvider(cfg),
|
||||
pluginStore: &fakePluginStore{},
|
||||
grafanaUpdateChecker: &updatechecker.GrafanaService{},
|
||||
AccessControl: accesscontrolmock.New().WithDisabled(),
|
||||
}
|
||||
|
||||
m := web.New()
|
||||
|
@ -119,7 +119,8 @@ type HTTPServer struct {
|
||||
DataSourcesService *datasources.Service
|
||||
cleanUpService *cleanup.CleanUpService
|
||||
tracer tracing.Tracer
|
||||
updateChecker *updatechecker.Service
|
||||
grafanaUpdateChecker *updatechecker.GrafanaService
|
||||
pluginsUpdateChecker *updatechecker.PluginsService
|
||||
searchUsersService searchusers.Service
|
||||
teamGuardian teamguardian.TeamGuardian
|
||||
queryDataService *query.Service
|
||||
@ -148,7 +149,8 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
|
||||
schemaService *schemaloader.SchemaLoaderService, alertNG *ngalert.AlertNG,
|
||||
libraryPanelService librarypanels.Service, libraryElementService libraryelements.Service,
|
||||
quotaService *quota.QuotaService, socialService social.Service, tracer tracing.Tracer,
|
||||
encryptionService encryption.Internal, updateChecker *updatechecker.Service, searchUsersService searchusers.Service,
|
||||
encryptionService encryption.Internal, grafanaUpdateChecker *updatechecker.GrafanaService,
|
||||
pluginsUpdateChecker *updatechecker.PluginsService, searchUsersService searchusers.Service,
|
||||
dataSourcesService *datasources.Service, secretsService secrets.Service, queryDataService *query.Service,
|
||||
teamGuardian teamguardian.TeamGuardian, serviceaccountsService serviceaccounts.Service,
|
||||
authInfoService authinfoservice.Service, resourcePermissionServices *resourceservices.ResourceServices) (*HTTPServer, error) {
|
||||
@ -171,7 +173,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,
|
||||
|
@ -611,8 +611,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: "app-grafana",
|
||||
FavIcon: "public/img/fav32.png",
|
||||
|
@ -74,8 +74,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,
|
||||
@ -83,6 +81,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
|
||||
@ -127,8 +131,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,
|
||||
@ -151,6 +153,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)
|
||||
}
|
||||
|
||||
|
@ -74,24 +74,6 @@ func (m *PluginManager) Init() error {
|
||||
}
|
||||
|
||||
func (m *PluginManager) Run(ctx context.Context) error {
|
||||
if m.cfg.CheckForUpdates {
|
||||
go func() {
|
||||
m.checkForUpdates(ctx)
|
||||
|
||||
ticker := time.NewTicker(time.Minute * 10)
|
||||
run := true
|
||||
|
||||
for run {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
m.checkForUpdates(ctx)
|
||||
case <-ctx.Done():
|
||||
run = false
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
<-ctx.Done()
|
||||
m.shutdown(ctx)
|
||||
return ctx.Err()
|
||||
|
@ -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(ctx context.Context) {
|
||||
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(ctx) {
|
||||
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
|
||||
}
|
@ -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
|
||||
@ -326,22 +318,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,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -26,12 +26,13 @@ import (
|
||||
)
|
||||
|
||||
func ProvideBackgroundServiceRegistry(
|
||||
httpServer *api.HTTPServer, ng *ngalert.AlertNG, cleanup *cleanup.CleanUpService,
|
||||
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,
|
||||
tracing tracing.Tracer, remoteCache *remotecache.RemoteCache, secretsService *secretsManager.SecretsService,
|
||||
httpServer *api.HTTPServer, ng *ngalert.AlertNG, cleanup *cleanup.CleanUpService, live *live.GrafanaLive,
|
||||
pushGateway *pushhttp.Gateway, notifications *notifications.NotificationService, pm *manager.PluginManager,
|
||||
rendering *rendering.RenderingService, tokenService models.UserTokenBackgroundService, tracing tracing.Tracer,
|
||||
provisioning *provisioning.ProvisioningServiceImpl, alerting *alerting.AlertEngine, usageStats *uss.UsageStats,
|
||||
grafanaUpdateChecker *updatechecker.GrafanaService, pluginsUpdateChecker *updatechecker.PluginsService,
|
||||
metrics *metrics.InternalMetricsService, secretsService *secretsManager.SecretsService,
|
||||
remoteCache *remotecache.RemoteCache,
|
||||
// Need to make sure these are initialized, is there a better place to put them?
|
||||
_ *plugindashboards.Service, _ *dashboardsnapshots.Service, _ *pluginsettings.Service,
|
||||
_ *alerting.AlertNotificationService, _ serviceaccounts.Service,
|
||||
@ -48,7 +49,8 @@ func ProvideBackgroundServiceRegistry(
|
||||
provisioning,
|
||||
alerting,
|
||||
pm,
|
||||
updateChecker,
|
||||
grafanaUpdateChecker,
|
||||
pluginsUpdateChecker,
|
||||
metrics,
|
||||
usageStats,
|
||||
tracing,
|
||||
|
@ -109,7 +109,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,
|
||||
|
115
pkg/services/updatechecker/grafana.go
Normal file
115
pkg/services/updatechecker/grafana.go
Normal file
@ -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
|
||||
}
|
@ -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
|
||||
}
|
167
pkg/services/updatechecker/plugins.go
Normal file
167
pkg/services/updatechecker/plugins.go
Normal file
@ -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
|
||||
}
|
225
pkg/services/updatechecker/plugins_test.go
Normal file
225
pkg/services/updatechecker/plugins_test.go
Normal file
@ -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{}) {}
|
Loading…
Reference in New Issue
Block a user