[v8.3.x] Plugins: Refactor Grafana and Plugin version update checkers (#44662)
* 44529 * fix test
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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{}) {}
|
||||
Reference in New Issue
Block a user