From f8a6380510909a0d0f05c83e6c7cc49605c33774 Mon Sep 17 00:00:00 2001 From: Andres Martinez Gotor Date: Tue, 21 Nov 2023 12:46:26 +0100 Subject: [PATCH] Chore: Change endpoint to check versions in cli (#78008) --- pkg/plugins/repo/errors.go | 8 ++++ pkg/plugins/repo/models.go | 9 ++-- pkg/plugins/repo/service.go | 26 ++++++++++-- pkg/plugins/repo/service_test.go | 71 +++++++++++++++++++++----------- pkg/plugins/repo/version.go | 6 +++ 5 files changed, 90 insertions(+), 30 deletions(-) diff --git a/pkg/plugins/repo/errors.go b/pkg/plugins/repo/errors.go index d8a402dd09c..cdc9a03f85f 100644 --- a/pkg/plugins/repo/errors.go +++ b/pkg/plugins/repo/errors.go @@ -79,3 +79,11 @@ type ErrChecksumMismatch struct { func (e ErrChecksumMismatch) Error() string { return fmt.Sprintf("expected SHA256 checksum does not match the downloaded archive (%s) - please contact security@grafana.com", e.archiveURL) } + +type ErrCorePlugin struct { + id string +} + +func (e ErrCorePlugin) Error() string { + return fmt.Sprintf("plugin %s is a core plugin and cannot be installed separately", e.id) +} diff --git a/pkg/plugins/repo/models.go b/pkg/plugins/repo/models.go index 4816bdaf30f..85c1519b5c8 100644 --- a/pkg/plugins/repo/models.go +++ b/pkg/plugins/repo/models.go @@ -12,14 +12,15 @@ type PluginArchiveInfo struct { Checksum string } -// PluginRepo is (a subset of) the JSON response from /api/plugins/repo/$pluginID -type PluginRepo struct { - Versions []Version `json:"versions"` +// PluginVersions is the JSON response from /api/plugins/$pluginID/versions +type PluginVersions struct { + Versions []Version `json:"items"` } type Version struct { Version string `json:"version"` - Arch map[string]ArchMeta `json:"arch"` + Arch map[string]ArchMeta `json:"packages"` + URL string `json:"url"` } type ArchMeta struct { diff --git a/pkg/plugins/repo/service.go b/pkg/plugins/repo/service.go index 0bd1b7a7e6b..4e1037ef29a 100644 --- a/pkg/plugins/repo/service.go +++ b/pkg/plugins/repo/service.go @@ -5,8 +5,10 @@ import ( "encoding/json" "errors" "fmt" + "net/http" "net/url" "path" + "strings" "github.com/grafana/grafana/pkg/plugins/config" "github.com/grafana/grafana/pkg/plugins/log" @@ -87,7 +89,19 @@ func (m *Manager) PluginVersion(pluginID, version string, compatOpts CompatOpts) return VersionData{}, errors.New("no system compatibility requirements set") } - return SelectSystemCompatibleVersion(m.log, versions, pluginID, version, sysCompatOpts) + compatibleVer, err := SelectSystemCompatibleVersion(m.log, versions, pluginID, version, sysCompatOpts) + if err != nil { + return VersionData{}, err + } + + isGrafanaCorePlugin := strings.HasPrefix(compatibleVer.URL, "https://github.com/grafana/grafana/tree/main/public/app/plugins/") + _, hasAnyArch := compatibleVer.Arch["any"] + if isGrafanaCorePlugin && hasAnyArch { + // Trying to install a coupled core plugin + return VersionData{}, ErrCorePlugin{id: pluginID} + } + + return compatibleVer, nil } func (m *Manager) downloadURL(pluginID, version string) string { @@ -102,19 +116,25 @@ func (m *Manager) grafanaCompatiblePluginVersions(pluginID string, compatOpts Co return nil, err } - u.Path = path.Join(u.Path, "repo", pluginID) + u.Path = path.Join(u.Path, pluginID, "versions") body, err := m.client.SendReq(u, compatOpts) if err != nil { return nil, err } - var v PluginRepo + var v PluginVersions err = json.Unmarshal(body, &v) if err != nil { m.log.Error("Failed to unmarshal plugin repo response", err) return nil, err } + if len(v.Versions) == 0 { + // /plugins/{pluginId}/versions returns 200 even if the plugin doesn't exists + // but the response is empty. In this case we return 404. + return nil, newErrResponse4xx(http.StatusNotFound).withMessage("Plugin not found") + } + return v.Versions, nil } diff --git a/pkg/plugins/repo/service_test.go b/pkg/plugins/repo/service_test.go index 1536b032deb..a84fe224679 100644 --- a/pkg/plugins/repo/service_test.go +++ b/pkg/plugins/repo/service_test.go @@ -21,9 +21,12 @@ const ( func TestGetPluginArchive(t *testing.T) { tcs := []struct { - name string - sha string - err error + name string + sha string + apiOpSys string + apiArch string + apiUrl string + err error }{ { name: "Happy path", @@ -34,6 +37,18 @@ func TestGetPluginArchive(t *testing.T) { sha: "1a2b3c", err: &ErrChecksumMismatch{}, }, + { + name: "Core plugin", + sha: "69f698961b6ea651211a187874434821c4727cc22de022e3a7059116d21c75b1", + apiOpSys: "any", + apiUrl: "https://github.com/grafana/grafana/tree/main/public/app/plugins/test", + err: &ErrCorePlugin{}, + }, + { + name: "Decoupled core plugin", + sha: "69f698961b6ea651211a187874434821c4727cc22de022e3a7059116d21c75b1", + apiUrl: "https://github.com/grafana/grafana/tree/main/public/app/plugins/test", + }, } pluginZip := createPluginArchive(t) @@ -57,17 +72,23 @@ func TestGetPluginArchive(t *testing.T) { grafanaVersion = "10.0.0" ) - srv := mockPluginRepoAPI(t, - srvData{ - pluginID: pluginID, - version: version, - opSys: opSys, - arch: arch, - grafanaVersion: grafanaVersion, - sha: tc.sha, - archive: d, - }, - ) + srvd := srvData{ + pluginID: pluginID, + version: version, + opSys: tc.apiOpSys, + arch: tc.apiArch, + url: tc.apiUrl, + grafanaVersion: grafanaVersion, + sha: tc.sha, + archive: d, + } + if srvd.opSys == "" { + srvd.opSys = opSys + } + if srvd.arch == "" && srvd.opSys != "any" { + srvd.arch = arch + } + srv := mockPluginVersionsAPI(t, srvd) t.Cleanup(srv.Close) m := NewManager(ManagerCfg{ @@ -125,34 +146,38 @@ type srvData struct { sha string grafanaVersion string archive []byte + url string } -func mockPluginRepoAPI(t *testing.T, data srvData) *httptest.Server { +func mockPluginVersionsAPI(t *testing.T, data srvData) *httptest.Server { t.Helper() mux := http.NewServeMux() // mock plugin version data - mux.HandleFunc(fmt.Sprintf("/repo/%s", data.pluginID), func(w http.ResponseWriter, r *http.Request) { + mux.HandleFunc(fmt.Sprintf("/%s/versions", data.pluginID), func(w http.ResponseWriter, r *http.Request) { require.Equal(t, data.grafanaVersion, r.Header.Get("grafana-version")) - require.Equal(t, data.opSys, r.Header.Get("grafana-os")) - require.Equal(t, data.arch, r.Header.Get("grafana-arch")) require.NotNil(t, fmt.Sprintf("grafana %s", data.grafanaVersion), r.Header.Get("User-Agent")) w.WriteHeader(http.StatusOK) w.Header().Set("Content-Type", "application/json") + platform := data.opSys + if data.arch != "" { + platform += "-" + data.arch + } _, _ = w.Write([]byte(fmt.Sprintf(` { - "versions": [{ + "items": [{ "version": "%s", - "arch": { - "%s-%s": { + "packages": { + "%s": { "sha256": "%s" } - } + }, + "url": "%s" }] } - `, data.version, data.opSys, data.arch, data.sha), + `, data.version, platform, data.sha, data.url), )) }) diff --git a/pkg/plugins/repo/version.go b/pkg/plugins/repo/version.go index 44489eff9a6..fc73d6e5eaa 100644 --- a/pkg/plugins/repo/version.go +++ b/pkg/plugins/repo/version.go @@ -9,6 +9,8 @@ import ( type VersionData struct { Version string Checksum string + Arch map[string]ArchMeta + URL string } // SelectSystemCompatibleVersion selects the most appropriate plugin version based on os + architecture @@ -33,6 +35,8 @@ func SelectSystemCompatibleVersion(log log.PrettyLogger, versions []Version, plu return VersionData{ Version: latestForArch.Version, Checksum: checksum(latestForArch, compatOpts), + Arch: latestForArch.Arch, + URL: latestForArch.URL, }, nil } for _, v := range versions { @@ -65,6 +69,8 @@ func SelectSystemCompatibleVersion(log log.PrettyLogger, versions []Version, plu return VersionData{ Version: ver.Version, Checksum: checksum(ver, compatOpts), + Arch: ver.Arch, + URL: ver.URL, }, nil }