From d1b8fe01c82ed3ad676091a540604739d1944e2e Mon Sep 17 00:00:00 2001 From: Vedant Madane <6527493+VedantMadane@users.noreply.github.com> Date: Sat, 22 Aug 2026 21:32:58 +0530 Subject: [PATCH 1/3] pkg/containerutil: update restart.StatusLabel to Stopped when stopping container (#5153) When a container with a restart policy (such as --restart=always) is explicitly stopped or killed via nerdctl stop / nerdctl kill, update the containerd.io/restart.status label to containerd.Stopped so that containerd's restart monitor does not automatically restart the explicitly stopped container. Signed-off-by: Vedant Madane <6527493+VedantMadane@users.noreply.github.com> --- pkg/cmd/container/kill.go | 8 ++++++++ pkg/containerutil/containerutil.go | 6 ++++++ 2 files changed, 14 insertions(+) diff --git a/pkg/cmd/container/kill.go b/pkg/cmd/container/kill.go index d42a7cd8c82..8c17821ce92 100644 --- a/pkg/cmd/container/kill.go +++ b/pkg/cmd/container/kill.go @@ -27,6 +27,7 @@ import ( "github.com/moby/sys/signal" containerd "github.com/containerd/containerd/v2/client" + "github.com/containerd/containerd/v2/core/runtime/restart" "github.com/containerd/containerd/v2/pkg/cio" "github.com/containerd/errdefs" "github.com/containerd/go-cni" @@ -88,6 +89,13 @@ func killContainer(ctx context.Context, container containerd.Container, signal s if err := containerutil.UpdateExplicitlyStoppedLabel(ctx, container, true); err != nil { return err } + if l, err := container.Labels(ctx); err == nil { + if _, ok := l[restart.PolicyLabel]; ok { + if err := containerutil.UpdateStatusLabel(ctx, container, containerd.Stopped); err != nil { + return err + } + } + } task, err := container.Task(ctx, cio.Load) if err != nil { return err diff --git a/pkg/containerutil/containerutil.go b/pkg/containerutil/containerutil.go index fa27533f080..c991ec552da 100644 --- a/pkg/containerutil/containerutil.go +++ b/pkg/containerutil/containerutil.go @@ -363,6 +363,12 @@ func Stop(ctx context.Context, container containerd.Container, timeout *time.Dur if err != nil { return err } + + if _, ok := l[restart.PolicyLabel]; ok { + if err := UpdateStatusLabel(ctx, container, containerd.Stopped); err != nil { + return err + } + } ipc, err := ipcutil.DecodeIPCLabel(l[labels.IPC]) if err != nil { return err From 37d9f73446fbc43b063729ada0db4dd6cd4fcd65 Mon Sep 17 00:00:00 2001 From: Vedant Madane <6527493+VedantMadane@users.noreply.github.com> Date: Sun, 23 Aug 2026 11:08:52 +0530 Subject: [PATCH 2/3] test(container): add integration tests for restart=always stop and kill (#5153) Add TestRunRestartAlwaysStop and TestRunRestartAlwaysKill to verify that containers created with --restart=always transition containerd.io/restart.status to stopped and remain exited after being stopped or killed. Signed-off-by: Vedant Madane <6527493+VedantMadane@users.noreply.github.com> --- .../container_run_restart_linux_test.go | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/cmd/nerdctl/container/container_run_restart_linux_test.go b/cmd/nerdctl/container/container_run_restart_linux_test.go index 17761188e39..8423fd6389d 100644 --- a/cmd/nerdctl/container/container_run_restart_linux_test.go +++ b/cmd/nerdctl/container/container_run_restart_linux_test.go @@ -382,3 +382,75 @@ func TestRunRestartStatusLabel(t *testing.T) { testCase.Run(t) } + +func TestRunRestartAlwaysStop(t *testing.T) { + testCase := nerdtest.Setup() + if !nerdtest.IsDocker() { + testCase.Require = nerdtest.ContainerdPlugin("io.containerd.internal.v1", "restart", []string{"always"}) + } + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "-d", "--restart=always", "--name", data.Identifier(), testutil.CommonImage, "sleep", "infinity") + helpers.Ensure("stop", data.Identifier()) + } + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + } + + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + time.Sleep(3 * time.Second) + return helpers.Command("inspect", data.Identifier()) + } + + testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: expect.JSON([]dockercompat.Container{}, func(dc []dockercompat.Container, t tig.T) { + assert.Equal(t, 1, len(dc)) + assert.Assert(t, dc[0].State != nil && dc[0].State.Status == "exited") + if !nerdtest.IsDocker() { + assert.Equal(t, "stopped", dc[0].Config.Labels[restart.StatusLabel]) + } + }), + } + } + + testCase.Run(t) +} + +func TestRunRestartAlwaysKill(t *testing.T) { + testCase := nerdtest.Setup() + if !nerdtest.IsDocker() { + testCase.Require = nerdtest.ContainerdPlugin("io.containerd.internal.v1", "restart", []string{"always"}) + } + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "-d", "--restart=always", "--name", data.Identifier(), testutil.CommonImage, "sleep", "infinity") + helpers.Ensure("kill", data.Identifier()) + } + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + } + + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + time.Sleep(3 * time.Second) + return helpers.Command("inspect", data.Identifier()) + } + + testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: expect.JSON([]dockercompat.Container{}, func(dc []dockercompat.Container, t tig.T) { + assert.Equal(t, 1, len(dc)) + assert.Assert(t, dc[0].State != nil && dc[0].State.Status == "exited") + if !nerdtest.IsDocker() { + assert.Equal(t, "stopped", dc[0].Config.Labels[restart.StatusLabel]) + } + }), + } + } + + testCase.Run(t) +} From 8fa235ef1323a3c63fb40ba8644bd2fa31e634a1 Mon Sep 17 00:00:00 2001 From: Vedant Madane <6527493+VedantMadane@users.noreply.github.com> Date: Mon, 24 Aug 2026 15:50:55 +0530 Subject: [PATCH 3/3] test(container): tolerate empty Status after stop on older containerd On containerd v1.7, inspect can report State.Status as empty once the task is gone even though Running is false and restart.status is stopped. Assert not-running / not-restarting plus the restart status label instead of requiring Status == exited. Signed-off-by: Vedant Madane <6527493+VedantMadane@users.noreply.github.com> --- .../container_run_restart_linux_test.go | 33 +++++++++++-------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/cmd/nerdctl/container/container_run_restart_linux_test.go b/cmd/nerdctl/container/container_run_restart_linux_test.go index 8423fd6389d..edf17a7e2d5 100644 --- a/cmd/nerdctl/container/container_run_restart_linux_test.go +++ b/cmd/nerdctl/container/container_run_restart_linux_test.go @@ -383,6 +383,23 @@ func TestRunRestartStatusLabel(t *testing.T) { testCase.Run(t) } +// assertStoppedNotRestarting checks that stop/kill left the container stopped +// and that the restart plugin will not bring it back. On older containerd +// (e.g. v1.7), inspect may report Status as "" once the task is gone even +// though Running is false and the container is effectively exited. +func assertStoppedNotRestarting(dc []dockercompat.Container, t tig.T) { + assert.Equal(t, 1, len(dc)) + assert.Assert(t, dc[0].State != nil, "State is nil") + assert.Equal(t, false, dc[0].State.Running) + // Must not still be running or in a restart loop. + assert.Assert(t, dc[0].State.Status != "running" && dc[0].State.Status != "restarting", + "unexpected Status %q", dc[0].State.Status) + if !nerdtest.IsDocker() { + assert.Assert(t, dc[0].Config != nil && dc[0].Config.Labels != nil) + assert.Equal(t, "stopped", dc[0].Config.Labels[restart.StatusLabel]) + } +} + func TestRunRestartAlwaysStop(t *testing.T) { testCase := nerdtest.Setup() if !nerdtest.IsDocker() { @@ -406,13 +423,7 @@ func TestRunRestartAlwaysStop(t *testing.T) { testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: expect.ExitCodeSuccess, - Output: expect.JSON([]dockercompat.Container{}, func(dc []dockercompat.Container, t tig.T) { - assert.Equal(t, 1, len(dc)) - assert.Assert(t, dc[0].State != nil && dc[0].State.Status == "exited") - if !nerdtest.IsDocker() { - assert.Equal(t, "stopped", dc[0].Config.Labels[restart.StatusLabel]) - } - }), + Output: expect.JSON([]dockercompat.Container{}, assertStoppedNotRestarting), } } @@ -442,13 +453,7 @@ func TestRunRestartAlwaysKill(t *testing.T) { testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: expect.ExitCodeSuccess, - Output: expect.JSON([]dockercompat.Container{}, func(dc []dockercompat.Container, t tig.T) { - assert.Equal(t, 1, len(dc)) - assert.Assert(t, dc[0].State != nil && dc[0].State.Status == "exited") - if !nerdtest.IsDocker() { - assert.Equal(t, "stopped", dc[0].Config.Labels[restart.StatusLabel]) - } - }), + Output: expect.JSON([]dockercompat.Container{}, assertStoppedNotRestarting), } }