@@ -198,6 +198,35 @@ func TestUpdateReboot(t *testing.T) {
198198 fmt .Sprintf ("expected update-marker to exist on host, kubectl exec output: %s" , string (out )))
199199
200200 t .Logf ("Verified update-marker exists on host via daemon pod" )
201+
202+ // Phase 7: Rollback to original image.
203+ originalRef := env .NodeImageDigestedPullSpec ()
204+
205+ modified = pool .DeepCopy ()
206+ modified .Spec .Image .Ref = originalRef
207+ g .Expect (env .Client .Patch (ctx , modified , client .MergeFrom (pool ))).To (Succeed ())
208+ * pool = * modified
209+
210+ t .Logf ("Patched pool to rollback to original image %s" , originalRef )
211+
212+ // Phase 8: Wait for Idle with the original digest — proves rollback succeeded.
213+ g .Eventually (func () (bootcv1alpha1.BootcNodeStatus , error ) {
214+ var bn2 bootcv1alpha1.BootcNode
215+ err := env .Client .Get (ctx , client.ObjectKey {Name : nodeName }, & bn2 )
216+ return bn2 .Status , err
217+ }).WithTimeout (5 * time .Minute ).Should (And (
218+ HaveField ("Booted" , And (
219+ Not (BeNil ()),
220+ HaveField ("ImageDigest" , Equal (env .NodeImageDigest ())),
221+ )),
222+ HaveField ("Conditions" , ContainElement (And (
223+ HaveField ("Type" , bootcv1alpha1 .NodeIdle ),
224+ HaveField ("Status" , metav1 .ConditionTrue ),
225+ HaveField ("Reason" , bootcv1alpha1 .NodeReasonIdle ),
226+ ))),
227+ ), "expected node to reach Idle with original image after rollback" )
228+
229+ t .Logf ("Node %q successfully rolled back to original image" , nodeName )
201230}
202231
203232// TestTagResolution creates a pool with a tag-based image ref, verifies
@@ -220,19 +249,19 @@ func TestTagResolution(t *testing.T) {
220249 g .Expect (env .Client .Create (ctx , pool )).To (Succeed ())
221250
222251 // Verify targetDigest is resolved to the original image digest.
223- g .Eventually (func (g Gomega ) string {
252+ g .Eventually (func () ( string , error ) {
224253 var p bootcv1alpha1.BootcNodePool
225- g . Expect ( env .Client .Get (ctx , client .ObjectKeyFromObject (pool ), & p )). To ( Succeed () )
226- return p .Status .TargetDigest
254+ err := env .Client .Get (ctx , client .ObjectKeyFromObject (pool ), & p )
255+ return p .Status .TargetDigest , err
227256 }).WithTimeout (1 * time .Minute ).Should (Equal (env .NodeImageDigest ()))
228257
229258 t .Logf ("Tag resolved to original digest %s" , env .NodeImageDigest ())
230259
231260 // Wait for node to reach Idle with the original image.
232- g .Eventually (func (g Gomega ) bootcv1alpha1.BootcNodeStatus {
261+ g .Eventually (func () ( bootcv1alpha1.BootcNodeStatus , error ) {
233262 var bn bootcv1alpha1.BootcNode
234- g . Expect ( env .Client .Get (ctx , client.ObjectKey {Name : nodeName }, & bn )). To ( Succeed () )
235- return bn .Status
263+ err := env .Client .Get (ctx , client.ObjectKey {Name : nodeName }, & bn )
264+ return bn .Status , err
236265 }).WithTimeout (3 * time .Minute ).Should (And (
237266 HaveField ("Booted" , And (
238267 Not (BeNil ()),
@@ -251,23 +280,30 @@ func TestTagResolution(t *testing.T) {
251280 "localhost:5000/node@" + env .NodeImageUpdateDigest (),
252281 "localhost:5000/node:latest" ,
253282 )
283+ // Restore the original tag on cleanup so later tests are not affected.
284+ t .Cleanup (func () {
285+ e2eutil .RetagImage (t ,
286+ "localhost:5000/node@" + env .NodeImageDigest (),
287+ "localhost:5000/node:latest" ,
288+ )
289+ })
254290
255291 t .Logf ("Retagged node:latest to update digest %s" , env .NodeImageUpdateDigest ())
256292
257293 // Wait for the controller to re-resolve and pick up the new digest.
258- g .Eventually (func (g Gomega ) string {
294+ g .Eventually (func () ( string , error ) {
259295 var p bootcv1alpha1.BootcNodePool
260- g . Expect ( env .Client .Get (ctx , client .ObjectKeyFromObject (pool ), & p )). To ( Succeed () )
261- return p .Status .TargetDigest
296+ err := env .Client .Get (ctx , client .ObjectKeyFromObject (pool ), & p )
297+ return p .Status .TargetDigest , err
262298 }).WithTimeout (1 * time .Minute ).Should (Equal (env .NodeImageUpdateDigest ()))
263299
264300 t .Logf ("Tag re-resolved to update digest %s" , env .NodeImageUpdateDigest ())
265301
266302 // Wait for node to reach Idle with the update image.
267- g .Eventually (func (g Gomega ) bootcv1alpha1.BootcNodeStatus {
303+ g .Eventually (func () ( bootcv1alpha1.BootcNodeStatus , error ) {
268304 var bn bootcv1alpha1.BootcNode
269- g . Expect ( env .Client .Get (ctx , client.ObjectKey {Name : nodeName }, & bn )). To ( Succeed () )
270- return bn .Status
305+ err := env .Client .Get (ctx , client.ObjectKey {Name : nodeName }, & bn )
306+ return bn .Status , err
271307 }).WithTimeout (5 * time .Minute ).Should (And (
272308 HaveField ("Booted" , And (
273309 Not (BeNil ()),
@@ -281,3 +317,181 @@ func TestTagResolution(t *testing.T) {
281317
282318 t .Logf ("Node %q is Idle with update image" , nodeName )
283319}
320+
321+ // TestPauseResume provisions a worker node, starts an update with the
322+ // pool paused, verifies the node stages but does not reboot, then resumes
323+ // and verifies the update completes.
324+ func TestPauseResume (t * testing.T ) {
325+ g := NewWithT (t )
326+ g .SetDefaultEventuallyTimeout (pollTimeout )
327+ g .SetDefaultEventuallyPollingInterval (pollInterval )
328+
329+ env := e2eutil .New (t )
330+ nodeName := env .AddNode (t )
331+
332+ ctx := context .Background ()
333+
334+ // Phase 1: Create pool with original image and wait for Idle.
335+ pool := env .NewPool ("bnp-pause" , env .NodeImageDigestedPullSpec ())
336+ g .Expect (env .Client .Create (ctx , pool )).To (Succeed ())
337+
338+ var bn bootcv1alpha1.BootcNode
339+ g .Eventually (func () (bootcv1alpha1.BootcNodeStatus , error ) {
340+ err := env .Client .Get (ctx , client.ObjectKey {Name : nodeName }, & bn )
341+ return bn .Status , err
342+ }).WithTimeout (3 * time .Minute ).Should (And (
343+ HaveField ("Booted" , Not (BeNil ())),
344+ HaveField ("Conditions" , ContainElement (And (
345+ HaveField ("Type" , bootcv1alpha1 .NodeIdle ),
346+ HaveField ("Status" , metav1 .ConditionTrue ),
347+ HaveField ("Reason" , bootcv1alpha1 .NodeReasonIdle ),
348+ ))),
349+ ))
350+
351+ t .Logf ("Node %q is Idle with original image" , nodeName )
352+
353+ // Phase 2: Patch pool to update image with paused=true.
354+ updateRef := env .NodeImageUpdateDigestedPullSpec ()
355+
356+ modified := pool .DeepCopy ()
357+ modified .Spec .Image .Ref = updateRef
358+ if modified .Spec .Rollout == nil {
359+ modified .Spec .Rollout = & bootcv1alpha1.RolloutSpec {}
360+ }
361+ modified .Spec .Rollout .Paused = true
362+ g .Expect (env .Client .Patch (ctx , modified , client .MergeFrom (pool ))).To (Succeed ())
363+ * pool = * modified
364+
365+ t .Logf ("Patched pool to update image %s with paused=true" , updateRef )
366+
367+ // Phase 3: Wait for node to stage the image. The node should reach
368+ // Staged state but not proceed to reboot because the pool is paused.
369+ g .Eventually (func () (bootcv1alpha1.BootcNodeStatus , error ) {
370+ err := env .Client .Get (ctx , client.ObjectKey {Name : nodeName }, & bn )
371+ return bn .Status , err
372+ }).WithTimeout (5 * time .Minute ).Should (And (
373+ HaveField ("Staged" , And (
374+ Not (BeNil ()),
375+ HaveField ("ImageDigest" , Equal (env .NodeImageUpdateDigest ())),
376+ )),
377+ HaveField ("Conditions" , ContainElement (And (
378+ HaveField ("Type" , bootcv1alpha1 .NodeIdle ),
379+ HaveField ("Status" , metav1 .ConditionFalse ),
380+ HaveField ("Reason" , bootcv1alpha1 .NodeReasonStaged ),
381+ ))),
382+ HaveField ("Booted" , And (
383+ Not (BeNil ()),
384+ HaveField ("ImageDigest" , Equal (env .NodeImageDigest ())),
385+ )),
386+ ))
387+
388+ t .Logf ("Node %q staged update but did not reboot (paused)" , nodeName )
389+
390+ // Verify the node stays Staged and does not proceed to reboot.
391+ g .Consistently (func () ([]metav1.Condition , error ) {
392+ var bn2 bootcv1alpha1.BootcNode
393+ err := env .Client .Get (ctx , client.ObjectKey {Name : nodeName }, & bn2 )
394+ return bn2 .Status .Conditions , err
395+ }).WithTimeout (10 * time .Second ).WithPolling (2 * time .Second ).Should (ContainElement (And (
396+ HaveField ("Type" , bootcv1alpha1 .NodeIdle ),
397+ HaveField ("Status" , metav1 .ConditionFalse ),
398+ HaveField ("Reason" , bootcv1alpha1 .NodeReasonStaged ),
399+ )), "node should remain Staged while paused" )
400+
401+ // Phase 4: Resume the rollout.
402+ modified = pool .DeepCopy ()
403+ modified .Spec .Rollout .Paused = false
404+ g .Expect (env .Client .Patch (ctx , modified , client .MergeFrom (pool ))).To (Succeed ())
405+ * pool = * modified
406+
407+ t .Logf ("Resumed rollout (paused=false)" )
408+
409+ // Phase 5: Wait for node to complete the update — proves the full
410+ // update lifecycle completed after resume (reboot, boot into new image).
411+ g .Eventually (func () (bootcv1alpha1.BootcNodeStatus , error ) {
412+ err := env .Client .Get (ctx , client.ObjectKey {Name : nodeName }, & bn )
413+ return bn .Status , err
414+ }).WithTimeout (5 * time .Minute ).Should (And (
415+ HaveField ("Booted" , And (
416+ Not (BeNil ()),
417+ HaveField ("ImageDigest" , Equal (env .NodeImageUpdateDigest ())),
418+ )),
419+ HaveField ("Conditions" , ContainElement (And (
420+ HaveField ("Type" , bootcv1alpha1 .NodeIdle ),
421+ HaveField ("Status" , metav1 .ConditionTrue ),
422+ HaveField ("Reason" , bootcv1alpha1 .NodeReasonIdle ),
423+ ))),
424+ ), "expected node to reach Idle with update image after resume" )
425+
426+ t .Logf ("Node %q completed update after resume" , nodeName )
427+ }
428+
429+ // TestNonExistingImage provisions a worker node, creates a pool with the
430+ // original image, then updates to a non-existing image and verifies the
431+ // node enters degraded state and the update does not proceed.
432+ func TestNonExistingImage (t * testing.T ) {
433+ g := NewWithT (t )
434+ g .SetDefaultEventuallyTimeout (pollTimeout )
435+ g .SetDefaultEventuallyPollingInterval (pollInterval )
436+
437+ env := e2eutil .New (t )
438+ nodeName := env .AddNode (t )
439+
440+ ctx := context .Background ()
441+
442+ // Phase 1: Create pool with original image and wait for Idle.
443+ pool := env .NewPool ("bnp-noimg" , env .NodeImageDigestedPullSpec ())
444+ g .Expect (env .Client .Create (ctx , pool )).To (Succeed ())
445+
446+ var bn bootcv1alpha1.BootcNode
447+ g .Eventually (func () (bootcv1alpha1.BootcNodeStatus , error ) {
448+ err := env .Client .Get (ctx , client.ObjectKey {Name : nodeName }, & bn )
449+ return bn .Status , err
450+ }).WithTimeout (3 * time .Minute ).Should (And (
451+ HaveField ("Booted" , Not (BeNil ())),
452+ HaveField ("Conditions" , ContainElement (And (
453+ HaveField ("Type" , bootcv1alpha1 .NodeIdle ),
454+ HaveField ("Status" , metav1 .ConditionTrue ),
455+ HaveField ("Reason" , bootcv1alpha1 .NodeReasonIdle ),
456+ ))),
457+ ))
458+
459+ t .Logf ("Node %q is Idle with original image" , nodeName )
460+
461+ // Phase 2: Patch pool to update to a non-existing image.
462+ nonExistingRef := "localhost:5000/node@sha256:0000000000000000000000000000000000000000000000000000000000000000"
463+
464+ modified := pool .DeepCopy ()
465+ modified .Spec .Image .Ref = nonExistingRef
466+ g .Expect (env .Client .Patch (ctx , modified , client .MergeFrom (pool ))).To (Succeed ())
467+ * pool = * modified
468+
469+ t .Logf ("Patched pool to non-existing image %s" , nonExistingRef )
470+
471+ // Phase 3: Wait for node to enter degraded state.
472+ // The daemon should fail to pull the image and report an error.
473+ g .Eventually (func () (bootcv1alpha1.BootcNodeStatus , error ) {
474+ err := env .Client .Get (ctx , client.ObjectKey {Name : nodeName }, & bn )
475+ return bn .Status , err
476+ }).WithTimeout (5 * time .Minute ).Should (And (
477+ HaveField ("Conditions" , ContainElement (And (
478+ HaveField ("Type" , bootcv1alpha1 .NodeDegraded ),
479+ HaveField ("Status" , metav1 .ConditionTrue ),
480+ HaveField ("Reason" , bootcv1alpha1 .NodeReasonError ),
481+ HaveField ("Message" , ContainSubstring ("stage failed" )),
482+ ))),
483+ HaveField ("Booted" , And (
484+ Not (BeNil ()),
485+ HaveField ("ImageDigest" , Equal (env .NodeImageDigest ())),
486+ )),
487+ ), "expected node to enter degraded state when pulling non-existing image" )
488+
489+ t .Logf ("Node %q entered degraded state as expected" , nodeName )
490+
491+ // Phase 4: Verify the node did not stage the non-existing image.
492+ g .Expect (env .Client .Get (ctx , client.ObjectKey {Name : nodeName }, & bn )).To (Succeed ())
493+ g .Expect (bn .Status .Staged ).To (BeNil (),
494+ "node should not have staged the non-existing image" )
495+
496+ t .Logf ("Verified node %q did not stage non-existing image" , nodeName )
497+ }
0 commit comments