Skip to content

Add signal container stop [fork-test] #11

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions cmd/nerdctl/container/container_stop.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand All @@ -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
}

Expand Down
52 changes: 48 additions & 4 deletions cmd/nerdctl/container/container_stop_linux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"io"
"strings"
"testing"
"time"

"github.com/coreos/go-iptables/iptables"
"gotest.tools/v3/assert"
Expand Down Expand Up @@ -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")
}
Expand Down Expand Up @@ -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")
}
3 changes: 3 additions & 0 deletions pkg/api/types/container_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
2 changes: 1 addition & 1 deletion pkg/cmd/container/restart.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion pkg/cmd/container/stop.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
23 changes: 15 additions & 8 deletions pkg/containerutil/containerutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
"path/filepath"
"strconv"
"strings"
"syscall"
"time"

dockercliopts "github.com/docker/cli/opts"
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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():
Expand Down
Loading