30c4637f03
If STDOUT or STDERR are attached and the container exits, the streams will be closed by the daemon while the container is exiting, causing the streamer to return an error https://github.com/docker/cli/blob/61b02e636d2afed778f2e871c3ec1c6adee69ca4/cli/command/container/hijack.go#L53 that gets sent https://github.com/docker/cli/blob/61b02e636d2afed778f2e871c3ec1c6adee69ca4/cli/command/container/run.go#L278 and received https://github.com/docker/cli/blob/61b02e636d2afed778f2e871c3ec1c6adee69ca4/cli/command/container/run.go#L225 on `errCh`. However, if only STDIN is attached, it's not closed (since this is attached to the user's TTY) when the container exits, so the streamer doesn't exit and nothing gets sent on `errCh`, meaning the CLI execution hangs receiving on `errCh` on L231. Change the logic to receive on both `errCh` and `statusChan` – this way, if the container exits, we get notified on `statusChan` (even if only STDIN is attached), and can cancel the streamer and exit. Signed-off-by: Laura Brehm <laurabrehm@hey.com>
284 lines
8.9 KiB
Go
284 lines
8.9 KiB
Go
package container
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"os/exec"
|
|
"strings"
|
|
"syscall"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/creack/pty"
|
|
"github.com/docker/cli/e2e/internal/fixtures"
|
|
"github.com/docker/cli/internal/test/environment"
|
|
"github.com/docker/docker/api/types/versions"
|
|
"gotest.tools/v3/assert"
|
|
is "gotest.tools/v3/assert/cmp"
|
|
"gotest.tools/v3/golden"
|
|
"gotest.tools/v3/icmd"
|
|
"gotest.tools/v3/poll"
|
|
"gotest.tools/v3/skip"
|
|
)
|
|
|
|
const registryPrefix = "registry:5000"
|
|
|
|
func TestRunAttachedFromRemoteImageAndRemove(t *testing.T) {
|
|
skip.If(t, environment.RemoteDaemon())
|
|
|
|
// Digests in golden file are linux/amd64 specific.
|
|
// TODO: Fix this test and make it work on all platforms.
|
|
environment.SkipIfNotPlatform(t, "linux/amd64")
|
|
|
|
image := createRemoteImage(t)
|
|
|
|
result := icmd.RunCommand("docker", "run", "--rm", image,
|
|
"echo", "this", "is", "output")
|
|
|
|
result.Assert(t, icmd.Success)
|
|
assert.Check(t, is.Equal("this is output\n", result.Stdout()))
|
|
golden.Assert(t, result.Stderr(), "run-attached-from-remote-and-remove.golden")
|
|
}
|
|
|
|
func TestRunAttach(t *testing.T) {
|
|
skip.If(t, environment.RemoteDaemon())
|
|
t.Parallel()
|
|
|
|
streams := []string{"stdin", "stdout", "stderr"}
|
|
for _, stream := range streams {
|
|
t.Run(stream, func(t *testing.T) {
|
|
t.Parallel()
|
|
c := exec.Command("docker", "run", "-a", stream, "--rm", "alpine",
|
|
"sh", "-c", "sleep 1 && exit 7")
|
|
d := bytes.Buffer{}
|
|
c.Stdout = &d
|
|
c.Stderr = &d
|
|
_, err := pty.Start(c)
|
|
assert.NilError(t, err)
|
|
|
|
done := make(chan error)
|
|
go func() {
|
|
done <- c.Wait()
|
|
}()
|
|
|
|
select {
|
|
case <-time.After(20 * time.Second):
|
|
t.Fatal("docker run took too long, likely hang", d.String())
|
|
case <-done:
|
|
}
|
|
|
|
assert.Equal(t, c.ProcessState.ExitCode(), 7)
|
|
assert.Check(t, is.Contains(d.String(), "exit status 7"))
|
|
})
|
|
}
|
|
}
|
|
|
|
// Regression test for https://github.com/docker/cli/issues/5053
|
|
func TestRunInvalidEntrypointWithAutoremove(t *testing.T) {
|
|
environment.SkipIfDaemonNotLinux(t)
|
|
|
|
result := make(chan *icmd.Result)
|
|
go func() {
|
|
result <- icmd.RunCommand("docker", "run", "--rm", fixtures.AlpineImage, "invalidcommand")
|
|
}()
|
|
select {
|
|
case r := <-result:
|
|
r.Assert(t, icmd.Expected{ExitCode: 127})
|
|
case <-time.After(4 * time.Second):
|
|
t.Fatal("test took too long, shouldn't hang")
|
|
}
|
|
}
|
|
|
|
func TestRunWithContentTrust(t *testing.T) {
|
|
skip.If(t, environment.RemoteDaemon())
|
|
|
|
dir := fixtures.SetupConfigFile(t)
|
|
defer dir.Remove()
|
|
image := fixtures.CreateMaskedTrustedRemoteImage(t, registryPrefix, "trust-run", "latest")
|
|
|
|
defer func() {
|
|
icmd.RunCommand("docker", "image", "rm", image).Assert(t, icmd.Success)
|
|
}()
|
|
|
|
result := icmd.RunCmd(
|
|
icmd.Command("docker", "run", image),
|
|
fixtures.WithConfig(dir.Path()),
|
|
fixtures.WithTrust,
|
|
fixtures.WithNotary,
|
|
)
|
|
result.Assert(t, icmd.Expected{
|
|
Err: fmt.Sprintf("Tagging %s@sha", image[:len(image)-7]),
|
|
})
|
|
}
|
|
|
|
func TestUntrustedRun(t *testing.T) {
|
|
dir := fixtures.SetupConfigFile(t)
|
|
defer dir.Remove()
|
|
image := registryPrefix + "/alpine:untrusted"
|
|
// tag the image and upload it to the private registry
|
|
icmd.RunCommand("docker", "tag", fixtures.AlpineImage, image).Assert(t, icmd.Success)
|
|
defer func() {
|
|
icmd.RunCommand("docker", "image", "rm", image).Assert(t, icmd.Success)
|
|
}()
|
|
|
|
// try trusted run on untrusted tag
|
|
result := icmd.RunCmd(
|
|
icmd.Command("docker", "run", image),
|
|
fixtures.WithConfig(dir.Path()),
|
|
fixtures.WithTrust,
|
|
fixtures.WithNotary,
|
|
)
|
|
result.Assert(t, icmd.Expected{
|
|
ExitCode: 125,
|
|
Err: "does not have trust data for",
|
|
})
|
|
}
|
|
|
|
func TestTrustedRunFromBadTrustServer(t *testing.T) {
|
|
evilImageName := registryPrefix + "/evil-alpine:latest"
|
|
dir := fixtures.SetupConfigFile(t)
|
|
defer dir.Remove()
|
|
|
|
// tag the image and upload it to the private registry
|
|
icmd.RunCmd(icmd.Command("docker", "tag", fixtures.AlpineImage, evilImageName),
|
|
fixtures.WithConfig(dir.Path()),
|
|
).Assert(t, icmd.Success)
|
|
icmd.RunCmd(icmd.Command("docker", "image", "push", evilImageName),
|
|
fixtures.WithConfig(dir.Path()),
|
|
fixtures.WithPassphrase("root_password", "repo_password"),
|
|
fixtures.WithTrust,
|
|
fixtures.WithNotary,
|
|
).Assert(t, icmd.Success)
|
|
icmd.RunCmd(icmd.Command("docker", "image", "rm", evilImageName)).Assert(t, icmd.Success)
|
|
|
|
// try run
|
|
icmd.RunCmd(icmd.Command("docker", "run", evilImageName),
|
|
fixtures.WithConfig(dir.Path()),
|
|
fixtures.WithTrust,
|
|
fixtures.WithNotary,
|
|
).Assert(t, icmd.Success)
|
|
icmd.RunCmd(icmd.Command("docker", "image", "rm", evilImageName)).Assert(t, icmd.Success)
|
|
|
|
// init a client with the evil-server and a new trust dir
|
|
evilNotaryDir := fixtures.SetupConfigWithNotaryURL(t, "evil-test", fixtures.EvilNotaryURL)
|
|
defer evilNotaryDir.Remove()
|
|
|
|
// tag the same image and upload it to the private registry but signed with evil notary server
|
|
icmd.RunCmd(icmd.Command("docker", "tag", fixtures.AlpineImage, evilImageName),
|
|
fixtures.WithConfig(evilNotaryDir.Path()),
|
|
).Assert(t, icmd.Success)
|
|
icmd.RunCmd(icmd.Command("docker", "image", "push", evilImageName),
|
|
fixtures.WithConfig(evilNotaryDir.Path()),
|
|
fixtures.WithPassphrase("root_password", "repo_password"),
|
|
fixtures.WithTrust,
|
|
fixtures.WithNotaryServer(fixtures.EvilNotaryURL),
|
|
).Assert(t, icmd.Success)
|
|
icmd.RunCmd(icmd.Command("docker", "image", "rm", evilImageName)).Assert(t, icmd.Success)
|
|
|
|
// try running with the original client from the evil notary server. This should failed
|
|
// because the new root is invalid
|
|
icmd.RunCmd(icmd.Command("docker", "run", evilImageName),
|
|
fixtures.WithConfig(dir.Path()),
|
|
fixtures.WithTrust,
|
|
fixtures.WithNotaryServer(fixtures.EvilNotaryURL),
|
|
).Assert(t, icmd.Expected{
|
|
ExitCode: 125,
|
|
Err: "could not rotate trust to a new trusted root",
|
|
})
|
|
}
|
|
|
|
// TODO: create this with registry API instead of engine API
|
|
func createRemoteImage(t *testing.T) string {
|
|
t.Helper()
|
|
image := registryPrefix + "/alpine:test-run-pulls"
|
|
icmd.RunCommand("docker", "pull", fixtures.AlpineImage).Assert(t, icmd.Success)
|
|
icmd.RunCommand("docker", "tag", fixtures.AlpineImage, image).Assert(t, icmd.Success)
|
|
icmd.RunCommand("docker", "push", image).Assert(t, icmd.Success)
|
|
icmd.RunCommand("docker", "rmi", image).Assert(t, icmd.Success)
|
|
return image
|
|
}
|
|
|
|
func TestRunWithCgroupNamespace(t *testing.T) {
|
|
environment.SkipIfDaemonNotLinux(t)
|
|
environment.SkipIfCgroupNamespacesNotSupported(t)
|
|
|
|
result := icmd.RunCommand("docker", "run", "--cgroupns=private", "--rm", fixtures.AlpineImage,
|
|
"cat", "/sys/fs/cgroup/cgroup.controllers")
|
|
result.Assert(t, icmd.Success)
|
|
}
|
|
|
|
func TestMountSubvolume(t *testing.T) {
|
|
skip.If(t, versions.LessThan(environment.DaemonAPIVersion(t), "1.45"))
|
|
|
|
volName := "test-volume-" + t.Name()
|
|
icmd.RunCommand("docker", "volume", "create", volName).Assert(t, icmd.Success)
|
|
|
|
t.Cleanup(func() {
|
|
icmd.RunCommand("docker", "volume", "remove", "-f", volName).Assert(t, icmd.Success)
|
|
})
|
|
|
|
defaultMountOpts := []string{
|
|
"type=volume",
|
|
"src=" + volName,
|
|
"dst=/volume",
|
|
}
|
|
|
|
// Populate the volume with test data.
|
|
icmd.RunCommand("docker", "run", "--rm", "--mount", strings.Join(defaultMountOpts, ","), fixtures.AlpineImage, "sh", "-c",
|
|
"echo foo > /volume/bar.txt && "+
|
|
"mkdir /volume/etc && echo root > /volume/etc/passwd && "+
|
|
"mkdir /volume/subdir && echo world > /volume/subdir/hello.txt;",
|
|
).Assert(t, icmd.Success)
|
|
|
|
runMount := func(cmd string, mountOpts ...string) *icmd.Result {
|
|
mountArg := strings.Join(append(defaultMountOpts, mountOpts...), ",")
|
|
return icmd.RunCommand("docker", "run", "--rm", "--mount", mountArg, fixtures.AlpineImage, cmd, "/volume")
|
|
}
|
|
|
|
for _, tc := range []struct {
|
|
name string
|
|
cmd string
|
|
subpath string
|
|
|
|
expectedOut string
|
|
expectedErr string
|
|
expectedCode int
|
|
}{
|
|
{name: "absolute", cmd: "cat", subpath: "/etc/passwd", expectedErr: "subpath must be a relative path within the volume", expectedCode: 125},
|
|
{name: "subpath not exists", cmd: "ls", subpath: "some-path/that/doesnt-exist", expectedErr: "cannot access path ", expectedCode: 127},
|
|
{name: "subdirectory mount", cmd: "ls", subpath: "subdir", expectedOut: "hello.txt"},
|
|
{name: "file mount", cmd: "cat", subpath: "bar.txt", expectedOut: "foo"},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
runMount(tc.cmd, "volume-subpath="+tc.subpath).Assert(t, icmd.Expected{
|
|
Err: tc.expectedErr,
|
|
ExitCode: tc.expectedCode,
|
|
Out: tc.expectedOut,
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestProcessTermination(t *testing.T) {
|
|
var out bytes.Buffer
|
|
cmd := icmd.Command("docker", "run", "--rm", "-i", fixtures.AlpineImage,
|
|
"sh", "-c", "echo 'starting trap'; trap 'echo got signal; exit 0;' TERM; while true; do sleep 10; done")
|
|
cmd.Stdout = &out
|
|
cmd.Stderr = &out
|
|
|
|
result := icmd.StartCmd(cmd).Assert(t, icmd.Success)
|
|
|
|
poll.WaitOn(t, func(t poll.LogT) poll.Result {
|
|
if strings.Contains(result.Stdout(), "starting trap") {
|
|
return poll.Success()
|
|
}
|
|
return poll.Continue("waiting for process to trap signal")
|
|
}, poll.WithDelay(1*time.Second), poll.WithTimeout(5*time.Second))
|
|
|
|
assert.NilError(t, result.Cmd.Process.Signal(syscall.SIGTERM))
|
|
|
|
icmd.WaitOnCmd(time.Second*10, result).Assert(t, icmd.Expected{
|
|
ExitCode: 0,
|
|
})
|
|
}
|