@@ -18,6 +18,7 @@ package validate
1818
1919import (
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