From 1b336e94c8d851c371b46baebb44d58f69a5b0b2 Mon Sep 17 00:00:00 2001 From: Andres Martinez Gotor Date: Thu, 22 Aug 2024 13:47:00 +0200 Subject: [PATCH] Chore: Check version compatibilty when installing a plugin (#92200) --- .../grafana-cli/commands/install_command.go | 4 +- pkg/plugins/repo/errors.go | 8 ++++ pkg/plugins/repo/models.go | 14 +++++-- pkg/plugins/repo/service.go | 8 +--- pkg/plugins/repo/service_test.go | 11 +++-- pkg/plugins/repo/version.go | 42 +++++++++++++------ pkg/plugins/repo/version_test.go | 37 +++++++++++++--- 7 files changed, 89 insertions(+), 35 deletions(-) diff --git a/pkg/cmd/grafana-cli/commands/install_command.go b/pkg/cmd/grafana-cli/commands/install_command.go index de4ae3f9b7c..42a8317a175 100644 --- a/pkg/cmd/grafana-cli/commands/install_command.go +++ b/pkg/cmd/grafana-cli/commands/install_command.go @@ -134,7 +134,9 @@ func doInstallPlugin(ctx context.Context, pluginID, version string, o pluginInst Logger: services.Logger, }) - compatOpts := repo.NewCompatOpts(services.GrafanaVersion, runtime.GOOS, runtime.GOARCH) + // FIXME: Re-enable grafanaVersion. This check was broken in 10.2 so disabling it for the moment. + // Expected to be re-enabled in 12.x. + compatOpts := repo.NewCompatOpts("", runtime.GOOS, runtime.GOARCH) var archive *repo.PluginArchive var err error diff --git a/pkg/plugins/repo/errors.go b/pkg/plugins/repo/errors.go index 15c0db83068..c275ce43afc 100644 --- a/pkg/plugins/repo/errors.go +++ b/pkg/plugins/repo/errors.go @@ -67,6 +67,10 @@ var ( ErrCorePluginMsg = "plugin {{.Public.PluginID}} is a core plugin and cannot be installed separately" ErrCorePluginBase = errutil.Forbidden("plugin.forbiddenCorePluginInstall"). MustTemplate(ErrCorePluginMsg, errutil.WithPublic(ErrCorePluginMsg)) + + ErrNotCompatibledMsg = "{{.Public.PluginID}} is not compatible with your Grafana version: {{.Public.GrafanaVersion}}" + ErrNotCompatibleBase = errutil.NotFound("plugin.grafanaVersionNotCompatible"). + MustTemplate(ErrNotCompatibledMsg, errutil.WithPublic(ErrNotCompatibledMsg)) ) func ErrVersionUnsupported(pluginID, requestedVersion, systemInfo string) error { @@ -88,3 +92,7 @@ func ErrChecksumMismatch(archiveURL string) error { func ErrCorePlugin(pluginID string) error { return ErrCorePluginBase.Build(errutil.TemplateData{Public: map[string]any{"PluginID": pluginID}}) } + +func ErrNoCompatibleVersions(pluginID, grafanaVersion string) error { + return ErrNotCompatibleBase.Build(errutil.TemplateData{Public: map[string]any{"PluginID": pluginID, "GrafanaVersion": grafanaVersion}}) +} diff --git a/pkg/plugins/repo/models.go b/pkg/plugins/repo/models.go index 85c1519b5c8..73c06259810 100644 --- a/pkg/plugins/repo/models.go +++ b/pkg/plugins/repo/models.go @@ -18,11 +18,17 @@ type PluginVersions struct { } type Version struct { - Version string `json:"version"` - Arch map[string]ArchMeta `json:"packages"` - URL string `json:"url"` + Version string `json:"version"` + Arch map[string]ArchMeta `json:"packages"` + URL string `json:"url"` + CreatedAt string `json:"createdAt"` + IsCompatible *bool `json:"isCompatible,omitempty"` + GrafanaDependency string `json:"grafanaDependency"` } type ArchMeta struct { - SHA256 string `json:"sha256"` + SHA256 string `json:"sha256"` + MD5 string `json:"md5"` + PackageName string `json:"packageName"` + DownloadURL string `json:"downloadUrl"` } diff --git a/pkg/plugins/repo/service.go b/pkg/plugins/repo/service.go index dea206ef5af..59af8d95906 100644 --- a/pkg/plugins/repo/service.go +++ b/pkg/plugins/repo/service.go @@ -3,7 +3,6 @@ package repo import ( "context" "encoding/json" - "errors" "fmt" "net/http" "net/url" @@ -84,12 +83,7 @@ func (m *Manager) PluginVersion(pluginID, version string, compatOpts CompatOpts) return VersionData{}, err } - sysCompatOpts, exists := compatOpts.System() - if !exists { - return VersionData{}, errors.New("no system compatibility requirements set") - } - - compatibleVer, err := SelectSystemCompatibleVersion(m.log, versions, pluginID, version, sysCompatOpts) + compatibleVer, err := SelectSystemCompatibleVersion(m.log, versions, pluginID, version, compatOpts) if err != nil { return VersionData{}, err } diff --git a/pkg/plugins/repo/service_test.go b/pkg/plugins/repo/service_test.go index ba050893420..887fe42868f 100644 --- a/pkg/plugins/repo/service_test.go +++ b/pkg/plugins/repo/service_test.go @@ -174,7 +174,8 @@ func mockPluginVersionsAPI(t *testing.T, data srvData) *httptest.Server { "sha256": "%s" } }, - "url": "%s" + "url": "%s", + "isCompatible": true }] } `, data.version, platform, data.sha, data.url), @@ -192,15 +193,17 @@ func mockPluginVersionsAPI(t *testing.T, data srvData) *httptest.Server { } type versionArg struct { - version string - arch []string + version string + arch []string + isCompatible *bool } func createPluginVersions(versions ...versionArg) []Version { vs := make([]Version, len(versions)) for i, version := range versions { ver := Version{ - Version: version.version, + Version: version.version, + IsCompatible: version.isCompatible, } if version.arch != nil { ver.Arch = map[string]ArchMeta{} diff --git a/pkg/plugins/repo/version.go b/pkg/plugins/repo/version.go index ad3716d1c2f..b589288019b 100644 --- a/pkg/plugins/repo/version.go +++ b/pkg/plugins/repo/version.go @@ -1,6 +1,8 @@ package repo import ( + "errors" + "slices" "strings" "github.com/grafana/grafana/pkg/plugins/log" @@ -19,19 +21,24 @@ type VersionData struct { // returns error if the supplied version does not exist. // returns error if supplied version exists but is not supported. // NOTE: It expects plugin.Versions to be sorted so the newest version is first. -func SelectSystemCompatibleVersion(log log.PrettyLogger, versions []Version, pluginID, version string, compatOpts SystemCompatOpts) (VersionData, error) { +func SelectSystemCompatibleVersion(log log.PrettyLogger, versions []Version, pluginID, version string, compatOpts CompatOpts) (VersionData, error) { version = normalizeVersion(version) - var ver Version - latestForArch, exists := latestSupportedVersion(versions, compatOpts) + sysCompatOpts, exists := compatOpts.System() if !exists { - return VersionData{}, ErrArcNotFound(pluginID, compatOpts.OSAndArch()) + return VersionData{}, errors.New("no system compatibility requirements set") + } + + var ver Version + latestForArch, err := latestSupportedVersion(pluginID, versions, compatOpts) + if err != nil { + return VersionData{}, err } if version == "" { return VersionData{ Version: latestForArch.Version, - Checksum: checksum(latestForArch, compatOpts), + Checksum: checksum(latestForArch, sysCompatOpts), Arch: latestForArch.Arch, URL: latestForArch.URL, }, nil @@ -46,18 +53,18 @@ func SelectSystemCompatibleVersion(log log.PrettyLogger, versions []Version, plu if len(ver.Version) == 0 { log.Debugf("Requested plugin version %s v%s not found but potential fallback version '%s' was found", pluginID, version, latestForArch.Version) - return VersionData{}, ErrVersionNotFound(pluginID, version, compatOpts.OSAndArch()) + return VersionData{}, ErrVersionNotFound(pluginID, version, sysCompatOpts.OSAndArch()) } - if !supportsCurrentArch(ver, compatOpts) { + if !supportsCurrentArch(ver, sysCompatOpts) { log.Debugf("Requested plugin version %s v%s is not supported on your system but potential fallback version '%s' was found", pluginID, version, latestForArch.Version) - return VersionData{}, ErrVersionUnsupported(pluginID, version, compatOpts.OSAndArch()) + return VersionData{}, ErrVersionUnsupported(pluginID, version, sysCompatOpts.OSAndArch()) } return VersionData{ Version: ver.Version, - Checksum: checksum(ver, compatOpts), + Checksum: checksum(ver, sysCompatOpts), Arch: ver.Arch, URL: ver.URL, }, nil @@ -86,13 +93,22 @@ func supportsCurrentArch(version Version, compatOpts SystemCompatOpts) bool { return false } -func latestSupportedVersion(versions []Version, compatOpts SystemCompatOpts) (Version, bool) { +func latestSupportedVersion(pluginID string, versions []Version, compatOpts CompatOpts) (Version, error) { + // First check if the version are compatible with the current Grafana version + versions = slices.DeleteFunc(versions, func(v Version) bool { + return v.IsCompatible != nil && !*v.IsCompatible + }) + if len(versions) == 0 { + return Version{}, ErrNoCompatibleVersions(pluginID, compatOpts.grafanaVersion) + } + + // Then check if the version are compatible with the current system for _, v := range versions { - if supportsCurrentArch(v, compatOpts) { - return v, true + if supportsCurrentArch(v, compatOpts.system) { + return v, nil } } - return Version{}, false + return Version{}, ErrArcNotFound(pluginID, compatOpts.system.OSAndArch()) } func normalizeVersion(version string) string { diff --git a/pkg/plugins/repo/version_test.go b/pkg/plugins/repo/version_test.go index e3ee36ebaa0..d62671e3b88 100644 --- a/pkg/plugins/repo/version_test.go +++ b/pkg/plugins/repo/version_test.go @@ -8,15 +8,25 @@ import ( "github.com/grafana/grafana/pkg/plugins/log" ) +func fakeCompatOpts() CompatOpts { + return NewCompatOpts("7.0.0", "linux", "amd64") +} + func TestSelectSystemCompatibleVersion(t *testing.T) { logger := log.NewTestPrettyLogger() t.Run("Should return error when requested version does not exist", func(t *testing.T) { - _, err := SelectSystemCompatibleVersion(log.NewTestPrettyLogger(), createPluginVersions(versionArg{version: "version"}), "test", "1.1.1", SystemCompatOpts{}) + _, err := SelectSystemCompatibleVersion( + log.NewTestPrettyLogger(), + createPluginVersions(versionArg{version: "version"}), + "test", "1.1.1", fakeCompatOpts()) require.Error(t, err) }) t.Run("Should return error when no version supports current arch", func(t *testing.T) { - _, err := SelectSystemCompatibleVersion(logger, createPluginVersions(versionArg{version: "version", arch: []string{"non-existent"}}), "test", "", SystemCompatOpts{}) + _, err := SelectSystemCompatibleVersion( + logger, + createPluginVersions(versionArg{version: "version", arch: []string{"non-existent"}}), + "test", "", fakeCompatOpts()) require.Error(t, err) }) @@ -24,7 +34,7 @@ func TestSelectSystemCompatibleVersion(t *testing.T) { _, err := SelectSystemCompatibleVersion(logger, createPluginVersions( versionArg{version: "2.0.0"}, versionArg{version: "1.1.1", arch: []string{"non-existent"}}, - ), "test", "1.1.1", SystemCompatOpts{}) + ), "test", "1.1.1", fakeCompatOpts()) require.Error(t, err) }) @@ -32,20 +42,35 @@ func TestSelectSystemCompatibleVersion(t *testing.T) { ver, err := SelectSystemCompatibleVersion(logger, createPluginVersions( versionArg{version: "2.0.0", arch: []string{"non-existent"}}, versionArg{version: "1.0.0"}, - ), "test", "", SystemCompatOpts{}) + ), "test", "", fakeCompatOpts()) require.NoError(t, err) require.Equal(t, "1.0.0", ver.Version) }) t.Run("Should return latest version when no version specified", func(t *testing.T) { - ver, err := SelectSystemCompatibleVersion(logger, createPluginVersions(versionArg{version: "2.0.0"}, versionArg{version: "1.0.0"}), "test", "", SystemCompatOpts{}) + ver, err := SelectSystemCompatibleVersion(logger, createPluginVersions( + versionArg{version: "2.0.0"}, + versionArg{version: "1.0.0"}), + "test", "", fakeCompatOpts()) require.NoError(t, err) require.Equal(t, "2.0.0", ver.Version) }) t.Run("Should return requested version", func(t *testing.T) { - ver, err := SelectSystemCompatibleVersion(logger, createPluginVersions(versionArg{version: "2.0.0"}, versionArg{version: "1.0.0"}), "test", "1.0.0", SystemCompatOpts{}) + ver, err := SelectSystemCompatibleVersion(logger, createPluginVersions( + versionArg{version: "2.0.0"}, + versionArg{version: "1.0.0"}), + "test", "1.0.0", fakeCompatOpts()) require.NoError(t, err) require.Equal(t, "1.0.0", ver.Version) }) + + t.Run("Should return error when requested version is not compatible", func(t *testing.T) { + isCompatible := false + _, err := SelectSystemCompatibleVersion(logger, + createPluginVersions(versionArg{version: "2.0.0", isCompatible: &isCompatible}), + "test", "2.0.0", fakeCompatOpts(), + ) + require.ErrorContains(t, err, "not compatible") + }) }