Grafana Advisor: Plugin checks (#99502)

This commit is contained in:
Andres Martinez Gotor 2025-01-27 16:39:46 +01:00 committed by GitHub
parent 50b14c533c
commit b0e74cf737
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 391 additions and 10 deletions

View File

@ -3,9 +3,13 @@ package checkregistry
import (
"github.com/grafana/grafana/apps/advisor/pkg/app/checks"
"github.com/grafana/grafana/apps/advisor/pkg/app/checks/datasourcecheck"
"github.com/grafana/grafana/apps/advisor/pkg/app/checks/plugincheck"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/repo"
"github.com/grafana/grafana/pkg/registry/apis/datasource"
"github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/pluginsintegration/managedplugins"
"github.com/grafana/grafana/pkg/services/pluginsintegration/plugininstaller"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
)
@ -18,15 +22,22 @@ type Service struct {
pluginStore pluginstore.Store
pluginContextProvider datasource.PluginContextWrapper
pluginClient plugins.Client
pluginRepo repo.Service
pluginPreinstall plugininstaller.Preinstall
managedPlugins managedplugins.Manager
}
func ProvideService(datasourceSvc datasources.DataSourceService, pluginStore pluginstore.Store,
pluginContextProvider datasource.PluginContextWrapper, pluginClient plugins.Client) *Service {
pluginContextProvider datasource.PluginContextWrapper, pluginClient plugins.Client,
pluginRepo repo.Service, pluginPreinstall plugininstaller.Preinstall, managedPlugins managedplugins.Manager) *Service {
return &Service{
datasourceSvc: datasourceSvc,
pluginStore: pluginStore,
pluginContextProvider: pluginContextProvider,
pluginClient: pluginClient,
pluginRepo: pluginRepo,
pluginPreinstall: pluginPreinstall,
managedPlugins: managedPlugins,
}
}
@ -38,5 +49,11 @@ func (s *Service) Checks() []checks.Check {
s.pluginContextProvider,
s.pluginClient,
),
plugincheck.New(
s.pluginStore,
s.pluginRepo,
s.pluginPreinstall,
s.managedPlugins,
),
}
}

View File

@ -0,0 +1,108 @@
package plugincheck
import (
"context"
"fmt"
sysruntime "runtime"
"github.com/Masterminds/semver/v3"
advisor "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1"
"github.com/grafana/grafana/apps/advisor/pkg/app/checks"
"github.com/grafana/grafana/pkg/cmd/grafana-cli/services"
"github.com/grafana/grafana/pkg/plugins/repo"
"github.com/grafana/grafana/pkg/services/pluginsintegration/managedplugins"
"github.com/grafana/grafana/pkg/services/pluginsintegration/plugininstaller"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
)
func New(
pluginStore pluginstore.Store,
pluginRepo repo.Service,
pluginPreinstall plugininstaller.Preinstall,
managedPlugins managedplugins.Manager,
) checks.Check {
return &check{
PluginStore: pluginStore,
PluginRepo: pluginRepo,
PluginPreinstall: pluginPreinstall,
ManagedPlugins: managedPlugins,
}
}
type check struct {
PluginStore pluginstore.Store
PluginRepo repo.Service
PluginPreinstall plugininstaller.Preinstall
ManagedPlugins managedplugins.Manager
}
func (c *check) Type() string {
return "plugin"
}
func (c *check) Run(ctx context.Context, _ *advisor.CheckSpec) (*advisor.CheckV0alpha1StatusReport, error) {
ps := c.PluginStore.Plugins(ctx)
errs := []advisor.CheckV0alpha1StatusReportErrors{}
for _, p := range ps {
// Skip if it's a core plugin
if p.IsCorePlugin() {
continue
}
// Check if plugin is deprecated
i, err := c.PluginRepo.PluginInfo(ctx, p.ID)
if err != nil {
continue
}
if i.Status == "deprecated" {
errs = append(errs, advisor.CheckV0alpha1StatusReportErrors{
Severity: advisor.CheckStatusSeverityHigh,
Reason: fmt.Sprintf("Plugin deprecated: %s", p.ID),
Action: "Look for alternatives",
})
}
// Check if plugin has a newer version, only if it's not managed or pinned
if c.isManaged(ctx, p.ID) || c.PluginPreinstall.IsPinned(p.ID) {
continue
}
compatOpts := repo.NewCompatOpts(services.GrafanaVersion, sysruntime.GOOS, sysruntime.GOARCH)
info, err := c.PluginRepo.GetPluginArchiveInfo(ctx, p.ID, "", compatOpts)
if err != nil {
continue
}
if hasUpdate(p, info) {
errs = append(errs, advisor.CheckV0alpha1StatusReportErrors{
Severity: advisor.CheckStatusSeverityLow,
Reason: fmt.Sprintf("New version available: %s", p.ID),
Action: "Update plugin",
})
}
}
return &advisor.CheckV0alpha1StatusReport{
Count: int64(len(ps)),
Errors: errs,
}, nil
}
func hasUpdate(current pluginstore.Plugin, latest *repo.PluginArchiveInfo) bool {
// If both versions are semver-valid, compare them
v1, err1 := semver.NewVersion(current.Info.Version)
v2, err2 := semver.NewVersion(latest.Version)
if err1 == nil && err2 == nil {
return v1.LessThan(v2)
}
// In other case, assume that a different latest version will always be newer
return current.Info.Version != latest.Version
}
func (c *check) isManaged(ctx context.Context, pluginID string) bool {
for _, managedPlugin := range c.ManagedPlugins.ManagedPlugins(ctx) {
if managedPlugin == pluginID {
return true
}
}
return false
}

