mirror of https://github.com/grafana/grafana
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 testspull/44665/head
parent
bf8694e709
commit
76603b93d6
@ -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 |
||||
} |
||||
@ -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{}) {} |
||||
Loading…
Reference in new issue