diff --git a/cmd/nerdctl/container/container_stop.go b/cmd/nerdctl/container/container_stop.go index cbc533b801f..708c422f2aa 100644 --- a/cmd/nerdctl/container/container_stop.go +++ b/cmd/nerdctl/container/container_stop.go @@ -41,6 +41,7 @@ func NewStopCommand() *cobra.Command { SilenceErrors: true, } stopCommand.Flags().IntP("time", "t", 10, "Seconds to wait before sending a SIGKILL") + stopCommand.Flags().StringP("signal", "s", "SIGTERM", "Signal to send to the container") return stopCommand } @@ -58,11 +59,20 @@ func processContainerStopOptions(cmd *cobra.Command) (types.ContainerStopOptions t := time.Duration(timeValue) * time.Second timeout = &t } + var signal string + if cmd.Flags().Changed("signal") { + signalValue, err := cmd.Flags().GetString("signal") + if err != nil { + return types.ContainerStopOptions{}, err + } + signal = signalValue + } return types.ContainerStopOptions{ Stdout: cmd.OutOrStdout(), Stderr: cmd.ErrOrStderr(), GOptions: globalOptions, Timeout: timeout, + Signal: signal, }, nil } diff --git a/cmd/nerdctl/container/container_stop_linux_test.go b/cmd/nerdctl/container/container_stop_linux_test.go index ab03dfd7e45..694c2976321 100644 --- a/cmd/nerdctl/container/container_stop_linux_test.go +++ b/cmd/nerdctl/container/container_stop_linux_test.go @@ -21,6 +21,7 @@ import ( "io" "strings" "testing" + "time" "github.com/coreos/go-iptables/iptables" "gotest.tools/v3/assert" @@ -81,13 +82,20 @@ func TestStopWithStopSignal(t *testing.T) { base.Cmd("run", "-d", "--stop-signal", "SIGQUIT", "--name", testContainerName, testutil.CommonImage, "sh", "-euxc", `#!/bin/sh set -eu -trap 'quit=1' QUIT +echo "Script started" quit=0 -while [ $quit -ne 1 ]; do - printf 'wait quit' +trap 'echo "SIGQUIT received"; quit=1' QUIT +echo "Trap set" +while true; do + if [ $quit -eq 1 ]; then + echo "Quitting loop" + break + fi + echo "In loop" sleep 1 done -echo "signal quit"`).AssertOK() +echo "signal quit" +sync`).AssertOK() base.Cmd("stop", testContainerName).AssertOK() base.Cmd("logs", "-f", testContainerName).AssertOutContains("signal quit") } @@ -159,3 +167,39 @@ func TestStopCreated(t *testing.T) { base.Cmd("stop", tID).AssertOK() } + +func TestStopWithLongTimeoutAndSIGKILL(t *testing.T) { + t.Parallel() + base := testutil.NewBase(t) + testContainerName := testutil.Identifier(t) + defer base.Cmd("rm", "-f", testContainerName).Run() + + // Start a container that sleeps forever + base.Cmd("run", "-d", "--name", testContainerName, testutil.CommonImage, "sleep", "Inf").AssertOK() + + // Stop the container with a 5-second timeout and SIGKILL + start := time.Now() + base.Cmd("stop", "--time=5", "--signal", "SIGKILL", testContainerName).AssertOK() + elapsed := time.Since(start) + + // The container should be stopped almost immediately, well before the 5-second timeout + assert.Assert(t, elapsed < 5*time.Second, "Container wasn't stopped immediately with SIGKILL") +} + +func TestStopWithTimeout(t *testing.T) { + t.Parallel() + base := testutil.NewBase(t) + testContainerName := testutil.Identifier(t) + defer base.Cmd("rm", "-f", testContainerName).Run() + + // Start a container that sleeps forever + base.Cmd("run", "-d", "--name", testContainerName, testutil.CommonImage, "sleep", "Inf").AssertOK() + + // Stop the container with a 3-second timeout + start := time.Now() + base.Cmd("stop", "--time=3", testContainerName).AssertOK() + elapsed := time.Since(start) + + // The container should get the SIGKILL before the 10s default timeout + assert.Assert(t, elapsed < 10*time.Second, "Container did not respect --timeout flag") +} diff --git a/pkg/api/types/container_types.go b/pkg/api/types/container_types.go index 13e798cecad..d38a0e5da11 100644 --- a/pkg/api/types/container_types.go +++ b/pkg/api/types/container_types.go @@ -275,6 +275,9 @@ type ContainerStopOptions struct { // Timeout specifies how long to wait after sending a SIGTERM and before sending a SIGKILL. // If it's nil, the default is 10 seconds. Timeout *time.Duration + + // Signal to send to the container, before sending SIGKILL + Signal string } // ContainerRestartOptions specifies options for `nerdctl (container) restart`. diff --git a/pkg/cmd/container/restart.go b/pkg/cmd/container/restart.go index 2be386ea323..1be264e056f 100644 --- a/pkg/cmd/container/restart.go +++ b/pkg/cmd/container/restart.go @@ -35,7 +35,7 @@ func Restart(ctx context.Context, client *containerd.Client, containers []string if found.MatchCount > 1 { return fmt.Errorf("multiple IDs found with provided prefix: %s", found.Req) } - if err := containerutil.Stop(ctx, found.Container, options.Timeout); err != nil { + if err := containerutil.Stop(ctx, found.Container, options.Timeout, "SIGTERM"); err != nil { return err } if err := containerutil.Start(ctx, found.Container, false, client, ""); err != nil { diff --git a/pkg/cmd/container/stop.go b/pkg/cmd/container/stop.go index 3000fd611b4..e1f347b6b96 100644 --- a/pkg/cmd/container/stop.go +++ b/pkg/cmd/container/stop.go @@ -39,7 +39,7 @@ func Stop(ctx context.Context, client *containerd.Client, reqs []string, opt typ if err := cleanupNetwork(ctx, found.Container, opt.GOptions); err != nil { return fmt.Errorf("unable to cleanup network for container: %s", found.Req) } - if err := containerutil.Stop(ctx, found.Container, opt.Timeout); err != nil { + if err := containerutil.Stop(ctx, found.Container, opt.Timeout, opt.Signal); err != nil { if errdefs.IsNotFound(err) { fmt.Fprintf(opt.Stderr, "No such container: %s\n", found.Req) return nil diff --git a/pkg/containerutil/containerutil.go b/pkg/containerutil/containerutil.go index fca15cb6669..c929a1716c4 100644 --- a/pkg/containerutil/containerutil.go +++ b/pkg/containerutil/containerutil.go @@ -26,6 +26,7 @@ import ( "path/filepath" "strconv" "strings" + "syscall" "time" dockercliopts "github.com/docker/cli/opts" @@ -336,7 +337,7 @@ func Start(ctx context.Context, container containerd.Container, flagA bool, clie } // Stop stops `container` by sending SIGTERM. If the container is not stopped after `timeout`, it sends a SIGKILL. -func Stop(ctx context.Context, container containerd.Container, timeout *time.Duration) (err error) { +func Stop(ctx context.Context, container containerd.Container, timeout *time.Duration, signalValue string) (err error) { // defer the storage of stop error in the dedicated label defer func() { if err != nil { @@ -409,16 +410,10 @@ func Stop(ctx context.Context, container containerd.Container, timeout *time.Dur } if *timeout > 0 { - sig, err := signal.ParseSignal("SIGTERM") + sig, err := getSignal(signalValue, l) if err != nil { return err } - if stopSignal, ok := l[containerd.StopSignalLabel]; ok { - sig, err = signal.ParseSignal(stopSignal) - if err != nil { - return err - } - } if err := task.Kill(ctx, sig); err != nil { return err @@ -465,6 +460,18 @@ func Stop(ctx context.Context, container containerd.Container, timeout *time.Dur return waitContainerStop(ctx, exitCh, container.ID()) } +func getSignal(signalValue string, containerLabels map[string]string) (syscall.Signal, error) { + if signalValue != "" { + return signal.ParseSignal(signalValue) + } + + if stopSignal, ok := containerLabels[containerd.StopSignalLabel]; ok { + return signal.ParseSignal(stopSignal) + } + + return signal.ParseSignal("SIGTERM") +} + func waitContainerStop(ctx context.Context, exitCh <-chan containerd.ExitStatus, id string) error { select { case <-ctx.Done():