Skip to content

Commit 3442502

Browse files
task: StopPodSandbox first test case
Add NRI test that validates the StopPodSandbox spec contract: workload containers must be stopped before the StopPodSandbox NRI hook fires, and sandbox infrastructure (PodSandboxStatus) must still be accessible while the hook is in progress. Extracted from kubernetes-sigs#2069 and aligned with the existing "RunPodSandbox contract" tests in pkg/validate/nri_linux.go (naming, AfterEach pattern using framework.Logf, hook blocking via channels, sync.WaitGroup.Go).
1 parent 5f40b65 commit 3442502

1 file changed

Lines changed: 163 additions & 0 deletions

File tree

pkg/validate/nri_linux.go

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,4 +411,167 @@ var _ = framework.KubeDescribe("NRI", func() {
411411
}
412412
})
413413
})
414+
415+
Context("StopPodSandbox contract", Serial, func() {
416+
var (
417+
testStub *NRITestStub
418+
podID string
419+
podConfig *runtimeapi.PodSandboxConfig
420+
containerID string
421+
)
422+
423+
AfterEach(func(ctx SpecContext) {
424+
// Stop the stub first to unblock any hooks that may be holding
425+
// a StopPodSandbox call, allowing it to complete.
426+
if testStub != nil {
427+
testStub.Cleanup()
428+
}
429+
430+
if containerID != "" {
431+
if err := rc.StopContainer(ctx, containerID, 0); err != nil {
432+
framework.Logf("AfterEach: StopContainer(%s) failed: %v", containerID, err)
433+
}
434+
435+
if err := rc.RemoveContainer(ctx, containerID); err != nil {
436+
framework.Logf("AfterEach: RemoveContainer(%s) failed: %v", containerID, err)
437+
}
438+
}
439+
440+
if podID != "" {
441+
if err := rc.StopPodSandbox(ctx, podID); err != nil {
442+
framework.Logf("AfterEach: StopPodSandbox(%s) failed: %v", podID, err)
443+
}
444+
445+
if err := rc.RemovePodSandbox(ctx, podID); err != nil {
446+
framework.Logf("AfterEach: RemovePodSandbox(%s) failed: %v", podID, err)
447+
}
448+
}
449+
})
450+
451+
It("should stop all workload containers and keep sandbox accessible while StopPodSandbox hook is in progress", func(ctx SpecContext) {
452+
// This test validates the spec contract: when StopPodSandbox is called,
453+
// all workload containers MUST already be stopped before the NRI hook
454+
// fires, and the sandbox infrastructure MUST still be accessible via
455+
// PodSandboxStatus while the hook is in progress.
456+
hookBlocking := make(chan struct{})
457+
hookReached := make(chan struct{})
458+
459+
var hookOnce sync.Once
460+
461+
var err error
462+
463+
testStub, err = StartNRITestStub("cri-test-nri-stop-state", "00")
464+
Expect(err).NotTo(HaveOccurred(), "failed to start NRI test stub")
465+
466+
// Configure stub to block during StopPodSandbox so the main
467+
// goroutine can inspect runtime state while the hook is active.
468+
// sync.Once guards against the hook being invoked multiple times
469+
// (e.g., idempotent stop redelivery), which would otherwise panic
470+
// on a double close of hookReached.
471+
testStub.Plugin.OnStopPodSandbox = func(hookCtx context.Context, _ *nri.PodSandbox) error {
472+
firstInvocation := false
473+
474+
hookOnce.Do(func() { firstInvocation = true })
475+
476+
// Skip duplicate invocations (e.g., AfterEach cleanup) so
477+
// they are not blocked by the test channel handshake.
478+
if !firstInvocation {
479+
return nil
480+
}
481+
482+
close(hookReached)
483+
484+
select {
485+
case <-hookBlocking:
486+
case <-hookCtx.Done():
487+
}
488+
489+
return nil
490+
}
491+
492+
By("creating a pod sandbox")
493+
494+
podSandboxName := "nri-test-stop-state-" + framework.NewUUID()
495+
uid := framework.DefaultUIDPrefix + framework.NewUUID()
496+
namespace := framework.DefaultNamespacePrefix + framework.NewUUID()
497+
podConfig = &runtimeapi.PodSandboxConfig{
498+
Metadata: framework.BuildPodSandboxMetadata(podSandboxName, uid, namespace, framework.DefaultAttempt),
499+
Linux: &runtimeapi.LinuxPodSandboxConfig{
500+
CgroupParent: common.GetCgroupParent(ctx, rc),
501+
},
502+
Labels: framework.DefaultPodLabels,
503+
}
504+
podID = framework.RunPodSandbox(ctx, rc, podConfig)
505+
Expect(podID).NotTo(BeEmpty())
506+
507+
By("creating and starting a container in the sandbox")
508+
framework.PullPublicImage(ctx, ic, framework.TestContext.TestImageList.DefaultTestContainerImage, nil)
509+
510+
containerName := "nri-test-stop-state-ctr-" + framework.NewUUID()
511+
containerConfig := &runtimeapi.ContainerConfig{
512+
Metadata: framework.BuildContainerMetadata(containerName, framework.DefaultAttempt),
513+
Image: &runtimeapi.ImageSpec{Image: framework.TestContext.TestImageList.DefaultTestContainerImage},
514+
Command: framework.DefaultPauseCommand,
515+
Linux: &runtimeapi.LinuxContainerConfig{},
516+
}
517+
containerID = framework.CreateContainer(ctx, rc, ic, containerConfig, podID, podConfig)
518+
Expect(containerID).NotTo(BeEmpty())
519+
Expect(rc.StartContainer(ctx, containerID)).NotTo(HaveOccurred())
520+
521+
By("triggering StopPodSandbox in a goroutine")
522+
523+
var (
524+
stopErr error
525+
stopWg sync.WaitGroup
526+
)
527+
528+
stopWg.Go(func() {
529+
stopErr = rc.StopPodSandbox(ctx, podID)
530+
})
531+
532+
By("waiting for StopPodSandbox hook to be reached")
533+
534+
select {
535+
case <-hookReached:
536+
// Hook is now blocking; main goroutine can inspect state
537+
case <-time.After(30 * time.Second):
538+
close(hookBlocking) // unblock to avoid goroutine leak
539+
Fail("Timed out waiting for StopPodSandbox NRI hook to fire")
540+
}
541+
542+
By("verifying all workload containers were stopped before the hook fired")
543+
544+
containers, listErr := rc.ListContainers(ctx, &runtimeapi.ContainerFilter{
545+
PodSandboxId: podID,
546+
})
547+
Expect(listErr).NotTo(HaveOccurred(), "ListContainers during StopPodSandbox hook")
548+
549+
for _, c := range containers {
550+
Expect(c.GetState()).To(Equal(runtimeapi.ContainerState_CONTAINER_EXITED),
551+
"container %s MUST be stopped before StopPodSandbox hook fires", c.GetId())
552+
}
553+
554+
By("verifying sandbox infrastructure is still accessible during the hook")
555+
556+
statusResp, statusErr := rc.PodSandboxStatus(ctx, podID, false)
557+
Expect(statusErr).NotTo(HaveOccurred(), "PodSandboxStatus during StopPodSandbox hook")
558+
Expect(statusResp.GetStatus()).NotTo(BeNil(),
559+
"PodSandboxStatus MUST be accessible during StopPodSandbox hook")
560+
Expect(statusResp.GetStatus().GetId()).To(Equal(podID))
561+
562+
By("releasing the hook and verifying StopPodSandbox succeeds")
563+
close(hookBlocking)
564+
stopWg.Wait()
565+
Expect(stopErr).NotTo(HaveOccurred(), "StopPodSandbox should succeed after hook returns")
566+
567+
// StopPodSandbox stopped the container; clear so AfterEach skips it.
568+
containerID = ""
569+
570+
// Remove the sandbox now and clear podID so AfterEach doesn't issue a
571+
// second StopPodSandbox (which could re-enter the hook callback).
572+
Expect(rc.RemovePodSandbox(ctx, podID)).NotTo(HaveOccurred(),
573+
"RemovePodSandbox should succeed after stop")
574+
podID = ""
575+
})
576+
})
414577
})

0 commit comments

Comments
 (0)