Skip to content

Commit b424e35

Browse files
authored
Merge pull request #2094 from SergeyKanzhelev/runpodsandbox-state-contract-a-5a4f9333
RunPodSandbox NRI state contract and blocking behavior
2 parents 8e68018 + 41d9994 commit b424e35

2 files changed

Lines changed: 290 additions & 2 deletions

File tree

pkg/validate/nri_linux.go

Lines changed: 263 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,12 @@ limitations under the License.
1717
package validate
1818

1919
import (
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 (
3235
var _ = 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
})

pkg/validate/nri_util_linux.go

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"errors"
2222
"fmt"
2323
"net"
24+
"slices"
2425
"sync"
2526
"time"
2627

@@ -176,6 +177,30 @@ func (p *NRITestPlugin) Events() []NRIEvent {
176177
return result
177178
}
178179

180+
// Reset clears all recorded events.
181+
func (p *NRITestPlugin) Reset() {
182+
p.mu.Lock()
183+
defer p.mu.Unlock()
184+
185+
p.events = nil
186+
}
187+
188+
// LastRunPodSandboxID returns the pod sandbox ID from the most recent RunPodSandbox event,
189+
// or empty string if none recorded. This is useful for cleanup in AfterEach when the test
190+
// may have failed before capturing the pod ID from the CRI call.
191+
func (p *NRITestPlugin) LastRunPodSandboxID() string {
192+
p.mu.Lock()
193+
defer p.mu.Unlock()
194+
195+
for i := range slices.Backward(p.events) {
196+
if p.events[i].Type == EventRunPodSandbox {
197+
return p.events[i].PodSandboxID
198+
}
199+
}
200+
201+
return ""
202+
}
203+
179204
// WaitForEventCount waits until at least count events are recorded, or times out.
180205
func (p *NRITestPlugin) WaitForEventCount(count int, timeout time.Duration) ([]NRIEvent, error) {
181206
deadline := time.Now().Add(timeout)
@@ -374,7 +399,8 @@ func (ts *NRITestStub) Stop() {
374399
}
375400
}
376401

377-
// Cleanup stops the stub.
402+
// Cleanup stops the stub and resets events.
378403
func (ts *NRITestStub) Cleanup() {
379404
ts.Stop()
405+
ts.Plugin.Reset()
380406
}

0 commit comments

Comments
 (0)