Skip to content

Commit cb7f285

Browse files
failed callbacks on create calls
1 parent 7d1ce30 commit cb7f285

1 file changed

Lines changed: 343 additions & 0 deletions

File tree

pkg/validate/nri_linux.go

Lines changed: 343 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package validate
1818

1919
import (
2020
"context"
21+
"errors"
2122
"fmt"
2223
"sync"
2324
"time"
@@ -781,6 +782,81 @@ var _ = framework.KubeDescribe("NRI", func() {
781782
framework.Logf("RemoveContainer(%s) failed: %v", containerID, err)
782783
}
783784
})
785+
786+
It("should fail RunPodSandbox and clean up when the NRI hook errors, then allow retry", func(ctx SpecContext) {
787+
var err error
788+
789+
testStub, err = StartNRITestStub("cri-test-nri-run-error", "00")
790+
Expect(err).NotTo(HaveOccurred(), "failed to start NRI test stub")
791+
792+
// Fail only the first RunPodSandbox invocation so the retry can pass.
793+
var failOnce sync.Once
794+
795+
testStub.Plugin.OnRunPodSandbox = func(_ context.Context, _ *nri.PodSandbox) error {
796+
shouldFail := false
797+
798+
failOnce.Do(func() { shouldFail = true })
799+
800+
if shouldFail {
801+
return errors.New("induced NRI RunPodSandbox failure")
802+
}
803+
804+
return nil
805+
}
806+
807+
By("building the pod sandbox config")
808+
809+
podSandboxName := "nri-test-run-error-" + framework.NewUUID()
810+
uid := framework.DefaultUIDPrefix + framework.NewUUID()
811+
namespace := framework.DefaultNamespacePrefix + framework.NewUUID()
812+
podConfig = &runtimeapi.PodSandboxConfig{
813+
Metadata: framework.BuildPodSandboxMetadata(podSandboxName, uid, namespace, framework.DefaultAttempt),
814+
Linux: &runtimeapi.LinuxPodSandboxConfig{
815+
CgroupParent: common.GetCgroupParent(ctx, rc),
816+
},
817+
Labels: framework.DefaultPodLabels,
818+
}
819+
820+
By("attempting RunPodSandbox while the NRI hook is failing")
821+
822+
failedPodID, runErr := rc.RunPodSandbox(ctx, podConfig, framework.TestContext.RuntimeHandler)
823+
Expect(runErr).To(HaveOccurred(),
824+
"RunPodSandbox MUST fail when the NRI RunPodSandbox hook returns an error")
825+
Expect(failedPodID).To(BeEmpty(),
826+
"No pod sandbox ID should be returned when RunPodSandbox fails")
827+
828+
By("verifying the NRI RunPodSandbox hook actually fired")
829+
// The hook records its event before returning the error, so the
830+
// attempted sandbox ID is available for the cleanup/leak check.
831+
attemptedID := testStub.Plugin.LastRunPodSandboxID()
832+
Expect(attemptedID).NotTo(BeEmpty(),
833+
"NRI RunPodSandbox hook should have fired before the failure")
834+
835+
By("verifying the failed sandbox is not left behind")
836+
837+
pods, listErr := rc.ListPodSandbox(ctx, nil)
838+
Expect(listErr).NotTo(HaveOccurred())
839+
840+
for _, pod := range pods {
841+
matches := pod.GetId() == attemptedID ||
842+
(pod.GetMetadata() != nil && pod.GetMetadata().GetName() == podSandboxName)
843+
Expect(matches).To(BeFalse(),
844+
"sandbox %s MUST be cleaned up after a failed RunPodSandbox", pod.GetId())
845+
}
846+
847+
By("retrying RunPodSandbox after the NRI hook stops failing")
848+
849+
podID = framework.RunPodSandbox(ctx, rc, podConfig)
850+
Expect(podID).NotTo(BeEmpty(),
851+
"RunPodSandbox retry should succeed after the NRI hook stops failing")
852+
853+
By("verifying the retried sandbox becomes Ready")
854+
855+
statusResp, err := rc.PodSandboxStatus(ctx, podID, false)
856+
Expect(err).NotTo(HaveOccurred())
857+
Expect(statusResp.GetStatus().GetState()).To(Equal(runtimeapi.PodSandboxState_SANDBOX_READY),
858+
"sandbox should become Ready after a successful RunPodSandbox retry")
859+
})
784860
})
785861

