Skip to content

Commit d6b04a2

Browse files
committed
e2e: add rollback, pause/resume, and non-existing image tests
Signed-off-by: Prachiti Talgulkar <ptalgulk01@users.noreply.github.com>
1 parent da998a0 commit d6b04a2

1 file changed

Lines changed: 226 additions & 12 deletions

File tree

‎test/e2e/bootcnode_test.go‎

Lines changed: 226 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)