Skip to content

Commit 9cd00de

Browse files
authored
Merge pull request #2108 from davidhsingyuchen/add-attach
feat: add 'nerdctl container attach'
2 parents d4c8093 + f0140a5 commit 9cd00de

File tree

9 files changed

+394
-9
lines changed

9 files changed

+394
-9
lines changed

cmd/nerdctl/container.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ func newContainerCommand() *cobra.Command {
5050
newRenameCommand(),
5151
newContainerPruneCommand(),
5252
newStatsCommand(),
53+
newAttachCommand(),
5354
)
5455
addCpCommand(containerCommand)
5556
return containerCommand

cmd/nerdctl/container_attach.go

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/*
2+
Copyright The containerd Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package main
18+
19+
import (
20+
"github.com/containerd/containerd"
21+
"github.com/containerd/nerdctl/pkg/api/types"
22+
"github.com/containerd/nerdctl/pkg/clientutil"
23+
"github.com/containerd/nerdctl/pkg/cmd/container"
24+
"github.com/containerd/nerdctl/pkg/consoleutil"
25+
"github.com/spf13/cobra"
26+
)
27+
28+
func newAttachCommand() *cobra.Command {
29+
var attachCommand = &cobra.Command{
30+
Use: "attach [flags] CONTAINER",
31+
Args: cobra.ExactArgs(1),
32+
Short: `Attach stdin, stdout, and stderr to a running container. For example:
33+
34+
1. 'nerdctl run -it --name test busybox' to start a container with a pty
35+
2. 'ctrl-p ctrl-q' to detach from the container
36+
3. 'nerdctl attach test' to attach to the container
37+
38+
Caveats:
39+
40+
- Currently only one attach session is allowed. When the second session tries to attach, currently no error will be returned from nerdctl.
41+
However, since behind the scenes, there's only one FIFO for stdin, stdout, and stderr respectively,
42+
if there are multiple sessions, all the sessions will be reading from and writing to the same 3 FIFOs, which will result in mixed input and partial output.
43+
- Until dual logging (issue #1946) is implemented,
44+
a container that is spun up by either 'nerdctl run -d' or 'nerdctl start' (without '--attach') cannot be attached to.`,
45+
RunE: containerAttachAction,
46+
ValidArgsFunction: attachShellComplete,
47+
SilenceUsage: true,
48+
SilenceErrors: true,
49+
}
50+
attachCommand.Flags().String("detach-keys", consoleutil.DefaultDetachKeys, "Override the default detach keys")
51+
return attachCommand
52+
}
53+
54+
func processContainerAttachOptions(cmd *cobra.Command) (types.ContainerAttachOptions, error) {
55+
globalOptions, err := processRootCmdFlags(cmd)
56+
if err != nil {
57+
return types.ContainerAttachOptions{}, err
58+
}
59+
detachKeys, err := cmd.Flags().GetString("detach-keys")
60+
if err != nil {
61+
return types.ContainerAttachOptions{}, err
62+
}
63+
return types.ContainerAttachOptions{
64+
GOptions: globalOptions,
65+
Stdin: cmd.InOrStdin(),
66+
Stdout: cmd.OutOrStdout(),
67+
Stderr: cmd.ErrOrStderr(),
68+
DetachKeys: detachKeys,
69+
}, nil
70+
}
71+
72+
func containerAttachAction(cmd *cobra.Command, args []string) error {
73+
options, err := processContainerAttachOptions(cmd)
74+
if err != nil {
75+
return err
76+
}
77+
78+
client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), options.GOptions.Namespace, options.GOptions.Address)
79+
if err != nil {
80+
return err
81+
}
82+
defer cancel()
83+
84+
return container.Attach(ctx, client, args[0], options)
85+
}
86+
87+
func attachShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
88+
statusFilterFn := func(st containerd.ProcessStatus) bool {
89+
return st == containerd.Running
90+
}
91+
return shellCompleteContainerNames(cmd, statusFilterFn)
92+
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
/*
2+
Copyright The containerd Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package main
18+
19+
import (
20+
"bytes"
21+
"strings"
22+
"testing"
23+
24+
"github.com/containerd/nerdctl/pkg/testutil"
25+
"gotest.tools/v3/assert"
26+
)
27+
28+
// skipAttachForDocker should be called by attach-related tests that assert 'read detach keys' in stdout.
29+
func skipAttachForDocker(t *testing.T) {
30+
t.Helper()
31+
if testutil.GetTarget() == testutil.Docker {
32+
t.Skip("When detaching from a container, for a session started with 'docker attach'" +
33+
", it prints 'read escape sequence', but for one started with 'docker (run|start)', it prints nothing." +
34+
" However, the flag is called '--detach-keys' in all cases" +
35+
", so nerdctl prints 'read detach keys' for all cases" +
36+
", and that's why this test is skipped for Docker.")
37+
}
38+
}
39+
40+
// prepareContainerToAttach spins up a container (entrypoint = shell) with `-it` and detaches from it
41+
// so that it can be re-attached to later.
42+
func prepareContainerToAttach(base *testutil.Base, containerName string) {
43+
opts := []func(*testutil.Cmd){
44+
testutil.WithStdin(testutil.NewDelayOnceReader(bytes.NewReader(
45+
[]byte{16, 17}, // ctrl+p,ctrl+q, see https://www.physics.udel.edu/~watson/scen103/ascii.html
46+
))),
47+
}
48+
// unbuffer(1) emulates tty, which is required by `nerdctl run -t`.
49+
// unbuffer(1) can be installed with `apt-get install expect`.
50+
//
51+
// "-p" is needed because we need unbuffer to read from stdin, and from [1]:
52+
// "Normally, unbuffer does not read from stdin. This simplifies use of unbuffer in some situations.
53+
// To use unbuffer in a pipeline, use the -p flag."
54+
//
55+
// [1] https://linux.die.net/man/1/unbuffer
56+
base.CmdWithHelper([]string{"unbuffer", "-p"}, "run", "-it", "--name", containerName, testutil.CommonImage).
57+
CmdOption(opts...).AssertOutContains("read detach keys")
58+
container := base.InspectContainer(containerName)
59+
assert.Equal(base.T, container.State.Running, true)
60+
}
61+
62+
func TestAttach(t *testing.T) {
63+
t.Parallel()
64+
65+
skipAttachForDocker(t)
66+
67+
base := testutil.NewBase(t)
68+
containerName := testutil.Identifier(t)
69+
70+
defer base.Cmd("container", "rm", "-f", containerName).AssertOK()
71+
prepareContainerToAttach(base, containerName)
72+
73+
opts := []func(*testutil.Cmd){
74+
testutil.WithStdin(testutil.NewDelayOnceReader(strings.NewReader("expr 1 + 1\nexit\n"))),
75+
}
76+
// `unbuffer -p` returns 0 even if the underlying nerdctl process returns a non-zero exit code,
77+
// so the exit code cannot be easily tested here.
78+
base.CmdWithHelper([]string{"unbuffer", "-p"}, "attach", containerName).CmdOption(opts...).AssertOutContains("2")
79+
container := base.InspectContainer(containerName)
80+
assert.Equal(base.T, container.State.Running, false)
81+
}
82+
83+
func TestAttachDetachKeys(t *testing.T) {
84+
t.Parallel()
85+
86+
skipAttachForDocker(t)
87+
88+
base := testutil.NewBase(t)
89+
containerName := testutil.Identifier(t)
90+
91+
defer base.Cmd("container", "rm", "-f", containerName).AssertOK()
92+
prepareContainerToAttach(base, containerName)
93+
94+
opts := []func(*testutil.Cmd){
95+
testutil.WithStdin(testutil.NewDelayOnceReader(bytes.NewReader(
96+
[]byte{1, 2}, // https://www.physics.udel.edu/~watson/scen103/ascii.html
97+
))),
98+
}
99+
base.CmdWithHelper([]string{"unbuffer", "-p"}, "attach", "--detach-keys=ctrl-a,ctrl-b", containerName).
100+
CmdOption(opts...).AssertOutContains("read detach keys")
101+
container := base.InspectContainer(containerName)
102+
assert.Equal(base.T, container.State.Running, true)
103+
}

cmd/nerdctl/container_start_linux_test.go

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,7 @@ import (
2828
func TestStartDetachKeys(t *testing.T) {
2929
t.Parallel()
3030

31-
if testutil.GetTarget() == testutil.Docker {
32-
t.Skip("When detaching from a container, for a session started with 'docker attach'" +
33-
", it prints 'read escape sequence', but for one started with 'docker (run|start)', it prints nothing." +
34-
" However, the flag is called '--detach-keys' in all cases" +
35-
", so nerdctl prints 'read detach keys' for all cases" +
36-
", and that's why this test is skipped for Docker.")
37-
}
31+
skipAttachForDocker(t)
3832

3933
base := testutil.NewBase(t)
4034
containerName := testutil.Identifier(t)

cmd/nerdctl/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,7 @@ Config file ($NERDCTL_TOML): %s
259259
newCommitCommand(),
260260
newWaitCommand(),
261261
newRenameCommand(),
262+
newAttachCommand(),
262263
// #endregion
263264

264265
// Build

docs/command-reference.md

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ It does not necessarily mean that the corresponding features are missing in cont
3131
- [:whale: nerdctl pause](#whale-nerdctl-pause)
3232
- [:whale: nerdctl unpause](#whale-nerdctl-unpause)
3333
- [:whale: nerdctl rename](#whale-nerdctl-rename)
34+
- [:whale: nerdctl attach](#whale-nerdctl-attach)
3435
- [:whale: nerdctl container prune](#whale-nerdctl-container-prune)
3536
- [Build](#build)
3637
- [:whale: nerdctl build](#whale-nerdctl-build)
@@ -609,6 +610,30 @@ Rename a container.
609610

610611
Usage: `nerdctl rename CONTAINER NEW_NAME`
611612

613+
### :whale: nerdctl attach
614+
615+
Attach stdin, stdout, and stderr to a running container. For example:
616+
617+
1. `nerdctl run -it --name test busybox` to start a container with a pty
618+
2. `ctrl-p ctrl-q` to detach from the container
619+
3. `nerdctl attach test` to attach to the container
620+
621+
Caveats:
622+
623+
- Currently only one attach session is allowed. When the second session tries to attach, currently no error will be returned from nerdctl.
624+
However, since behind the scenes, there's only one FIFO for stdin, stdout, and stderr respectively,
625+
if there are multiple sessions, all the sessions will be reading from and writing to the same 3 FIFOs, which will result in mixed input and partial output.
626+
- Until dual logging (issue #1946) is implemented,
627+
a container that is spun up by either `nerdctl run -d` or `nerdctl start` (without `--attach`) cannot be attached to.
628+
629+
Usage: `nerdctl attach CONTAINER`
630+
631+
Flags:
632+
633+
- :whale: `--detach-keys`: Override the default detach keys
634+
635+
Unimplemented `docker attach` flags: `--no-stdin`, `--sig-proxy`
636+
612637
### :whale: nerdctl container prune
613638

614639
Remove all stopped containers.
@@ -1620,7 +1645,6 @@ See [`./config.md`](./config.md).
16201645

16211646
Container management:
16221647

1623-
- `docker attach`
16241648
- `docker diff`
16251649
- `docker checkpoint *`
16261650

pkg/api/types/container_types.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,18 @@ type ContainerWaitOptions struct {
370370
GOptions GlobalCommandOptions
371371
}
372372

373+
// ContainerAttachOptions specifies options for `nerdctl (container) attach`.
374+
type ContainerAttachOptions struct {
375+
Stdin io.Reader
376+
Stdout io.Writer
377+
Stderr io.Writer
378+
379+
// GOptions is the global options.
380+
GOptions GlobalCommandOptions
381+
// DetachKeys is the key sequences to detach from the container.
382+
DetachKeys string
383+
}
384+
373385
// ContainerExecOptions specifies options for `nerdctl (container) exec`
374386
type ContainerExecOptions struct {
375387
GOptions GlobalCommandOptions

0 commit comments

Comments
 (0)