mirror of
https://github.com/docker/cli.git
synced 2026-11-06 13:10:41 -04:00
Explain how to install the Offload plugin when --cloud cannot find it, and link to the cloud flag documentation. Preserve diagnostic details for invalid or incompatible plugins and limit installation guidance to Offload. Use standard default-value handling in the resolution tests to satisfy the lint complexity limit without changing test coverage. Co-authored-by: Allie Sadler <102604716+aevesdocker@users.noreply.github.com> Signed-off-by: Nicolas Beck <nicolas.beck@docker.com>
683 lines
21 KiB
Go
683 lines
21 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"cmp"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/creack/pty"
|
|
"github.com/docker/cli/cli-plugins/metadata"
|
|
"github.com/docker/cli/cli/command"
|
|
"github.com/docker/cli/cli/config"
|
|
"github.com/docker/cli/cli/config/configfile"
|
|
contextdocker "github.com/docker/cli/cli/context/docker"
|
|
"github.com/docker/cli/cli/context/store"
|
|
"gotest.tools/v3/assert"
|
|
is "gotest.tools/v3/assert/cmp"
|
|
)
|
|
|
|
func TestCloudFlagHidden(t *testing.T) {
|
|
dockerCli, err := command.NewDockerCli()
|
|
assert.NilError(t, err)
|
|
|
|
tcmd := newDockerCommand(dockerCli)
|
|
tcmd.SetArgs([]string{"--cloud=team", "--help"})
|
|
cmd, _, err := tcmd.HandleGlobalFlags()
|
|
assert.NilError(t, err)
|
|
|
|
name, err := cmd.Flags().GetString("cloud")
|
|
assert.NilError(t, err)
|
|
assert.Equal(t, name, "team")
|
|
assert.Assert(t, cmd.Flags().Lookup("cloud").Hidden)
|
|
assert.Assert(t, !strings.Contains(cmd.UsageString(), "--cloud"))
|
|
}
|
|
|
|
func TestCloudHelpRequest(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
name string
|
|
args []string
|
|
help bool
|
|
}{
|
|
{name: "no command", help: true},
|
|
{name: "global help", args: []string{"--help"}, help: true},
|
|
{name: "global version", args: []string{"--version"}, help: true},
|
|
{name: "help command", args: []string{"help", "run"}, help: true},
|
|
{name: "completion request", args: []string{"__complete", "run", ""}, help: true},
|
|
{name: "completion script", args: []string{"completion", "fish"}, help: true},
|
|
{name: "builtin help", args: []string{"run", "--help"}, help: true},
|
|
{name: "boolean help value", args: []string{"run", "--help=1"}, help: true},
|
|
{name: "nested help", args: []string{"container", "run", "--help"}, help: true},
|
|
{name: "plugin help", args: []string{"compose", "--help"}, help: true},
|
|
{name: "command execution", args: []string{"ps"}},
|
|
{name: "container help", args: []string{"run", "alpine", "--help"}},
|
|
{name: "flag value", args: []string{"run", "--name", "--help", "alpine"}},
|
|
{name: "help disabled", args: []string{"run", "--help=false", "alpine"}},
|
|
{name: "build help", args: []string{"build", "--help"}, help: true},
|
|
{name: "build buildkit-only flag", args: []string{"build", "--secret", "id=s,src=./s", "."}},
|
|
{name: "image build buildkit-only flag", args: []string{"image", "build", "--push", "."}},
|
|
{name: "builder build buildkit-only flag", args: []string{"builder", "build", "--ssh", "default", "."}},
|
|
{name: "bake help", args: []string{"bake", "--help"}, help: true},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
dockerCli, err := command.NewDockerCli()
|
|
assert.NilError(t, err)
|
|
tcmd := newDockerCommand(dockerCli)
|
|
tcmd.SetArgs(append([]string{"--cloud"}, tc.args...))
|
|
cmd, args, err := tcmd.HandleGlobalFlags()
|
|
assert.NilError(t, err)
|
|
|
|
help, err := cloudHelpRequest(cmd, args)
|
|
assert.NilError(t, err)
|
|
assert.Equal(t, help, tc.help)
|
|
})
|
|
}
|
|
}
|
|
|
|
// Flag errors are reported before cloud resolution, so they must not run the
|
|
// command's daemon feature checks against the engine selected before it.
|
|
func TestCloudFlagErrorSkipsDaemon(t *testing.T) {
|
|
originalConfig := config.Dir()
|
|
t.Cleanup(func() { config.SetDir(originalConfig) })
|
|
config.SetDir(t.TempDir())
|
|
|
|
var pings atomic.Int32
|
|
daemon := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
pings.Add(1)
|
|
w.Header().Set("Docker-Experimental", "false")
|
|
}))
|
|
defer daemon.Close()
|
|
t.Setenv("DOCKER_HOST", "tcp://"+daemon.Listener.Addr().String())
|
|
|
|
dockerCli, err := command.NewDockerCli()
|
|
assert.NilError(t, err)
|
|
tcmd := newDockerCommand(dockerCli)
|
|
tcmd.SetArgs([]string{"--cloud", "checkpoint", "ls", "--unknown"})
|
|
cmd, args, err := tcmd.HandleGlobalFlags()
|
|
assert.NilError(t, err)
|
|
assert.NilError(t, tcmd.Initialize())
|
|
|
|
_, err = cloudHelpRequest(cmd, args)
|
|
assert.Check(t, is.ErrorContains(err, "unknown flag: --unknown"))
|
|
assert.Check(t, is.Equal(pings.Load(), int32(0)))
|
|
}
|
|
|
|
func TestCloudPluginArgs(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
name string
|
|
args []string
|
|
context string
|
|
want []string
|
|
}{
|
|
{
|
|
name: "global flag only",
|
|
args: []string{"docker", "--cloud=foo", "compose", "up", "--cloud=child"},
|
|
context: "cloud-foo",
|
|
want: []string{"docker", "--context=cloud-foo", "compose", "up", "--cloud=child"},
|
|
},
|
|
{
|
|
name: "flag value is not cloud flag",
|
|
args: []string{"docker", "--config", "--cloud", "--cloud", "-D", "compose", "up"},
|
|
context: "cloud-default",
|
|
want: []string{"docker", "--context=cloud-default", "--config=--cloud", "--debug=true", "compose", "up"},
|
|
},
|
|
{
|
|
name: "delimiter preserved",
|
|
args: []string{"docker", "--cloud=foo", "--", "compose", "--", "--cloud"},
|
|
context: "cloud-foo",
|
|
want: []string{"docker", "--context=cloud-foo", "--", "compose", "--", "--cloud"},
|
|
},
|
|
{
|
|
name: "repeated cloud flags",
|
|
args: []string{"docker", "--cloud=foo", "--cloud=bar", "compose", "up"},
|
|
context: "cloud-bar",
|
|
want: []string{"docker", "--context=cloud-bar", "compose", "up"},
|
|
},
|
|
{
|
|
name: "help without resolution",
|
|
args: []string{"docker", "--cloud", "compose", "--help"},
|
|
want: []string{"docker", "compose", "--help"},
|
|
},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
dockerCli, err := command.NewDockerCli()
|
|
assert.NilError(t, err)
|
|
tcmd := newDockerCommand(dockerCli)
|
|
tcmd.SetArgs(tc.args[1:])
|
|
cmd, _, err := tcmd.HandleGlobalFlags()
|
|
assert.NilError(t, err)
|
|
|
|
got, err := cloudPluginArgs(cmd, tc.args, tc.context)
|
|
assert.NilError(t, err)
|
|
assert.DeepEqual(t, got, tc.want)
|
|
})
|
|
}
|
|
}
|
|
|
|
// Exercise the real startup and subprocess boundary without a daemon or cloud
|
|
// service, including failures that must never dispatch the requested command.
|
|
func TestCloudResolution(t *testing.T) {
|
|
if runtime.GOOS == "windows" {
|
|
t.Skip("fixture plugins use shell scripts")
|
|
}
|
|
|
|
executable, err := os.Executable()
|
|
assert.NilError(t, err)
|
|
|
|
for _, tc := range []struct {
|
|
name string
|
|
args []string
|
|
response string
|
|
exit int
|
|
wantName string
|
|
wantErr string
|
|
plugin bool
|
|
skip bool
|
|
provider string
|
|
noProvider bool
|
|
legacyProvider bool
|
|
invalidProvider bool
|
|
wantHelp bool
|
|
command []string
|
|
visible bool
|
|
}{
|
|
{
|
|
name: "default target",
|
|
args: []string{"--cloud"},
|
|
wantName: "default",
|
|
},
|
|
{
|
|
name: "empty target",
|
|
args: []string{"--cloud="},
|
|
wantName: "default",
|
|
},
|
|
{
|
|
name: "named target",
|
|
args: []string{"--cloud=team"},
|
|
wantName: "team",
|
|
},
|
|
{
|
|
name: "plugin dispatch",
|
|
args: []string{"--cloud=team"},
|
|
wantName: "team",
|
|
plugin: true,
|
|
},
|
|
{
|
|
name: "nonzero exit",
|
|
args: []string{"--cloud"},
|
|
exit: 1,
|
|
wantErr: "cloud resolver failed",
|
|
},
|
|
{
|
|
name: "malformed JSON",
|
|
args: []string{"--cloud"},
|
|
response: "not JSON",
|
|
wantErr: "invalid cloud resolver response",
|
|
},
|
|
{
|
|
name: "multiple responses",
|
|
args: []string{"--cloud"},
|
|
response: `{} {}`,
|
|
wantErr: "invalid cloud resolver response",
|
|
},
|
|
{
|
|
name: "missing field",
|
|
args: []string{"--cloud"},
|
|
response: `{}`,
|
|
wantErr: "must return a non-default DOCKER_CONTEXT",
|
|
},
|
|
{
|
|
name: "local context",
|
|
args: []string{"--cloud"},
|
|
response: `{"DOCKER_CONTEXT":"default"}`,
|
|
wantErr: "must return a non-default DOCKER_CONTEXT",
|
|
},
|
|
{
|
|
name: "missing context",
|
|
args: []string{"--cloud"},
|
|
response: `{"DOCKER_CONTEXT":"missing"}`,
|
|
wantErr: `loading resolved context "missing"`,
|
|
},
|
|
{
|
|
name: "missing endpoint host",
|
|
args: []string{"--cloud"},
|
|
response: `{"DOCKER_CONTEXT":"empty-host"}`,
|
|
wantErr: "has no Docker endpoint host",
|
|
},
|
|
{
|
|
name: "explicit context conflict",
|
|
args: []string{"--cloud", "--context=other"},
|
|
wantErr: "conflicting options",
|
|
skip: true,
|
|
},
|
|
{
|
|
name: "explicit host conflict",
|
|
args: []string{"--cloud", "--host=tcp://localhost:2375"},
|
|
wantErr: "conflicting options",
|
|
skip: true,
|
|
},
|
|
{
|
|
name: "invalid flag before help",
|
|
args: []string{"--cloud"},
|
|
command: []string{"run", "--unknown", "--help"},
|
|
wantErr: "unknown flag: --unknown",
|
|
skip: true,
|
|
},
|
|
{
|
|
name: "default provider",
|
|
args: []string{"--cloud"},
|
|
noProvider: true,
|
|
wantName: "default",
|
|
},
|
|
{
|
|
name: "legacy default provider",
|
|
args: []string{"--cloud"},
|
|
noProvider: true,
|
|
legacyProvider: true,
|
|
wantErr: `plugin "offload" does not support --cloud`,
|
|
wantHelp: true,
|
|
skip: true,
|
|
},
|
|
{
|
|
name: "invalid default provider",
|
|
args: []string{"--cloud"},
|
|
noProvider: true,
|
|
invalidProvider: true,
|
|
wantErr: `invalid plugin "offload": plugin metadata does not define a vendor`,
|
|
wantHelp: true,
|
|
skip: true,
|
|
},
|
|
{
|
|
name: "invalid configured provider",
|
|
args: []string{"--cloud"},
|
|
invalidProvider: true,
|
|
wantErr: `invalid plugin "foobar": plugin metadata does not define a vendor`,
|
|
skip: true,
|
|
},
|
|
{
|
|
name: "legacy configured provider",
|
|
args: []string{"--cloud"},
|
|
legacyProvider: true,
|
|
wantErr: `plugin "foobar" does not support --cloud`,
|
|
skip: true,
|
|
},
|
|
{
|
|
name: "configured provider unavailable",
|
|
args: []string{"--cloud"},
|
|
provider: "missing",
|
|
wantErr: `plugin "missing" unavailable`,
|
|
skip: true,
|
|
},
|
|
{
|
|
name: "provider cannot be a path",
|
|
args: []string{"--cloud"},
|
|
provider: "../foobar",
|
|
wantErr: `plugin "../foobar" unavailable`,
|
|
skip: true,
|
|
},
|
|
{
|
|
name: "no cloud option",
|
|
noProvider: true,
|
|
skip: true,
|
|
},
|
|
{
|
|
name: "help",
|
|
args: []string{"--cloud", "--help"},
|
|
noProvider: true,
|
|
skip: true,
|
|
},
|
|
{
|
|
name: "version",
|
|
args: []string{"--cloud", "--version"},
|
|
noProvider: true,
|
|
skip: true,
|
|
},
|
|
{
|
|
name: "help exposes supported provider",
|
|
command: []string{"--help"},
|
|
noProvider: true,
|
|
visible: true,
|
|
skip: true,
|
|
},
|
|
{
|
|
name: "help hides legacy provider",
|
|
command: []string{"--help"},
|
|
noProvider: true,
|
|
legacyProvider: true,
|
|
skip: true,
|
|
},
|
|
{
|
|
name: "help hides missing provider",
|
|
command: []string{"--help"},
|
|
provider: "missing",
|
|
skip: true,
|
|
},
|
|
{
|
|
name: "completion exposes supported provider",
|
|
command: []string{"__complete", "--cl"},
|
|
noProvider: true,
|
|
visible: true,
|
|
skip: true,
|
|
},
|
|
{
|
|
name: "completion hides legacy provider",
|
|
command: []string{"__complete", "--cl"},
|
|
noProvider: true,
|
|
legacyProvider: true,
|
|
skip: true,
|
|
},
|
|
{
|
|
name: "completion hides missing provider",
|
|
command: []string{"__complete", "--cl"},
|
|
provider: "missing",
|
|
skip: true,
|
|
},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
// Explicit --cloud must override inherited endpoint selection.
|
|
t.Setenv("DOCKER_CONTEXT", "inherited")
|
|
t.Setenv("DOCKER_HOST", "tcp://127.0.0.1:1")
|
|
t.Setenv("DOCKER_CLI_HOOKS", "false")
|
|
t.Setenv("DOCKER_CONFIG", t.TempDir())
|
|
|
|
configDir := t.TempDir()
|
|
installedProvider := "offload"
|
|
if !tc.noProvider {
|
|
installedProvider = "foobar"
|
|
cfg := configfile.New(filepath.Join(configDir, config.ConfigFileName))
|
|
provider := cmp.Or(tc.provider, "foobar")
|
|
cfg.Features = map[string]string{"cloud": provider}
|
|
assert.NilError(t, cfg.Save())
|
|
}
|
|
|
|
pluginDir := filepath.Join(configDir, "cli-plugins")
|
|
assert.NilError(t, os.MkdirAll(pluginDir, 0o755))
|
|
|
|
response := cmp.Or(tc.response, `{"DOCKER_CONTEXT":"resolved"}`)
|
|
|
|
providerMetadata := metadata.Metadata{SchemaVersion: "0.1.0", Vendor: "test"}
|
|
if tc.invalidProvider {
|
|
providerMetadata.Vendor = ""
|
|
}
|
|
if !tc.legacyProvider {
|
|
providerMetadata.Features = map[string]any{cloudResolverFeature: true}
|
|
}
|
|
pluginMetadata, err := json.Marshal(providerMetadata)
|
|
assert.NilError(t, err)
|
|
|
|
script := fmt.Sprintf(`#!/bin/sh
|
|
if [ "$1" = docker-cli-plugin-metadata ]; then
|
|
echo '%s'
|
|
exit 0
|
|
fi
|
|
printf '%%s\n' "$@" >> "$DOCKER_CONFIG/resolver-args"
|
|
echo provisioning >&2
|
|
printf '%%s\n' '%s'
|
|
exit %d
|
|
`, pluginMetadata, response, tc.exit)
|
|
assert.NilError(t, os.WriteFile(filepath.Join(pluginDir, "docker-"+installedProvider), []byte(script), 0o755))
|
|
assert.NilError(t, os.WriteFile(filepath.Join(pluginDir, "docker-cloudtest"), []byte(`#!/bin/sh
|
|
if [ "$1" = docker-cli-plugin-metadata ]; then
|
|
echo '{"SchemaVersion":"0.1.0","Vendor":"test"}'
|
|
exit 0
|
|
fi
|
|
printf '%s\n' "$@" > "$CLOUD_TEST_ARGS"
|
|
`), 0o755))
|
|
|
|
dispatchFile := filepath.Join(configDir, "dispatch-args")
|
|
t.Setenv("CLOUD_TEST_ARGS", dispatchFile)
|
|
|
|
contextStore := store.New(filepath.Join(configDir, "contexts"), command.DefaultContextStoreConfig())
|
|
assert.NilError(t, contextStore.CreateOrUpdate(store.Metadata{
|
|
Name: "resolved",
|
|
Endpoints: map[string]any{contextdocker.DockerEndpoint: contextdocker.EndpointMeta{Host: "tcp://127.0.0.1:1"}},
|
|
}))
|
|
assert.NilError(t, contextStore.CreateOrUpdate(store.Metadata{
|
|
Name: "empty-host",
|
|
Endpoints: map[string]any{contextdocker.DockerEndpoint: contextdocker.EndpointMeta{}},
|
|
}))
|
|
|
|
args := append([]string{"docker", "--config=" + configDir}, tc.args...)
|
|
switch {
|
|
case tc.command != nil:
|
|
args = append(args, tc.command...)
|
|
case tc.plugin || tc.wantErr != "":
|
|
args = append(args, "cloudtest", "up", "--cloud=child")
|
|
default:
|
|
args = append(args, "context", "show")
|
|
}
|
|
|
|
payload, err := json.Marshal(args)
|
|
assert.NilError(t, err)
|
|
|
|
var stdout, stderr bytes.Buffer
|
|
cmd := exec.CommandContext(t.Context(), executable, "-test.run=^TestCloudCommandProcess$")
|
|
cmd.Env = append(os.Environ(), "CLOUD_TEST_COMMAND="+string(payload))
|
|
cmd.Stdout = &stdout
|
|
cmd.Stderr = &stderr
|
|
|
|
err = cmd.Run()
|
|
if tc.wantErr != "" {
|
|
assert.Assert(t, err != nil)
|
|
assert.Check(t, is.Contains(stderr.String(), tc.wantErr))
|
|
assert.Equal(t, strings.Contains(stderr.String(), cloudPluginHelp), tc.wantHelp, stderr.String())
|
|
_, statErr := os.Stat(dispatchFile)
|
|
assert.Assert(t, os.IsNotExist(statErr))
|
|
assert.Equal(t, stdout.String(), "")
|
|
} else {
|
|
assert.NilError(t, err, stderr.String())
|
|
if tc.command != nil {
|
|
assert.Equal(t, strings.Contains(stdout.String(), "--cloud"), tc.visible, stdout.String())
|
|
}
|
|
if !tc.skip {
|
|
if tc.plugin {
|
|
args, err := os.ReadFile(dispatchFile)
|
|
assert.NilError(t, err)
|
|
assert.Equal(t, string(args), "--context=resolved\n--config="+configDir+"\ncloudtest\nup\n--cloud=child\n")
|
|
} else {
|
|
assert.Equal(t, stdout.String(), "resolved\n")
|
|
}
|
|
}
|
|
}
|
|
|
|
invocation, err := os.ReadFile(filepath.Join(configDir, "resolver-args"))
|
|
if tc.skip {
|
|
assert.Assert(t, os.IsNotExist(err))
|
|
} else {
|
|
assert.NilError(t, err)
|
|
assert.Check(t, is.Contains(stderr.String(), "provisioning"))
|
|
name := cmp.Or(tc.wantName, "default")
|
|
assert.Equal(t, string(invocation), "--config="+configDir+"\n"+installedProvider+"\n__resolve-context\n--\n"+name+"\n")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCloudResolverInput(t *testing.T) {
|
|
if runtime.GOOS == "windows" {
|
|
t.Skip("fixture plugins use shell scripts and pseudo-terminals")
|
|
}
|
|
|
|
executable, err := os.Executable()
|
|
assert.NilError(t, err)
|
|
|
|
for _, tc := range []struct {
|
|
name string
|
|
stdinType string
|
|
terminalErr bool
|
|
prompt bool
|
|
}{
|
|
{name: "interactive", stdinType: "terminal", terminalErr: true, prompt: true},
|
|
{name: "piped stdin", stdinType: "pipe", terminalErr: true},
|
|
{name: "redirected stdin", stdinType: "file", terminalErr: true},
|
|
{name: "redirected stderr", stdinType: "terminal"},
|
|
{name: "noninteractive", stdinType: "pipe"},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Setenv("DOCKER_CLI_HOOKS", "false")
|
|
configDir := t.TempDir()
|
|
pluginDir := filepath.Join(configDir, "cli-plugins")
|
|
assert.NilError(t, os.MkdirAll(pluginDir, 0o755))
|
|
assert.NilError(t, os.WriteFile(filepath.Join(pluginDir, "docker-offload"), []byte(`#!/bin/sh
|
|
if [ "$1" = docker-cli-plugin-metadata ]; then
|
|
echo '{"SchemaVersion":"0.1.0","Vendor":"test","Features":{"cloud-context-resolver":true}}'
|
|
exit 0
|
|
fi
|
|
if [ -t 0 ]; then
|
|
[ -t 2 ] || exit 1
|
|
printf 'Continue? ' >&2
|
|
IFS= read -r reply || exit 1
|
|
printf '%s\n' "$reply" > "$DOCKER_CONFIG/resolver-input"
|
|
elif IFS= read -r unexpected; then
|
|
echo 'resolver consumed command input' >&2
|
|
exit 1
|
|
fi
|
|
echo '{"DOCKER_CONTEXT":"resolved"}'
|
|
`), 0o755))
|
|
assert.NilError(t, os.WriteFile(filepath.Join(pluginDir, "docker-cloudtest"), []byte(`#!/bin/sh
|
|
if [ "$1" = docker-cli-plugin-metadata ]; then
|
|
echo '{"SchemaVersion":"0.1.0","Vendor":"test"}'
|
|
exit 0
|
|
fi
|
|
IFS= read -r input || exit 1
|
|
printf '%s\n' "$input"
|
|
`), 0o755))
|
|
|
|
contextStore := store.New(filepath.Join(configDir, "contexts"), command.DefaultContextStoreConfig())
|
|
assert.NilError(t, contextStore.CreateOrUpdate(store.Metadata{
|
|
Name: "resolved",
|
|
Endpoints: map[string]any{contextdocker.DockerEndpoint: contextdocker.EndpointMeta{Host: "tcp://127.0.0.1:1"}},
|
|
}))
|
|
|
|
terminal, tty, err := pty.Open()
|
|
assert.NilError(t, err)
|
|
t.Cleanup(func() {
|
|
_ = tty.Close()
|
|
_ = terminal.Close()
|
|
})
|
|
|
|
const commandInput = "input for the original command\n"
|
|
var stdin *os.File
|
|
switch tc.stdinType {
|
|
case "terminal":
|
|
stdin = tty
|
|
input := commandInput
|
|
if tc.prompt {
|
|
input = "yes\n" + input
|
|
}
|
|
_, err = terminal.WriteString(input)
|
|
assert.NilError(t, err)
|
|
case "pipe":
|
|
var writer *os.File
|
|
stdin, writer, err = os.Pipe()
|
|
assert.NilError(t, err)
|
|
t.Cleanup(func() { _ = stdin.Close() })
|
|
t.Cleanup(func() { _ = writer.Close() })
|
|
_, err = writer.WriteString(commandInput)
|
|
assert.NilError(t, err)
|
|
assert.NilError(t, writer.Close())
|
|
case "file":
|
|
inputPath := filepath.Join(configDir, "command-input")
|
|
assert.NilError(t, os.WriteFile(inputPath, []byte(commandInput), 0o600))
|
|
stdin, err = os.Open(inputPath)
|
|
assert.NilError(t, err)
|
|
t.Cleanup(func() { _ = stdin.Close() })
|
|
}
|
|
|
|
payload, err := json.Marshal([]string{"docker", "--config=" + configDir, "--cloud", "cloudtest"})
|
|
assert.NilError(t, err)
|
|
ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second)
|
|
defer cancel()
|
|
var stdout, stderr bytes.Buffer
|
|
cmd := exec.CommandContext(ctx, executable, "-test.run=^TestCloudCommandProcess$")
|
|
cmd.Env = append(os.Environ(), "CLOUD_TEST_COMMAND="+string(payload))
|
|
cmd.Stdin = stdin
|
|
cmd.Stdout = &stdout
|
|
cmd.Stderr = &stderr
|
|
cmd.WaitDelay = time.Second
|
|
if tc.terminalErr {
|
|
cmd.Stderr = tty
|
|
}
|
|
assert.NilError(t, cmd.Run(), stderr.String())
|
|
assert.Equal(t, stdout.String(), commandInput)
|
|
|
|
reply, err := os.ReadFile(filepath.Join(configDir, "resolver-input"))
|
|
if tc.prompt {
|
|
assert.NilError(t, err)
|
|
assert.Equal(t, string(reply), "yes\n")
|
|
} else {
|
|
assert.Assert(t, os.IsNotExist(err))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// Run startup in a separate process because it installs process-wide signal
|
|
// handlers that intentionally outlive an individual command.
|
|
func TestCloudCommandProcess(t *testing.T) {
|
|
payload := os.Getenv("CLOUD_TEST_COMMAND")
|
|
if payload == "" {
|
|
return
|
|
}
|
|
|
|
assert.NilError(t, json.Unmarshal([]byte(payload), &os.Args))
|
|
|
|
err := dockerMain(context.Background())
|
|
if err != nil {
|
|
_, _ = fmt.Fprintln(os.Stderr, err)
|
|
}
|
|
if err == nil {
|
|
os.Exit(0)
|
|
}
|
|
|
|
os.Exit(getExitCode(err))
|
|
}
|
|
|
|
func TestCloudResolutionCancellation(t *testing.T) {
|
|
if runtime.GOOS == "windows" {
|
|
t.Skip("fixture plugin uses a shell script")
|
|
}
|
|
|
|
originalConfig := config.Dir()
|
|
t.Cleanup(func() { config.SetDir(originalConfig) })
|
|
config.SetDir(t.TempDir())
|
|
|
|
cfg := configfile.New(filepath.Join(config.Dir(), config.ConfigFileName))
|
|
cfg.Features = map[string]string{"cloud": "foobar"}
|
|
assert.NilError(t, cfg.Save())
|
|
|
|
pluginDir := filepath.Join(config.Dir(), "cli-plugins")
|
|
assert.NilError(t, os.MkdirAll(pluginDir, 0o755))
|
|
assert.NilError(t, os.WriteFile(filepath.Join(pluginDir, "docker-foobar"), []byte(`#!/bin/sh
|
|
if [ "$1" = docker-cli-plugin-metadata ]; then
|
|
echo '{"SchemaVersion":"0.1.0","Vendor":"test","Features":{"cloud-context-resolver":true}}'
|
|
exit 0
|
|
fi
|
|
exec sleep 30
|
|
`), 0o755))
|
|
|
|
dockerCli, err := command.NewDockerCli()
|
|
assert.NilError(t, err)
|
|
tcmd := newDockerCommand(dockerCli)
|
|
tcmd.SetArgs(strings.Fields("--cloud context show"))
|
|
cmd, _, err := tcmd.HandleGlobalFlags()
|
|
assert.NilError(t, err)
|
|
|
|
ctx, cancel := context.WithTimeout(t.Context(), time.Second)
|
|
defer cancel()
|
|
|
|
_, err = resolveCloudContext(ctx, dockerCli, cmd, "default")
|
|
assert.ErrorIs(t, err, context.DeadlineExceeded)
|
|
}
|