Skip to content

Commit e6e47df

Browse files
authored
Merge pull request containerd#3614 from haytok/issue_3568
fix: not to be deleted a container created with --rm when detaching
2 parents 9bd0ab9 + 718e7cd commit e6e47df

File tree

7 files changed

+156
-1
lines changed

7 files changed

+156
-1
lines changed

cmd/nerdctl/container/container_attach_linux_test.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ import (
2424
"gotest.tools/v3/assert"
2525

2626
"github.com/containerd/nerdctl/v2/pkg/testutil"
27+
"github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest"
28+
"github.com/containerd/nerdctl/v2/pkg/testutil/test"
2729
)
2830

2931
// skipAttachForDocker should be called by attach-related tests that assert 'read detach keys' in stdout.
@@ -104,3 +106,56 @@ func TestAttachDetachKeys(t *testing.T) {
104106
container := base.InspectContainer(containerName)
105107
assert.Equal(base.T, container.State.Running, true)
106108
}
109+
110+
// TestIssue3568 tests https://github.com/containerd/nerdctl/issues/3568
111+
func TestDetachAttachKeysForAutoRemovedContainer(t *testing.T) {
112+
testCase := nerdtest.Setup()
113+
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+
}
156+
},
157+
},
158+
}
159+
160+
testCase.Run(t)
161+
}

