Skip to content

Commit 4beadf1

Browse files
authored
Merge pull request #3591 from apostasie/series-4
Add support for writing to ptys
2 parents 66a7a75 + 11e6d04 commit 4beadf1

11 files changed

+370
-297
lines changed

cmd/nerdctl/container/container_attach_linux_test.go

+164-106
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,11 @@
1717
package container
1818

1919
import (
20-
"bytes"
20+
"errors"
21+
"os"
2122
"strings"
2223
"testing"
24+
"time"
2325

2426
"gotest.tools/v3/assert"
2527

@@ -28,133 +30,189 @@ import (
2830
"github.com/containerd/nerdctl/v2/pkg/testutil/test"
2931
)
3032

31-
// skipAttachForDocker should be called by attach-related tests that assert 'read detach keys' in stdout.
32-
func skipAttachForDocker(t *testing.T) {
33-
t.Helper()
34-
if testutil.GetTarget() == testutil.Docker {
35-
t.Skip("When detaching from a container, for a session started with 'docker attach'" +
36-
", it prints 'read escape sequence', but for one started with 'docker (run|start)', it prints nothing." +
37-
" However, the flag is called '--detach-keys' in all cases" +
38-
", so nerdctl prints 'read detach keys' for all cases" +
39-
", and that's why this test is skipped for Docker.")
40-
}
41-
}
42-
43-
// prepareContainerToAttach spins up a container (entrypoint = shell) with `-it` and detaches from it
44-
// so that it can be re-attached to later.
45-
func prepareContainerToAttach(base *testutil.Base, containerName string) {
46-
opts := []func(*testutil.Cmd){
47-
testutil.WithStdin(testutil.NewDelayOnceReader(bytes.NewReader(
48-
[]byte{16, 17}, // ctrl+p,ctrl+q, see https://www.physics.udel.edu/~watson/scen103/ascii.html
49-
))),
50-
}
51-
// unbuffer(1) emulates tty, which is required by `nerdctl run -t`.
52-
// unbuffer(1) can be installed with `apt-get install expect`.
53-
//
54-
// "-p" is needed because we need unbuffer to read from stdin, and from [1]:
55-
// "Normally, unbuffer does not read from stdin. This simplifies use of unbuffer in some situations.
56-
// To use unbuffer in a pipeline, use the -p flag."
57-
//
58-
// [1] https://linux.die.net/man/1/unbuffer
59-
base.CmdWithHelper([]string{"unbuffer", "-p"}, "run", "-it", "--name", containerName, testutil.CommonImage).
60-
CmdOption(opts...).AssertOutContains("read detach keys")
61-
container := base.InspectContainer(containerName)
62-
assert.Equal(base.T, container.State.Running, true)
63-
}
33+
/*
34+
Important notes:
35+
- for both docker and nerdctl, you can run+detach of a container and exit 0, while the container would actually fail starting
36+
- nerdctl (not docker): on run, detach will race anything on stdin before the detach sequence from reaching the container
37+
- nerdctl AND docker: on attach ^
38+
- exit code variants: https://github.com/containerd/nerdctl/issues/3571
39+
*/
6440

6541
func TestAttach(t *testing.T) {
66-
t.Parallel()
42+
// In nerdctl the detach return code from the container after attach is 0, but in docker the return code is 1.
43+
// This behaviour is reported in https://github.com/containerd/nerdctl/issues/3571
44+
ex := 0
45+
if nerdtest.IsDocker() {
46+
ex = 1
47+
}
6748

68-
t.Skip("This test is very unstable and currently skipped. See https://github.com/containerd/nerdctl/issues/3558")
49+
testCase := nerdtest.Setup()
6950

70-
skipAttachForDocker(t)
51+
testCase.Cleanup = func(data test.Data, helpers test.Helpers) {
52+
helpers.Anyhow("rm", "-f", data.Identifier())
53+
}
7154

72-
base := testutil.NewBase(t)
73-
containerName := testutil.Identifier(t)
55+
testCase.Setup = func(data test.Data, helpers test.Helpers) {
56+
cmd := helpers.Command("run", "--rm", "-it", "--name", data.Identifier(), testutil.CommonImage)
57+
cmd.WithPseudoTTY(func(f *os.File) error {
58+
// ctrl+p and ctrl+q (see https://en.wikipedia.org/wiki/C0_and_C1_control_codes)
59+
_, err := f.Write([]byte{16, 17})
60+
return err
61+
})
62+
63+
cmd.Run(&test.Expected{
64+
ExitCode: 0,
65+
Errors: []error{errors.New("read detach keys")},
66+
Output: func(stdout string, info string, t *testing.T) {
67+
assert.Assert(t, strings.Contains(helpers.Capture("inspect", "--format", "json", data.Identifier()), "\"Running\":true"))
68+
},
69+
})
70+
}
7471

75-
defer base.Cmd("container", "rm", "-f", containerName).AssertOK()
76-
prepareContainerToAttach(base, containerName)
72+
testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand {
73+
// Run interactively and detach
74+
cmd := helpers.Command("attach", data.Identifier())
75+
cmd.WithPseudoTTY(func(f *os.File) error {
76+
_, _ = f.WriteString("echo mark${NON}mark\n")
77+
// Interestingly, and unlike with run, on attach, docker (like nerdctl) ALSO needs a pause so that the
78+
// container can read stdin before we detach
79+
time.Sleep(time.Second)
80+
// ctrl+p and ctrl+q (see https://en.wikipedia.org/wiki/C0_and_C1_control_codes)
81+
_, err := f.Write([]byte{16, 17})
82+
83+
return err
84+
})
85+
86+
return cmd
87+
}
7788

78-
opts := []func(*testutil.Cmd){
79-
testutil.WithStdin(testutil.NewDelayOnceReader(strings.NewReader("expr 1 + 1\nexit\n"))),
89+
testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected {
90+
return &test.Expected{
91+
ExitCode: ex,
92+
Errors: []error{errors.New("read detach keys")},
93+
Output: test.All(
94+
test.Contains("markmark"),
95+
func(stdout string, info string, t *testing.T) {
96+
assert.Assert(t, strings.Contains(helpers.Capture("inspect", "--format", "json", data.Identifier()), "\"Running\":true"))
97+
},
98+
),
99+
}
80100
}
81-
// `unbuffer -p` returns 0 even if the underlying nerdctl process returns a non-zero exit code,
82-
// so the exit code cannot be easily tested here.
83-
base.CmdWithHelper([]string{"unbuffer", "-p"}, "attach", containerName).CmdOption(opts...).AssertOutContains("2")
84-
container := base.InspectContainer(containerName)
85-
assert.Equal(base.T, container.State.Running, false)
101+
102+
testCase.Run(t)
86103
}
87104

88105
func TestAttachDetachKeys(t *testing.T) {
89-
t.Parallel()
106+
// In nerdctl the detach return code from the container after attach is 0, but in docker the return code is 1.
107+
// This behaviour is reported in https://github.com/containerd/nerdctl/issues/3571
108+
ex := 0
109+
if nerdtest.IsDocker() {
110+
ex = 1
111+
}
90112

91-
skipAttachForDocker(t)
113+
testCase := nerdtest.Setup()
92114

93-
base := testutil.NewBase(t)
94-
containerName := testutil.Identifier(t)
115+
testCase.Cleanup = func(data test.Data, helpers test.Helpers) {
116+
helpers.Anyhow("rm", "-f", data.Identifier())
117+
}
95118

96-
defer base.Cmd("container", "rm", "-f", containerName).AssertOK()
97-
prepareContainerToAttach(base, containerName)
119+
testCase.Setup = func(data test.Data, helpers test.Helpers) {
120+
cmd := helpers.Command("run", "--rm", "-it", "--detach-keys=ctrl-q", "--name", data.Identifier(), testutil.CommonImage)
121+
cmd.WithPseudoTTY(func(f *os.File) error {
122+
_, err := f.Write([]byte{17})
123+
return err
124+
})
125+
126+
cmd.Run(&test.Expected{
127+
ExitCode: 0,
128+
Errors: []error{errors.New("read detach keys")},
129+
Output: func(stdout string, info string, t *testing.T) {
130+
assert.Assert(t, strings.Contains(helpers.Capture("inspect", "--format", "json", data.Identifier()), "\"Running\":true"))
131+
},
132+
})
133+
}
98134

99-
opts := []func(*testutil.Cmd){
100-
testutil.WithStdin(testutil.NewDelayOnceReader(bytes.NewReader(
101-
[]byte{1, 2}, // https://www.physics.udel.edu/~watson/scen103/ascii.html
102-
))),
135+
testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand {
136+
// Run interactively and detach
137+
cmd := helpers.Command("attach", "--detach-keys=ctrl-a,ctrl-b", data.Identifier())
138+
cmd.WithPseudoTTY(func(f *os.File) error {
139+
_, _ = f.WriteString("echo mark${NON}mark\n")
140+
// Interestingly, and unlike with run, on attach, docker (like nerdctl) ALSO needs a pause so that the
141+
// container can read stdin before we detach
142+
time.Sleep(time.Second)
143+
// ctrl+a and ctrl+b (see https://en.wikipedia.org/wiki/C0_and_C1_control_codes)
144+
_, err := f.Write([]byte{1, 2})
145+
146+
return err
147+
})
148+
149+
return cmd
103150
}
104-
base.CmdWithHelper([]string{"unbuffer", "-p"}, "attach", "--detach-keys=ctrl-a,ctrl-b", containerName).
105-
CmdOption(opts...).AssertOutContains("read detach keys")
106-
container := base.InspectContainer(containerName)
107-
assert.Equal(base.T, container.State.Running, true)
151+
152+
testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected {
153+
return &test.Expected{
154+
ExitCode: ex,
155+
Errors: []error{errors.New("read detach keys")},
156+
Output: test.All(
157+
test.Contains("markmark"),
158+
func(stdout string, info string, t *testing.T) {
159+
assert.Assert(t, strings.Contains(helpers.Capture("inspect", "--format", "json", data.Identifier()), "\"Running\":true"))
160+
},
161+
),
162+
}
163+
}
164+
165+
testCase.Run(t)
108166
}
109167

110168
// TestIssue3568 tests https://github.com/containerd/nerdctl/issues/3568
111-
func TestDetachAttachKeysForAutoRemovedContainer(t *testing.T) {
169+
func TestAttachForAutoRemovedContainer(t *testing.T) {
112170
testCase := nerdtest.Setup()
113171

114-
testCase.SubTests = []*test.Case{
115-
{
116-
Description: "Issue #3568 - A container should be deleted when detaching and attaching a container started with the --rm option.",
117-
// In nerdctl the detach return code from the container is 0, but in docker the return code is 1.
118-
// This behaviour is reported in https://github.com/containerd/nerdctl/issues/3571 so this test is skipped for Docker.
119-
Require: test.Require(
120-
test.Not(nerdtest.Docker),
121-
),
122-
Setup: func(data test.Data, helpers test.Helpers) {
123-
cmd := helpers.Command("run", "--rm", "-it", "--detach-keys=ctrl-a,ctrl-b", "--name", data.Identifier(), testutil.CommonImage)
124-
// unbuffer(1) can be installed with `apt-get install expect`.
125-
//
126-
// "-p" is needed because we need unbuffer to read from stdin, and from [1]:
127-
// "Normally, unbuffer does not read from stdin. This simplifies use of unbuffer in some situations.
128-
// To use unbuffer in a pipeline, use the -p flag."
129-
//
130-
// [1] https://linux.die.net/man/1/unbuffer
131-
cmd.WithWrapper("unbuffer", "-p")
132-
cmd.WithStdin(testutil.NewDelayOnceReader(bytes.NewReader([]byte{1, 2}))) // https://www.physics.udel.edu/~watson/scen103/ascii.html
133-
cmd.Run(&test.Expected{
134-
ExitCode: 0,
135-
})
136-
},
137-
Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
138-
cmd := helpers.Command("attach", data.Identifier())
139-
cmd.WithWrapper("unbuffer", "-p")
140-
cmd.WithStdin(testutil.NewDelayOnceReader(strings.NewReader("exit\n")))
141-
return cmd
142-
},
143-
Cleanup: func(data test.Data, helpers test.Helpers) {
144-
helpers.Anyhow("rm", "-f", data.Identifier())
145-
},
146-
Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
147-
return &test.Expected{
148-
ExitCode: 0,
149-
Errors: []error{},
150-
Output: test.All(
151-
func(stdout string, info string, t *testing.T) {
152-
assert.Assert(t, !strings.Contains(helpers.Capture("ps", "-a"), data.Identifier()))
153-
},
154-
),
155-
}
172+
testCase.Description = "Issue #3568 - A container should be deleted when detaching and attaching a container started with the --rm option."
173+
174+
testCase.Cleanup = func(data test.Data, helpers test.Helpers) {
175+
helpers.Anyhow("rm", "-f", data.Identifier())
176+
}
177+
178+
testCase.Setup = func(data test.Data, helpers test.Helpers) {
179+
cmd := helpers.Command("run", "--rm", "-it", "--detach-keys=ctrl-a,ctrl-b", "--name", data.Identifier(), testutil.CommonImage)
180+
cmd.WithPseudoTTY(func(f *os.File) error {
181+
// ctrl+a and ctrl+b (see https://en.wikipedia.org/wiki/C0_and_C1_control_codes)
182+
_, err := f.Write([]byte{1, 2})
183+
return err
184+
})
185+
186+
cmd.Run(&test.Expected{
187+
ExitCode: 0,
188+
Errors: []error{errors.New("read detach keys")},
189+
Output: func(stdout string, info string, t *testing.T) {
190+
assert.Assert(t, strings.Contains(helpers.Capture("inspect", "--format", "json", data.Identifier()), "\"Running\":true"), info)
156191
},
157-
},
192+
})
193+
}
194+
195+
testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand {
196+
// Run interactively and detach
197+
cmd := helpers.Command("attach", data.Identifier())
198+
cmd.WithPseudoTTY(func(f *os.File) error {
199+
_, err := f.WriteString("echo mark${NON}mark\nexit 42\n")
200+
return err
201+
})
202+
203+
return cmd
204+
}
205+
206+
testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected {
207+
return &test.Expected{
208+
ExitCode: 42,
209+
Output: test.All(
210+
test.Contains("markmark"),
211+
func(stdout string, info string, t *testing.T) {
212+
assert.Assert(t, !strings.Contains(helpers.Capture("ps", "-a"), data.Identifier()))
213+
},
214+
),
215+
}
158216
}
159217

160218
testCase.Run(t)

0 commit comments

Comments
 (0)