From 6ff97adbfb921735e2e5da43aac74cbb148c9611 Mon Sep 17 00:00:00 2001 From: Ben Liderman <18233089+benldrmn@users.noreply.github.com> Date: Mon, 20 Jul 2026 02:37:08 +0300 Subject: [PATCH 1/2] guard pod creation on non-terminal sandbox state --- .../operator/controller/sandbox_controller.go | 6 ++- .../controller/sandbox_controller_pod_test.go | 44 +++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/internal/operator/controller/sandbox_controller.go b/internal/operator/controller/sandbox_controller.go index 7c6a2573..ee01e5aa 100644 --- a/internal/operator/controller/sandbox_controller.go +++ b/internal/operator/controller/sandbox_controller.go @@ -943,7 +943,11 @@ func (r *SandboxReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct } } - if sandboxPod == nil { + // Don't (re-)create the pod for a terminal sandbox: a Succeeded/Failed sandbox + // whose pod was garbage-collected out-of-band must not re-run its workload. + isTerminal := meta.FindStatusCondition(sandbox.Status.Conditions, sandboxv1alpha1.SandboxSucceededCondition) != nil + + if sandboxPod == nil && !isTerminal { if err := r.CreateSandboxPod(ctx, sandbox, baseSandbox); err != nil { return ctrl.Result{}, err } diff --git a/internal/operator/controller/sandbox_controller_pod_test.go b/internal/operator/controller/sandbox_controller_pod_test.go index 7bd84eb1..fe351562 100644 --- a/internal/operator/controller/sandbox_controller_pod_test.go +++ b/internal/operator/controller/sandbox_controller_pod_test.go @@ -378,6 +378,50 @@ var _ = Describe("Sandbox Controller", func() { sandbox := getSandbox(ctx, sandboxName) Expect(meta.FindStatusCondition(sandbox.Status.Conditions, sandboxv1alpha1.SandboxSucceededCondition)).To(BeNil()) }) + + It("should not re-create pod for a terminal sandbox whose pod was deleted out-of-band", func() { + sandboxName := "sandbox-pod-terminal-gc" + + createSandbox(ctx, sandboxName) + defer deleteSandbox(ctx, sandboxName) + + podName := sandboxName + "-pod" + defer deletePod(ctx, podName) + + // First reconcile creates the pod + _, err := doReconcile(ctx, reconciler, sandboxName) + Expect(err).NotTo(HaveOccurred()) + + pod := getPod(ctx, podName) + Expect(pod).NotTo(BeNil()) + + // Drive the sandbox to a terminal (Succeeded) state + pod.Status.Phase = corev1.PodSucceeded + pod.Status.ContainerStatuses = []corev1.ContainerStatus{ + {Name: "sandbox", State: corev1.ContainerState{Terminated: &corev1.ContainerStateTerminated{ExitCode: 0, Reason: "Completed"}}}, + } + Expect(k8sClient.Status().Update(ctx, pod)).To(Succeed()) + + _, err = doReconcile(ctx, reconciler, sandboxName) + Expect(err).NotTo(HaveOccurred()) + + sandbox := getSandbox(ctx, sandboxName) + Expect(hasConditionWithReason(sandbox, sandboxv1alpha1.SandboxSucceededCondition, metav1.ConditionTrue, CondReasonPodSucceeded)).To(BeTrue()) + + // The completed pod is garbage-collected out-of-band + Expect(k8sClient.Delete(ctx, pod)).To(Succeed()) + Expect(getPod(ctx, podName)).To(BeNil()) + + // Reconcile must not re-run the workload by re-creating the pod + _, err = doReconcile(ctx, reconciler, sandboxName) + Expect(err).NotTo(HaveOccurred()) + + Expect(getPod(ctx, podName)).To(BeNil(), "terminal sandbox pod must not be re-created after out-of-band deletion") + + // Sandbox stays terminal + sandbox = getSandbox(ctx, sandboxName) + Expect(hasConditionWithReason(sandbox, sandboxv1alpha1.SandboxSucceededCondition, metav1.ConditionTrue, CondReasonPodSucceeded)).To(BeTrue()) + }) }) // ============================================ From 89125df6680f58314e770d8acac7593fa0163d4b Mon Sep 17 00:00:00 2001 From: Ben Liderman <18233089+benldrmn@users.noreply.github.com> Date: Mon, 20 Jul 2026 20:16:27 +0300 Subject: [PATCH 2/2] remove redundant comment --- internal/operator/controller/sandbox_controller.go | 2 -- internal/operator/controller/sandbox_controller_pod_test.go | 5 ----- 2 files changed, 7 deletions(-) diff --git a/internal/operator/controller/sandbox_controller.go b/internal/operator/controller/sandbox_controller.go index ee01e5aa..3b87a3ba 100644 --- a/internal/operator/controller/sandbox_controller.go +++ b/internal/operator/controller/sandbox_controller.go @@ -943,8 +943,6 @@ func (r *SandboxReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct } } - // Don't (re-)create the pod for a terminal sandbox: a Succeeded/Failed sandbox - // whose pod was garbage-collected out-of-band must not re-run its workload. isTerminal := meta.FindStatusCondition(sandbox.Status.Conditions, sandboxv1alpha1.SandboxSucceededCondition) != nil if sandboxPod == nil && !isTerminal { diff --git a/internal/operator/controller/sandbox_controller_pod_test.go b/internal/operator/controller/sandbox_controller_pod_test.go index fe351562..4be1df7d 100644 --- a/internal/operator/controller/sandbox_controller_pod_test.go +++ b/internal/operator/controller/sandbox_controller_pod_test.go @@ -388,14 +388,12 @@ var _ = Describe("Sandbox Controller", func() { podName := sandboxName + "-pod" defer deletePod(ctx, podName) - // First reconcile creates the pod _, err := doReconcile(ctx, reconciler, sandboxName) Expect(err).NotTo(HaveOccurred()) pod := getPod(ctx, podName) Expect(pod).NotTo(BeNil()) - // Drive the sandbox to a terminal (Succeeded) state pod.Status.Phase = corev1.PodSucceeded pod.Status.ContainerStatuses = []corev1.ContainerStatus{ {Name: "sandbox", State: corev1.ContainerState{Terminated: &corev1.ContainerStateTerminated{ExitCode: 0, Reason: "Completed"}}}, @@ -408,17 +406,14 @@ var _ = Describe("Sandbox Controller", func() { sandbox := getSandbox(ctx, sandboxName) Expect(hasConditionWithReason(sandbox, sandboxv1alpha1.SandboxSucceededCondition, metav1.ConditionTrue, CondReasonPodSucceeded)).To(BeTrue()) - // The completed pod is garbage-collected out-of-band Expect(k8sClient.Delete(ctx, pod)).To(Succeed()) Expect(getPod(ctx, podName)).To(BeNil()) - // Reconcile must not re-run the workload by re-creating the pod _, err = doReconcile(ctx, reconciler, sandboxName) Expect(err).NotTo(HaveOccurred()) Expect(getPod(ctx, podName)).To(BeNil(), "terminal sandbox pod must not be re-created after out-of-band deletion") - // Sandbox stays terminal sandbox = getSandbox(ctx, sandboxName) Expect(hasConditionWithReason(sandbox, sandboxv1alpha1.SandboxSucceededCondition, metav1.ConditionTrue, CondReasonPodSucceeded)).To(BeTrue()) })