Before this patch, a broken symlink would print a warning;
docker info > /dev/null
WARNING: Plugin "/Users/thajeztah/.docker/cli-plugins/docker-feedback" is not valid: failed to fetch metadata: fork/exec /Users/thajeztah/.docker/cli-plugins/docker-feedback: no such file or directory
After this patch, such symlinks are ignored:
docker info > /dev/null
With debug enabled, we don't ignore the faulty plugin, which will
make the warning shown on docker info;
mkdir -p ~/.docker/cli-plugins
ln -s nosuchplugin ~/.docker/cli-plugins/docker-brokenplugin
docker --debug info
Client:
Version: 29.0.0-dev
Context: default
Debug Mode: true
Plugins:
buildx: Docker Buildx (Docker Inc.)
Version: v0.25.0
Path: /usr/libexec/docker/cli-plugins/docker-buildx
WARNING: Plugin "/Users/thajeztah/.docker/cli-plugins/docker-brokenplugin" is not valid: failed to fetch metadata: fork/exec /Users/thajeztah/.docker/cli-plugins/docker-brokenplugin: no such file or directory
# ...
We should als consider passing a "seen" map to de-duplicate entries.
Entries can be either a direct symlink or in a symlinked path (for
which we can filepath.EvalSymlinks). We need to benchmark the overhead
of resolving the symlink vs possibly calling the plugin (to get their
metadata) further down the line.
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
190 lines
5.7 KiB
Go
190 lines
5.7 KiB
Go
package manager
|
|
|
|
import (
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/containerd/errdefs"
|
|
"github.com/docker/cli/cli/config"
|
|
"github.com/docker/cli/cli/config/configfile"
|
|
"github.com/docker/cli/internal/test"
|
|
"github.com/spf13/cobra"
|
|
"gotest.tools/v3/assert"
|
|
"gotest.tools/v3/fs"
|
|
)
|
|
|
|
func TestListPluginCandidates(t *testing.T) {
|
|
// Populate a selection of directories with various shadowed and bogus/obscure plugin candidates.
|
|
// For the purposes of this test no contents is required and permissions are irrelevant.
|
|
dir := fs.NewDir(t, t.Name(),
|
|
fs.WithDir(
|
|
"plugins1",
|
|
fs.WithFile("docker-plugin1", ""), // This appears in each directory
|
|
fs.WithFile("not-a-plugin", ""), // Should be ignored
|
|
fs.WithFile("docker-symlinked1", ""), // This and ...
|
|
fs.WithSymlink("docker-symlinked2", "docker-symlinked1"), // ... this should both appear
|
|
fs.WithDir("ignored1"), // A directory should be ignored
|
|
),
|
|
fs.WithDir(
|
|
"plugins2",
|
|
fs.WithFile("docker-plugin1", ""),
|
|
fs.WithFile("also-not-a-plugin", ""),
|
|
fs.WithFile("docker-hardlink1", ""), // This and ...
|
|
fs.WithHardlink("docker-hardlink2", "docker-hardlink1"), // ... this should both appear
|
|
fs.WithDir("ignored2"),
|
|
),
|
|
fs.WithDir(
|
|
"plugins3-target", // Will be referenced as a symlink from below
|
|
fs.WithFile("docker-plugin1", ""),
|
|
fs.WithDir("ignored3"),
|
|
fs.WithSymlink("docker-brokensymlink", "broken"), // A broken symlink is ignored
|
|
fs.WithFile("non-plugin-symlinked", ""), // This shouldn't appear, but ...
|
|
fs.WithSymlink("docker-symlinked", "non-plugin-symlinked"), // ... this link to it should.
|
|
),
|
|
fs.WithSymlink("plugins3", "plugins3-target"),
|
|
fs.WithFile("/plugins4", ""),
|
|
fs.WithSymlink("plugins5", "plugins5-nonexistent-target"),
|
|
)
|
|
defer dir.Remove()
|
|
|
|
dirs := make([]string, 0, 6)
|
|
for _, d := range []string{"plugins1", "nonexistent", "plugins2", "plugins3", "plugins4", "plugins5"} {
|
|
dirs = append(dirs, dir.Join(d))
|
|
}
|
|
|
|
candidates := listPluginCandidates(dirs)
|
|
exp := map[string][]string{
|
|
"plugin1": {
|
|
dir.Join("plugins1", "docker-plugin1"),
|
|
dir.Join("plugins2", "docker-plugin1"),
|
|
dir.Join("plugins3", "docker-plugin1"),
|
|
},
|
|
"symlinked1": {
|
|
dir.Join("plugins1", "docker-symlinked1"),
|
|
},
|
|
"symlinked2": {
|
|
dir.Join("plugins1", "docker-symlinked2"),
|
|
},
|
|
"hardlink1": {
|
|
dir.Join("plugins2", "docker-hardlink1"),
|
|
},
|
|
"hardlink2": {
|
|
dir.Join("plugins2", "docker-hardlink2"),
|
|
},
|
|
"symlinked": {
|
|
dir.Join("plugins3", "docker-symlinked"),
|
|
},
|
|
}
|
|
|
|
assert.DeepEqual(t, candidates, exp)
|
|
}
|
|
|
|
func TestListPluginCandidatesEmpty(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
candidates := listPluginCandidates([]string{tmpDir, filepath.Join(tmpDir, "no-such-dir")})
|
|
assert.Assert(t, len(candidates) == 0)
|
|
}
|
|
|
|
// Regression test for https://github.com/docker/cli/issues/5643.
|
|
// Check that inaccessible directories that come before accessible ones are ignored
|
|
// and do not prevent the latter from being processed.
|
|
func TestListPluginCandidatesInaccesibleDir(t *testing.T) {
|
|
dir := fs.NewDir(t, t.Name(),
|
|
fs.WithDir("no-perm", fs.WithMode(0)),
|
|
fs.WithDir("plugins",
|
|
fs.WithFile("docker-buildx", ""),
|
|
),
|
|
)
|
|
defer dir.Remove()
|
|
|
|
candidates := listPluginCandidates([]string{
|
|
dir.Join("no-perm"),
|
|
dir.Join("plugins"),
|
|
})
|
|
assert.DeepEqual(t, candidates, map[string][]string{
|
|
"buildx": {
|
|
dir.Join("plugins", "docker-buildx"),
|
|
},
|
|
})
|
|
}
|
|
|
|
func TestGetPlugin(t *testing.T) {
|
|
dir := fs.NewDir(t, t.Name(),
|
|
fs.WithFile("docker-bbb", `
|
|
#!/bin/sh
|
|
echo '{"SchemaVersion":"0.1.0"}'`, fs.WithMode(0o777)),
|
|
fs.WithFile("docker-aaa", `
|
|
#!/bin/sh
|
|
echo '{"SchemaVersion":"0.1.0"}'`, fs.WithMode(0o777)),
|
|
)
|
|
defer dir.Remove()
|
|
|
|
cli := test.NewFakeCli(nil)
|
|
cli.SetConfigFile(&configfile.ConfigFile{CLIPluginsExtraDirs: []string{dir.Path()}})
|
|
|
|
plugin, err := GetPlugin("bbb", cli, &cobra.Command{})
|
|
assert.NilError(t, err)
|
|
assert.Equal(t, plugin.Name, "bbb")
|
|
|
|
_, err = GetPlugin("ccc", cli, &cobra.Command{})
|
|
assert.Error(t, err, "Error: No such CLI plugin: ccc")
|
|
assert.Assert(t, errdefs.IsNotFound(err))
|
|
}
|
|
|
|
func TestListPluginsIsSorted(t *testing.T) {
|
|
dir := fs.NewDir(t, t.Name(),
|
|
fs.WithFile("docker-bbb", `
|
|
#!/bin/sh
|
|
echo '{"SchemaVersion":"0.1.0"}'`, fs.WithMode(0o777)),
|
|
fs.WithFile("docker-aaa", `
|
|
#!/bin/sh
|
|
echo '{"SchemaVersion":"0.1.0"}'`, fs.WithMode(0o777)),
|
|
)
|
|
defer dir.Remove()
|
|
|
|
cli := test.NewFakeCli(nil)
|
|
cli.SetConfigFile(&configfile.ConfigFile{CLIPluginsExtraDirs: []string{dir.Path()}})
|
|
|
|
plugins, err := ListPlugins(cli, &cobra.Command{})
|
|
assert.NilError(t, err)
|
|
|
|
// We're only interested in the plugins we created for testing this, and only
|
|
// if they appear in the expected order
|
|
var names []string
|
|
for _, p := range plugins {
|
|
if p.Name == "aaa" || p.Name == "bbb" {
|
|
names = append(names, p.Name)
|
|
}
|
|
}
|
|
assert.DeepEqual(t, names, []string{"aaa", "bbb"})
|
|
}
|
|
|
|
func TestErrPluginNotFound(t *testing.T) {
|
|
var err error = errPluginNotFound("test")
|
|
err.(errPluginNotFound).NotFound()
|
|
assert.Error(t, err, "Error: No such CLI plugin: test")
|
|
assert.Assert(t, errdefs.IsNotFound(err))
|
|
assert.Assert(t, !errdefs.IsNotFound(nil))
|
|
}
|
|
|
|
func TestGetPluginDirs(t *testing.T) {
|
|
cli := test.NewFakeCli(nil)
|
|
|
|
pluginDir := filepath.Join(config.Dir(), "cli-plugins")
|
|
expected := append([]string{pluginDir}, defaultSystemPluginDirs...)
|
|
|
|
pluginDirs := getPluginDirs(cli.ConfigFile())
|
|
assert.Equal(t, strings.Join(expected, ":"), strings.Join(pluginDirs, ":"))
|
|
|
|
extras := []string{
|
|
"foo", "bar", "baz",
|
|
}
|
|
expected = append(extras, expected...)
|
|
cli.SetConfigFile(&configfile.ConfigFile{
|
|
CLIPluginsExtraDirs: extras,
|
|
})
|
|
pluginDirs = getPluginDirs(cli.ConfigFile())
|
|
assert.DeepEqual(t, expected, pluginDirs)
|
|
}
|