diff --git a/cli/command/container/attach.go b/cli/command/container/attach.go index a54e8fb530..d5303485f0 100644 --- a/cli/command/container/attach.go +++ b/cli/command/container/attach.go @@ -145,7 +145,8 @@ func RunAttach(ctx context.Context, dockerCLI command.Cli, containerID string, o detachKeys: options.DetachKeys, } - if err := streamer.stream(ctx); err != nil { + // if the context was canceled, this was likely intentional and we shouldn't return an error + if err := streamer.stream(ctx); err != nil && !errors.Is(err, context.Canceled) { return err } @@ -162,6 +163,9 @@ func getExitStatus(errC <-chan error, resultC <-chan container.WaitResponse) err return cli.StatusError{StatusCode: int(result.StatusCode)} } case err := <-errC: + if errors.Is(err, context.Canceled) { + return nil + } return err } diff --git a/cli/command/container/attach_test.go b/cli/command/container/attach_test.go index 33aa4f93e3..6ffa9ff3d7 100644 --- a/cli/command/container/attach_test.go +++ b/cli/command/container/attach_test.go @@ -1,6 +1,7 @@ package container import ( + "context" "io" "testing" @@ -117,6 +118,10 @@ func TestGetExitStatus(t *testing.T) { }, expectedError: cli.StatusError{StatusCode: 15}, }, + { + err: context.Canceled, + expectedError: nil, + }, } for _, testcase := range testcases { diff --git a/e2e/container/attach_test.go b/e2e/container/attach_test.go index 9b2ee9cdb8..6822ca60f7 100644 --- a/e2e/container/attach_test.go +++ b/e2e/container/attach_test.go @@ -1,11 +1,17 @@ package container import ( + "bytes" "fmt" + "os" + "os/exec" "strings" "testing" + "time" + "github.com/creack/pty" "github.com/docker/cli/e2e/internal/fixtures" + "gotest.tools/v3/assert" "gotest.tools/v3/icmd" ) @@ -23,3 +29,26 @@ func TestAttachExitCode(t *testing.T) { func withStdinNewline(cmd *icmd.Cmd) { cmd.Stdin = strings.NewReader("\n") } + +// Regression test for https://github.com/docker/cli/issues/5294 +func TestAttachInterrupt(t *testing.T) { + result := icmd.RunCommand("docker", "run", "-d", fixtures.AlpineImage, "sh", "-c", "sleep 5") + result.Assert(t, icmd.Success) + containerID := strings.TrimSpace(result.Stdout()) + + // run it as such so we can signal it later + c := exec.Command("docker", "attach", containerID) + d := bytes.Buffer{} + c.Stdout = &d + c.Stderr = &d + _, err := pty.Start(c) + assert.NilError(t, err) + + // have to wait a bit to give time for the command to execute/print + time.Sleep(500 * time.Millisecond) + c.Process.Signal(os.Interrupt) + + _ = c.Wait() + assert.Equal(t, c.ProcessState.ExitCode(), 0) + assert.Equal(t, d.String(), "") +}