786862
Context("StopPodSandbox contract", Serial, func() {
@@ -1061,4 +1137,271 @@ var _ = framework.KubeDescribe("NRI", func() {
10611137
"Failed CreateContainer on a stopped sandbox MUST NOT generate an NRI CreateContainer event")
10621138
})
10631139
})
1140+
1141+
Context("CreateContainer contract", Serial, func() {
1142+
var (
1143+
testStub *NRITestStub
1144+
podID string
1145+
podConfig *runtimeapi.PodSandboxConfig
1146+
// containerID holds the successfully created retry container so
1147+
// AfterEach can remove it even if an inline assertion fails.
1148+
containerID string
1149+
)
1150+
1151+
BeforeEach(func(ctx SpecContext) {
1152+
var err error
1153+
1154+
testStub, err = StartNRITestStub("cri-test-nri-create", "00")
1155+
Expect(err).NotTo(HaveOccurred(), "failed to start NRI test stub")
1156+
1157+
// Ensure the test image is available before creating containers.
1158+
framework.PullPublicImage(ctx, ic, framework.TestContext.TestImageList.DefaultTestContainerImage, nil)
1159+
1160+
By("creating a pod sandbox")
1161+
1162+
podSandboxName := "nri-test-create-" + framework.NewUUID()
1163+
uid := framework.DefaultUIDPrefix + framework.NewUUID()
1164+
namespace := framework.DefaultNamespacePrefix + framework.NewUUID()
1165+
podConfig = &runtimeapi.PodSandboxConfig{
1166+
Metadata: framework.BuildPodSandboxMetadata(podSandboxName, uid, namespace, framework.DefaultAttempt),
1167+
Linux: &runtimeapi.LinuxPodSandboxConfig{
1168+
CgroupParent: common.GetCgroupParent(ctx, rc),
1169+
},
1170+
Labels: framework.DefaultPodLabels,
1171+
}
1172+
podID = framework.RunPodSandbox(ctx, rc, podConfig)
1173+
Expect(podID).NotTo(BeEmpty())
1174+
})
1175+
1176+
AfterEach(func(ctx SpecContext) {
1177+
// Stop the stub first so a still-failing hook cannot interfere with
1178+
// teardown of the container or sandbox.
1179+
if testStub != nil {
1180+
testStub.Cleanup()
1181+
}
1182+
1183+
if containerID != "" {
1184+
if err := rc.StopContainer(ctx, containerID, 0); err != nil {
1185+
framework.Logf("AfterEach: StopContainer(%s) failed: %v", containerID, err)
1186+
}
1187+
1188+
if err := rc.RemoveContainer(ctx, containerID); err != nil {
1189+
framework.Logf("AfterEach: RemoveContainer(%s) failed: %v", containerID, err)
1190+
}
1191+
}
1192+
1193+
if podID != "" {
1194+
if err := rc.StopPodSandbox(ctx, podID); err != nil {
1195+
framework.Logf("AfterEach: StopPodSandbox(%s) failed: %v", podID, err)
1196+
}
1197+
1198+
if err := rc.RemovePodSandbox(ctx, podID); err != nil {
1199+
framework.Logf("AfterEach: RemovePodSandbox(%s) failed: %v", podID, err)
1200+
}
1201+
}
1202+
})
1203+
1204+
It("should not expose container while CreateContainer hook is in progress", func(ctx SpecContext) {
1205+
// This test validates the spec contract: during CreateContainer hook
1206+
// execution, the container MUST NOT be visible via ListContainers or
1207+
// ContainerStatus.
1208+
hookBlocking := make(chan struct{})
1209+
hookReached := make(chan struct{})
1210+
1211+
var hookContainerID string
1212+
1213+
testStub.Plugin.OnCreateContainer = func(hookCtx context.Context, _ *nri.PodSandbox, container *nri.Container) error {
1214+
hookContainerID = container.GetId()
1215+
1216+
close(hookReached)
1217+
1218+
select {
1219+
case <-hookBlocking:
1220+
case <-hookCtx.Done():
1221+
}
1222+
1223+
return nil
1224+
}
1225+
1226+
By("triggering CreateContainer in a goroutine")
1227+
1228+
containerName := "nri-test-block-create-" + framework.NewUUID()
1229+
containerConfig := &runtimeapi.ContainerConfig{
1230+
Metadata: framework.BuildContainerMetadata(containerName, framework.DefaultAttempt),
1231+
Image: &runtimeapi.ImageSpec{
1232+
Image: framework.TestContext.TestImageList.DefaultTestContainerImage,
1233+
UserSpecifiedImage: framework.TestContext.TestImageList.DefaultTestContainerImage,
1234+
},
1235+
Command: framework.DefaultPauseCommand,
1236+
Linux: &runtimeapi.LinuxContainerConfig{},
1237+
}
1238+
1239+
var (
1240+
createErr error
1241+
createdID string
1242+
createWg sync.WaitGroup
1243+
)
1244+
1245+
createWg.Go(func() {
1246+
createdID, createErr = rc.CreateContainer(ctx, podID, containerConfig, podConfig)
1247+
})
1248+
1249+
By("waiting for CreateContainer hook to be reached")
1250+
1251+
select {
1252+
case <-hookReached:
1253+
// Hook is now blocking
1254+
case <-time.After(30 * time.Second):
1255+
close(hookBlocking) // unblock to avoid goroutine leak
1256+
Fail("Timed out waiting for CreateContainer NRI hook to fire")
1257+
}
1258+
1259+
By("verifying container is NOT listed while hook is blocking")
1260+
1261+
containers, listErr := rc.ListContainers(ctx, &runtimeapi.ContainerFilter{
1262+
PodSandboxId: podID,
1263+
})
1264+
Expect(listErr).NotTo(HaveOccurred())
1265+
1266+
for _, c := range containers {
1267+
Expect(c.GetId()).NotTo(Equal(hookContainerID),
1268+
"Container %s MUST NOT be listed while CreateContainer hook is blocking", hookContainerID)
1269+
}
1270+
1271+
By("verifying ContainerStatus is not accessible while hook is blocking")
1272+
1273+
if hookContainerID != "" {
1274+
statusResp, statusErr := rc.ContainerStatus(ctx, hookContainerID, false)
1275+
if statusErr == nil && statusResp != nil && statusResp.GetStatus() != nil {
1276+
Expect(statusResp.GetStatus().GetState()).NotTo(Equal(runtimeapi.ContainerState_CONTAINER_CREATED),
1277+
"Container MUST NOT report CREATED state while CreateContainer hook is in progress")
1278+
}
1279+
}
1280+
1281+
By("releasing the hook and verifying container is created")
1282+
close(hookBlocking)
1283+
createWg.Wait()
1284+
Expect(createErr).NotTo(HaveOccurred(), "CreateContainer should succeed after hook returns")
1285+
Expect(createdID).NotTo(BeEmpty())
1286+
containerID = createdID
1287+
1288+
// After hook completes, container should be in CREATED state
1289+
statusResp, err := rc.ContainerStatus(ctx, containerID, false)
1290+
Expect(err).NotTo(HaveOccurred())
1291+
Expect(statusResp.GetStatus().GetState()).To(Equal(runtimeapi.ContainerState_CONTAINER_CREATED),
1292+
"Container should be in CREATED state after CreateContainer hook completes")
1293+
})
1294+
1295+
It("should fail CreateContainer when the NRI hook errors, leak nothing, and allow retry", func(ctx SpecContext) {
1296+
// Fail only the first CreateContainer invocation so the retry passes.
1297+
var failOnce sync.Once
1298+
1299+
testStub.Plugin.OnCreateContainer = func(_ context.Context, _ *nri.PodSandbox, _ *nri.Container) error {
1300+
shouldFail := false
1301+
1302+
failOnce.Do(func() { shouldFail = true })
1303+
1304+
if shouldFail {
1305+
return errors.New("induced NRI CreateContainer failure")
1306+
}
1307+
1308+
return nil
1309+
}
1310+
1311+
// Reset events so we only observe container events from this point.
1312+
testStub.Plugin.Reset()
1313+
1314+
By("attempting CreateContainer while the NRI hook is failing")
1315+
1316+
containerName := "nri-test-create-error-ctr-" + framework.NewUUID()
1317+
containerConfig := &runtimeapi.ContainerConfig{
1318+
Metadata: framework.BuildContainerMetadata(containerName, framework.DefaultAttempt),
1319+
Image: &runtimeapi.ImageSpec{Image: framework.TestContext.TestImageList.DefaultTestContainerImage},
1320+
Command: framework.DefaultPauseCommand,
1321+
Linux: &runtimeapi.LinuxContainerConfig{},
1322+
}
1323+
1324+
failedContainerID, createErr := framework.CreateContainerWithError(ctx, rc, ic, containerConfig, podID, podConfig)
1325+
Expect(createErr).To(HaveOccurred(),
1326+
"CreateContainer MUST fail when the NRI CreateContainer hook returns an error")
1327+
Expect(failedContainerID).To(BeEmpty(),
1328+
"No container ID should be returned when CreateContainer fails")
1329+
1330+
By("verifying the NRI CreateContainer hook actually fired")
1331+
// The hook records its event before returning the error, confirming
1332+
// the failure was induced on the creation path as intended.
1333+
Eventually(func() int {
1334+
count := 0
1335+
1336+
for _, e := range testStub.Plugin.Events() {
1337+
if e.Type == EventCreateContainer {
1338+
count++
1339+
}
1340+
}
1341+
1342+
return count
1343+
}, 10*time.Second, 50*time.Millisecond).Should(BeNumerically(">=", 1),
1344+
"NRI CreateContainer hook should have fired before the failure")
1345+
1346+
By("verifying the failed CreateContainer leaked no container")
1347+
1348+
containers, listErr := rc.ListContainers(ctx, &runtimeapi.ContainerFilter{
1349+
PodSandboxId: podID,
1350+
})
1351+
Expect(listErr).NotTo(HaveOccurred(), "ListContainers after failed CreateContainer")
1352+
1353+
for _, c := range containers {
1354+
if c.GetMetadata() != nil && c.GetMetadata().GetName() == containerName {
1355+
containerID = c.GetId() // hand to AfterEach for cleanup
1356+
Fail(fmt.Sprintf("container %s was leaked after a failed CreateContainer", c.GetId()))
1357+
}
1358+
}
1359+
1360+
By("verifying the failed CreateContainer did not start a container")
1361+
// A failed CreateContainer MUST NOT result in a started container.
1362+
// The runtime may emit Stop/Remove events as part of internal cleanup
1363+
// of the partially created container, but a StartContainer event must
1364+
// never appear. Use Consistently so events delivered slightly after the
1365+
// failure are still caught.
1366+
Consistently(func() int {
1367+
count := 0
1368+
1369+
for _, e := range testStub.Plugin.Events() {
1370+
if e.Type == EventStartContainer {
1371+
count++
1372+
}
1373+
}
1374+
1375+
return count
1376+
}, 2*time.Second, 200*time.Millisecond).Should(BeZero(),
1377+
"a failed CreateContainer MUST NOT result in a started container")
1378+
1379+
By("retrying CreateContainer after the NRI hook stops failing")
1380+
1381+
retryName := "nri-test-create-retry-ctr-" + framework.NewUUID()
1382+
retryConfig := &runtimeapi.ContainerConfig{
1383+
Metadata: framework.BuildContainerMetadata(retryName, framework.DefaultAttempt),
1384+
Image: &runtimeapi.ImageSpec{Image: framework.TestContext.TestImageList.DefaultTestContainerImage},
1385+
Command: framework.DefaultPauseCommand,
1386+
Linux: &runtimeapi.LinuxContainerConfig{},
1387+
}
1388+
1389+
retryID := framework.CreateContainer(ctx, rc, ic, retryConfig, podID, podConfig)
1390+
Expect(retryID).NotTo(BeEmpty(),
1391+
"CreateContainer retry should succeed after the NRI hook stops failing")
1392+
// Hand the retry container to AfterEach immediately so a failure in
1393+
// the start/stop/remove assertions below cannot leak it.
1394+
containerID = retryID
1395+
1396+
By("verifying the retried container can be started, stopped, and removed")
1397+
Expect(rc.StartContainer(ctx, retryID)).NotTo(HaveOccurred(),
1398+
"the retried container should start successfully")
1399+
Expect(rc.StopContainer(ctx, retryID, 0)).NotTo(HaveOccurred(),
1400+
"the retried container should stop successfully")
1401+
Expect(rc.RemoveContainer(ctx, retryID)).NotTo(HaveOccurred(),
1402+
"the retried container should be removable")
1403+
// Clear containerID so AfterEach does not attempt to stop/remove it again.
1404+
containerID = ""
1405+
})
1406+
})
10641407
})

0 commit comments

Comments
 (0)