View File

@ -0,0 +1,181 @@
package plugincheck
import (
"context"
"testing"
advisor "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/repo"
"github.com/grafana/grafana/pkg/services/pluginsintegration/managedplugins"
"github.com/grafana/grafana/pkg/services/pluginsintegration/plugininstaller"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
"github.com/stretchr/testify/assert"
)
func TestRun(t *testing.T) {
tests := []struct {
name string
plugins []pluginstore.Plugin
pluginInfo map[string]*repo.PluginInfo
pluginArchives map[string]*repo.PluginArchiveInfo
pluginPreinstalled []string
pluginManaged []string
expectedErrors []advisor.CheckV0alpha1StatusReportErrors
}{
{
name: "No plugins",
plugins: []pluginstore.Plugin{},
expectedErrors: []advisor.CheckV0alpha1StatusReportErrors{},
},
{
name: "Deprecated plugin",
plugins: []pluginstore.Plugin{
{JSONData: plugins.JSONData{ID: "plugin1", Info: plugins.Info{Version: "1.0.0"}}},
},
pluginInfo: map[string]*repo.PluginInfo{
"plugin1": {Status: "deprecated"},
},
pluginArchives: map[string]*repo.PluginArchiveInfo{
"plugin1": {Version: "1.0.0"},
},
expectedErrors: []advisor.CheckV0alpha1StatusReportErrors{
{
Severity: advisor.CheckStatusSeverityHigh,
Reason: "Plugin deprecated: plugin1",
Action: "Look for alternatives",
},
},
},
{
name: "Plugin with update",
plugins: []pluginstore.Plugin{
{JSONData: plugins.JSONData{ID: "plugin2", Info: plugins.Info{Version: "1.0.0"}}},
},
pluginInfo: map[string]*repo.PluginInfo{
"plugin2": {Status: "active"},
},
pluginArchives: map[string]*repo.PluginArchiveInfo{
"plugin2": {Version: "1.1.0"},
},
expectedErrors: []advisor.CheckV0alpha1StatusReportErrors{
{
Severity: advisor.CheckStatusSeverityLow,
Reason: "New version available: plugin2",
Action: "Update plugin",
},
},
},
{
name: "Plugin with update (non semver)",
plugins: []pluginstore.Plugin{
{JSONData: plugins.JSONData{ID: "plugin2", Info: plugins.Info{Version: "alpha"}}},
},
pluginInfo: map[string]*repo.PluginInfo{
"plugin2": {Status: "active"},
},
pluginArchives: map[string]*repo.PluginArchiveInfo{
"plugin2": {Version: "beta"},
},
expectedErrors: []advisor.CheckV0alpha1StatusReportErrors{
{
Severity: advisor.CheckStatusSeverityLow,
Reason: "New version available: plugin2",
Action: "Update plugin",
},
},
},
{
name: "Plugin pinned",
plugins: []pluginstore.Plugin{
{JSONData: plugins.JSONData{ID: "plugin3", Info: plugins.Info{Version: "1.0.0"}}},
},
pluginInfo: map[string]*repo.PluginInfo{
"plugin3": {Status: "active"},
},
pluginArchives: map[string]*repo.PluginArchiveInfo{
"plugin3": {Version: "1.1.0"},
},
pluginPreinstalled: []string{"plugin3"},
expectedErrors: []advisor.CheckV0alpha1StatusReportErrors{},
},
{
name: "Managed plugin",
plugins: []pluginstore.Plugin{
{JSONData: plugins.JSONData{ID: "plugin4", Info: plugins.Info{Version: "1.0.0"}}},
},
pluginInfo: map[string]*repo.PluginInfo{
"plugin4": {Status: "active"},
},
pluginArchives: map[string]*repo.PluginArchiveInfo{
"plugin4": {Version: "1.1.0"},
},
pluginManaged: []string{"plugin4"},
expectedErrors: []advisor.CheckV0alpha1StatusReportErrors{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
pluginStore := &mockPluginStore{plugins: tt.plugins}
pluginRepo := &mockPluginRepo{
pluginInfo: tt.pluginInfo,
pluginArchiveInfo: tt.pluginArchives,
}
pluginPreinstall := &mockPluginPreinstall{pinned: tt.pluginPreinstalled}
managedPlugins := &mockManagedPlugins{managed: tt.pluginManaged}
check := New(pluginStore, pluginRepo, pluginPreinstall, managedPlugins)
report, err := check.Run(context.Background(), nil)
assert.NoError(t, err)
assert.Equal(t, int64(len(tt.plugins)), report.Count)
assert.Equal(t, tt.expectedErrors, report.Errors)
})
}
}
type mockPluginStore struct {
pluginstore.Store
plugins []pluginstore.Plugin
}
func (m *mockPluginStore) Plugins(ctx context.Context, t ...plugins.Type) []pluginstore.Plugin {
return m.plugins
}
type mockPluginRepo struct {
repo.Service
pluginInfo map[string]*repo.PluginInfo
pluginArchiveInfo map[string]*repo.PluginArchiveInfo
}
func (m *mockPluginRepo) PluginInfo(ctx context.Context, id string) (*repo.PluginInfo, error) {
return m.pluginInfo[id], nil
}
func (m *mockPluginRepo) GetPluginArchiveInfo(ctx context.Context, id, version string, opts repo.CompatOpts) (*repo.PluginArchiveInfo, error) {
return m.pluginArchiveInfo[id], nil
}
type mockPluginPreinstall struct {
plugininstaller.Preinstall
pinned []string
}
func (m *mockPluginPreinstall) IsPinned(pluginID string) bool {
for _, p := range m.pinned {
if p == pluginID {
return true
}
}
return false
}
type mockManagedPlugins struct {
managedplugins.Manager
managed []string
}
func (m *mockManagedPlugins) ManagedPlugins(ctx context.Context) []string {
return m.managed
}

