1
0
mirror of https://github.com/docker/cli.git synced 2026-10-29 23:02:33 +03:00
Files
cli/cli/command/container/exec_test.go
Sebastiaan van Stijn fe3157419c improve validation of "--detach-keys" options
Before this change, the detach-keys were not validated, and the code either
fell back to the default sequence, or returned an obscure error if the
invalid sequence would produce an error on the daemon;

Before this patch:

    docker run -it --rm --detach-keys=shift-a,b busybox
    unable to upgrade to tcp, received 400

With this patch:

    docker run -it --rm --detach-keys=shift-a,b busybox
    invalid detach keys (shift-a,b): Unknown character: 'shift-a'

Note that the "unable to upgrade to tcp, received 400" error is still
something to be looked into; the client currently discards error messages
coming from the daemon.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-01-14 16:57:38 +01:00

282 lines
7.1 KiB
Go

package container
import (
"context"
"errors"
"io"
"os"
"strconv"
"testing"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/internal/test"
"github.com/docker/cli/opts"
"github.com/moby/moby/client"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
"gotest.tools/v3/fs"
)
func withDefaultOpts(options ExecOptions) ExecOptions {
options.Env = opts.NewListOpts(opts.ValidateEnv)
options.EnvFile = opts.NewListOpts(nil)
if len(options.Command) == 0 {
options.Command = []string{"command"}
}
return options
}
func TestParseExec(t *testing.T) {
content := `ONE=1
TWO=2
`
tmpFile := fs.NewFile(t, t.Name(), fs.WithContent(content))
defer tmpFile.Remove()
testcases := []struct {
options ExecOptions
configFile configfile.ConfigFile
expected client.ExecCreateOptions
}{
{
expected: client.ExecCreateOptions{
Cmd: []string{"command"},
AttachStdout: true,
AttachStderr: true,
},
options: withDefaultOpts(ExecOptions{}),
},
{
expected: client.ExecCreateOptions{
Cmd: []string{"command1", "command2"},
AttachStdout: true,
AttachStderr: true,
},
options: withDefaultOpts(ExecOptions{
Command: []string{"command1", "command2"},
}),
},
{
options: withDefaultOpts(ExecOptions{
Interactive: true,
TTY: true,
User: "uid",
}),
expected: client.ExecCreateOptions{
User: "uid",
AttachStdin: true,
AttachStdout: true,
AttachStderr: true,
TTY: true,
Cmd: []string{"command"},
},
},
{
options: withDefaultOpts(ExecOptions{Detach: true}),
expected: client.ExecCreateOptions{
Cmd: []string{"command"},
},
},
{
options: withDefaultOpts(ExecOptions{
TTY: true,
Interactive: true,
Detach: true,
}),
expected: client.ExecCreateOptions{
TTY: true,
Cmd: []string{"command"},
},
},
{
options: withDefaultOpts(ExecOptions{Detach: true}),
configFile: configfile.ConfigFile{DetachKeys: "ctrl-d,e"},
expected: client.ExecCreateOptions{
Cmd: []string{"command"},
DetachKeys: "ctrl-d,e",
},
},
{
options: withDefaultOpts(ExecOptions{
Detach: true,
DetachKeys: "ctrl-a,b",
}),
configFile: configfile.ConfigFile{DetachKeys: "ctrl-d,e"},
expected: client.ExecCreateOptions{
Cmd: []string{"command"},
DetachKeys: "ctrl-a,b",
},
},
{
expected: client.ExecCreateOptions{
Cmd: []string{"command"},
AttachStdout: true,
AttachStderr: true,
Env: []string{"ONE=1", "TWO=2"},
},
options: func() ExecOptions {
o := withDefaultOpts(ExecOptions{})
_ = o.EnvFile.Set(tmpFile.Path())
return o
}(),
},
{
expected: client.ExecCreateOptions{
Cmd: []string{"command"},
AttachStdout: true,
AttachStderr: true,
Env: []string{"ONE=1", "TWO=2", "ONE=override"},
},
options: func() ExecOptions {
o := withDefaultOpts(ExecOptions{})
_ = o.EnvFile.Set(tmpFile.Path())
_ = o.Env.Set("ONE=override")
return o
}(),
},
}
for i, testcase := range testcases {
t.Run("test "+strconv.Itoa(i+1), func(t *testing.T) {
execConfig, err := parseExec(testcase.options, &testcase.configFile)
assert.NilError(t, err)
assert.Check(t, is.DeepEqual(testcase.expected, *execConfig))
})
}
}
func TestParseExecErrors(t *testing.T) {
t.Run("missing env-file", func(t *testing.T) {
execOpts := withDefaultOpts(ExecOptions{})
assert.Check(t, execOpts.EnvFile.Set("no-such-env-file"))
execConfig, err := parseExec(execOpts, &configfile.ConfigFile{})
assert.ErrorContains(t, err, "no-such-env-file")
assert.Check(t, os.IsNotExist(err))
assert.Check(t, execConfig == nil)
})
t.Run("invalid detach keys", func(t *testing.T) {
execOpts := withDefaultOpts(ExecOptions{
DetachKeys: "shift-a",
})
execConfig, err := parseExec(execOpts, &configfile.ConfigFile{})
assert.Check(t, is.ErrorContains(err, "invalid detach keys (shift-a):"))
assert.Check(t, is.Nil(execConfig))
})
}
func TestRunExec(t *testing.T) {
testcases := []struct {
doc string
options ExecOptions
client *fakeClient
expectedError string
expectedOut string
expectedErr string
}{
{
doc: "successful detach",
options: withDefaultOpts(ExecOptions{
Detach: true,
}),
client: &fakeClient{execCreateFunc: execCreateWithID},
},
{
doc: "inspect error",
options: NewExecOptions(),
client: &fakeClient{
inspectFunc: func(string) (client.ContainerInspectResult, error) {
return client.ContainerInspectResult{}, errors.New("failed inspect")
},
},
expectedError: "failed inspect",
},
{
doc: "missing exec ID",
options: NewExecOptions(),
expectedError: "exec ID empty",
client: &fakeClient{},
},
}
for _, testcase := range testcases {
t.Run(testcase.doc, func(t *testing.T) {
fakeCLI := test.NewFakeCli(testcase.client)
err := RunExec(context.TODO(), fakeCLI, "the-container", testcase.options)
if testcase.expectedError != "" {
assert.ErrorContains(t, err, testcase.expectedError)
} else if !assert.Check(t, err) {
return
}
assert.Check(t, is.Equal(testcase.expectedOut, fakeCLI.OutBuffer().String()))
assert.Check(t, is.Equal(testcase.expectedErr, fakeCLI.ErrBuffer().String()))
})
}
}
func execCreateWithID(_ string, _ client.ExecCreateOptions) (client.ExecCreateResult, error) {
return client.ExecCreateResult{ID: "exec-id"}, nil
}
func TestGetExecExitStatus(t *testing.T) {
execID := "the exec id"
expectedErr := errors.New("unexpected error")
testcases := []struct {
inspectError error
exitCode int
expectedError error
}{
{
inspectError: nil,
exitCode: 0,
},
{
inspectError: expectedErr,
expectedError: expectedErr,
},
{
exitCode: 15,
expectedError: cli.StatusError{StatusCode: 15},
},
}
for _, testcase := range testcases {
apiClient := &fakeClient{
execInspectFunc: func(id string) (client.ExecInspectResult, error) {
assert.Check(t, is.Equal(execID, id))
return client.ExecInspectResult{ExitCode: testcase.exitCode}, testcase.inspectError
},
}
err := getExecExitStatus(context.Background(), apiClient, execID)
assert.Check(t, is.Equal(testcase.expectedError, err))
}
}
func TestNewExecCommandErrors(t *testing.T) {
testCases := []struct {
name string
args []string
expectedError string
containerInspectFunc func(img string) (client.ContainerInspectResult, error)
}{
{
name: "client-error",
args: []string{"5cb5bb5e4a3b", "-t", "-i", "bash"},
expectedError: "something went wrong",
containerInspectFunc: func(containerID string) (client.ContainerInspectResult, error) {
return client.ContainerInspectResult{}, errors.New("something went wrong")
},
},
}
for _, tc := range testCases {
fakeCLI := test.NewFakeCli(&fakeClient{inspectFunc: tc.containerInspectFunc})
cmd := newExecCommand(fakeCLI)
cmd.SetOut(io.Discard)
cmd.SetArgs(tc.args)
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
}
}