Skip to content

Commit 89003a5

Browse files
authored
Merge pull request containerd#3892 from coderbirju/add-signal-container-stop
add signal option to container stop command
2 parents 87cc074 + d3f5d10 commit 89003a5

File tree

6 files changed

+78
-14
lines changed

6 files changed

+78
-14
lines changed

cmd/nerdctl/container/container_stop.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ func NewStopCommand() *cobra.Command {
4141
SilenceErrors: true,
4242
}
4343
stopCommand.Flags().IntP("time", "t", 10, "Seconds to wait before sending a SIGKILL")
44+
stopCommand.Flags().StringP("signal", "s", "SIGTERM", "Signal to send to the container")
4445
return stopCommand
4546
}
4647

@@ -58,11 +59,20 @@ func processContainerStopOptions(cmd *cobra.Command) (types.ContainerStopOptions
5859
t := time.Duration(timeValue) * time.Second
5960
timeout = &t
6061
}
62+
var signal string
63+
if cmd.Flags().Changed("signal") {
64+
signalValue, err := cmd.Flags().GetString("signal")
65+
if err != nil {
66+
return types.ContainerStopOptions{}, err
67+
}
68+
signal = signalValue
69+
}
6170
return types.ContainerStopOptions{
6271
Stdout: cmd.OutOrStdout(),
6372
Stderr: cmd.ErrOrStderr(),
6473
GOptions: globalOptions,
6574
Timeout: timeout,
75+
Signal: signal,
6676
}, nil
6777
}
6878

cmd/nerdctl/container/container_stop_linux_test.go

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"io"
2222
"strings"
2323
"testing"
24+
"time"
2425

