mirror of https://github.com/grafana/grafana.git
				
				
				
			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"] | ||||
| 
 | ||||
| 	err := hs.pluginInstaller.Add(c.Req.Context(), pluginID, dto.Version, plugins.CompatOpts{ | ||||
| 		GrafanaVersion: hs.Cfg.BuildVersion, | ||||
| 		OS:             runtime.GOOS, | ||||
| 		Arch:           runtime.GOARCH, | ||||
| 	}) | ||||
| 	compatOpts := plugins.NewCompatOpts(hs.Cfg.BuildVersion, runtime.GOOS, runtime.GOARCH) | ||||
| 	err := hs.pluginInstaller.Add(c.Req.Context(), pluginID, dto.Version, compatOpts) | ||||
| 	if err != nil { | ||||
| 		var dupeErr plugins.DuplicateError | ||||
| 		if errors.As(err, &dupeErr) { | ||||
|  | @ -462,9 +459,9 @@ func (hs *HTTPServer) InstallPlugin(c *contextmodel.ReqContext) response.Respons | |||
| 		if errors.As(err, &versionNotFoundErr) { | ||||
| 			return response.Error(http.StatusNotFound, "Plugin version not found", err) | ||||
| 		} | ||||
| 		var clientError repo.Response4xxError | ||||
| 		var clientError repo.ErrResponse4xx | ||||
| 		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) { | ||||
| 			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
 | ||||
| // and then extracts the zip into the plugin's directory.
 | ||||
| func installPlugin(ctx context.Context, pluginID, version string, c utils.CommandLine) error { | ||||
| 	skipTLSVerify := c.Bool("insecure") | ||||
| 	repository := repo.New(skipTLSVerify, c.PluginRepoURL(), services.Logger) | ||||
| 	repository := repo.NewManager(repo.ManagerCfg{ | ||||
| 		SkipTLSVerify: c.Bool("insecure"), | ||||
| 		BaseURL:       c.PluginRepoURL(), | ||||
| 		Logger:        services.Logger, | ||||
| 	}) | ||||
| 
 | ||||
| 	compatOpts := repo.NewCompatOpts(services.GrafanaVersion, runtime.GOOS, runtime.GOARCH) | ||||
| 
 | ||||
|  |  | |||
|  | @ -42,9 +42,30 @@ type File struct { | |||
| } | ||||
| 
 | ||||
| type CompatOpts struct { | ||||
| 	GrafanaVersion string | ||||
| 	OS             string | ||||
| 	Arch           string | ||||
| 	grafanaVersion 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 { | ||||
|  |  | |||
|  | @ -46,3 +46,22 @@ type Logs struct { | |||
| 	Message string | ||||
| 	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 { | ||||
| 	GetPluginArchiveFunc      func(_ context.Context, pluginID, version 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.
 | ||||
|  | @ -223,12 +223,12 @@ func (r *FakePluginRepo) GetPluginArchiveByURL(ctx context.Context, archiveURL s | |||
| 	return &repo.PluginArchive{}, nil | ||||
| } | ||||
| 
 | ||||
| // GetPluginDownloadOptions fetches information for downloading the requested plugin.
 | ||||
| func (r *FakePluginRepo) GetPluginDownloadOptions(ctx context.Context, pluginID, version string, opts repo.CompatOpts) (*repo.PluginDownloadOptions, error) { | ||||
| 	if r.GetPluginDownloadOptionsFunc != nil { | ||||
| 		return r.GetPluginDownloadOptionsFunc(ctx, pluginID, version, opts) | ||||
| // GetPluginArchiveInfo fetches information for downloading the requested plugin.
 | ||||
| func (r *FakePluginRepo) GetPluginArchiveInfo(ctx context.Context, pluginID, version string, opts repo.CompatOpts) (*repo.PluginArchiveInfo, error) { | ||||
| 	if r.GetPluginArchiveInfoFunc != nil { | ||||
| 		return r.GetPluginArchiveInfoFunc(ctx, pluginID, version, opts) | ||||
| 	} | ||||
| 	return &repo.PluginDownloadOptions{}, nil | ||||
| 	return &repo.PluginArchiveInfo{}, nil | ||||
| } | ||||
| 
 | ||||
| type FakePluginStorage struct { | ||||
|  |  | |||
|  | @ -2,6 +2,7 @@ package manager | |||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 
 | ||||
| 	"github.com/grafana/grafana/pkg/plugins" | ||||
|  | @ -26,7 +27,8 @@ type PluginInstaller struct { | |||
| 
 | ||||
| func ProvideInstaller(cfg *config.Cfg, pluginRegistry registry.Service, pluginLoader loader.Service, | ||||
| 	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, | ||||
|  | @ -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 { | ||||
| 	compatOpts := repo.NewCompatOpts(opts.GrafanaVersion, opts.OS, opts.Arch) | ||||
| 	compatOpts, err := repoCompatOpts(opts) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	var pluginArchive *repo.PluginArchive | ||||
| 	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
 | ||||
| 		dlOpts, err := m.pluginRepo.GetPluginDownloadOptions(ctx, pluginID, version, compatOpts) | ||||
| 		pluginArchiveInfo, err := m.pluginRepo.GetPluginArchiveInfo(ctx, pluginID, version, compatOpts) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 
 | ||||
| 		// 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{ | ||||
| 				PluginID: plugin.ID, | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		if dlOpts.PluginZipURL == "" && dlOpts.Version == "" { | ||||
| 		if pluginArchiveInfo.URL == "" && pluginArchiveInfo.Version == "" { | ||||
| 			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 | ||||
| 		} | ||||
| 
 | ||||
| 		if dlOpts.PluginZipURL != "" { | ||||
| 			pluginArchive, err = m.pluginRepo.GetPluginArchiveByURL(ctx, dlOpts.PluginZipURL, compatOpts) | ||||
| 		if pluginArchiveInfo.URL != "" { | ||||
| 			pluginArchive, err = m.pluginRepo.GetPluginArchiveByURL(ctx, pluginArchiveInfo.URL, compatOpts) | ||||
| 			if err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 		} else { | ||||
| 			pluginArchive, err = m.pluginRepo.GetPluginArchive(ctx, pluginID, dlOpts.Version, compatOpts) | ||||
| 			pluginArchive, err = m.pluginRepo.GetPluginArchive(ctx, pluginID, pluginArchiveInfo.Version, compatOpts) | ||||
| 			if err != nil { | ||||
| 				return err | ||||
| 			} | ||||
|  | @ -153,3 +158,18 @@ func (m *PluginInstaller) plugin(ctx context.Context, pluginID string) (*plugins | |||
| 
 | ||||
| 	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" | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 	"runtime" | ||||
| 	"testing" | ||||
| 
 | ||||
| 	"github.com/stretchr/testify/require" | ||||
|  | @ -62,7 +63,7 @@ func TestPluginManager_Add_Remove(t *testing.T) { | |||
| 		} | ||||
| 
 | ||||
| 		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) | ||||
| 
 | ||||
| 		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{ | ||||
| 				PluginID: pluginV1.ID, | ||||
| 			}, err) | ||||
|  | @ -96,9 +97,9 @@ func TestPluginManager_Add_Remove(t *testing.T) { | |||
| 				require.Equal(t, []string{zipNameV2}, src.PluginURIs(ctx)) | ||||
| 				return []*plugins.Plugin{pluginV2}, nil | ||||
| 			} | ||||
| 			pluginRepo.GetPluginDownloadOptionsFunc = func(_ context.Context, pluginID, version string, _ repo.CompatOpts) (*repo.PluginDownloadOptions, error) { | ||||
| 				return &repo.PluginDownloadOptions{ | ||||
| 					PluginZipURL: "https://grafanaplugins.com", | ||||
| 			pluginRepo.GetPluginArchiveInfoFunc = func(_ context.Context, _, _ string, _ repo.CompatOpts) (*repo.PluginArchiveInfo, error) { | ||||
| 				return &repo.PluginArchiveInfo{ | ||||
| 					URL: "https://grafanaplugins.com", | ||||
| 				}, nil | ||||
| 			} | ||||
| 			pluginRepo.GetPluginArchiveByURLFunc = func(_ context.Context, pluginZipURL string, _ repo.CompatOpts) (*repo.PluginArchive, error) { | ||||
|  | @ -115,7 +116,7 @@ func TestPluginManager_Add_Remove(t *testing.T) { | |||
| 				}, nil | ||||
| 			} | ||||
| 
 | ||||
| 			err = inst.Add(context.Background(), pluginID, v2, plugins.CompatOpts{}) | ||||
| 			err = inst.Add(context.Background(), pluginID, v2, testCompatOpts()) | ||||
| 			require.NoError(t, err) | ||||
| 		}) | ||||
| 
 | ||||
|  | @ -168,10 +169,10 @@ func TestPluginManager_Add_Remove(t *testing.T) { | |||
| 			} | ||||
| 
 | ||||
| 			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) | ||||
| 
 | ||||
| 			err = pm.Add(context.Background(), testPluginID, "", plugins.CompatOpts{}) | ||||
| 			err = pm.Add(context.Background(), testPluginID, "", testCompatOpts()) | ||||
| 			require.Equal(t, plugins.ErrInstallCorePlugin, err) | ||||
| 
 | ||||
| 			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 | ||||
| } | ||||
| 
 | ||||
| func testCompatOpts() plugins.CompatOpts { | ||||
| 	return plugins.NewCompatOpts("10.0.0", runtime.GOOS, runtime.GOARCH) | ||||
| } | ||||
|  |  | |||
|  | @ -26,7 +26,7 @@ type Client struct { | |||
| 	log log.PrettyLogger | ||||
| } | ||||
| 
 | ||||
| func newClient(skipTLSVerify bool, logger log.PrettyLogger) *Client { | ||||
| func NewClient(skipTLSVerify bool, logger log.PrettyLogger) *Client { | ||||
| 	return &Client{ | ||||
| 		httpClient:          makeHttpClient(skipTLSVerify, 10*time.Second), | ||||
| 		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
 | ||||
| 	tmpFile, err := os.CreateTemp("", "*.zip") | ||||
| 	if err != nil { | ||||
|  | @ -53,7 +53,7 @@ func (c *Client) download(_ context.Context, pluginZipURL, checksum string, comp | |||
| 		if err := tmpFile.Close(); err != nil { | ||||
| 			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()) | ||||
|  | @ -61,9 +61,29 @@ func (c *Client) download(_ context.Context, pluginZipURL, checksum string, comp | |||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	return &PluginArchive{ | ||||
| 		File: rc, | ||||
| 	}, nil | ||||
| 	return &PluginArchive{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) { | ||||
|  | @ -122,8 +142,8 @@ func (c *Client) downloadFile(tmpFile *os.File, pluginURL, checksum string, comp | |||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	// Using no timeout here as some plugins can be bigger and smaller timeout would prevent to download a plugin on
 | ||||
| 	// slow network. As this is CLI operation hanging is not a big of an issue as user can just abort.
 | ||||
| 	// Using no timeout as some plugin archives make take longer to fetch due to size, network performance, etc.
 | ||||
| 	// Note: This is also used as part of the grafana plugin install CLI operation
 | ||||
| 	bodyReader, err := c.sendReqNoTimeout(u, compatOpts) | ||||
| 	if err != nil { | ||||
| 		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 { | ||||
| 		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) | ||||
| 	} | ||||
| 	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 | ||||
| } | ||||
| 
 | ||||
| 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) { | ||||
| 	req, err := c.createReq(url, compatOpts) | ||||
| 	if err != nil { | ||||
|  | @ -189,10 +187,18 @@ func (c *Client) createReq(url *url.URL, compatOpts CompatOpts) (*http.Request, | |||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	req.Header.Set("grafana-version", compatOpts.GrafanaVersion) | ||||
| 	req.Header.Set("grafana-os", compatOpts.OS) | ||||
| 	req.Header.Set("grafana-arch", compatOpts.Arch) | ||||
| 	req.Header.Set("User-Agent", "grafana "+compatOpts.GrafanaVersion) | ||||
| 	if gVer, exists := compatOpts.GrafanaVersion(); exists { | ||||
| 		req.Header.Set("grafana-version", gVer) | ||||
| 		req.Header.Set("User-Agent", "grafana "+gVer) | ||||
| 	} | ||||
| 
 | ||||
| 	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 | ||||
| } | ||||
|  | @ -206,7 +212,7 @@ func (c *Client) handleResp(res *http.Response, compatOpts CompatOpts) (io.ReadC | |||
| 			} | ||||
| 		}() | ||||
| 		if err != nil || len(body) == 0 { | ||||
| 			return nil, Response4xxError{StatusCode: res.StatusCode} | ||||
| 			return nil, newErrResponse4xx(res.StatusCode) | ||||
| 		} | ||||
| 		var message string | ||||
| 		var jsonBody map[string]string | ||||
|  | @ -216,7 +222,8 @@ func (c *Client) handleResp(res *http.Response, compatOpts CompatOpts) (io.ReadC | |||
| 		} else { | ||||
| 			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 { | ||||
|  | @ -227,7 +234,9 @@ func (c *Client) handleResp(res *http.Response, compatOpts CompatOpts) (io.ReadC | |||
| } | ||||
| 
 | ||||
| func makeHttpClient(skipTLSVerify bool, timeout time.Duration) http.Client { | ||||
| 	tr := &http.Transport{ | ||||
| 	return http.Client{ | ||||
| 		Timeout: timeout, | ||||
| 		Transport: &http.Transport{ | ||||
| 			Proxy: http.ProxyFromEnvironment, | ||||
| 			DialContext: (&net.Dialer{ | ||||
| 				Timeout:   30 * time.Second, | ||||
|  | @ -240,10 +249,6 @@ func makeHttpClient(skipTLSVerify bool, timeout time.Duration) http.Client { | |||
| 			TLSClientConfig: &tls.Config{ | ||||
| 				InsecureSkipVerify: skipTLSVerify, | ||||
| 			}, | ||||
| 	} | ||||
| 
 | ||||
| 	return http.Client{ | ||||
| 		Timeout:   timeout, | ||||
| 		Transport: tr, | ||||
| 		}, | ||||
| 	} | ||||
| } | ||||
|  |  | |||
|  | @ -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) | ||||
| } | ||||
|  | @ -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" | ||||
| ) | ||||
| 
 | ||||
| // Service is responsible for retrieving plugin information from a repository.
 | ||||
| // Service is responsible for retrieving plugin archive information from a repository.
 | ||||
| type Service interface { | ||||
| 	// GetPluginArchive fetches the requested plugin archive.
 | ||||
| 	GetPluginArchive(ctx context.Context, pluginID, version string, opts CompatOpts) (*PluginArchive, error) | ||||
| 	// GetPluginArchiveByURL fetches the requested plugin from the specified URL.
 | ||||
| 	GetPluginArchiveByURL(ctx context.Context, archiveURL string, opts CompatOpts) (*PluginArchive, error) | ||||
| 	// GetPluginDownloadOptions fetches information for downloading the requested plugin.
 | ||||
| 	GetPluginDownloadOptions(ctx context.Context, pluginID, version string, opts CompatOpts) (*PluginDownloadOptions, error) | ||||
| 	// GetPluginArchiveInfo fetches information needed for downloading the requested plugin.
 | ||||
| 	GetPluginArchiveInfo(ctx context.Context, pluginID, version string, opts CompatOpts) (*PluginArchiveInfo, error) | ||||
| } | ||||
| 
 | ||||
| type CompatOpts struct { | ||||
| 	GrafanaVersion string | ||||
| 	OS             string | ||||
| 	Arch           string | ||||
| 	grafanaVersion string | ||||
| 	system         SystemCompatOpts | ||||
| } | ||||
| 
 | ||||
| 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 { | ||||
| 	return CompatOpts{ | ||||
| 		GrafanaVersion: grafanaVersion, | ||||
| 		OS:             os, | ||||
| 		Arch:           arch, | ||||
| 		grafanaVersion: grafanaVersion, | ||||
| 		system: SystemCompatOpts{ | ||||
| 			os:   os, | ||||
| 			arch: arch, | ||||
| 		}, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (co CompatOpts) OSAndArch() string { | ||||
| 	return fmt.Sprintf("%s-%s", strings.ToLower(co.OS), co.Arch) | ||||
| func NewSystemCompatOpts(os, arch string) CompatOpts { | ||||
| 	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 { | ||||
| 	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 | ||||
| 
 | ||||
| import ( | ||||
| 	"archive/zip" | ||||
| 	"fmt" | ||||
| ) | ||||
| import "archive/zip" | ||||
| 
 | ||||
| type PluginArchive struct { | ||||
| 	File *zip.ReadCloser | ||||
| } | ||||
| 
 | ||||
| type PluginDownloadOptions struct { | ||||
| 	PluginZipURL string | ||||
| type PluginArchiveInfo struct { | ||||
| 	URL      string | ||||
| 	Version  string | ||||
| 	Checksum string | ||||
| } | ||||
| 
 | ||||
| type Plugin struct { | ||||
| 	ID       string    `json:"id"` | ||||
| 	Category string    `json:"category"` | ||||
| // PluginRepo is (a subset of) the JSON response from /api/plugins/repo/$pluginID
 | ||||
| type PluginRepo struct { | ||||
| 	Versions []Version `json:"versions"` | ||||
| } | ||||
| 
 | ||||
| type Version struct { | ||||
| 	Commit  string              `json:"commit"` | ||||
| 	URL     string              `json:"repoURL"` | ||||
| 	Version string              `json:"version"` | ||||
| 	Arch    map[string]ArchMeta `json:"arch"` | ||||
| } | ||||
|  | @ -31,53 +25,3 @@ type Version struct { | |||
| type ArchMeta struct { | ||||
| 	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 ( | ||||
| 	"context" | ||||
| 	"encoding/json" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"net/url" | ||||
| 	"path" | ||||
| 	"strings" | ||||
| 
 | ||||
| 	"github.com/grafana/grafana/pkg/plugins/config" | ||||
| 	"github.com/grafana/grafana/pkg/plugins/log" | ||||
|  | @ -20,167 +20,101 @@ type Manager struct { | |||
| } | ||||
| 
 | ||||
| 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 { | ||||
| 		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{ | ||||
| 		client:  newClient(skipTLSVerify, logger), | ||||
| 		baseURL: baseURL, | ||||
| 		log:     logger, | ||||
| 		baseURL: cfg.BaseURL, | ||||
| 		client:  NewClient(cfg.SkipTLSVerify, cfg.Logger), | ||||
| 		log:     cfg.Logger, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // GetPluginArchive fetches the requested plugin archive
 | ||||
| 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 { | ||||
| 		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`
 | ||||
| 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`)
 | ||||
| func (m *Manager) GetPluginDownloadOptions(_ context.Context, pluginID, version string, compatOpts CompatOpts) (*PluginDownloadOptions, error) { | ||||
| 	plugin, err := m.pluginMetadata(pluginID, compatOpts) | ||||
| // GetPluginArchiveInfo returns the options for downloading the requested plugin (with optional `version`)
 | ||||
| func (m *Manager) GetPluginArchiveInfo(_ context.Context, pluginID, version string, compatOpts CompatOpts) (*PluginArchiveInfo, error) { | ||||
| 	v, err := m.pluginVersion(pluginID, version, compatOpts) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	v, err := m.selectVersion(&plugin, version, compatOpts) | ||||
| 	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{ | ||||
| 	return &PluginArchiveInfo{ | ||||
| 		Version:  v.Version, | ||||
| 		Checksum:     checksum, | ||||
| 		PluginZipURL: fmt.Sprintf("%s/%s/versions/%s/download", m.baseURL, pluginID, v.Version), | ||||
| 		Checksum: v.Checksum, | ||||
| 		URL:      m.downloadURL(pluginID, v.Version), | ||||
| 	}, nil | ||||
| } | ||||
| 
 | ||||
| func (m *Manager) pluginMetadata(pluginID string, compatOpts CompatOpts) (Plugin, error) { | ||||
| 	m.log.Debugf("Fetching metadata for plugin \"%s\" from repo %s", pluginID, m.baseURL) | ||||
| // pluginVersion will return plugin version based on the requested information
 | ||||
| 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) | ||||
| 	if err != nil { | ||||
| 		return Plugin{}, err | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	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 { | ||||
| 		return Plugin{}, err | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	var data Plugin | ||||
| 	err = json.Unmarshal(body, &data) | ||||
| 	var v PluginRepo | ||||
| 	err = json.Unmarshal(body, &v) | ||||
| 	if err != nil { | ||||
| 		m.log.Error("Failed to unmarshal plugin repo response error", err) | ||||
| 		return Plugin{}, err | ||||
| 		m.log.Error("Failed to unmarshal plugin repo response", err) | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	return data, 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 | ||||
| 	return v.Versions, nil | ||||
| } | ||||
|  |  | |||
|  | @ -1,53 +1,169 @@ | |||
| package repo | ||||
| 
 | ||||
| import ( | ||||
| 	"archive/zip" | ||||
| 	"bytes" | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 	"net/http" | ||||
| 	"net/http/httptest" | ||||
| 	"os" | ||||
| 	"testing" | ||||
| 
 | ||||
| 	"github.com/stretchr/testify/require" | ||||
| 
 | ||||
| 	"github.com/grafana/grafana/pkg/plugins/log" | ||||
| ) | ||||
| 
 | ||||
| func TestSelectVersion(t *testing.T) { | ||||
| 	i := &Manager{log: &fakeLogger{}} | ||||
| const ( | ||||
| 	dummyPluginJSON = `{ "id": "grafana-test-datasource" }` | ||||
| ) | ||||
| 
 | ||||
| 	t.Run("Should return error when requested version does not exist", func(t *testing.T) { | ||||
| 		_, err := i.selectVersion(createPlugin(versionArg{version: "version"}), "1.1.1", CompatOpts{}) | ||||
| 		require.Error(t, err) | ||||
| 	}) | ||||
| func TestGetPluginArchive(t *testing.T) { | ||||
| 	tcs := []struct { | ||||
| 		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) { | ||||
| 		_, err := i.selectVersion(createPlugin(versionArg{version: "version", arch: []string{"non-existent"}}), "", CompatOpts{}) | ||||
| 		require.Error(t, err) | ||||
| 	}) | ||||
| 
 | ||||
| 	t.Run("Should return error when requested version does not support current arch", func(t *testing.T) { | ||||
| 		_, err := i.selectVersion(createPlugin( | ||||
| 			versionArg{version: "2.0.0"}, | ||||
| 			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{}) | ||||
| 	pluginZip := createPluginArchive(t) | ||||
| 	d, err := os.ReadFile(pluginZip.Name()) | ||||
| 	require.NoError(t, err) | ||||
| 
 | ||||
| 	t.Cleanup(func() { | ||||
| 		err = pluginZip.Close() | ||||
| 		require.NoError(t, err) | ||||
| 		err = os.RemoveAll(pluginZip.Name()) | ||||
| 		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 := i.selectVersion(createPlugin(versionArg{version: "2.0.0"}, versionArg{version: "1.0.0"}), "", CompatOpts{}) | ||||
| 	for _, tc := range tcs { | ||||
| 		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.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) { | ||||
| 		ver, err := i.selectVersion(createPlugin(versionArg{version: "2.0.0"}, versionArg{version: "1.0.0"}), "1.0.0", CompatOpts{}) | ||||
| 		require.NoError(t, err) | ||||
| 		require.Equal(t, "1.0.0", ver.Version) | ||||
| 	// mock plugin archive
 | ||||
| 	mux.HandleFunc(fmt.Sprintf("/%s/versions/%s/download", data.pluginID, data.version), func(w http.ResponseWriter, r *http.Request) { | ||||
| 		w.WriteHeader(http.StatusOK) | ||||
| 		w.Header().Set("Content-Type", "application/zip") | ||||
| 		_, _ = w.Write(data.archive) | ||||
| 	}) | ||||
| 
 | ||||
| 	return httptest.NewServer(mux) | ||||
| } | ||||
| 
 | ||||
| type versionArg struct { | ||||
|  | @ -55,16 +171,12 @@ type versionArg struct { | |||
| 	arch    []string | ||||
| } | ||||
| 
 | ||||
| func createPlugin(versions ...versionArg) *Plugin { | ||||
| 	p := &Plugin{ | ||||
| 		Versions: []Version{}, | ||||
| 	} | ||||
| func createPluginVersions(versions ...versionArg) []Version { | ||||
| 	var vs []Version | ||||
| 
 | ||||
| 	for _, version := range versions { | ||||
| 		ver := Version{ | ||||
| 			Version: version.version, | ||||
| 			Commit:  fmt.Sprintf("commit_%s", version.version), | ||||
| 			URL:     fmt.Sprintf("url_%s", version.version), | ||||
| 		} | ||||
| 		if version.arch != nil { | ||||
| 			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{})   {} | ||||
|  |  | |||
|  | @ -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 | ||||
| } | ||||
|  | @ -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" | ||||
| 
 | ||||
| 	"github.com/stretchr/testify/require" | ||||
| 
 | ||||
| 	"github.com/grafana/grafana/pkg/plugins/log" | ||||
| ) | ||||
| 
 | ||||
| func TestAdd(t *testing.T) { | ||||
|  | @ -24,7 +26,7 @@ func TestAdd(t *testing.T) { | |||
| 
 | ||||
| 	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")) | ||||
| 	require.NotNil(t, archive) | ||||
| 	require.NoError(t, err) | ||||
|  | @ -51,7 +53,7 @@ func TestAdd(t *testing.T) { | |||
| func TestExtractFiles(t *testing.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) { | ||||
| 		skipWindows(t) | ||||
|  | @ -282,16 +284,3 @@ func skipWindows(t *testing.T) { | |||
| 		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, | ||||
| 	}) | ||||
| 
 | ||||
| 	origBuildVersion := setting.BuildVersion | ||||
| 	setting.BuildVersion = "0.0.0-test" | ||||
| 	t.Cleanup(func() { | ||||
| 		setting.BuildVersion = origBuildVersion | ||||
| 	}) | ||||
| 
 | ||||
| 	grafanaListedAddr, store := testinfra.StartGrafana(t, dir, cfgPath) | ||||
| 
 | ||||
| 	type testCase struct { | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue