@@ -17,9 +17,12 @@ limitations under the License.
1717package validate
1818
1919import (
20+ "context"
2021 "fmt"
22+ "sync"
2123 "time"
2224
25+ nri "github.com/containerd/nri/pkg/api"
2326 . "github.com/onsi/ginkgo/v2"
2427 . "github.com/onsi/gomega"
2528 internalapi "k8s.io/cri-api/pkg/apis"
@@ -32,14 +35,18 @@ import (
3235var _ = framework .KubeDescribe ("NRI" , func () {
3336 f := framework .NewDefaultCRIFramework ()
3437
35- var rc internalapi.RuntimeService
38+ var (
39+ rc internalapi.RuntimeService
40+ ic internalapi.ImageManagerService
41+ )
3642
3743 BeforeEach (func () {
3844 if framework .TestContext .NRISocketPath == "" {
3945 Skip ("NRI socket not configured (use -nri-socket flag)" )
4046 }
4147
4248 rc = f .CRIClient .CRIRuntimeClient
49+ ic = f .CRIClient .CRIImageClient
4350 })
4451
4552 Context ("pod sandbox lifecycle" , Serial , func () {
@@ -149,4 +156,259 @@ var _ = framework.KubeDescribe("NRI", func() {
149156 podID = ""
150157 })
151158 })
159+
160+ Context ("RunPodSandbox contract" , Serial , func () {
161+ var (
162+ testStub * NRITestStub
163+ podID string
164+ podConfig * runtimeapi.PodSandboxConfig
165+ )
166+
167+ AfterEach (func (ctx SpecContext ) {
168+ // Capture the fallback sandbox ID before cleanup resets events.
169+ cleanupID := podID
170+ if cleanupID == "" && testStub != nil {
171+ cleanupID = testStub .Plugin .LastRunPodSandboxID ()
172+ }
173+
174+ // Stop the stub to unblock any hooks that may be holding
175+ // a RunPodSandbox call, allowing it to complete.
176+ if testStub != nil {
177+ testStub .Cleanup ()
178+ }
179+
180+ if cleanupID != "" {
181+ if err := rc .StopPodSandbox (ctx , cleanupID ); err != nil {
182+ framework .Logf ("AfterEach: StopPodSandbox(%s) failed: %v" , cleanupID , err )
183+ }
184+
185+ if err := rc .RemovePodSandbox (ctx , cleanupID ); err != nil {
186+ framework .Logf ("AfterEach: RemovePodSandbox(%s) failed: %v" , cleanupID , err )
187+ }
188+ }
189+ })
190+
191+ It ("should not expose sandbox while RunPodSandbox hook is in progress" , func (ctx SpecContext ) {
192+ // This test validates the spec contract: during RunPodSandbox hook execution,
193+ // the sandbox MUST NOT be visible via List or Status, and workload containers
194+ // MUST NOT start.
195+
196+ // Channel to control blocking behavior
197+ hookBlocking := make (chan struct {})
198+ hookReached := make (chan struct {})
199+
200+ var hookPodID string
201+
202+ var err error
203+
204+ testStub , err = StartNRITestStub ("cri-test-nri-block-run" , "00" )
205+ Expect (err ).NotTo (HaveOccurred (), "failed to start NRI test stub" )
206+
207+ // Configure stub to block on RunPodSandbox
208+ testStub .Plugin .OnRunPodSandbox = func (hookCtx context.Context , pod * nri.PodSandbox ) error {
209+ hookPodID = pod .GetId ()
210+
211+ close (hookReached )
212+ // Block until test signals to continue or context is cancelled (cleanup)
213+ select {
214+ case <- hookBlocking :
215+ case <- hookCtx .Done ():
216+ }
217+
218+ return nil
219+ }
220+
221+ By ("triggering RunPodSandbox in a goroutine" )
222+
223+ podSandboxName := "nri-test-block-run-" + framework .NewUUID ()
224+ uid := framework .DefaultUIDPrefix + framework .NewUUID ()
225+ namespace := framework .DefaultNamespacePrefix + framework .NewUUID ()
226+ podConfig = & runtimeapi.PodSandboxConfig {
227+ Metadata : framework .BuildPodSandboxMetadata (podSandboxName , uid , namespace , framework .DefaultAttempt ),
228+ Linux : & runtimeapi.LinuxPodSandboxConfig {
229+ CgroupParent : common .GetCgroupParent (ctx , rc ),
230+ },
231+ Labels : framework .DefaultPodLabels ,
232+ }
233+
234+ var (
235+ runErr error
236+ runPodID string
237+ runWg sync.WaitGroup
238+ )
239+
240+ runWg .Go (func () {
241+ runPodID , runErr = rc .RunPodSandbox (ctx , podConfig , framework .TestContext .RuntimeHandler )
242+ })
243+
244+ By ("waiting for RunPodSandbox hook to be reached" )
245+
246+ select {
247+ case <- hookReached :
248+ // Hook is now blocking
249+ case <- time .After (30 * time .Second ):
250+ close (hookBlocking ) // unblock to avoid goroutine leak
251+ Fail ("Timed out waiting for RunPodSandbox NRI hook to fire" )
252+ }
253+
254+ By ("verifying sandbox is NOT listed while hook is blocking" )
255+ // The sandbox should not appear in ListPodSandbox in any state
256+ pods , listErr := rc .ListPodSandbox (ctx , nil )
257+ Expect (listErr ).NotTo (HaveOccurred ())
258+
259+ sandboxFound := false
260+
261+ for _ , pod := range pods {
262+ if pod .GetId () == hookPodID {
263+ sandboxFound = true
264+
265+ break
266+ }
267+ }
268+
269+ Expect (sandboxFound ).To (BeFalse (),
270+ "Sandbox %s MUST NOT be listed while RunPodSandbox hook is blocking" , hookPodID )
271+
272+ By ("verifying PodSandboxStatus is not accessible while hook is blocking" )
273+
274+ if hookPodID != "" {
275+ statusResp , statusErr := rc .PodSandboxStatus (ctx , hookPodID , false )
276+ // Ideally the sandbox should not be found at all. Some runtimes may
277+ // return a non-Ready status instead of NotFound — both are acceptable.
278+ if statusErr == nil && statusResp != nil && statusResp .GetStatus () != nil {
279+ Expect (statusResp .GetStatus ().GetState ()).NotTo (Equal (runtimeapi .PodSandboxState_SANDBOX_READY ),
280+ "Sandbox MUST NOT report Ready state while RunPodSandbox hook is in progress" )
281+ }
282+ }
283+
284+ By ("releasing the hook and verifying pod becomes Ready" )
285+ close (hookBlocking )
286+ runWg .Wait ()
287+ Expect (runErr ).NotTo (HaveOccurred (), "RunPodSandbox should succeed after hook returns" )
288+ Expect (runPodID ).NotTo (BeEmpty ())
289+ podID = runPodID
290+
291+ // After hook completes, sandbox should be Ready
292+ statusResp , err := rc .PodSandboxStatus (ctx , podID , false )
293+ Expect (err ).NotTo (HaveOccurred ())
294+ Expect (statusResp .GetStatus ().GetState ()).To (Equal (runtimeapi .PodSandboxState_SANDBOX_READY ),
295+ "Sandbox should be Ready after RunPodSandbox hook completes" )
296+ })
297+
298+ It ("should not start workload containers until RunPodSandbox hook completes" , func (ctx SpecContext ) {
299+ // This test validates that workload container creation fails while the
300+ // RunPodSandbox hook is still running. The NRI hook callback receives the
301+ // sandbox ID, so we use that to attempt CreateContainer before RunPodSandbox
302+ // returns. The container creation must fail because the sandbox is not ready.
303+ hookBlocking := make (chan struct {})
304+ hookReached := make (chan struct {})
305+
306+ var (
307+ err error
308+ hookPodID string
309+ )
310+
311+ testStub , err = StartNRITestStub ("cri-test-nri-block-container" , "00" )
312+ Expect (err ).NotTo (HaveOccurred (), "failed to start NRI test stub" )
313+
314+ // Configure stub to block RunPodSandbox and capture the sandbox ID
315+ testStub .Plugin .OnRunPodSandbox = func (hookCtx context.Context , pod * nri.PodSandbox ) error {
316+ hookPodID = pod .GetId ()
317+
318+ close (hookReached )
319+
320+ select {
321+ case <- hookBlocking :
322+ case <- hookCtx .Done ():
323+ }
324+
325+ return nil
326+ }
327+
328+ By ("pulling the test image before triggering RunPodSandbox" )
329+ framework .PullPublicImage (ctx , ic , framework .TestContext .TestImageList .DefaultTestContainerImage , nil )
330+
331+ By ("triggering RunPodSandbox in a goroutine" )
332+
333+ podSandboxName := "nri-test-block-container-" + framework .NewUUID ()
334+ uid := framework .DefaultUIDPrefix + framework .NewUUID ()
335+ namespace := framework .DefaultNamespacePrefix + framework .NewUUID ()
336+ podConfig = & runtimeapi.PodSandboxConfig {
337+ Metadata : framework .BuildPodSandboxMetadata (podSandboxName , uid , namespace , framework .DefaultAttempt ),
338+ Linux : & runtimeapi.LinuxPodSandboxConfig {
339+ CgroupParent : common .GetCgroupParent (ctx , rc ),
340+ },
341+ Labels : framework .DefaultPodLabels ,
342+ }
343+
344+ var (
345+ runErr error
346+ runPodID string
347+ runWg sync.WaitGroup
348+ )
349+
350+ runWg .Go (func () {
351+ runPodID , runErr = rc .RunPodSandbox (ctx , podConfig , framework .TestContext .RuntimeHandler )
352+ })
353+
354+ By ("waiting for RunPodSandbox hook to be reached" )
355+
356+ select {
357+ case <- hookReached :
358+ // Hook is now blocking
359+ case <- time .After (30 * time .Second ):
360+ close (hookBlocking )
361+ Fail ("Timed out waiting for RunPodSandbox NRI hook to fire" )
362+ }
363+
364+ By ("attempting container creation while RunPodSandbox hook is blocking" )
365+
366+ containerName := "nri-test-while-blocked-" + framework .NewUUID ()
367+ containerConfig := & runtimeapi.ContainerConfig {
368+ Metadata : framework .BuildContainerMetadata (containerName , framework .DefaultAttempt ),
369+ Image : & runtimeapi.ImageSpec {
370+ Image : framework .TestContext .TestImageList .DefaultTestContainerImage ,
371+ UserSpecifiedImage : framework .TestContext .TestImageList .DefaultTestContainerImage ,
372+ },
373+ Command : framework .DefaultPauseCommand ,
374+ Linux : & runtimeapi.LinuxContainerConfig {},
375+ }
376+
377+ blockedContainerID , createErr := rc .CreateContainer (ctx , hookPodID , containerConfig , podConfig )
378+ Expect (createErr ).To (HaveOccurred (),
379+ "CreateContainer MUST fail while RunPodSandbox hook is in progress " +
380+ "(sandbox %s is not ready)" , hookPodID )
381+ Expect (blockedContainerID ).To (BeEmpty (),
382+ "No container ID should be returned when creation fails" )
383+
384+ By ("releasing the hook and verifying pod becomes Ready" )
385+ close (hookBlocking )
386+ runWg .Wait ()
387+ Expect (runErr ).NotTo (HaveOccurred (), "RunPodSandbox should succeed after hook returns" )
388+ Expect (runPodID ).NotTo (BeEmpty ())
389+ podID = runPodID
390+
391+ By ("verifying container creation succeeds after hook completes" )
392+
393+ containerName = "nri-test-after-hook-" + framework .NewUUID ()
394+ containerConfig = & runtimeapi.ContainerConfig {
395+ Metadata : framework .BuildContainerMetadata (containerName , framework .DefaultAttempt ),
396+ Image : & runtimeapi.ImageSpec {Image : framework .TestContext .TestImageList .DefaultTestContainerImage },
397+ Command : framework .DefaultPauseCommand ,
398+ Linux : & runtimeapi.LinuxContainerConfig {},
399+ }
400+ containerID := framework .CreateContainer (ctx , rc , ic , containerConfig , podID , podConfig )
401+ Expect (containerID ).NotTo (BeEmpty (),
402+ "Container creation should succeed after RunPodSandbox hook completes" )
403+
404+ // Clean up container
405+ if err := rc .StopContainer (ctx , containerID , 0 ); err != nil {
406+ framework .Logf ("StopContainer(%s) failed: %v" , containerID , err )
407+ }
408+
409+ if err := rc .RemoveContainer (ctx , containerID ); err != nil {
410+ framework .Logf ("RemoveContainer(%s) failed: %v" , containerID , err )
411+ }
412+ })
413+ })
152414})
0 commit comments