[v8.5.x] CLI: Allow relative symlinks in zip archives when installing plugins (#52049)
* resolve conflicts * fix build issue Co-authored-by: Marcus Efraimsson <marcus.efraimsson@gmail.com>
This commit is contained in:
co-authored by
Marcus Efraimsson
parent
e62f2a7b74
commit
022d7c14d9
@@ -1,27 +1,17 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/grafana/grafana/pkg/cmd/grafana-cli/models"
|
||||
"github.com/grafana/grafana/pkg/cmd/grafana-cli/services"
|
||||
"github.com/grafana/grafana/pkg/cmd/grafana-cli/utils"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/installer"
|
||||
"github.com/grafana/grafana/pkg/util/errutil"
|
||||
|
||||
"github.com/grafana/grafana/pkg/cmd/grafana-cli/logger"
|
||||
)
|
||||
|
||||
func validateInput(c utils.CommandLine, pluginFolder string) error {
|
||||
@@ -58,102 +48,16 @@ func (cmd Command) installCommand(c utils.CommandLine) error {
|
||||
|
||||
pluginID := c.Args().First()
|
||||
version := c.Args().Get(1)
|
||||
skipTLSVerify := c.Bool("insecure")
|
||||
|
||||
i := installer.New(skipTLSVerify, services.GrafanaVersion, services.Logger)
|
||||
return i.Install(context.Background(), pluginID, version, c.PluginDirectory(), c.PluginURL(), c.PluginRepoURL())
|
||||
return InstallPlugin(pluginID, version, c)
|
||||
}
|
||||
|
||||
// InstallPlugin downloads the plugin code as a zip file from the Grafana.com API
|
||||
// and then extracts the zip into the plugins directory.
|
||||
func InstallPlugin(pluginName, version string, c utils.CommandLine, client utils.ApiClient) error {
|
||||
pluginFolder := c.PluginDirectory()
|
||||
downloadURL := c.PluginURL()
|
||||
isInternal := false
|
||||
func InstallPlugin(pluginID, version string, c utils.CommandLine) error {
|
||||
skipTLSVerify := c.Bool("insecure")
|
||||
|
||||
var checksum string
|
||||
if downloadURL == "" {
|
||||
if strings.HasPrefix(pluginName, "grafana-") {
|
||||
// At this point the plugin download is going through grafana.com API and thus the name is validated.
|
||||
// Checking for grafana prefix is how it is done there so no 3rd party plugin should have that prefix.
|
||||
// You can supply custom plugin name and then set custom download url to 3rd party plugin but then that
|
||||
// is up to the user to know what she is doing.
|
||||
isInternal = true
|
||||
}
|
||||
plugin, err := client.GetPlugin(pluginName, c.PluginRepoURL())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
v, err := SelectVersion(&plugin, version)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if version == "" {
|
||||
version = v.Version
|
||||
}
|
||||
downloadURL = fmt.Sprintf("%s/%s/versions/%s/download",
|
||||
c.String("repo"),
|
||||
pluginName,
|
||||
version,
|
||||
)
|
||||
|
||||
// Plugins which are downloaded just as sourcecode zipball from github do not have checksum
|
||||
if v.Arch != nil {
|
||||
archMeta, exists := v.Arch[osAndArchString()]
|
||||
if !exists {
|
||||
archMeta = v.Arch["any"]
|
||||
}
|
||||
checksum = archMeta.SHA256
|
||||
}
|
||||
}
|
||||
|
||||
logger.Infof("installing %v @ %v\n", pluginName, version)
|
||||
logger.Infof("from: %v\n", downloadURL)
|
||||
logger.Infof("into: %v\n", pluginFolder)
|
||||
logger.Info("\n")
|
||||
|
||||
// Create temp file for downloading zip file
|
||||
tmpFile, err := ioutil.TempFile("", "*.zip")
|
||||
if err != nil {
|
||||
return errutil.Wrap("failed to create temporary file", err)
|
||||
}
|
||||
defer func() {
|
||||
if err := os.Remove(tmpFile.Name()); err != nil {
|
||||
logger.Warn("Failed to remove temporary file", "file", tmpFile.Name(), "err", err)
|
||||
}
|
||||
}()
|
||||
|
||||
err = client.DownloadFile(pluginName, tmpFile, downloadURL, checksum)
|
||||
if err != nil {
|
||||
if err := tmpFile.Close(); err != nil {
|
||||
logger.Warn("Failed to close file", "err", err)
|
||||
}
|
||||
return errutil.Wrap("failed to download plugin archive", err)
|
||||
}
|
||||
err = tmpFile.Close()
|
||||
if err != nil {
|
||||
return errutil.Wrap("failed to close tmp file", err)
|
||||
}
|
||||
|
||||
err = extractFiles(tmpFile.Name(), pluginName, pluginFolder, isInternal)
|
||||
if err != nil {
|
||||
return errutil.Wrap("failed to extract plugin archive", err)
|
||||
}
|
||||
|
||||
logger.Infof("%s Installed %s successfully \n", color.GreenString("✔"), pluginName)
|
||||
|
||||
res, _ := services.ReadPlugin(pluginFolder, pluginName)
|
||||
for _, v := range res.Dependencies.Plugins {
|
||||
if err := InstallPlugin(v.ID, "", c, client); err != nil {
|
||||
return errutil.Wrapf(err, "failed to install plugin '%s'", v.ID)
|
||||
}
|
||||
|
||||
logger.Infof("Installed dependency: %v ✔\n", v.ID)
|
||||
}
|
||||
|
||||
return err
|
||||
i := installer.New(skipTLSVerify, services.GrafanaVersion, services.Logger)
|
||||
return i.Install(context.Background(), pluginID, version, c.PluginDirectory(), c.PluginURL(), c.PluginRepoURL())
|
||||
}
|
||||
|
||||
func osAndArchString() string {
|
||||
@@ -183,177 +87,3 @@ func latestSupportedVersion(plugin *models.Plugin) *models.Version {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SelectVersion returns latest version if none is specified or the specified version. If the version string is not
|
||||
// matched to existing version it errors out. It also errors out if version that is matched is not available for current
|
||||
// os and platform. It expects plugin.Versions to be sorted so the newest version is first.
|
||||
func SelectVersion(plugin *models.Plugin, version string) (*models.Version, error) {
|
||||
var ver models.Version
|
||||
|
||||
latestForArch := latestSupportedVersion(plugin)
|
||||
if latestForArch == nil {
|
||||
return nil, fmt.Errorf("plugin is not supported on your architecture and OS")
|
||||
}
|
||||
|
||||
if version == "" {
|
||||
return latestForArch, nil
|
||||
}
|
||||
for _, v := range plugin.Versions {
|
||||
if v.Version == version {
|
||||
ver = v
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if len(ver.Version) == 0 {
|
||||
return nil, fmt.Errorf("could not find the version you're looking for")
|
||||
}
|
||||
|
||||
if !supportsCurrentArch(&ver) {
|
||||
return nil, fmt.Errorf(
|
||||
"the version you want is not supported on your architecture and OS, latest suitable version is %s",
|
||||
latestForArch.Version)
|
||||
}
|
||||
|
||||
return &ver, nil
|
||||
}
|
||||
|
||||
var reGitBuild = regexp.MustCompile("^[a-zA-Z0-9_.-]*/")
|
||||
|
||||
func removeGitBuildFromName(pluginName, filename string) string {
|
||||
return reGitBuild.ReplaceAllString(filename, pluginName+"/")
|
||||
}
|
||||
|
||||
const permissionsDeniedMessage = "could not create %q, permission denied, make sure you have write access to plugin dir"
|
||||
|
||||
func extractFiles(archiveFile string, pluginName string, dstDir string, allowSymlinks bool) error {
|
||||
var err error
|
||||
dstDir, err = filepath.Abs(dstDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
logger.Debugf("Extracting archive %q to %q...\n", archiveFile, dstDir)
|
||||
|
||||
existingInstallDir := filepath.Join(dstDir, pluginName)
|
||||
if _, err := os.Stat(existingInstallDir); !os.IsNotExist(err) {
|
||||
err = os.RemoveAll(existingInstallDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logger.Infof("Removed existing installation of %s\n\n", pluginName)
|
||||
}
|
||||
|
||||
r, err := zip.OpenReader(archiveFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, zf := range r.File {
|
||||
if filepath.IsAbs(zf.Name) || strings.HasPrefix(zf.Name, ".."+string(filepath.Separator)) {
|
||||
return fmt.Errorf(
|
||||
"archive member %q tries to write outside of plugin directory: %q, this can be a security risk",
|
||||
zf.Name, dstDir)
|
||||
}
|
||||
|
||||
dstPath := filepath.Clean(filepath.Join(dstDir, removeGitBuildFromName(pluginName, zf.Name)))
|
||||
|
||||
if zf.FileInfo().IsDir() {
|
||||
// We can ignore gosec G304 here since it makes sense to give all users read access
|
||||
// nolint:gosec
|
||||
if err := os.MkdirAll(dstPath, 0755); err != nil {
|
||||
if os.IsPermission(err) {
|
||||
return fmt.Errorf(permissionsDeniedMessage, dstPath)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
// Create needed directories to extract file
|
||||
// We can ignore gosec G304 here since it makes sense to give all users read access
|
||||
// nolint:gosec
|
||||
if err := os.MkdirAll(filepath.Dir(dstPath), 0755); err != nil {
|
||||
return errutil.Wrap("failed to create directory to extract plugin files", err)
|
||||
}
|
||||
|
||||
if isSymlink(zf) {
|
||||
if !allowSymlinks {
|
||||
logger.Warnf("%v: plugin archive contains a symlink, which is not allowed. Skipping \n", zf.Name)
|
||||
continue
|
||||
}
|
||||
if err := extractSymlink(zf, dstPath); err != nil {
|
||||
logger.Errorf("Failed to extract symlink: %v \n", err)
|
||||
continue
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if err := extractFile(zf, dstPath); err != nil {
|
||||
return errutil.Wrap("failed to extract file", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func isSymlink(file *zip.File) bool {
|
||||
return file.Mode()&os.ModeSymlink == os.ModeSymlink
|
||||
}
|
||||
|
||||
func extractSymlink(file *zip.File, filePath string) error {
|
||||
// symlink target is the contents of the file
|
||||
src, err := file.Open()
|
||||
if err != nil {
|
||||
return errutil.Wrap("failed to extract file", err)
|
||||
}
|
||||
buf := new(bytes.Buffer)
|
||||
if _, err := io.Copy(buf, src); err != nil {
|
||||
return errutil.Wrap("failed to copy symlink contents", err)
|
||||
}
|
||||
if err := os.Symlink(strings.TrimSpace(buf.String()), filePath); err != nil {
|
||||
return errutil.Wrapf(err, "failed to make symbolic link for %v", filePath)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func extractFile(file *zip.File, filePath string) (err error) {
|
||||
fileMode := file.Mode()
|
||||
// This is entry point for backend plugins so we want to make them executable
|
||||
if strings.HasSuffix(filePath, "_linux_amd64") || strings.HasSuffix(filePath, "_darwin_amd64") {
|
||||
fileMode = os.FileMode(0755)
|
||||
}
|
||||
|
||||
// We can ignore the gosec G304 warning on this one, since the variable part of the file path stems
|
||||
// from command line flag "pluginsDir", and the only possible damage would be writing to the wrong directory.
|
||||
// If the user shouldn't be writing to this directory, they shouldn't have the permission in the file system.
|
||||
// nolint:gosec
|
||||
dst, err := os.OpenFile(filePath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, fileMode)
|
||||
if err != nil {
|
||||
if os.IsPermission(err) {
|
||||
return fmt.Errorf(permissionsDeniedMessage, filePath)
|
||||
}
|
||||
|
||||
unwrappedError := errors.Unwrap(err)
|
||||
if unwrappedError != nil && strings.EqualFold(unwrappedError.Error(), "text file busy") {
|
||||
return fmt.Errorf("file %q is in use - please stop Grafana, install the plugin and restart Grafana", filePath)
|
||||
}
|
||||
|
||||
return errutil.Wrap("failed to open file", err)
|
||||
}
|
||||
defer func() {
|
||||
err = dst.Close()
|
||||
}()
|
||||
|
||||
src, err := file.Open()
|
||||
if err != nil {
|
||||
return errutil.Wrap("failed to extract file", err)
|
||||
}
|
||||
defer func() {
|
||||
err = src.Close()
|
||||
}()
|
||||
|
||||
_, err = io.Copy(dst, src)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1,268 +0,0 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/cmd/grafana-cli/commands/commandstest"
|
||||
"github.com/grafana/grafana/pkg/cmd/grafana-cli/models"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestRemoveGitBuildFromName(t *testing.T) {
|
||||
pluginName := "datasource-kairosdb"
|
||||
|
||||
// The root directory should get renamed to the plugin name
|
||||
paths := map[string]string{
|
||||
"datasource-plugin-kairosdb-cc4a3965ef5d3eb1ae0ee4f93e9e78ec7db69e64/": "datasource-kairosdb/",
|
||||
"datasource-plugin-kairosdb-cc4a3965ef5d3eb1ae0ee4f93e9e78ec7db69e64/README.md": "datasource-kairosdb/README.md",
|
||||
"datasource-plugin-kairosdb-cc4a3965ef5d3eb1ae0ee4f93e9e78ec7db69e64/partials/": "datasource-kairosdb/partials/",
|
||||
"datasource-plugin-kairosdb-cc4a3965ef5d3eb1ae0ee4f93e9e78ec7db69e64/partials/config.html": "datasource-kairosdb/partials/config.html",
|
||||
}
|
||||
for pth, exp := range paths {
|
||||
name := removeGitBuildFromName(pluginName, pth)
|
||||
assert.Equal(t, exp, name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractFiles(t *testing.T) {
|
||||
t.Run("Should preserve file permissions for plugin backend binaries for linux and darwin", func(t *testing.T) {
|
||||
skipWindows(t)
|
||||
pluginsDir := setupFakePluginsDir(t)
|
||||
|
||||
archive := filepath.Join("testdata", "grafana-simple-json-datasource-ec18fa4da8096a952608a7e4c7782b4260b41bcf.zip")
|
||||
err := extractFiles(archive, "grafana-simple-json-datasource", pluginsDir, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
// File in zip has permissions 755
|
||||
fileInfo, err := os.Stat(filepath.Join(pluginsDir, "grafana-simple-json-datasource",
|
||||
"simple-plugin_darwin_amd64"))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "-rwxr-xr-x", fileInfo.Mode().String())
|
||||
|
||||
// File in zip has permission 755
|
||||
fileInfo, err = os.Stat(pluginsDir + "/grafana-simple-json-datasource/simple-plugin_linux_amd64")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "-rwxr-xr-x", fileInfo.Mode().String())
|
||||
|
||||
// File in zip has permission 644
|
||||
fileInfo, err = os.Stat(pluginsDir + "/grafana-simple-json-datasource/simple-plugin_windows_amd64.exe")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "-rw-r--r--", fileInfo.Mode().String())
|
||||
|
||||
// File in zip has permission 755
|
||||
fileInfo, err = os.Stat(pluginsDir + "/grafana-simple-json-datasource/non-plugin-binary")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "-rwxr-xr-x", fileInfo.Mode().String())
|
||||
})
|
||||
|
||||
t.Run("Should ignore symlinks if not allowed", func(t *testing.T) {
|
||||
pluginsDir := setupFakePluginsDir(t)
|
||||
|
||||
err := extractFiles("testdata/plugin-with-symlink.zip", "plugin-with-symlink", pluginsDir, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = os.Stat(pluginsDir + "/plugin-with-symlink/text.txt")
|
||||
require.NoError(t, err)
|
||||
_, err = os.Stat(pluginsDir + "/plugin-with-symlink/symlink_to_txt")
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("Should extract symlinks if allowed", func(t *testing.T) {
|
||||
skipWindows(t)
|
||||
pluginsDir := setupFakePluginsDir(t)
|
||||
|
||||
err := extractFiles("testdata/plugin-with-symlink.zip", "plugin-with-symlink", pluginsDir, true)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = os.Stat(pluginsDir + "/plugin-with-symlink/symlink_to_txt")
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("Should detect if archive members point outside of the destination directory", func(t *testing.T) {
|
||||
pluginsDir := setupFakePluginsDir(t)
|
||||
|
||||
err := extractFiles("testdata/plugin-with-parent-member.zip", "plugin-with-parent-member",
|
||||
pluginsDir, true)
|
||||
require.EqualError(t, err, fmt.Sprintf(
|
||||
`archive member "../member.txt" tries to write outside of plugin directory: %q, this can be a security risk`,
|
||||
pluginsDir,
|
||||
))
|
||||
})
|
||||
|
||||
t.Run("Should detect if archive members are absolute", func(t *testing.T) {
|
||||
pluginsDir := setupFakePluginsDir(t)
|
||||
|
||||
err := extractFiles("testdata/plugin-with-absolute-member.zip", "plugin-with-absolute-member",
|
||||
pluginsDir, true)
|
||||
require.EqualError(t, err, fmt.Sprintf(
|
||||
`archive member "/member.txt" tries to write outside of plugin directory: %q, this can be a security risk`,
|
||||
pluginsDir,
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
func TestInstallPluginCommand(t *testing.T) {
|
||||
pluginsDir := setupFakePluginsDir(t)
|
||||
c, err := commandstest.NewCliContext(map[string]string{"pluginsDir": pluginsDir})
|
||||
require.NoError(t, err)
|
||||
|
||||
client := &commandstest.FakeGrafanaComClient{
|
||||
GetPluginFunc: func(pluginId, repoUrl string) (models.Plugin, error) {
|
||||
require.Equal(t, "test-plugin-panel", pluginId)
|
||||
plugin := models.Plugin{
|
||||
ID: "test-plugin-panel",
|
||||
Category: "",
|
||||
Versions: []models.Version{
|
||||
{
|
||||
Commit: "commit",
|
||||
URL: "url",
|
||||
Version: "1.0.0",
|
||||
Arch: map[string]models.ArchMeta{
|
||||
fmt.Sprintf("%s-%s", runtime.GOOS, runtime.GOARCH): {
|
||||
SHA256: "test",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
return plugin, nil
|
||||
},
|
||||
DownloadFileFunc: func(pluginName string, tmpFile *os.File, url string, checksum string) (err error) {
|
||||
require.Equal(t, "test-plugin-panel", pluginName)
|
||||
require.Equal(t, "/test-plugin-panel/versions/1.0.0/download", url)
|
||||
require.Equal(t, "test", checksum)
|
||||
f, err := os.Open("testdata/grafana-simple-json-datasource-ec18fa4da8096a952608a7e4c7782b4260b41bcf.zip")
|
||||
require.NoError(t, err)
|
||||
_, err = io.Copy(tmpFile, f)
|
||||
require.NoError(t, err)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
err = InstallPlugin("test-plugin-panel", "", c, client)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestSelectVersion(t *testing.T) {
|
||||
t.Run("Should return error when requested version does not exist", func(t *testing.T) {
|
||||
_, err := SelectVersion(
|
||||
makePluginWithVersions(versionArg{Version: "version"}),
|
||||
"1.1.1",
|
||||
)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("Should return error when no version supports current arch", func(t *testing.T) {
|
||||
_, err := SelectVersion(
|
||||
makePluginWithVersions(versionArg{Version: "version", Arch: []string{"non-existent"}}),
|
||||
"",
|
||||
)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("Should return error when requested version does not support current arch", func(t *testing.T) {
|
||||
_, err := SelectVersion(
|
||||
makePluginWithVersions(
|
||||
versionArg{Version: "2.0.0"},
|
||||
versionArg{Version: "1.1.1", Arch: []string{"non-existent"}},
|
||||
),
|
||||
"1.1.1",
|
||||
)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("Should return latest available for arch when no version specified", func(t *testing.T) {
|
||||
ver, err := SelectVersion(
|
||||
makePluginWithVersions(
|
||||
versionArg{Version: "2.0.0", Arch: []string{"non-existent"}},
|
||||
versionArg{Version: "1.0.0"},
|
||||
),
|
||||
"",
|
||||
)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "1.0.0", ver.Version)
|
||||
})
|
||||
|
||||
t.Run("Should return latest version when no version specified", func(t *testing.T) {
|
||||
ver, err := SelectVersion(
|
||||
makePluginWithVersions(versionArg{Version: "2.0.0"}, versionArg{Version: "1.0.0"}),
|
||||
"",
|
||||
)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "2.0.0", ver.Version)
|
||||
})
|
||||
|
||||
t.Run("Should return requested version", func(t *testing.T) {
|
||||
ver, err := SelectVersion(
|
||||
makePluginWithVersions(
|
||||
versionArg{Version: "2.0.0"},
|
||||
versionArg{Version: "1.0.0"},
|
||||
),
|
||||
"1.0.0",
|
||||
)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "1.0.0", ver.Version)
|
||||
})
|
||||
}
|
||||
|
||||
func setupFakePluginsDir(t *testing.T) string {
|
||||
dirname := "testdata/fake-plugins-dir"
|
||||
err := os.RemoveAll(dirname)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = os.MkdirAll(dirname, 0750)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
err := os.RemoveAll(dirname)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
dirname, err = filepath.Abs(dirname)
|
||||
require.NoError(t, err)
|
||||
|
||||
return dirname
|
||||
}
|
||||
|
||||
func skipWindows(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("Skipping test on Windows")
|
||||
}
|
||||
}
|
||||
|
||||
type versionArg struct {
|
||||
Version string
|
||||
Arch []string
|
||||
}
|
||||
|
||||
func makePluginWithVersions(versions ...versionArg) *models.Plugin {
|
||||
plugin := &models.Plugin{
|
||||
ID: "",
|
||||
Category: "",
|
||||
Versions: []models.Version{},
|
||||
}
|
||||
|
||||
for _, version := range versions {
|
||||
ver := models.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]models.ArchMeta{}
|
||||
for _, arch := range version.Arch {
|
||||
ver.Arch[arch] = models.ArchMeta{
|
||||
SHA256: fmt.Sprintf("sha256_%s", arch),
|
||||
}
|
||||
}
|
||||
}
|
||||
plugin.Versions = append(plugin.Versions, ver)
|
||||
}
|
||||
|
||||
return plugin
|
||||
}
|
||||
@@ -54,7 +54,7 @@ func (cmd Command) upgradeAllCommand(c utils.CommandLine) error {
|
||||
return err
|
||||
}
|
||||
|
||||
err = InstallPlugin(p.ID, "", c, cmd.Client)
|
||||
err = InstallPlugin(p.ID, "", c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ func (cmd Command) upgradeCommand(c utils.CommandLine) error {
|
||||
return errutil.Wrapf(err, "failed to remove plugin '%s'", pluginName)
|
||||
}
|
||||
|
||||
return InstallPlugin(pluginName, "", c, cmd.Client)
|
||||
return InstallPlugin(pluginName, "", c)
|
||||
}
|
||||
|
||||
logger.Infof("%s %s is up to date \n", color.GreenString("✔"), pluginName)
|
||||
|
||||
@@ -24,7 +24,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/util/errutil"
|
||||
)
|
||||
|
||||
type Installer struct {
|
||||
@@ -80,7 +79,7 @@ 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)
|
||||
}
|
||||
|
||||
func New(skipTLSVerify bool, grafanaVersion string, logger Logger) plugins.Installer {
|
||||
func New(skipTLSVerify bool, grafanaVersion string, logger Logger) *Installer {
|
||||
return &Installer{
|
||||
httpClient: makeHttpClient(skipTLSVerify, 10*time.Second),
|
||||
httpClientNoTimeout: makeHttpClient(skipTLSVerify, 0),
|
||||
@@ -92,17 +91,8 @@ func New(skipTLSVerify bool, grafanaVersion string, logger Logger) plugins.Insta
|
||||
// Install downloads the plugin code as a zip file from specified URL
|
||||
// and then extracts the zip into the provided plugins directory.
|
||||
func (i *Installer) Install(ctx context.Context, pluginID, version, pluginsDir, pluginZipURL, pluginRepoURL string) error {
|
||||
isInternal := false
|
||||
|
||||
var checksum string
|
||||
if pluginZipURL == "" {
|
||||
if strings.HasPrefix(pluginID, "grafana-") {
|
||||
// At this point the plugin download is going through grafana.com API and thus the name is validated.
|
||||
// Checking for grafana prefix is how it is done there so no 3rd party plugin should have that prefix.
|
||||
// You can supply custom plugin name and then set custom download url to 3rd party plugin but then that
|
||||
// is up to the user to know what she is doing.
|
||||
isInternal = true
|
||||
}
|
||||
plugin, err := i.getPluginMetadataFromPluginRepo(pluginID, pluginRepoURL)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -137,7 +127,7 @@ func (i *Installer) Install(ctx context.Context, pluginID, version, pluginsDir,
|
||||
// Create temp file for downloading zip file
|
||||
tmpFile, err := ioutil.TempFile("", "*.zip")
|
||||
if err != nil {
|
||||
return errutil.Wrap("failed to create temporary file", err)
|
||||
return fmt.Errorf("%v: %w", "failed to create temporary file", err)
|
||||
}
|
||||
defer func() {
|
||||
if err := os.Remove(tmpFile.Name()); err != nil {
|
||||
@@ -150,16 +140,16 @@ func (i *Installer) Install(ctx context.Context, pluginID, version, pluginsDir,
|
||||
if err := tmpFile.Close(); err != nil {
|
||||
i.log.Warn("Failed to close file", "err", err)
|
||||
}
|
||||
return errutil.Wrap("failed to download plugin archive", err)
|
||||
return fmt.Errorf("%v: %w", "failed to download plugin archive", err)
|
||||
}
|
||||
err = tmpFile.Close()
|
||||
if err != nil {
|
||||
return errutil.Wrap("failed to close tmp file", err)
|
||||
return fmt.Errorf("%v: %w", "failed to close tmp file", err)
|
||||
}
|
||||
|
||||
err = i.extractFiles(tmpFile.Name(), pluginID, pluginsDir, isInternal)
|
||||
err = i.extractFiles(tmpFile.Name(), pluginID, pluginsDir)
|
||||
if err != nil {
|
||||
return errutil.Wrap("failed to extract plugin archive", err)
|
||||
return fmt.Errorf("%v: %w", "failed to extract plugin archive", err)
|
||||
}
|
||||
|
||||
res, _ := toPluginDTO(pluginsDir, pluginID)
|
||||
@@ -170,7 +160,7 @@ func (i *Installer) Install(ctx context.Context, pluginID, version, pluginsDir,
|
||||
for _, dep := range res.Dependencies.Plugins {
|
||||
i.log.Infof("Fetching %s dependencies...", res.ID)
|
||||
if err := i.Install(ctx, dep.ID, normalizeVersion(dep.Version), pluginsDir, "", pluginRepoURL); err != nil {
|
||||
return errutil.Wrapf(err, "failed to install plugin %s", dep.ID)
|
||||
return fmt.Errorf("failed to install plugin %s: %w", dep.ID, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -203,7 +193,7 @@ func (i *Installer) DownloadFile(pluginID string, tmpFile *os.File, url string,
|
||||
// nolint:gosec
|
||||
f, err := os.Open(url)
|
||||
if err != nil {
|
||||
return errutil.Wrap("Failed to read plugin archive", err)
|
||||
return fmt.Errorf("%v: %w", "Failed to read plugin archive", err)
|
||||
}
|
||||
defer func() {
|
||||
if err := f.Close(); err != nil {
|
||||
@@ -212,7 +202,7 @@ func (i *Installer) DownloadFile(pluginID string, tmpFile *os.File, url string,
|
||||
}()
|
||||
_, err = io.Copy(tmpFile, f)
|
||||
if err != nil {
|
||||
return errutil.Wrap("Failed to copy plugin archive", err)
|
||||
return fmt.Errorf("%v: %w", "Failed to copy plugin archive", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -260,7 +250,7 @@ func (i *Installer) DownloadFile(pluginID string, tmpFile *os.File, url string,
|
||||
w := bufio.NewWriter(tmpFile)
|
||||
h := sha256.New()
|
||||
if _, err = io.Copy(w, io.TeeReader(bodyReader, h)); err != nil {
|
||||
return errutil.Wrap("failed to compute SHA256 checksum", err)
|
||||
return fmt.Errorf("%v: %w", "failed to compute SHA256 checksum", err)
|
||||
}
|
||||
if err := w.Flush(); err != nil {
|
||||
return fmt.Errorf("failed to write to %q: %w", tmpFile.Name(), err)
|
||||
@@ -509,7 +499,7 @@ func latestSupportedVersion(plugin *Plugin) *Version {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *Installer) extractFiles(archiveFile string, pluginID string, dest string, allowSymlinks bool) error {
|
||||
func (i *Installer) extractFiles(archiveFile string, pluginID string, dest string) error {
|
||||
var err error
|
||||
dest, err = filepath.Abs(dest)
|
||||
if err != nil {
|
||||
@@ -571,15 +561,11 @@ func (i *Installer) extractFiles(archiveFile string, pluginID string, dest strin
|
||||
// We can ignore gosec G304 here since it makes sense to give all users read access
|
||||
// nolint:gosec
|
||||
if err := os.MkdirAll(filepath.Dir(dstPath), 0755); err != nil {
|
||||
return errutil.Wrap("failed to create directory to extract plugin files", err)
|
||||
return fmt.Errorf("%v: %w", "failed to create directory to extract plugin files", err)
|
||||
}
|
||||
|
||||
if isSymlink(zf) {
|
||||
if !allowSymlinks {
|
||||
i.log.Warnf("%v: plugin archive contains a symlink, which is not allowed. Skipping", zf.Name)
|
||||
continue
|
||||
}
|
||||
if err := extractSymlink(zf, dstPath); err != nil {
|
||||
if err := extractSymlink(existingInstallDir, zf, dstPath); err != nil {
|
||||
i.log.Warn("failed to extract symlink", "err", err)
|
||||
continue
|
||||
}
|
||||
@@ -587,7 +573,7 @@ func (i *Installer) extractFiles(archiveFile string, pluginID string, dest strin
|
||||
}
|
||||
|
||||
if err := extractFile(zf, dstPath); err != nil {
|
||||
return errutil.Wrap("failed to extract file", err)
|
||||
return fmt.Errorf("%v: %w", "failed to extract file", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -598,22 +584,49 @@ func isSymlink(file *zip.File) bool {
|
||||
return file.Mode()&os.ModeSymlink == os.ModeSymlink
|
||||
}
|
||||
|
||||
func extractSymlink(file *zip.File, filePath string) error {
|
||||
func extractSymlink(basePath string, file *zip.File, filePath string) error {
|
||||
// symlink target is the contents of the file
|
||||
src, err := file.Open()
|
||||
if err != nil {
|
||||
return errutil.Wrap("failed to extract file", err)
|
||||
return fmt.Errorf("%v: %w", "failed to extract file", err)
|
||||
}
|
||||
buf := new(bytes.Buffer)
|
||||
if _, err := io.Copy(buf, src); err != nil {
|
||||
return errutil.Wrap("failed to copy symlink contents", err)
|
||||
return fmt.Errorf("%v: %w", "failed to copy symlink contents", err)
|
||||
}
|
||||
if err := os.Symlink(strings.TrimSpace(buf.String()), filePath); err != nil {
|
||||
return errutil.Wrapf(err, "failed to make symbolic link for %v", filePath)
|
||||
|
||||
symlinkPath := strings.TrimSpace(buf.String())
|
||||
if !isSymlinkRelativeTo(basePath, symlinkPath, filePath) {
|
||||
return fmt.Errorf("symlink %q pointing outside plugin directory is not allowed", filePath)
|
||||
}
|
||||
|
||||
if err := os.Symlink(symlinkPath, filePath); err != nil {
|
||||
return fmt.Errorf("failed to make symbolic link for %v: %w", filePath, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// isSymlinkRelativeTo checks whether symlinkDestPath is relative to basePath.
|
||||
// symlinkOrigPath is the path to file holding the symbolic link.
|
||||
func isSymlinkRelativeTo(basePath string, symlinkDestPath string, symlinkOrigPath string) bool {
|
||||
if filepath.IsAbs(symlinkDestPath) {
|
||||
return false
|
||||
} else {
|
||||
fileDir := filepath.Dir(symlinkOrigPath)
|
||||
cleanPath := filepath.Clean(filepath.Join(fileDir, "/", symlinkDestPath))
|
||||
p, err := filepath.Rel(basePath, cleanPath)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if strings.HasPrefix(p, ".."+string(filepath.Separator)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func extractFile(file *zip.File, filePath string) (err error) {
|
||||
fileMode := file.Mode()
|
||||
// This is entry point for backend plugins so we want to make them executable
|
||||
@@ -636,7 +649,7 @@ func extractFile(file *zip.File, filePath string) (err error) {
|
||||
return fmt.Errorf("file %q is in use - please stop Grafana, install the plugin and restart Grafana", filePath)
|
||||
}
|
||||
|
||||
return errutil.Wrap("failed to open file", err)
|
||||
return fmt.Errorf("%v: %w", "failed to open file", err)
|
||||
}
|
||||
defer func() {
|
||||
err = dst.Close()
|
||||
@@ -644,7 +657,7 @@ func extractFile(file *zip.File, filePath string) (err error) {
|
||||
|
||||
src, err := file.Open()
|
||||
if err != nil {
|
||||
return errutil.Wrap("failed to extract file", err)
|
||||
return fmt.Errorf("%v: %w", "failed to extract file", err)
|
||||
}
|
||||
defer func() {
|
||||
err = src.Close()
|
||||
|
||||
@@ -0,0 +1,358 @@
|
||||
package installer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestInstall(t *testing.T) {
|
||||
testDir := "./testdata/tmpInstallPluginDir"
|
||||
err := os.Mkdir(testDir, os.ModePerm)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Cleanup(func() {
|
||||
err = os.RemoveAll(testDir)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
pluginID := "test-app"
|
||||
|
||||
i := &Installer{log: &fakeLogger{}}
|
||||
err = i.Install(context.Background(), pluginID, "", testDir, "./testdata/plugin-with-symlinks.zip", "")
|
||||
require.NoError(t, err)
|
||||
|
||||
// verify extracted contents
|
||||
files, err := ioutil.ReadDir(filepath.Join(testDir, pluginID))
|
||||
require.NoError(t, err)
|
||||
require.Len(t, files, 6)
|
||||
require.Equal(t, files[0].Name(), "MANIFEST.txt")
|
||||
require.Equal(t, files[1].Name(), "dashboards")
|
||||
require.Equal(t, files[2].Name(), "extra")
|
||||
require.Equal(t, os.ModeSymlink, files[2].Mode()&os.ModeSymlink)
|
||||
require.Equal(t, files[3].Name(), "plugin.json")
|
||||
require.Equal(t, files[4].Name(), "symlink_to_txt")
|
||||
require.Equal(t, os.ModeSymlink, files[4].Mode()&os.ModeSymlink)
|
||||
require.Equal(t, files[5].Name(), "text.txt")
|
||||
}
|
||||
|
||||
func TestUninstall(t *testing.T) {
|
||||
i := &Installer{log: &fakeLogger{}}
|
||||
|
||||
pluginDir := t.TempDir()
|
||||
pluginJSON := filepath.Join(pluginDir, "plugin.json")
|
||||
_, err := os.Create(pluginJSON)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = i.Uninstall(context.Background(), pluginDir)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = os.Stat(pluginDir)
|
||||
require.True(t, os.IsNotExist(err))
|
||||
|
||||
t.Run("Uninstall will search in nested dir folder for plugin.json", func(t *testing.T) {
|
||||
pluginDistDir := filepath.Join(t.TempDir(), "dist")
|
||||
err = os.Mkdir(pluginDistDir, os.ModePerm)
|
||||
require.NoError(t, err)
|
||||
pluginJSON = filepath.Join(pluginDistDir, "plugin.json")
|
||||
_, err = os.Create(pluginJSON)
|
||||
require.NoError(t, err)
|
||||
|
||||
pluginDir = filepath.Dir(pluginDistDir)
|
||||
|
||||
err = i.Uninstall(context.Background(), pluginDir)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = os.Stat(pluginDir)
|
||||
require.True(t, os.IsNotExist(err))
|
||||
})
|
||||
|
||||
t.Run("Uninstall will not delete folder if cannot recognize plugin structure", func(t *testing.T) {
|
||||
pluginDir = t.TempDir()
|
||||
err = i.Uninstall(context.Background(), pluginDir)
|
||||
require.EqualError(t, err, fmt.Sprintf("tried to remove %s, but it doesn't seem to be a plugin", pluginDir))
|
||||
|
||||
_, err = os.Stat(pluginDir)
|
||||
require.False(t, os.IsNotExist(err))
|
||||
})
|
||||
}
|
||||
|
||||
func TestExtractFiles(t *testing.T) {
|
||||
i := &Installer{log: &fakeLogger{}}
|
||||
pluginsDir := setupFakePluginsDir(t)
|
||||
|
||||
t.Run("Should preserve file permissions for plugin backend binaries for linux and darwin", func(t *testing.T) {
|
||||
skipWindows(t)
|
||||
|
||||
archive := filepath.Join("testdata", "grafana-simple-json-datasource-ec18fa4da8096a952608a7e4c7782b4260b41bcf.zip")
|
||||
err := i.extractFiles(archive, "grafana-simple-json-datasource", pluginsDir)
|
||||
require.NoError(t, err)
|
||||
|
||||
// File in zip has permissions 755
|
||||
fileInfo, err := os.Stat(filepath.Join(pluginsDir, "grafana-simple-json-datasource", "simple-plugin_darwin_amd64"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "-rwxr-xr-x", fileInfo.Mode().String())
|
||||
|
||||
// File in zip has permission 755
|
||||
fileInfo, err = os.Stat(pluginsDir + "/grafana-simple-json-datasource/simple-plugin_linux_amd64")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "-rwxr-xr-x", fileInfo.Mode().String())
|
||||
|
||||
// File in zip has permission 644
|
||||
fileInfo, err = os.Stat(pluginsDir + "/grafana-simple-json-datasource/simple-plugin_windows_amd64.exe")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "-rw-r--r--", fileInfo.Mode().String())
|
||||
|
||||
// File in zip has permission 755
|
||||
fileInfo, err = os.Stat(pluginsDir + "/grafana-simple-json-datasource/non-plugin-binary")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "-rwxr-xr-x", fileInfo.Mode().String())
|
||||
})
|
||||
|
||||
t.Run("Should extract file with relative symlink", func(t *testing.T) {
|
||||
skipWindows(t)
|
||||
|
||||
err := i.extractFiles("testdata/plugin-with-symlink.zip", "plugin-with-symlink", pluginsDir)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = os.Stat(pluginsDir + "/plugin-with-symlink/symlink_to_txt")
|
||||
require.NoError(t, err)
|
||||
|
||||
target, err := filepath.EvalSymlinks(pluginsDir + "/plugin-with-symlink/symlink_to_txt")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, pluginsDir+"/plugin-with-symlink/text.txt", target)
|
||||
})
|
||||
|
||||
t.Run("Should extract directory with relative symlink", func(t *testing.T) {
|
||||
skipWindows(t)
|
||||
|
||||
err := i.extractFiles("testdata/plugin-with-symlink-dir.zip", "plugin-with-symlink-dir", pluginsDir)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = os.Stat(pluginsDir + "/plugin-with-symlink-dir/symlink_to_dir")
|
||||
require.NoError(t, err)
|
||||
|
||||
target, err := filepath.EvalSymlinks(pluginsDir + "/plugin-with-symlink-dir/symlink_to_dir")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, pluginsDir+"/plugin-with-symlink-dir/dir", target)
|
||||
})
|
||||
|
||||
t.Run("Should not extract file with absolute symlink", func(t *testing.T) {
|
||||
skipWindows(t)
|
||||
|
||||
err := i.extractFiles("testdata/plugin-with-absolute-symlink.zip", "plugin-with-absolute-symlink", pluginsDir)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = os.Stat(pluginsDir + "/plugin-with-absolute-symlink/test.txt")
|
||||
require.True(t, os.IsNotExist(err))
|
||||
})
|
||||
|
||||
t.Run("Should not extract directory with absolute symlink", func(t *testing.T) {
|
||||
skipWindows(t)
|
||||
|
||||
err := i.extractFiles("testdata/plugin-with-absolute-symlink-dir.zip", "plugin-with-absolute-symlink-dir", pluginsDir)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = os.Stat(pluginsDir + "/plugin-with-absolute-symlink-dir/target")
|
||||
require.True(t, os.IsNotExist(err))
|
||||
})
|
||||
|
||||
t.Run("Should detect if archive members point outside of the destination directory", func(t *testing.T) {
|
||||
err := i.extractFiles("testdata/plugin-with-parent-member.zip", "plugin-with-parent-member", pluginsDir)
|
||||
require.EqualError(t, err, fmt.Sprintf(
|
||||
`archive member "../member.txt" tries to write outside of plugin directory: %q, this can be a security risk`,
|
||||
pluginsDir,
|
||||
))
|
||||
})
|
||||
|
||||
t.Run("Should detect if archive members are absolute", func(t *testing.T) {
|
||||
err := i.extractFiles("testdata/plugin-with-absolute-member.zip", "plugin-with-absolute-member", pluginsDir)
|
||||
require.EqualError(t, err, fmt.Sprintf(
|
||||
`archive member "/member.txt" tries to write outside of plugin directory: %q, this can be a security risk`,
|
||||
pluginsDir,
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
func TestSelectVersion(t *testing.T) {
|
||||
i := &Installer{log: &fakeLogger{}}
|
||||
|
||||
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")
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
||||
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"}}), "")
|
||||
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")
|
||||
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"},
|
||||
), "")
|
||||
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"}), "")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "2.0.0", ver.Version)
|
||||
})
|
||||
|
||||
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")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "1.0.0", ver.Version)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRemoveGitBuildFromName(t *testing.T) {
|
||||
// The root directory should get renamed to the plugin name
|
||||
paths := map[string]string{
|
||||
"datasource-plugin-kairosdb-cc4a3965ef5d3eb1ae0ee4f93e9e78ec7db69e64/": "datasource-kairosdb/",
|
||||
"datasource-plugin-kairosdb-cc4a3965ef5d3eb1ae0ee4f93e9e78ec7db69e64/README.md": "datasource-kairosdb/README.md",
|
||||
"datasource-plugin-kairosdb-cc4a3965ef5d3eb1ae0ee4f93e9e78ec7db69e64/partials/": "datasource-kairosdb/partials/",
|
||||
"datasource-plugin-kairosdb-cc4a3965ef5d3eb1ae0ee4f93e9e78ec7db69e64/partials/config.html": "datasource-kairosdb/partials/config.html",
|
||||
}
|
||||
for p, exp := range paths {
|
||||
name := removeGitBuildFromName(p, "datasource-kairosdb")
|
||||
require.Equal(t, exp, name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsSymlinkRelativeTo(t *testing.T) {
|
||||
tcs := []struct {
|
||||
desc string
|
||||
basePath string
|
||||
symlinkDestPath string
|
||||
symlinkOrigPath string
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
desc: "Symbolic link pointing to relative file within basePath should return true",
|
||||
basePath: "/dir",
|
||||
symlinkDestPath: "test.txt",
|
||||
symlinkOrigPath: "/dir/sub-dir/test1.txt",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
desc: "Symbolic link pointing to relative file within basePath should return true",
|
||||
basePath: "/dir",
|
||||
symlinkDestPath: "test.txt",
|
||||
symlinkOrigPath: "/dir/test1.txt",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
desc: "Symbolic link pointing to relative file within basePath should return true",
|
||||
basePath: "/dir",
|
||||
symlinkDestPath: "../etc/test.txt",
|
||||
symlinkOrigPath: "/dir/sub-dir/test1.txt",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
desc: "Symbolic link pointing to absolute directory outside basePath should return false",
|
||||
basePath: "/dir",
|
||||
symlinkDestPath: "/etc/test.txt",
|
||||
symlinkOrigPath: "/dir/sub-dir/test1.txt",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
desc: "Symbolic link pointing to relative file outside basePath should return false",
|
||||
basePath: "/dir",
|
||||
symlinkDestPath: "../../etc/test.txt",
|
||||
symlinkOrigPath: "/dir/sub-dir/test1.txt",
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tcs {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
actual := isSymlinkRelativeTo(tc.basePath, tc.symlinkDestPath, tc.symlinkOrigPath)
|
||||
require.Equal(t, tc.expected, actual)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func setupFakePluginsDir(t *testing.T) string {
|
||||
dir := "testdata/fake-plugins-dir"
|
||||
err := os.RemoveAll(dir)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = os.MkdirAll(dir, 0750)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
err = os.RemoveAll(dir)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
dir, err = filepath.Abs(dir)
|
||||
require.NoError(t, err)
|
||||
|
||||
return dir
|
||||
}
|
||||
|
||||
func skipWindows(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("Skipping test on Windows")
|
||||
}
|
||||
}
|
||||
|
||||
type versionArg struct {
|
||||
version string
|
||||
arch []string
|
||||
}
|
||||
|
||||
func createPlugin(versions ...versionArg) *Plugin {
|
||||
p := &Plugin{
|
||||
Versions: []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{}
|
||||
for _, arch := range version.arch {
|
||||
ver.Arch[arch] = ArchMeta{
|
||||
SHA256: fmt.Sprintf("sha256_%s", arch),
|
||||
}
|
||||
}
|
||||
}
|
||||
p.Versions = append(p.Versions, ver)
|
||||
}
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
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{}) {}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user