2526
"github.com/coreos/go-iptables/iptables"
2627
"gotest.tools/v3/assert"
@@ -81,13 +82,20 @@ func TestStopWithStopSignal(t *testing.T) {
8182

8283
base.Cmd("run", "-d", "--stop-signal", "SIGQUIT", "--name", testContainerName, testutil.CommonImage, "sh", "-euxc", `#!/bin/sh
8384
set -eu
84-
trap 'quit=1' QUIT
85+
echo "Script started"
8586
quit=0
86-
while [ $quit -ne 1 ]; do
87-
printf 'wait quit'
87+
trap 'echo "SIGQUIT received"; quit=1' QUIT
88+
echo "Trap set"
89+
while true; do
90+
if [ $quit -eq 1 ]; then
91+
echo "Quitting loop"
92+
break
93+
fi
94+
echo "In loop"
8895
sleep 1
8996
done
90-
echo "signal quit"`).AssertOK()
97+
echo "signal quit"
98+
sync`).AssertOK()
9199
base.Cmd("stop", testContainerName).AssertOK()
92100
base.Cmd("logs", "-f", testContainerName).AssertOutContains("signal quit")
93101
}
@@ -159,3 +167,39 @@ func TestStopCreated(t *testing.T) {
159167

160168
base.Cmd("stop", tID).AssertOK()
161169
}
170+
171+
func TestStopWithLongTimeoutAndSIGKILL(t *testing.T) {
172+
t.Parallel()
173+
base := testutil.NewBase(t)
174+
testContainerName := testutil.Identifier(t)
175+
defer base.Cmd("rm", "-f", testContainerName).Run()
176+
177+
// Start a container that sleeps forever
178+
base.Cmd("run", "-d", "--name", testContainerName, testutil.CommonImage, "sleep", "Inf").AssertOK()
179+
180+
// Stop the container with a 5-second timeout and SIGKILL
181+
start := time.Now()
182+
base.Cmd("stop", "--time=5", "--signal", "SIGKILL", testContainerName).AssertOK()
183+
elapsed := time.Since(start)
184+
185+
// The container should be stopped almost immediately, well before the 5-second timeout
186+
assert.Assert(t, elapsed < 5*time.Second, "Container wasn't stopped immediately with SIGKILL")
187+
}
188+
189+
func TestStopWithTimeout(t *testing.T) {
190+
t.Parallel()
191+
base := testutil.NewBase(t)
192+
testContainerName := testutil.Identifier(t)
193+
defer base.Cmd("rm", "-f", testContainerName).Run()
194+
195+
// Start a container that sleeps forever
196+
base.Cmd("run", "-d", "--name", testContainerName, testutil.CommonImage, "sleep", "Inf").AssertOK()
197+
198+
// Stop the container with a 3-second timeout
199+
start := time.Now()
200+
base.Cmd("stop", "--time=3", testContainerName).AssertOK()
201+
elapsed := time.Since(start)
202+
203+
// The container should get the SIGKILL before the 10s default timeout
204+
assert.Assert(t, elapsed < 10*time.Second, "Container did not respect --timeout flag")
205+
}

pkg/api/types/container_types.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,9 @@ type ContainerStopOptions struct {
275275
// Timeout specifies how long to wait after sending a SIGTERM and before sending a SIGKILL.
276276
// If it's nil, the default is 10 seconds.
277277
Timeout *time.Duration
278+
279+
// Signal to send to the container, before sending SIGKILL
280+
Signal string
278281
}
279282

280283
// ContainerRestartOptions specifies options for `nerdctl (container) restart`.

pkg/cmd/container/restart.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ func Restart(ctx context.Context, client *containerd.Client, containers []string
3535
if found.MatchCount > 1 {
3636
return fmt.Errorf("multiple IDs found with provided prefix: %s", found.Req)
3737
}
38-
if err := containerutil.Stop(ctx, found.Container, options.Timeout); err != nil {
38+
if err := containerutil.Stop(ctx, found.Container, options.Timeout, "SIGTERM"); err != nil {
3939
return err
4040
}
4141
if err := containerutil.Start(ctx, found.Container, false, client, ""); err != nil {

pkg/cmd/container/stop.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ func Stop(ctx context.Context, client *containerd.Client, reqs []string, opt typ
3939
if err := cleanupNetwork(ctx, found.Container, opt.GOptions); err != nil {
4040
return fmt.Errorf("unable to cleanup network for container: %s", found.Req)
4141
}
42-
if err := containerutil.Stop(ctx, found.Container, opt.Timeout); err != nil {
42+
if err := containerutil.Stop(ctx, found.Container, opt.Timeout, opt.Signal); err != nil {
4343
if errdefs.IsNotFound(err) {
4444
fmt.Fprintf(opt.Stderr, "No such container: %s\n", found.Req)
4545
return nil

pkg/containerutil/containerutil.go

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
"path/filepath"
2727
"strconv"
2828
"strings"
29+
"syscall"
2930
"time"
3031

3132
dockercliopts "github.com/docker/cli/opts"
@@ -336,7 +337,7 @@ func Start(ctx context.Context, container containerd.Container, flagA bool, clie
336337
}
337338

338339
// Stop stops `container` by sending SIGTERM. If the container is not stopped after `timeout`, it sends a SIGKILL.
339-
func Stop(ctx context.Context, container containerd.Container, timeout *time.Duration) (err error) {
340+
func Stop(ctx context.Context, container containerd.Container, timeout *time.Duration, signalValue string) (err error) {
340341
// defer the storage of stop error in the dedicated label
341342
defer func() {
342343
if err != nil {
@@ -409,16 +410,10 @@ func Stop(ctx context.Context, container containerd.Container, timeout *time.Dur
409410
}
410411

411412
if *timeout > 0 {
412-
sig, err := signal.ParseSignal("SIGTERM")
413+
sig, err := getSignal(signalValue, l)
413414
if err != nil {
414415
return err
415416
}
416-
if stopSignal, ok := l[containerd.StopSignalLabel]; ok {
417-
sig, err = signal.ParseSignal(stopSignal)
418-
if err != nil {
419-
return err
420-
}
421-
}
422417

423418
if err := task.Kill(ctx, sig); err != nil {
424419
return err
@@ -465,6 +460,18 @@ func Stop(ctx context.Context, container containerd.Container, timeout *time.Dur
465460
return waitContainerStop(ctx, exitCh, container.ID())
466461
}
467462

463+
func getSignal(signalValue string, containerLabels map[string]string) (syscall.Signal, error) {
464+
if signalValue != "" {
465+
return signal.ParseSignal(signalValue)
466+
}
467+
468+
if stopSignal, ok := containerLabels[containerd.StopSignalLabel]; ok {
469+
return signal.ParseSignal(stopSignal)
470+
}
471+
472+
return signal.ParseSignal("SIGTERM")
473+
}
474+
468475
func waitContainerStop(ctx context.Context, exitCh <-chan containerd.ExitStatus, id string) error {
469476
select {
470477
case <-ctx.Done():

0 commit comments

Comments
 (0)