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/2] 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/2] 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) +}