View File

@ -82,6 +82,7 @@ import (
"github.com/grafana/grafana/pkg/services/pluginsintegration/managedplugins"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginassets"
"github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext"
"github.com/grafana/grafana/pkg/services/pluginsintegration/plugininstaller"
pluginSettings "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
pref "github.com/grafana/grafana/pkg/services/preference"
@ -149,6 +150,7 @@ type HTTPServer struct {
pluginStaticRouteResolver plugins.StaticRouteResolver
pluginErrorResolver plugins.ErrorResolver
pluginAssets *pluginassets.Service
pluginPreinstall plugininstaller.Preinstall
SearchService search.Service
ShortURLService shorturls.Service
QueryHistoryService queryhistory.Service
@ -270,7 +272,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
annotationRepo annotations.Repository, tagService tag.Service, searchv2HTTPService searchV2.SearchHTTPService, oauthTokenService oauthtoken.OAuthTokenService,
statsService stats.Service, authnService authn.Service, pluginsCDNService *pluginscdn.Service, promGatherer prometheus.Gatherer,
starApi *starApi.API, promRegister prometheus.Registerer, clientConfigProvider grafanaapiserver.DirectRestConfigProvider, anonService anonymous.Service,
userVerifier user.Verifier,
userVerifier user.Verifier, pluginPreinstall plugininstaller.Preinstall,
) (*HTTPServer, error) {
web.Env = cfg.Env
m := web.New()
@ -295,6 +297,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
pluginFileStore: pluginFileStore,
grafanaUpdateChecker: grafanaUpdateChecker,
pluginsUpdateChecker: pluginsUpdateChecker,
pluginPreinstall: pluginPreinstall,
SettingsProvider: settingsProvider,
DataSourceCache: dataSourceCache,
AuthTokenService: userTokenService,

View File

@ -466,10 +466,8 @@ func (hs *HTTPServer) InstallPlugin(c *contextmodel.ReqContext) response.Respons
hs.log.Info("Plugin install/update requested", "pluginId", pluginID, "user", c.Login)
for _, preinstalled := range hs.Cfg.PreinstallPlugins {
if preinstalled.ID == pluginID && preinstalled.Version != "" {
return response.Error(http.StatusConflict, "Cannot update a pinned pre-installed plugin", nil)
}
for hs.pluginPreinstall.IsPinned(pluginID) {
return response.Error(http.StatusConflict, "Cannot update a pinned pre-installed plugin", nil)
}
compatOpts := plugins.NewAddOpts(hs.Cfg.BuildVersion, runtime.GOOS, runtime.GOARCH, "")
@ -511,10 +509,8 @@ func (hs *HTTPServer) UninstallPlugin(c *contextmodel.ReqContext) response.Respo
return response.Error(http.StatusNotFound, "Plugin not installed", nil)
}
for _, preinstalled := range hs.Cfg.PreinstallPlugins {
if preinstalled.ID == pluginID {
return response.Error(http.StatusConflict, "Cannot uninstall a pre-installed plugin", nil)
}
for hs.pluginPreinstall.IsPreinstalled(pluginID) {
return response.Error(http.StatusConflict, "Cannot uninstall a pre-installed plugin", nil)
}
err := hs.pluginInstaller.Remove(c.Req.Context(), pluginID, plugin.Info.Version)

View File

@ -45,6 +45,7 @@ import (
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginaccesscontrol"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginassets"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginerrs"
"github.com/grafana/grafana/pkg/services/pluginsintegration/plugininstaller"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
"github.com/grafana/grafana/pkg/services/updatechecker"
@ -111,6 +112,7 @@ func Test_PluginsInstallAndUninstall(t *testing.T) {
},
})
hs.managedPluginsService = managedplugins.NewNoop()
hs.pluginPreinstall = plugininstaller.ProvidePreinstall(hs.Cfg)
expectedIdentity := &authn.Identity{
OrgID: tc.permissionOrg,

View File

@ -0,0 +1,36 @@
package plugininstaller
import "github.com/grafana/grafana/pkg/setting"
type Preinstall interface {
IsPreinstalled(pluginID string) bool
IsPinned(pluginID string) bool
}
func ProvidePreinstall(
cfg *setting.Cfg,
) *PreinstallImpl {
plugins := make(map[string]*setting.InstallPlugin)
for _, p := range cfg.PreinstallPlugins {
plugins[p.ID] = &p
}
return &PreinstallImpl{
plugins: plugins,
}
}
type PreinstallImpl struct {
plugins map[string]*setting.InstallPlugin
}
func (c *PreinstallImpl) IsPreinstalled(pluginID string) bool {
_, ok := c.plugins[pluginID]
return ok
}
func (c *PreinstallImpl) IsPinned(pluginID string) bool {
if p, ok := c.plugins[pluginID]; ok {
return p.Version != ""
}
return false
}

View File

@ -0,0 +1,36 @@
package plugininstaller
import (
"testing"
"github.com/grafana/grafana/pkg/setting"
"github.com/stretchr/testify/assert"
)
func TestIsPreinstalled(t *testing.T) {
cfg := &setting.Cfg{
PreinstallPlugins: []setting.InstallPlugin{
{ID: "plugin1"},
{ID: "plugin2"},
},
}
preinstall := ProvidePreinstall(cfg)
assert.True(t, preinstall.IsPreinstalled("plugin1"))
assert.True(t, preinstall.IsPreinstalled("plugin2"))
assert.False(t, preinstall.IsPreinstalled("plugin3"))
}
func TestIsPinned(t *testing.T) {
cfg := &setting.Cfg{
PreinstallPlugins: []setting.InstallPlugin{
{ID: "plugin1", Version: "1.0.0"},
{ID: "plugin2"},
},
}
preinstall := ProvidePreinstall(cfg)
assert.True(t, preinstall.IsPinned("plugin1"))
assert.False(t, preinstall.IsPinned("plugin2"))
assert.False(t, preinstall.IsPinned("plugin3"))
}

View File

@ -129,6 +129,8 @@ var WireSet = wire.NewSet(
wire.Bind(new(plugincontext.BasePluginContextProvider), new(*plugincontext.BaseProvider)),
plugininstaller.ProvideService,
pluginassets.ProvideService,
plugininstaller.ProvidePreinstall,
wire.Bind(new(plugininstaller.Preinstall), new(*plugininstaller.PreinstallImpl)),
)
// WireExtensionSet provides a wire.ProviderSet of plugin providers that can be