cmd/nerdctl/container/container_run.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,8 @@ func processCreateCommandFlagsInRun(cmd *cobra.Command) (types.ContainerCreateOp
334334
// runAction is heavily based on ctr implementation:
335335
// https://github.com/containerd/containerd/blob/v1.4.3/cmd/ctr/commands/run/run.go
336336
func runAction(cmd *cobra.Command, args []string) error {
337+
var isDetached bool
338+
337339
createOpt, err := processCreateCommandFlagsInRun(cmd)
338340
if err != nil {
339341
return err
@@ -378,8 +380,11 @@ func runAction(cmd *cobra.Command, args []string) error {
378380
}()
379381

380382
id := c.ID()
381-
if createOpt.Rm && !createOpt.Detach {
383+
if createOpt.Rm {
382384
defer func() {
385+
if isDetached {
386+
return
387+
}
383388
if err := netManager.CleanupNetworking(ctx, c); err != nil {
384389
log.L.Warnf("failed to clean up container networking: %s", err)
385390
}
@@ -449,6 +454,7 @@ func runAction(cmd *cobra.Command, args []string) error {
449454
return errors.New("got a nil IO from the task")
450455
}
451456
io.Wait()
457+
isDetached = true
452458
case status := <-statusC:
453459
if createOpt.Rm {
454460
if _, taskDeleteErr := task.Delete(ctx); taskDeleteErr != nil {

cmd/nerdctl/container/container_run_linux_test.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import (
4040
"github.com/containerd/nerdctl/v2/pkg/strutil"
4141
"github.com/containerd/nerdctl/v2/pkg/testutil"
4242
"github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest"
43+
"github.com/containerd/nerdctl/v2/pkg/testutil/test"
4344
)
4445

4546
func TestRunCustomRootfs(t *testing.T) {
@@ -520,3 +521,49 @@ func TestRunWithTtyAndDetached(t *testing.T) {
520521
withTtyContainer := base.InspectContainer(withTtyContainerName)
521522
assert.Equal(base.T, 0, withTtyContainer.State.ExitCode)
522523
}
524+
525+
// TestIssue3568 tests https://github.com/containerd/nerdctl/issues/3568
526+
func TestIssue3568(t *testing.T) {
527+
testCase := nerdtest.Setup()
528+
529+
testCase.SubTests = []*test.Case{
530+
{
531+
Description: "Issue #3568 - Detaching from a container started by using --rm option causes the container to be deleted.",
532+
// When detaching from a container, for a session started with 'docker attach', it prints 'read escape sequence', but for one started with 'docker (run|start)', it prints nothing.
533+
// However, the flag is called '--detach-keys' in all cases, so nerdctl prints 'read detach keys' for all cases, and that's why this test is skipped for Docker.
534+
Require: test.Require(
535+
test.Not(nerdtest.Docker),
536+
),
537+
Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
538+
cmd := helpers.Command("run", "--rm", "-it", "--detach-keys=ctrl-a,ctrl-b", "--name", data.Identifier(), testutil.CommonImage)
539+
// unbuffer(1) can be installed with `apt-get install expect`.
540+
//
541+
// "-p" is needed because we need unbuffer to read from stdin, and from [1]:
542+
// "Normally, unbuffer does not read from stdin. This simplifies use of unbuffer in some situations.
543+
// To use unbuffer in a pipeline, use the -p flag."
544+
//
545+
// [1] https://linux.die.net/man/1/unbuffer
546+
cmd.WithWrapper("unbuffer", "-p")
547+
cmd.WithStdin(testutil.NewDelayOnceReader(bytes.NewReader([]byte{1, 2}))) // https://www.physics.udel.edu/~watson/scen103/ascii.html
548+
return cmd
549+
},
550+
Cleanup: func(data test.Data, helpers test.Helpers) {
551+
helpers.Anyhow("rm", "-f", data.Identifier())
552+
},
553+
Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
554+
return &test.Expected{
555+
ExitCode: 0,
556+
Errors: []error{},
557+
Output: test.All(
558+
test.Contains("read detach keys"),
559+
func(stdout string, info string, t *testing.T) {
560+
assert.Assert(t, strings.Contains(helpers.Capture("ps"), data.Identifier()))
561+
},
562+
),
563+
}
564+
},
565+
},
566+
}
567+
568+
testCase.Run(t)
569+
}

pkg/cmd/container/attach.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,19 @@ import (
2828

2929
"github.com/containerd/nerdctl/v2/pkg/api/types"
3030
"github.com/containerd/nerdctl/v2/pkg/consoleutil"
31+
"github.com/containerd/nerdctl/v2/pkg/containerutil"
3132
"github.com/containerd/nerdctl/v2/pkg/errutil"
3233
"github.com/containerd/nerdctl/v2/pkg/idutil/containerwalker"
34+
"github.com/containerd/nerdctl/v2/pkg/labels"
3335
"github.com/containerd/nerdctl/v2/pkg/signalutil"
3436
)
3537

3638
// Attach attaches stdin, stdout, and stderr to a running container.
3739
func Attach(ctx context.Context, client *containerd.Client, req string, options types.ContainerAttachOptions) error {
3840
// Find the container.
3941
var container containerd.Container
42+
var cStatus containerd.Status
43+
4044
walker := &containerwalker.ContainerWalker{
4145
Client: client,
4246
OnFound: func(ctx context.Context, found containerwalker.Found) error {
@@ -54,6 +58,24 @@ func Attach(ctx context.Context, client *containerd.Client, req string, options
5458
return fmt.Errorf("more than one containers are found given the string: %s", req)
5559
}
5660

61+
defer func() {
62+
containerLabels, err := container.Labels(ctx)
63+
if err != nil {
64+
log.G(ctx).WithError(err).Errorf("failed to getting container labels: %s", err)
65+
return
66+
}
67+
rm, err := containerutil.DecodeContainerRmOptLabel(containerLabels[labels.ContainerAutoRemove])
68+
if err != nil {
69+
log.G(ctx).WithError(err).Errorf("failed to decode string to bool value: %s", err)
70+
return
71+
}
72+
if rm && cStatus.Status == containerd.Stopped {
73+
if err = RemoveContainer(ctx, container, options.GOptions, true, true, client); err != nil {
74+
log.L.WithError(err).Warnf("failed to remove container %s: %s", req, err)
75+
}
76+
}
77+
}()
78+
5779
// Attach to the container.
5880
var task containerd.Task
5981
detachC := make(chan struct{})
@@ -129,6 +151,10 @@ func Attach(ctx context.Context, client *containerd.Client, req string, options
129151
}
130152
io.Wait()
131153
case status := <-statusC:
154+
cStatus, err = task.Status(ctx)
155+
if err != nil {
156+
return err
157+
}
132158
code, _, err := status.Result()
133159
if err != nil {
134160
return err

pkg/cmd/container/create.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,8 @@ func Create(ctx context.Context, client *containerd.Client, args []string, netMa
340340
}
341341
}
342342

343+
internalLabels.rm = containerutil.EncodeContainerRmOptLabel(options.Rm)
344+
343345
// TODO: abolish internal labels and only use annotations
344346
ilOpt, err := withInternalLabels(internalLabels)
345347
if err != nil {
@@ -655,6 +657,8 @@ type internalLabels struct {
655657
ipc string
656658
// log
657659
logURI string
660+
// a label to check whether the --rm option is specified.
661+
rm string
658662
}
659663

660664
// WithInternalLabels sets the internal labels for a container.
@@ -732,6 +736,10 @@ func withInternalLabels(internalLabels internalLabels) (containerd.NewContainerO
732736
m[labels.IPC] = internalLabels.ipc
733737
}
734738

739+
if internalLabels.rm != "" {
740+
m[labels.ContainerAutoRemove] = internalLabels.rm
741+
}
742+
735743
return containerd.WithAdditionalContainerLabels(m), nil
736744
}
737745

pkg/containerutil/containerutil.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -600,3 +600,13 @@ func GetContainerName(containerLabels map[string]string) string {
600600
}
601601
return ""
602602
}
603+
604+
// EncodeContainerRmOptLabel encodes bool value for the --rm option into string value for a label.
605+
func EncodeContainerRmOptLabel(rmOpt bool) string {
606+
return fmt.Sprintf("%t", rmOpt)
607+
}
608+
609+
// DecodeContainerRmOptLabel decodes bool value for the --rm option from string value for a label.
610+
func DecodeContainerRmOptLabel(rmOptLabel string) (bool, error) {
611+
return strconv.ParseBool(rmOptLabel)
612+
}

pkg/labels/labels.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,4 +100,7 @@ const (
100100
// Boolean value which can be parsed with strconv.ParseBool() is required.
101101
// (like "nerdctl/default-network=true" or "nerdctl/default-network=false")
102102
NerdctlDefaultNetwork = Prefix + "default-network"
103+
104+
// ContainerAutoRemove is to check whether the --rm option is specified.
105+
ContainerAutoRemove = Prefix + "auto-remove"
103106
)

0 commit comments

Comments
 (0)