mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Plugins: Refactor plugin repository API (#69063)
* support grafana wildcard version * undo go.mod changes * tidy * flesh out tests * refactor * add tests * tidy naming * undo some changes * split interfaces * separation * update new signature * simplify * update var namings * unexport types * introduce opts pattern * reorder test * fix compat checks * middle ground * unexport client * move back * fix tests * inline logger * make client usable * use fake logger * tidy errors * remove unused types * fix test * review fixes * rework compatibility * adjust installer * fix tests * opts => cfg * remove unused var * fix var name
This commit is contained in:
parent
e7e70dbac6
commit
12dc56ad0c
@ -444,11 +444,8 @@ func (hs *HTTPServer) InstallPlugin(c *contextmodel.ReqContext) response.Respons
|
|||||||
}
|
}
|
||||||
pluginID := web.Params(c.Req)[":pluginId"]
|
pluginID := web.Params(c.Req)[":pluginId"]
|
||||||
|
|
||||||
err := hs.pluginInstaller.Add(c.Req.Context(), pluginID, dto.Version, plugins.CompatOpts{
|
compatOpts := plugins.NewCompatOpts(hs.Cfg.BuildVersion, runtime.GOOS, runtime.GOARCH)
|
||||||
GrafanaVersion: hs.Cfg.BuildVersion,
|
err := hs.pluginInstaller.Add(c.Req.Context(), pluginID, dto.Version, compatOpts)
|
||||||
OS: runtime.GOOS,
|
|
||||||
Arch: runtime.GOARCH,
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
var dupeErr plugins.DuplicateError
|
var dupeErr plugins.DuplicateError
|
||||||
if errors.As(err, &dupeErr) {
|
if errors.As(err, &dupeErr) {
|
||||||
@ -462,9 +459,9 @@ func (hs *HTTPServer) InstallPlugin(c *contextmodel.ReqContext) response.Respons
|
|||||||
if errors.As(err, &versionNotFoundErr) {
|
if errors.As(err, &versionNotFoundErr) {
|
||||||
return response.Error(http.StatusNotFound, "Plugin version not found", err)
|
return response.Error(http.StatusNotFound, "Plugin version not found", err)
|
||||||
}
|
}
|
||||||
var clientError repo.Response4xxError
|
var clientError repo.ErrResponse4xx
|
||||||
if errors.As(err, &clientError) {
|
if errors.As(err, &clientError) {
|
||||||
return response.Error(clientError.StatusCode, clientError.Message, err)
|
return response.Error(clientError.StatusCode(), clientError.Message(), err)
|
||||||
}
|
}
|
||||||
if errors.Is(err, plugins.ErrInstallCorePlugin) {
|
if errors.Is(err, plugins.ErrInstallCorePlugin) {
|
||||||
return response.Error(http.StatusForbidden, "Cannot install or change a Core plugin", err)
|
return response.Error(http.StatusForbidden, "Cannot install or change a Core plugin", err)
|
||||||
|
@ -67,8 +67,11 @@ func installCommand(c utils.CommandLine) error {
|
|||||||
// installPlugin downloads the plugin code as a zip file from the Grafana.com API
|
// installPlugin downloads the plugin code as a zip file from the Grafana.com API
|
||||||
// and then extracts the zip into the plugin's directory.
|
// and then extracts the zip into the plugin's directory.
|
||||||
func installPlugin(ctx context.Context, pluginID, version string, c utils.CommandLine) error {
|
func installPlugin(ctx context.Context, pluginID, version string, c utils.CommandLine) error {
|
||||||
skipTLSVerify := c.Bool("insecure")
|
repository := repo.NewManager(repo.ManagerCfg{
|
||||||
repository := repo.New(skipTLSVerify, c.PluginRepoURL(), services.Logger)
|
SkipTLSVerify: c.Bool("insecure"),
|
||||||
|
BaseURL: c.PluginRepoURL(),
|
||||||
|
Logger: services.Logger,
|
||||||
|
})
|
||||||
|
|
||||||
compatOpts := repo.NewCompatOpts(services.GrafanaVersion, runtime.GOOS, runtime.GOARCH)
|
compatOpts := repo.NewCompatOpts(services.GrafanaVersion, runtime.GOOS, runtime.GOARCH)
|
||||||
|
|
||||||
|
@ -42,9 +42,30 @@ type File struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type CompatOpts struct {
|
type CompatOpts struct {
|
||||||
GrafanaVersion string
|
grafanaVersion string
|
||||||
OS string
|
|
||||||
Arch string
|
os string
|
||||||
|
arch string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (co CompatOpts) GrafanaVersion() string {
|
||||||
|
return co.grafanaVersion
|
||||||
|
}
|
||||||
|
|
||||||
|
func (co CompatOpts) OS() string {
|
||||||
|
return co.os
|
||||||
|
}
|
||||||
|
|
||||||
|
func (co CompatOpts) Arch() string {
|
||||||
|
return co.arch
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCompatOpts(grafanaVersion, os, arch string) CompatOpts {
|
||||||
|
return CompatOpts{grafanaVersion: grafanaVersion, arch: arch, os: os}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSystemCompatOpts(os, arch string) CompatOpts {
|
||||||
|
return CompatOpts{arch: arch, os: os}
|
||||||
}
|
}
|
||||||
|
|
||||||
type UpdateInfo struct {
|
type UpdateInfo struct {
|
||||||
|
@ -46,3 +46,22 @@ type Logs struct {
|
|||||||
Message string
|
Message string
|
||||||
Ctx []interface{}
|
Ctx []interface{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var _ PrettyLogger = (*TestPrettyLogger)(nil)
|
||||||
|
|
||||||
|
type TestPrettyLogger struct{}
|
||||||
|
|
||||||
|
func NewTestPrettyLogger() *TestPrettyLogger {
|
||||||
|
return &TestPrettyLogger{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *TestPrettyLogger) Successf(_ string, _ ...interface{}) {}
|
||||||
|
func (f *TestPrettyLogger) Failuref(_ string, _ ...interface{}) {}
|
||||||
|
func (f *TestPrettyLogger) Info(_ ...interface{}) {}
|
||||||
|
func (f *TestPrettyLogger) Infof(_ string, _ ...interface{}) {}
|
||||||
|
func (f *TestPrettyLogger) Debug(_ ...interface{}) {}
|
||||||
|
func (f *TestPrettyLogger) Debugf(_ string, _ ...interface{}) {}
|
||||||
|
func (f *TestPrettyLogger) Warn(_ ...interface{}) {}
|
||||||
|
func (f *TestPrettyLogger) Warnf(_ string, _ ...interface{}) {}
|
||||||
|
func (f *TestPrettyLogger) Error(_ ...interface{}) {}
|
||||||
|
func (f *TestPrettyLogger) Errorf(_ string, _ ...interface{}) {}
|
||||||
|
@ -202,7 +202,7 @@ func (f *FakePluginRegistry) Remove(_ context.Context, id string) error {
|
|||||||
type FakePluginRepo struct {
|
type FakePluginRepo struct {
|
||||||
GetPluginArchiveFunc func(_ context.Context, pluginID, version string, _ repo.CompatOpts) (*repo.PluginArchive, error)
|
GetPluginArchiveFunc func(_ context.Context, pluginID, version string, _ repo.CompatOpts) (*repo.PluginArchive, error)
|
||||||
GetPluginArchiveByURLFunc func(_ context.Context, archiveURL string, _ repo.CompatOpts) (*repo.PluginArchive, error)
|
GetPluginArchiveByURLFunc func(_ context.Context, archiveURL string, _ repo.CompatOpts) (*repo.PluginArchive, error)
|
||||||
GetPluginDownloadOptionsFunc func(_ context.Context, pluginID, version string, _ repo.CompatOpts) (*repo.PluginDownloadOptions, error)
|
GetPluginArchiveInfoFunc func(_ context.Context, pluginID, version string, _ repo.CompatOpts) (*repo.PluginArchiveInfo, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetPluginArchive fetches the requested plugin archive.
|
// GetPluginArchive fetches the requested plugin archive.
|
||||||
@ -223,12 +223,12 @@ func (r *FakePluginRepo) GetPluginArchiveByURL(ctx context.Context, archiveURL s
|
|||||||
return &repo.PluginArchive{}, nil
|
return &repo.PluginArchive{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetPluginDownloadOptions fetches information for downloading the requested plugin.
|
// GetPluginArchiveInfo fetches information for downloading the requested plugin.
|
||||||
func (r *FakePluginRepo) GetPluginDownloadOptions(ctx context.Context, pluginID, version string, opts repo.CompatOpts) (*repo.PluginDownloadOptions, error) {
|
func (r *FakePluginRepo) GetPluginArchiveInfo(ctx context.Context, pluginID, version string, opts repo.CompatOpts) (*repo.PluginArchiveInfo, error) {
|
||||||
if r.GetPluginDownloadOptionsFunc != nil {
|
if r.GetPluginArchiveInfoFunc != nil {
|
||||||
return r.GetPluginDownloadOptionsFunc(ctx, pluginID, version, opts)
|
return r.GetPluginArchiveInfoFunc(ctx, pluginID, version, opts)
|
||||||
}
|
}
|
||||||
return &repo.PluginDownloadOptions{}, nil
|
return &repo.PluginArchiveInfo{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type FakePluginStorage struct {
|
type FakePluginStorage struct {
|
||||||
|
@ -2,6 +2,7 @@ package manager
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/plugins"
|
"github.com/grafana/grafana/pkg/plugins"
|
||||||
@ -26,7 +27,8 @@ type PluginInstaller struct {
|
|||||||
|
|
||||||
func ProvideInstaller(cfg *config.Cfg, pluginRegistry registry.Service, pluginLoader loader.Service,
|
func ProvideInstaller(cfg *config.Cfg, pluginRegistry registry.Service, pluginLoader loader.Service,
|
||||||
pluginRepo repo.Service) *PluginInstaller {
|
pluginRepo repo.Service) *PluginInstaller {
|
||||||
return New(pluginRegistry, pluginLoader, pluginRepo, storage.FileSystem(log.NewPrettyLogger("installer.fs"), cfg.PluginsPath))
|
return New(pluginRegistry, pluginLoader, pluginRepo,
|
||||||
|
storage.FileSystem(log.NewPrettyLogger("installer.fs"), cfg.PluginsPath))
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(pluginRegistry registry.Service, pluginLoader loader.Service, pluginRepo repo.Service,
|
func New(pluginRegistry registry.Service, pluginLoader loader.Service, pluginRepo repo.Service,
|
||||||
@ -41,7 +43,10 @@ func New(pluginRegistry registry.Service, pluginLoader loader.Service, pluginRep
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *PluginInstaller) Add(ctx context.Context, pluginID, version string, opts plugins.CompatOpts) error {
|
func (m *PluginInstaller) Add(ctx context.Context, pluginID, version string, opts plugins.CompatOpts) error {
|
||||||
compatOpts := repo.NewCompatOpts(opts.GrafanaVersion, opts.OS, opts.Arch)
|
compatOpts, err := repoCompatOpts(opts)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
var pluginArchive *repo.PluginArchive
|
var pluginArchive *repo.PluginArchive
|
||||||
if plugin, exists := m.plugin(ctx, pluginID); exists {
|
if plugin, exists := m.plugin(ctx, pluginID); exists {
|
||||||
@ -56,19 +61,19 @@ func (m *PluginInstaller) Add(ctx context.Context, pluginID, version string, opt
|
|||||||
}
|
}
|
||||||
|
|
||||||
// get plugin update information to confirm if target update is possible
|
// get plugin update information to confirm if target update is possible
|
||||||
dlOpts, err := m.pluginRepo.GetPluginDownloadOptions(ctx, pluginID, version, compatOpts)
|
pluginArchiveInfo, err := m.pluginRepo.GetPluginArchiveInfo(ctx, pluginID, version, compatOpts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// if existing plugin version is the same as the target update version
|
// if existing plugin version is the same as the target update version
|
||||||
if dlOpts.Version == plugin.Info.Version {
|
if pluginArchiveInfo.Version == plugin.Info.Version {
|
||||||
return plugins.DuplicateError{
|
return plugins.DuplicateError{
|
||||||
PluginID: plugin.ID,
|
PluginID: plugin.ID,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if dlOpts.PluginZipURL == "" && dlOpts.Version == "" {
|
if pluginArchiveInfo.URL == "" && pluginArchiveInfo.Version == "" {
|
||||||
return fmt.Errorf("could not determine update options for %s", pluginID)
|
return fmt.Errorf("could not determine update options for %s", pluginID)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -78,13 +83,13 @@ func (m *PluginInstaller) Add(ctx context.Context, pluginID, version string, opt
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if dlOpts.PluginZipURL != "" {
|
if pluginArchiveInfo.URL != "" {
|
||||||
pluginArchive, err = m.pluginRepo.GetPluginArchiveByURL(ctx, dlOpts.PluginZipURL, compatOpts)
|
pluginArchive, err = m.pluginRepo.GetPluginArchiveByURL(ctx, pluginArchiveInfo.URL, compatOpts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
pluginArchive, err = m.pluginRepo.GetPluginArchive(ctx, pluginID, dlOpts.Version, compatOpts)
|
pluginArchive, err = m.pluginRepo.GetPluginArchive(ctx, pluginID, pluginArchiveInfo.Version, compatOpts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -153,3 +158,18 @@ func (m *PluginInstaller) plugin(ctx context.Context, pluginID string) (*plugins
|
|||||||
|
|
||||||
return p, true
|
return p, true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func repoCompatOpts(opts plugins.CompatOpts) (repo.CompatOpts, error) {
|
||||||
|
os := opts.OS()
|
||||||
|
arch := opts.Arch()
|
||||||
|
if len(os) == 0 || len(arch) == 0 {
|
||||||
|
return repo.CompatOpts{}, errors.New("invalid system compatibility options provided")
|
||||||
|
}
|
||||||
|
|
||||||
|
grafanaVersion := opts.GrafanaVersion()
|
||||||
|
if len(grafanaVersion) == 0 {
|
||||||
|
return repo.NewSystemCompatOpts(os, arch), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return repo.NewCompatOpts(grafanaVersion, os, arch), nil
|
||||||
|
}
|
||||||
|
@ -4,6 +4,7 @@ import (
|
|||||||
"archive/zip"
|
"archive/zip"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"runtime"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
@ -62,7 +63,7 @@ func TestPluginManager_Add_Remove(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
inst := New(fakes.NewFakePluginRegistry(), loader, pluginRepo, fs)
|
inst := New(fakes.NewFakePluginRegistry(), loader, pluginRepo, fs)
|
||||||
err := inst.Add(context.Background(), pluginID, v1, plugins.CompatOpts{})
|
err := inst.Add(context.Background(), pluginID, v1, testCompatOpts())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
t.Run("Won't add if already exists", func(t *testing.T) {
|
t.Run("Won't add if already exists", func(t *testing.T) {
|
||||||
@ -72,7 +73,7 @@ func TestPluginManager_Add_Remove(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
err = inst.Add(context.Background(), pluginID, v1, plugins.CompatOpts{})
|
err = inst.Add(context.Background(), pluginID, v1, testCompatOpts())
|
||||||
require.Equal(t, plugins.DuplicateError{
|
require.Equal(t, plugins.DuplicateError{
|
||||||
PluginID: pluginV1.ID,
|
PluginID: pluginV1.ID,
|
||||||
}, err)
|
}, err)
|
||||||
@ -96,9 +97,9 @@ func TestPluginManager_Add_Remove(t *testing.T) {
|
|||||||
require.Equal(t, []string{zipNameV2}, src.PluginURIs(ctx))
|
require.Equal(t, []string{zipNameV2}, src.PluginURIs(ctx))
|
||||||
return []*plugins.Plugin{pluginV2}, nil
|
return []*plugins.Plugin{pluginV2}, nil
|
||||||
}
|
}
|
||||||
pluginRepo.GetPluginDownloadOptionsFunc = func(_ context.Context, pluginID, version string, _ repo.CompatOpts) (*repo.PluginDownloadOptions, error) {
|
pluginRepo.GetPluginArchiveInfoFunc = func(_ context.Context, _, _ string, _ repo.CompatOpts) (*repo.PluginArchiveInfo, error) {
|
||||||
return &repo.PluginDownloadOptions{
|
return &repo.PluginArchiveInfo{
|
||||||
PluginZipURL: "https://grafanaplugins.com",
|
URL: "https://grafanaplugins.com",
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
pluginRepo.GetPluginArchiveByURLFunc = func(_ context.Context, pluginZipURL string, _ repo.CompatOpts) (*repo.PluginArchive, error) {
|
pluginRepo.GetPluginArchiveByURLFunc = func(_ context.Context, pluginZipURL string, _ repo.CompatOpts) (*repo.PluginArchive, error) {
|
||||||
@ -115,7 +116,7 @@ func TestPluginManager_Add_Remove(t *testing.T) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
err = inst.Add(context.Background(), pluginID, v2, plugins.CompatOpts{})
|
err = inst.Add(context.Background(), pluginID, v2, testCompatOpts())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -168,10 +169,10 @@ func TestPluginManager_Add_Remove(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pm := New(reg, &fakes.FakeLoader{}, &fakes.FakePluginRepo{}, &fakes.FakePluginStorage{})
|
pm := New(reg, &fakes.FakeLoader{}, &fakes.FakePluginRepo{}, &fakes.FakePluginStorage{})
|
||||||
err := pm.Add(context.Background(), p.ID, "3.2.0", plugins.CompatOpts{})
|
err := pm.Add(context.Background(), p.ID, "3.2.0", testCompatOpts())
|
||||||
require.ErrorIs(t, err, plugins.ErrInstallCorePlugin)
|
require.ErrorIs(t, err, plugins.ErrInstallCorePlugin)
|
||||||
|
|
||||||
err = pm.Add(context.Background(), testPluginID, "", plugins.CompatOpts{})
|
err = pm.Add(context.Background(), testPluginID, "", testCompatOpts())
|
||||||
require.Equal(t, plugins.ErrInstallCorePlugin, err)
|
require.Equal(t, plugins.ErrInstallCorePlugin, err)
|
||||||
|
|
||||||
t.Run(fmt.Sprintf("Can't uninstall %s plugin", tc.class), func(t *testing.T) {
|
t.Run(fmt.Sprintf("Can't uninstall %s plugin", tc.class), func(t *testing.T) {
|
||||||
@ -206,3 +207,7 @@ func createPlugin(t *testing.T, pluginID string, class plugins.Class, managed, b
|
|||||||
|
|
||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testCompatOpts() plugins.CompatOpts {
|
||||||
|
return plugins.NewCompatOpts("10.0.0", runtime.GOOS, runtime.GOARCH)
|
||||||
|
}
|
||||||
|
@ -26,7 +26,7 @@ type Client struct {
|
|||||||
log log.PrettyLogger
|
log log.PrettyLogger
|
||||||
}
|
}
|
||||||
|
|
||||||
func newClient(skipTLSVerify bool, logger log.PrettyLogger) *Client {
|
func NewClient(skipTLSVerify bool, logger log.PrettyLogger) *Client {
|
||||||
return &Client{
|
return &Client{
|
||||||
httpClient: makeHttpClient(skipTLSVerify, 10*time.Second),
|
httpClient: makeHttpClient(skipTLSVerify, 10*time.Second),
|
||||||
httpClientNoTimeout: makeHttpClient(skipTLSVerify, 0),
|
httpClientNoTimeout: makeHttpClient(skipTLSVerify, 0),
|
||||||
@ -34,7 +34,7 @@ func newClient(skipTLSVerify bool, logger log.PrettyLogger) *Client {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) download(_ context.Context, pluginZipURL, checksum string, compatOpts CompatOpts) (*PluginArchive, error) {
|
func (c *Client) Download(_ context.Context, pluginZipURL, checksum string, compatOpts CompatOpts) (*PluginArchive, error) {
|
||||||
// Create temp file for downloading zip file
|
// Create temp file for downloading zip file
|
||||||
tmpFile, err := os.CreateTemp("", "*.zip")
|
tmpFile, err := os.CreateTemp("", "*.zip")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -53,7 +53,7 @@ func (c *Client) download(_ context.Context, pluginZipURL, checksum string, comp
|
|||||||
if err := tmpFile.Close(); err != nil {
|
if err := tmpFile.Close(); err != nil {
|
||||||
c.log.Warn("Failed to close file", "err", err)
|
c.log.Warn("Failed to close file", "err", err)
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("%w: failed to download plugin archive (%s)", err, pluginZipURL)
|
return nil, fmt.Errorf("failed to download plugin archive: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
rc, err := zip.OpenReader(tmpFile.Name())
|
rc, err := zip.OpenReader(tmpFile.Name())
|
||||||
@ -61,9 +61,29 @@ func (c *Client) download(_ context.Context, pluginZipURL, checksum string, comp
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &PluginArchive{
|
return &PluginArchive{File: rc}, nil
|
||||||
File: rc,
|
}
|
||||||
}, nil
|
|
||||||
|
func (c *Client) SendReq(url *url.URL, compatOpts CompatOpts) ([]byte, error) {
|
||||||
|
req, err := c.createReq(url, compatOpts)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := c.httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
bodyReader, err := c.handleResp(res, compatOpts)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if err = bodyReader.Close(); err != nil {
|
||||||
|
c.log.Warn("Failed to close stream", "err", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return io.ReadAll(bodyReader)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) downloadFile(tmpFile *os.File, pluginURL, checksum string, compatOpts CompatOpts) (err error) {
|
func (c *Client) downloadFile(tmpFile *os.File, pluginURL, checksum string, compatOpts CompatOpts) (err error) {
|
||||||
@ -122,8 +142,8 @@ func (c *Client) downloadFile(tmpFile *os.File, pluginURL, checksum string, comp
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Using no timeout here as some plugins can be bigger and smaller timeout would prevent to download a plugin on
|
// Using no timeout as some plugin archives make take longer to fetch due to size, network performance, etc.
|
||||||
// slow network. As this is CLI operation hanging is not a big of an issue as user can just abort.
|
// Note: This is also used as part of the grafana plugin install CLI operation
|
||||||
bodyReader, err := c.sendReqNoTimeout(u, compatOpts)
|
bodyReader, err := c.sendReqNoTimeout(u, compatOpts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -139,37 +159,15 @@ func (c *Client) downloadFile(tmpFile *os.File, pluginURL, checksum string, comp
|
|||||||
if _, err = io.Copy(w, io.TeeReader(bodyReader, h)); err != nil {
|
if _, err = io.Copy(w, io.TeeReader(bodyReader, h)); err != nil {
|
||||||
return fmt.Errorf("%v: %w", "failed to compute SHA256 checksum", err)
|
return fmt.Errorf("%v: %w", "failed to compute SHA256 checksum", err)
|
||||||
}
|
}
|
||||||
if err := w.Flush(); err != nil {
|
if err = w.Flush(); err != nil {
|
||||||
return fmt.Errorf("failed to write to %q: %w", tmpFile.Name(), err)
|
return fmt.Errorf("failed to write to %q: %w", tmpFile.Name(), err)
|
||||||
}
|
}
|
||||||
if len(checksum) > 0 && checksum != fmt.Sprintf("%x", h.Sum(nil)) {
|
if len(checksum) > 0 && checksum != fmt.Sprintf("%x", h.Sum(nil)) {
|
||||||
return fmt.Errorf("expected SHA256 checksum does not match the downloaded archive (%s) - please contact security@grafana.com", pluginURL)
|
return ErrChecksumMismatch{archiveURL: pluginURL}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) sendReq(url *url.URL, compatOpts CompatOpts) ([]byte, error) {
|
|
||||||
req, err := c.createReq(url, compatOpts)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
res, err := c.httpClient.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
bodyReader, err := c.handleResp(res, compatOpts)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
if err := bodyReader.Close(); err != nil {
|
|
||||||
c.log.Warn("Failed to close stream", "err", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
return io.ReadAll(bodyReader)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) sendReqNoTimeout(url *url.URL, compatOpts CompatOpts) (io.ReadCloser, error) {
|
func (c *Client) sendReqNoTimeout(url *url.URL, compatOpts CompatOpts) (io.ReadCloser, error) {
|
||||||
req, err := c.createReq(url, compatOpts)
|
req, err := c.createReq(url, compatOpts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -189,10 +187,18 @@ func (c *Client) createReq(url *url.URL, compatOpts CompatOpts) (*http.Request,
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
req.Header.Set("grafana-version", compatOpts.GrafanaVersion)
|
if gVer, exists := compatOpts.GrafanaVersion(); exists {
|
||||||
req.Header.Set("grafana-os", compatOpts.OS)
|
req.Header.Set("grafana-version", gVer)
|
||||||
req.Header.Set("grafana-arch", compatOpts.Arch)
|
req.Header.Set("User-Agent", "grafana "+gVer)
|
||||||
req.Header.Set("User-Agent", "grafana "+compatOpts.GrafanaVersion)
|
}
|
||||||
|
|
||||||
|
if sysOS, exists := compatOpts.system.OS(); exists {
|
||||||
|
req.Header.Set("grafana-os", sysOS)
|
||||||
|
}
|
||||||
|
|
||||||
|
if sysArch, exists := compatOpts.system.Arch(); exists {
|
||||||
|
req.Header.Set("grafana-arch", sysArch)
|
||||||
|
}
|
||||||
|
|
||||||
return req, err
|
return req, err
|
||||||
}
|
}
|
||||||
@ -206,7 +212,7 @@ func (c *Client) handleResp(res *http.Response, compatOpts CompatOpts) (io.ReadC
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
if err != nil || len(body) == 0 {
|
if err != nil || len(body) == 0 {
|
||||||
return nil, Response4xxError{StatusCode: res.StatusCode}
|
return nil, newErrResponse4xx(res.StatusCode)
|
||||||
}
|
}
|
||||||
var message string
|
var message string
|
||||||
var jsonBody map[string]string
|
var jsonBody map[string]string
|
||||||
@ -216,7 +222,8 @@ func (c *Client) handleResp(res *http.Response, compatOpts CompatOpts) (io.ReadC
|
|||||||
} else {
|
} else {
|
||||||
message = jsonBody["message"]
|
message = jsonBody["message"]
|
||||||
}
|
}
|
||||||
return nil, Response4xxError{StatusCode: res.StatusCode, Message: message, SystemInfo: compatOpts.String()}
|
|
||||||
|
return nil, newErrResponse4xx(res.StatusCode).withMessage(message).withCompatibilityInfo(compatOpts)
|
||||||
}
|
}
|
||||||
|
|
||||||
if res.StatusCode/100 != 2 {
|
if res.StatusCode/100 != 2 {
|
||||||
@ -227,7 +234,9 @@ func (c *Client) handleResp(res *http.Response, compatOpts CompatOpts) (io.ReadC
|
|||||||
}
|
}
|
||||||
|
|
||||||
func makeHttpClient(skipTLSVerify bool, timeout time.Duration) http.Client {
|
func makeHttpClient(skipTLSVerify bool, timeout time.Duration) http.Client {
|
||||||
tr := &http.Transport{
|
return http.Client{
|
||||||
|
Timeout: timeout,
|
||||||
|
Transport: &http.Transport{
|
||||||
Proxy: http.ProxyFromEnvironment,
|
Proxy: http.ProxyFromEnvironment,
|
||||||
DialContext: (&net.Dialer{
|
DialContext: (&net.Dialer{
|
||||||
Timeout: 30 * time.Second,
|
Timeout: 30 * time.Second,
|
||||||
@ -240,10 +249,6 @@ func makeHttpClient(skipTLSVerify bool, timeout time.Duration) http.Client {
|
|||||||
TLSClientConfig: &tls.Config{
|
TLSClientConfig: &tls.Config{
|
||||||
InsecureSkipVerify: skipTLSVerify,
|
InsecureSkipVerify: skipTLSVerify,
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
|
|
||||||
return http.Client{
|
|
||||||
Timeout: timeout,
|
|
||||||
Transport: tr,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
81
pkg/plugins/repo/errors.go
Normal file
81
pkg/plugins/repo/errors.go
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
package repo
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
type ErrResponse4xx struct {
|
||||||
|
message string
|
||||||
|
statusCode int
|
||||||
|
compatibilityInfo CompatOpts
|
||||||
|
}
|
||||||
|
|
||||||
|
func newErrResponse4xx(statusCode int) ErrResponse4xx {
|
||||||
|
return ErrResponse4xx{
|
||||||
|
statusCode: statusCode,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ErrResponse4xx) Message() string {
|
||||||
|
return e.message
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ErrResponse4xx) StatusCode() int {
|
||||||
|
return e.statusCode
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ErrResponse4xx) withMessage(message string) ErrResponse4xx {
|
||||||
|
e.message = message
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ErrResponse4xx) withCompatibilityInfo(compatibilityInfo CompatOpts) ErrResponse4xx {
|
||||||
|
e.compatibilityInfo = compatibilityInfo
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ErrResponse4xx) Error() string {
|
||||||
|
if len(e.message) > 0 {
|
||||||
|
compatInfo := e.compatibilityInfo.String()
|
||||||
|
if len(compatInfo) > 0 {
|
||||||
|
return fmt.Sprintf("%d: %s (%s)", e.statusCode, e.message, compatInfo)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%d: %s", e.statusCode, e.message)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%d", e.statusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ErrVersionUnsupported struct {
|
||||||
|
pluginID string
|
||||||
|
requestedVersion string
|
||||||
|
systemInfo string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ErrVersionUnsupported) Error() string {
|
||||||
|
return fmt.Sprintf("%s v%s is not supported on your system (%s)", e.pluginID, e.requestedVersion, e.systemInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ErrVersionNotFound struct {
|
||||||
|
pluginID string
|
||||||
|
requestedVersion string
|
||||||
|
systemInfo string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ErrVersionNotFound) Error() string {
|
||||||
|
return fmt.Sprintf("%s v%s either does not exist or is not supported on your system (%s)", e.pluginID, e.requestedVersion, e.systemInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ErrArcNotFound struct {
|
||||||
|
pluginID string
|
||||||
|
systemInfo string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ErrArcNotFound) Error() string {
|
||||||
|
return fmt.Sprintf("%s is not compatible with your system architecture: %s", e.pluginID, e.systemInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ErrChecksumMismatch struct {
|
||||||
|
archiveURL string
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
26
pkg/plugins/repo/errors_test.go
Normal file
26
pkg/plugins/repo/errors_test.go
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
package repo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestErrResponse4xx(t *testing.T) {
|
||||||
|
t.Run("newErrResponse4xx returns expected error string", func(t *testing.T) {
|
||||||
|
err := newErrResponse4xx(http.StatusBadRequest)
|
||||||
|
require.Equal(t, "400", err.Error())
|
||||||
|
require.Equal(t, http.StatusBadRequest, err.StatusCode())
|
||||||
|
|
||||||
|
msg := "This is terrible news"
|
||||||
|
err = err.withMessage(msg)
|
||||||
|
require.Equal(t, "400: This is terrible news", err.Error())
|
||||||
|
require.Equal(t, msg, err.Message())
|
||||||
|
|
||||||
|
compatInfo := NewCompatOpts("10.0.0", "darwin", "amd64")
|
||||||
|
err = err.withCompatibilityInfo(compatInfo)
|
||||||
|
require.Equal(t, "400: This is terrible news (Grafana v10.0.0 darwin-amd64)", err.Error())
|
||||||
|
require.Equal(t, compatInfo, err.compatibilityInfo)
|
||||||
|
})
|
||||||
|
}
|
@ -6,34 +6,90 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Service is responsible for retrieving plugin information from a repository.
|
// Service is responsible for retrieving plugin archive information from a repository.
|
||||||
type Service interface {
|
type Service interface {
|
||||||
// GetPluginArchive fetches the requested plugin archive.
|
// GetPluginArchive fetches the requested plugin archive.
|
||||||
GetPluginArchive(ctx context.Context, pluginID, version string, opts CompatOpts) (*PluginArchive, error)
|
GetPluginArchive(ctx context.Context, pluginID, version string, opts CompatOpts) (*PluginArchive, error)
|
||||||
// GetPluginArchiveByURL fetches the requested plugin from the specified URL.
|
// GetPluginArchiveByURL fetches the requested plugin from the specified URL.
|
||||||
GetPluginArchiveByURL(ctx context.Context, archiveURL string, opts CompatOpts) (*PluginArchive, error)
|
GetPluginArchiveByURL(ctx context.Context, archiveURL string, opts CompatOpts) (*PluginArchive, error)
|
||||||
// GetPluginDownloadOptions fetches information for downloading the requested plugin.
|
// GetPluginArchiveInfo fetches information needed for downloading the requested plugin.
|
||||||
GetPluginDownloadOptions(ctx context.Context, pluginID, version string, opts CompatOpts) (*PluginDownloadOptions, error)
|
GetPluginArchiveInfo(ctx context.Context, pluginID, version string, opts CompatOpts) (*PluginArchiveInfo, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type CompatOpts struct {
|
type CompatOpts struct {
|
||||||
GrafanaVersion string
|
grafanaVersion string
|
||||||
OS string
|
system SystemCompatOpts
|
||||||
Arch string
|
}
|
||||||
|
|
||||||
|
type SystemCompatOpts struct {
|
||||||
|
os string
|
||||||
|
arch string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (co CompatOpts) GrafanaVersion() (string, bool) {
|
||||||
|
if len(co.grafanaVersion) > 0 {
|
||||||
|
return co.grafanaVersion, true
|
||||||
|
}
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (co CompatOpts) System() (SystemCompatOpts, bool) {
|
||||||
|
os, osSet := co.system.OS()
|
||||||
|
arch, archSet := co.system.Arch()
|
||||||
|
if !osSet || !archSet {
|
||||||
|
return SystemCompatOpts{}, false
|
||||||
|
}
|
||||||
|
return SystemCompatOpts{os: os, arch: arch}, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (co SystemCompatOpts) OS() (string, bool) {
|
||||||
|
if len(co.os) > 0 {
|
||||||
|
return co.os, true
|
||||||
|
}
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (co SystemCompatOpts) Arch() (string, bool) {
|
||||||
|
if len(co.arch) > 0 {
|
||||||
|
return co.arch, true
|
||||||
|
}
|
||||||
|
return "", false
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCompatOpts(grafanaVersion, os, arch string) CompatOpts {
|
func NewCompatOpts(grafanaVersion, os, arch string) CompatOpts {
|
||||||
return CompatOpts{
|
return CompatOpts{
|
||||||
GrafanaVersion: grafanaVersion,
|
grafanaVersion: grafanaVersion,
|
||||||
OS: os,
|
system: SystemCompatOpts{
|
||||||
Arch: arch,
|
os: os,
|
||||||
|
arch: arch,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (co CompatOpts) OSAndArch() string {
|
func NewSystemCompatOpts(os, arch string) CompatOpts {
|
||||||
return fmt.Sprintf("%s-%s", strings.ToLower(co.OS), co.Arch)
|
return CompatOpts{
|
||||||
|
system: SystemCompatOpts{
|
||||||
|
os: os,
|
||||||
|
arch: arch,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (co SystemCompatOpts) OSAndArch() string {
|
||||||
|
if os, exists := co.OS(); !exists {
|
||||||
|
return ""
|
||||||
|
} else if arch, exists := co.Arch(); !exists {
|
||||||
|
return ""
|
||||||
|
} else {
|
||||||
|
return fmt.Sprintf("%s-%s", strings.ToLower(os), arch)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (co CompatOpts) String() string {
|
func (co CompatOpts) String() string {
|
||||||
return fmt.Sprintf("Grafana v%s %s", co.GrafanaVersion, co.OSAndArch())
|
grafanaVersion, exists := co.GrafanaVersion()
|
||||||
|
if !exists {
|
||||||
|
return co.system.OSAndArch()
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("Grafana v%s %s", grafanaVersion, co.system.OSAndArch())
|
||||||
}
|
}
|
||||||
|
@ -1,29 +1,23 @@
|
|||||||
package repo
|
package repo
|
||||||
|
|
||||||
import (
|
import "archive/zip"
|
||||||
"archive/zip"
|
|
||||||
"fmt"
|
|
||||||
)
|
|
||||||
|
|
||||||
type PluginArchive struct {
|
type PluginArchive struct {
|
||||||
File *zip.ReadCloser
|
File *zip.ReadCloser
|
||||||
}
|
}
|
||||||
|
|
||||||
type PluginDownloadOptions struct {
|
type PluginArchiveInfo struct {
|
||||||
PluginZipURL string
|
URL string
|
||||||
Version string
|
Version string
|
||||||
Checksum string
|
Checksum string
|
||||||
}
|
}
|
||||||
|
|
||||||
type Plugin struct {
|
// PluginRepo is (a subset of) the JSON response from /api/plugins/repo/$pluginID
|
||||||
ID string `json:"id"`
|
type PluginRepo struct {
|
||||||
Category string `json:"category"`
|
|
||||||
Versions []Version `json:"versions"`
|
Versions []Version `json:"versions"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Version struct {
|
type Version struct {
|
||||||
Commit string `json:"commit"`
|
|
||||||
URL string `json:"repoURL"`
|
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
Arch map[string]ArchMeta `json:"arch"`
|
Arch map[string]ArchMeta `json:"arch"`
|
||||||
}
|
}
|
||||||
@ -31,53 +25,3 @@ type Version struct {
|
|||||||
type ArchMeta struct {
|
type ArchMeta struct {
|
||||||
SHA256 string `json:"sha256"`
|
SHA256 string `json:"sha256"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PluginRepo struct {
|
|
||||||
Plugins []Plugin `json:"plugins"`
|
|
||||||
Version string `json:"version"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Response4xxError struct {
|
|
||||||
Message string
|
|
||||||
StatusCode int
|
|
||||||
SystemInfo string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e Response4xxError) Error() string {
|
|
||||||
if len(e.Message) > 0 {
|
|
||||||
if len(e.SystemInfo) > 0 {
|
|
||||||
return fmt.Sprintf("%s (%s)", e.Message, e.SystemInfo)
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("%d: %s", e.StatusCode, e.Message)
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("%d", e.StatusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
type ErrArcNotFound struct {
|
|
||||||
PluginID string
|
|
||||||
SystemInfo string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e ErrArcNotFound) Error() string {
|
|
||||||
return fmt.Sprintf("%s is not compatible with your system architecture: %s", e.PluginID, e.SystemInfo)
|
|
||||||
}
|
|
||||||
|
|
||||||
type ErrVersionUnsupported struct {
|
|
||||||
PluginID string
|
|
||||||
RequestedVersion string
|
|
||||||
SystemInfo string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e ErrVersionUnsupported) Error() string {
|
|
||||||
return fmt.Sprintf("%s v%s is not supported on your system (%s)", e.PluginID, e.RequestedVersion, e.SystemInfo)
|
|
||||||
}
|
|
||||||
|
|
||||||
type ErrVersionNotFound struct {
|
|
||||||
PluginID string
|
|
||||||
RequestedVersion string
|
|
||||||
SystemInfo string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e ErrVersionNotFound) Error() string {
|
|
||||||
return fmt.Sprintf("%s v%s either does not exist or is not supported on your system (%s)", e.PluginID, e.RequestedVersion, e.SystemInfo)
|
|
||||||
}
|
|
||||||
|
@ -3,10 +3,10 @@ package repo
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
"path"
|
"path"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/plugins/config"
|
"github.com/grafana/grafana/pkg/plugins/config"
|
||||||
"github.com/grafana/grafana/pkg/plugins/log"
|
"github.com/grafana/grafana/pkg/plugins/log"
|
||||||
@ -20,167 +20,101 @@ type Manager struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ProvideService(cfg *config.Cfg) (*Manager, error) {
|
func ProvideService(cfg *config.Cfg) (*Manager, error) {
|
||||||
defaultBaseURL, err := url.JoinPath(cfg.GrafanaComURL, "/api/plugins")
|
baseURL, err := url.JoinPath(cfg.GrafanaComURL, "/api/plugins")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return New(false, defaultBaseURL, log.NewPrettyLogger("plugin.repository")), nil
|
|
||||||
|
return NewManager(ManagerCfg{
|
||||||
|
SkipTLSVerify: false,
|
||||||
|
BaseURL: baseURL,
|
||||||
|
Logger: log.NewPrettyLogger("plugin.repository"),
|
||||||
|
}), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(skipTLSVerify bool, baseURL string, logger log.PrettyLogger) *Manager {
|
type ManagerCfg struct {
|
||||||
|
SkipTLSVerify bool
|
||||||
|
BaseURL string
|
||||||
|
Logger log.PrettyLogger
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewManager(cfg ManagerCfg) *Manager {
|
||||||
return &Manager{
|
return &Manager{
|
||||||
client: newClient(skipTLSVerify, logger),
|
baseURL: cfg.BaseURL,
|
||||||
baseURL: baseURL,
|
client: NewClient(cfg.SkipTLSVerify, cfg.Logger),
|
||||||
log: logger,
|
log: cfg.Logger,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetPluginArchive fetches the requested plugin archive
|
// GetPluginArchive fetches the requested plugin archive
|
||||||
func (m *Manager) GetPluginArchive(ctx context.Context, pluginID, version string, compatOpts CompatOpts) (*PluginArchive, error) {
|
func (m *Manager) GetPluginArchive(ctx context.Context, pluginID, version string, compatOpts CompatOpts) (*PluginArchive, error) {
|
||||||
dlOpts, err := m.GetPluginDownloadOptions(ctx, pluginID, version, compatOpts)
|
dlOpts, err := m.GetPluginArchiveInfo(ctx, pluginID, version, compatOpts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return m.client.download(ctx, dlOpts.PluginZipURL, dlOpts.Checksum, compatOpts)
|
return m.client.Download(ctx, dlOpts.URL, dlOpts.Checksum, compatOpts)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetPluginArchiveByURL fetches the requested plugin archive from the provided `pluginZipURL`
|
// GetPluginArchiveByURL fetches the requested plugin archive from the provided `pluginZipURL`
|
||||||
func (m *Manager) GetPluginArchiveByURL(ctx context.Context, pluginZipURL string, compatOpts CompatOpts) (*PluginArchive, error) {
|
func (m *Manager) GetPluginArchiveByURL(ctx context.Context, pluginZipURL string, compatOpts CompatOpts) (*PluginArchive, error) {
|
||||||
return m.client.download(ctx, pluginZipURL, "", compatOpts)
|
return m.client.Download(ctx, pluginZipURL, "", compatOpts)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetPluginDownloadOptions returns the options for downloading the requested plugin (with optional `version`)
|
// GetPluginArchiveInfo returns the options for downloading the requested plugin (with optional `version`)
|
||||||
func (m *Manager) GetPluginDownloadOptions(_ context.Context, pluginID, version string, compatOpts CompatOpts) (*PluginDownloadOptions, error) {
|
func (m *Manager) GetPluginArchiveInfo(_ context.Context, pluginID, version string, compatOpts CompatOpts) (*PluginArchiveInfo, error) {
|
||||||
plugin, err := m.pluginMetadata(pluginID, compatOpts)
|
v, err := m.pluginVersion(pluginID, version, compatOpts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
v, err := m.selectVersion(&plugin, version, compatOpts)
|
return &PluginArchiveInfo{
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Plugins which are downloaded just as sourcecode zipball from GitHub do not have checksum
|
|
||||||
var checksum string
|
|
||||||
if v.Arch != nil {
|
|
||||||
archMeta, exists := v.Arch[compatOpts.OSAndArch()]
|
|
||||||
if !exists {
|
|
||||||
archMeta = v.Arch["any"]
|
|
||||||
}
|
|
||||||
checksum = archMeta.SHA256
|
|
||||||
}
|
|
||||||
|
|
||||||
return &PluginDownloadOptions{
|
|
||||||
Version: v.Version,
|
Version: v.Version,
|
||||||
Checksum: checksum,
|
Checksum: v.Checksum,
|
||||||
PluginZipURL: fmt.Sprintf("%s/%s/versions/%s/download", m.baseURL, pluginID, v.Version),
|
URL: m.downloadURL(pluginID, v.Version),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) pluginMetadata(pluginID string, compatOpts CompatOpts) (Plugin, error) {
|
// pluginVersion will return plugin version based on the requested information
|
||||||
m.log.Debugf("Fetching metadata for plugin \"%s\" from repo %s", pluginID, m.baseURL)
|
func (m *Manager) pluginVersion(pluginID, version string, compatOpts CompatOpts) (VersionData, error) {
|
||||||
|
versions, err := m.grafanaCompatiblePluginVersions(pluginID, compatOpts)
|
||||||
|
if err != nil {
|
||||||
|
return VersionData{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
sysCompatOpts, exists := compatOpts.System()
|
||||||
|
if !exists {
|
||||||
|
return VersionData{}, errors.New("no system compatibility requirements set")
|
||||||
|
}
|
||||||
|
|
||||||
|
return SelectSystemCompatibleVersion(m.log, versions, pluginID, version, sysCompatOpts)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) downloadURL(pluginID, version string) string {
|
||||||
|
return fmt.Sprintf("%s/%s/versions/%s/download", m.baseURL, pluginID, version)
|
||||||
|
}
|
||||||
|
|
||||||
|
// grafanaCompatiblePluginVersions will get version info from /api/plugins/repo/$pluginID based on
|
||||||
|
// the provided compatibility information (sent via HTTP headers)
|
||||||
|
func (m *Manager) grafanaCompatiblePluginVersions(pluginID string, compatOpts CompatOpts) ([]Version, error) {
|
||||||
u, err := url.Parse(m.baseURL)
|
u, err := url.Parse(m.baseURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Plugin{}, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
u.Path = path.Join(u.Path, "repo", pluginID)
|
u.Path = path.Join(u.Path, "repo", pluginID)
|
||||||
|
|
||||||
body, err := m.client.sendReq(u, compatOpts)
|
body, err := m.client.SendReq(u, compatOpts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Plugin{}, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var data Plugin
|
var v PluginRepo
|
||||||
err = json.Unmarshal(body, &data)
|
err = json.Unmarshal(body, &v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
m.log.Error("Failed to unmarshal plugin repo response error", err)
|
m.log.Error("Failed to unmarshal plugin repo response", err)
|
||||||
return Plugin{}, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return data, nil
|
return v.Versions, nil
|
||||||
}
|
|
||||||
|
|
||||||
// selectVersion selects the most appropriate plugin version
|
|
||||||
// returns the specified version if supported.
|
|
||||||
// returns the latest version if no specific version is specified.
|
|
||||||
// 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 (m *Manager) selectVersion(plugin *Plugin, version string, compatOpts CompatOpts) (*Version, error) {
|
|
||||||
version = normalizeVersion(version)
|
|
||||||
|
|
||||||
var ver Version
|
|
||||||
latestForArch := latestSupportedVersion(plugin, compatOpts)
|
|
||||||
if latestForArch == nil {
|
|
||||||
return nil, ErrArcNotFound{
|
|
||||||
PluginID: plugin.ID,
|
|
||||||
SystemInfo: compatOpts.OSAndArch(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if version == "" {
|
|
||||||
return latestForArch, nil
|
|
||||||
}
|
|
||||||
for _, v := range plugin.Versions {
|
|
||||||
if v.Version == version {
|
|
||||||
ver = v
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(ver.Version) == 0 {
|
|
||||||
m.log.Debugf("Requested plugin version %s v%s not found but potential fallback version '%s' was found",
|
|
||||||
plugin.ID, version, latestForArch.Version)
|
|
||||||
return nil, ErrVersionNotFound{
|
|
||||||
PluginID: plugin.ID,
|
|
||||||
RequestedVersion: version,
|
|
||||||
SystemInfo: compatOpts.String(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !supportsCurrentArch(&ver, compatOpts) {
|
|
||||||
m.log.Debugf("Requested plugin version %s v%s is not supported on your system but potential fallback version '%s' was found",
|
|
||||||
plugin.ID, version, latestForArch.Version)
|
|
||||||
return nil, ErrVersionUnsupported{
|
|
||||||
PluginID: plugin.ID,
|
|
||||||
RequestedVersion: version,
|
|
||||||
SystemInfo: compatOpts.String(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &ver, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func supportsCurrentArch(version *Version, compatOpts CompatOpts) bool {
|
|
||||||
if version.Arch == nil {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
for arch := range version.Arch {
|
|
||||||
if arch == compatOpts.OSAndArch() || arch == "any" {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func latestSupportedVersion(plugin *Plugin, compatOpts CompatOpts) *Version {
|
|
||||||
for _, v := range plugin.Versions {
|
|
||||||
ver := v
|
|
||||||
if supportsCurrentArch(&ver, compatOpts) {
|
|
||||||
return &ver
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func normalizeVersion(version string) string {
|
|
||||||
normalized := strings.ReplaceAll(version, " ", "")
|
|
||||||
if strings.HasPrefix(normalized, "^") || strings.HasPrefix(normalized, "v") {
|
|
||||||
return normalized[1:]
|
|
||||||
}
|
|
||||||
|
|
||||||
return normalized
|
|
||||||
}
|
}
|
||||||
|
@ -1,53 +1,169 @@
|
|||||||
package repo
|
package repo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"archive/zip"
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/plugins/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestSelectVersion(t *testing.T) {
|
const (
|
||||||
i := &Manager{log: &fakeLogger{}}
|
dummyPluginJSON = `{ "id": "grafana-test-datasource" }`
|
||||||
|
)
|
||||||
|
|
||||||
t.Run("Should return error when requested version does not exist", func(t *testing.T) {
|
func TestGetPluginArchive(t *testing.T) {
|
||||||
_, err := i.selectVersion(createPlugin(versionArg{version: "version"}), "1.1.1", CompatOpts{})
|
tcs := []struct {
|
||||||
require.Error(t, err)
|
name string
|
||||||
})
|
sha string
|
||||||
|
err error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Happy path",
|
||||||
|
sha: "69f698961b6ea651211a187874434821c4727cc22de022e3a7059116d21c75b1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Incorrect SHA returns error",
|
||||||
|
sha: "1a2b3c",
|
||||||
|
err: &ErrChecksumMismatch{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
t.Run("Should return error when no version supports current arch", func(t *testing.T) {
|
pluginZip := createPluginArchive(t)
|
||||||
_, err := i.selectVersion(createPlugin(versionArg{version: "version", arch: []string{"non-existent"}}), "", CompatOpts{})
|
d, err := os.ReadFile(pluginZip.Name())
|
||||||
require.Error(t, err)
|
require.NoError(t, err)
|
||||||
})
|
|
||||||
|
t.Cleanup(func() {
|
||||||
t.Run("Should return error when requested version does not support current arch", func(t *testing.T) {
|
err = pluginZip.Close()
|
||||||
_, err := i.selectVersion(createPlugin(
|
require.NoError(t, err)
|
||||||
versionArg{version: "2.0.0"},
|
err = os.RemoveAll(pluginZip.Name())
|
||||||
versionArg{version: "1.1.1", arch: []string{"non-existent"}},
|
|
||||||
), "1.1.1", CompatOpts{})
|
|
||||||
require.Error(t, err)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("Should return latest available for arch when no version specified", func(t *testing.T) {
|
|
||||||
ver, err := i.selectVersion(createPlugin(
|
|
||||||
versionArg{version: "2.0.0", arch: []string{"non-existent"}},
|
|
||||||
versionArg{version: "1.0.0"},
|
|
||||||
), "", CompatOpts{})
|
|
||||||
require.NoError(t, err)
|
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) {
|
for _, tc := range tcs {
|
||||||
ver, err := i.selectVersion(createPlugin(versionArg{version: "2.0.0"}, versionArg{version: "1.0.0"}), "", CompatOpts{})
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
const (
|
||||||
|
pluginID = "grafana-test-datasource"
|
||||||
|
version = "1.0.2"
|
||||||
|
opSys = "darwin"
|
||||||
|
arch = "amd64"
|
||||||
|
grafanaVersion = "10.0.0"
|
||||||
|
)
|
||||||
|
|
||||||
|
srv := mockPluginRepoAPI(t,
|
||||||
|
srvData{
|
||||||
|
pluginID: pluginID,
|
||||||
|
version: version,
|
||||||
|
opSys: opSys,
|
||||||
|
arch: arch,
|
||||||
|
grafanaVersion: grafanaVersion,
|
||||||
|
sha: tc.sha,
|
||||||
|
archive: d,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
t.Cleanup(srv.Close)
|
||||||
|
|
||||||
|
m := NewManager(ManagerCfg{
|
||||||
|
SkipTLSVerify: false,
|
||||||
|
BaseURL: srv.URL,
|
||||||
|
Logger: log.NewTestPrettyLogger(),
|
||||||
|
})
|
||||||
|
co := NewCompatOpts(grafanaVersion, opSys, arch)
|
||||||
|
archive, err := m.GetPluginArchive(context.Background(), pluginID, version, co)
|
||||||
|
if tc.err != nil {
|
||||||
|
require.ErrorAs(t, err, tc.err)
|
||||||
|
return
|
||||||
|
}
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, "2.0.0", ver.Version)
|
verifyArchive(t, archive)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func verifyArchive(t *testing.T, archive *PluginArchive) {
|
||||||
|
t.Helper()
|
||||||
|
require.NotNil(t, archive)
|
||||||
|
|
||||||
|
pJSON, err := archive.File.Open("plugin.json")
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer func() { require.NoError(t, pJSON.Close()) }()
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
_, err = buf.ReadFrom(pJSON)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, dummyPluginJSON, buf.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func createPluginArchive(t *testing.T) *os.File {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
pluginZip, err := os.CreateTemp(".", "test-plugin.zip")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
zipWriter := zip.NewWriter(pluginZip)
|
||||||
|
pJSON, err := zipWriter.Create("plugin.json")
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, err = pJSON.Write([]byte(dummyPluginJSON))
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = zipWriter.Close()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
return pluginZip
|
||||||
|
}
|
||||||
|
|
||||||
|
type srvData struct {
|
||||||
|
pluginID string
|
||||||
|
version string
|
||||||
|
opSys string
|
||||||
|
arch string
|
||||||
|
sha string
|
||||||
|
grafanaVersion string
|
||||||
|
archive []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func mockPluginRepoAPI(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) {
|
||||||
|
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")
|
||||||
|
|
||||||
|
_, _ = w.Write([]byte(fmt.Sprintf(`
|
||||||
|
{
|
||||||
|
"versions": [{
|
||||||
|
"version": "%s",
|
||||||
|
"arch": {
|
||||||
|
"%s-%s": {
|
||||||
|
"sha256": "%s"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
`, data.version, data.opSys, data.arch, data.sha),
|
||||||
|
))
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Should return requested version", func(t *testing.T) {
|
// mock plugin archive
|
||||||
ver, err := i.selectVersion(createPlugin(versionArg{version: "2.0.0"}, versionArg{version: "1.0.0"}), "1.0.0", CompatOpts{})
|
mux.HandleFunc(fmt.Sprintf("/%s/versions/%s/download", data.pluginID, data.version), func(w http.ResponseWriter, r *http.Request) {
|
||||||
require.NoError(t, err)
|
w.WriteHeader(http.StatusOK)
|
||||||
require.Equal(t, "1.0.0", ver.Version)
|
w.Header().Set("Content-Type", "application/zip")
|
||||||
|
_, _ = w.Write(data.archive)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
return httptest.NewServer(mux)
|
||||||
}
|
}
|
||||||
|
|
||||||
type versionArg struct {
|
type versionArg struct {
|
||||||
@ -55,16 +171,12 @@ type versionArg struct {
|
|||||||
arch []string
|
arch []string
|
||||||
}
|
}
|
||||||
|
|
||||||
func createPlugin(versions ...versionArg) *Plugin {
|
func createPluginVersions(versions ...versionArg) []Version {
|
||||||
p := &Plugin{
|
var vs []Version
|
||||||
Versions: []Version{},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, version := range versions {
|
for _, version := range versions {
|
||||||
ver := Version{
|
ver := Version{
|
||||||
Version: version.version,
|
Version: version.version,
|
||||||
Commit: fmt.Sprintf("commit_%s", version.version),
|
|
||||||
URL: fmt.Sprintf("url_%s", version.version),
|
|
||||||
}
|
}
|
||||||
if version.arch != nil {
|
if version.arch != nil {
|
||||||
ver.Arch = map[string]ArchMeta{}
|
ver.Arch = map[string]ArchMeta{}
|
||||||
@ -74,21 +186,8 @@ func createPlugin(versions ...versionArg) *Plugin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
p.Versions = append(p.Versions, ver)
|
vs = append(vs, ver)
|
||||||
}
|
}
|
||||||
|
|
||||||
return p
|
return vs
|
||||||
}
|
}
|
||||||
|
|
||||||
type fakeLogger struct{}
|
|
||||||
|
|
||||||
func (f *fakeLogger) Successf(_ string, _ ...interface{}) {}
|
|
||||||
func (f *fakeLogger) Failuref(_ string, _ ...interface{}) {}
|
|
||||||
func (f *fakeLogger) Info(_ ...interface{}) {}
|
|
||||||
func (f *fakeLogger) Infof(_ string, _ ...interface{}) {}
|
|
||||||
func (f *fakeLogger) Debug(_ ...interface{}) {}
|
|
||||||
func (f *fakeLogger) Debugf(_ string, _ ...interface{}) {}
|
|
||||||
func (f *fakeLogger) Warn(_ ...interface{}) {}
|
|
||||||
func (f *fakeLogger) Warnf(_ string, _ ...interface{}) {}
|
|
||||||
func (f *fakeLogger) Error(_ ...interface{}) {}
|
|
||||||
func (f *fakeLogger) Errorf(_ string, _ ...interface{}) {}
|
|
||||||
|
110
pkg/plugins/repo/version.go
Normal file
110
pkg/plugins/repo/version.go
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
package repo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/plugins/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
type VersionData struct {
|
||||||
|
Version string
|
||||||
|
Checksum string
|
||||||
|
}
|
||||||
|
|
||||||
|
// SelectSystemCompatibleVersion selects the most appropriate plugin version based on os + architecture
|
||||||
|
// returns the specified version if supported.
|
||||||
|
// returns the latest version if no specific version is specified.
|
||||||
|
// 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) {
|
||||||
|
version = normalizeVersion(version)
|
||||||
|
|
||||||
|
var ver Version
|
||||||
|
latestForArch, exists := latestSupportedVersion(versions, compatOpts)
|
||||||
|
if !exists {
|
||||||
|
return VersionData{}, ErrArcNotFound{
|
||||||
|
pluginID: pluginID,
|
||||||
|
systemInfo: compatOpts.OSAndArch(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if version == "" {
|
||||||
|
return VersionData{
|
||||||
|
Version: latestForArch.Version,
|
||||||
|
Checksum: checksum(latestForArch, compatOpts),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
for _, v := range versions {
|
||||||
|
if v.Version == version {
|
||||||
|
ver = v
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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: pluginID,
|
||||||
|
requestedVersion: version,
|
||||||
|
systemInfo: compatOpts.OSAndArch(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !supportsCurrentArch(ver, compatOpts) {
|
||||||
|
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: pluginID,
|
||||||
|
requestedVersion: version,
|
||||||
|
systemInfo: compatOpts.OSAndArch(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return VersionData{
|
||||||
|
Version: ver.Version,
|
||||||
|
Checksum: checksum(ver, compatOpts),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func checksum(v Version, compatOpts SystemCompatOpts) string {
|
||||||
|
if v.Arch != nil {
|
||||||
|
archMeta, exists := v.Arch[compatOpts.OSAndArch()]
|
||||||
|
if !exists {
|
||||||
|
archMeta = v.Arch["any"]
|
||||||
|
}
|
||||||
|
return archMeta.SHA256
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func supportsCurrentArch(version Version, compatOpts SystemCompatOpts) bool {
|
||||||
|
if version.Arch == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
for arch := range version.Arch {
|
||||||
|
if arch == compatOpts.OSAndArch() || arch == "any" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func latestSupportedVersion(versions []Version, compatOpts SystemCompatOpts) (Version, bool) {
|
||||||
|
for _, v := range versions {
|
||||||
|
if supportsCurrentArch(v, compatOpts) {
|
||||||
|
return v, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Version{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeVersion(version string) string {
|
||||||
|
normalized := strings.ReplaceAll(version, " ", "")
|
||||||
|
if strings.HasPrefix(normalized, "^") || strings.HasPrefix(normalized, "v") {
|
||||||
|
return normalized[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalized
|
||||||
|
}
|
51
pkg/plugins/repo/version_test.go
Normal file
51
pkg/plugins/repo/version_test.go
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
package repo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/plugins/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
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{})
|
||||||
|
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{})
|
||||||
|
require.Error(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Should return error when requested version does not support current arch", func(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{})
|
||||||
|
require.Error(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Should return latest available for arch when no version specified", func(t *testing.T) {
|
||||||
|
ver, err := SelectSystemCompatibleVersion(logger, createPluginVersions(
|
||||||
|
versionArg{version: "2.0.0", arch: []string{"non-existent"}},
|
||||||
|
versionArg{version: "1.0.0"},
|
||||||
|
), "test", "", SystemCompatOpts{})
|
||||||
|
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{})
|
||||||
|
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{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, "1.0.0", ver.Version)
|
||||||
|
})
|
||||||
|
}
|
@ -10,6 +10,8 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/plugins/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAdd(t *testing.T) {
|
func TestAdd(t *testing.T) {
|
||||||
@ -24,7 +26,7 @@ func TestAdd(t *testing.T) {
|
|||||||
|
|
||||||
pluginID := "test-app"
|
pluginID := "test-app"
|
||||||
|
|
||||||
fs := FileSystem(&fakeLogger{}, testDir)
|
fs := FileSystem(log.NewTestPrettyLogger(), testDir)
|
||||||
archive, err := fs.Extract(context.Background(), pluginID, zipFile(t, "./testdata/plugin-with-symlinks.zip"))
|
archive, err := fs.Extract(context.Background(), pluginID, zipFile(t, "./testdata/plugin-with-symlinks.zip"))
|
||||||
require.NotNil(t, archive)
|
require.NotNil(t, archive)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@ -51,7 +53,7 @@ func TestAdd(t *testing.T) {
|
|||||||
func TestExtractFiles(t *testing.T) {
|
func TestExtractFiles(t *testing.T) {
|
||||||
pluginsDir := setupFakePluginsDir(t)
|
pluginsDir := setupFakePluginsDir(t)
|
||||||
|
|
||||||
i := &FS{log: &fakeLogger{}, pluginsDir: pluginsDir}
|
i := &FS{log: log.NewTestPrettyLogger(), pluginsDir: pluginsDir}
|
||||||
|
|
||||||
t.Run("Should preserve file permissions for plugin backend binaries for linux and darwin", func(t *testing.T) {
|
t.Run("Should preserve file permissions for plugin backend binaries for linux and darwin", func(t *testing.T) {
|
||||||
skipWindows(t)
|
skipWindows(t)
|
||||||
@ -282,16 +284,3 @@ func skipWindows(t *testing.T) {
|
|||||||
t.Skip("Skipping test on Windows")
|
t.Skip("Skipping test on Windows")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type fakeLogger struct{}
|
|
||||||
|
|
||||||
func (f *fakeLogger) Successf(_ string, _ ...interface{}) {}
|
|
||||||
func (f *fakeLogger) Failuref(_ string, _ ...interface{}) {}
|
|
||||||
func (f *fakeLogger) Info(_ ...interface{}) {}
|
|
||||||
func (f *fakeLogger) Infof(_ string, _ ...interface{}) {}
|
|
||||||
func (f *fakeLogger) Debug(_ ...interface{}) {}
|
|
||||||
func (f *fakeLogger) Debugf(_ string, _ ...interface{}) {}
|
|
||||||
func (f *fakeLogger) Warn(_ ...interface{}) {}
|
|
||||||
func (f *fakeLogger) Warnf(_ string, _ ...interface{}) {}
|
|
||||||
func (f *fakeLogger) Error(_ ...interface{}) {}
|
|
||||||
func (f *fakeLogger) Errorf(_ string, _ ...interface{}) {}
|
|
||||||
|
@ -41,6 +41,12 @@ func TestIntegrationPlugins(t *testing.T) {
|
|||||||
PluginAdminEnabled: true,
|
PluginAdminEnabled: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
origBuildVersion := setting.BuildVersion
|
||||||
|
setting.BuildVersion = "0.0.0-test"
|
||||||
|
t.Cleanup(func() {
|
||||||
|
setting.BuildVersion = origBuildVersion
|
||||||
|
})
|
||||||
|
|
||||||
grafanaListedAddr, store := testinfra.StartGrafana(t, dir, cfgPath)
|
grafanaListedAddr, store := testinfra.StartGrafana(t, dir, cfgPath)
|
||||||
|
|
||||||
type testCase struct {
|
type testCase struct {
|
||||||
|
Loading…
Reference in New Issue
Block a user