diff --git a/hibernation/reconciler_test.go b/hibernation/reconciler_test.go index 5635fbc..5569705 100644 --- a/hibernation/reconciler_test.go +++ b/hibernation/reconciler_test.go @@ -463,6 +463,58 @@ func TestReconcileWakeRecoversFromFailed(t *testing.T) { } } +func TestReconcileWakeClearsHibernateResidueOnFastPath(t *testing.T) { + hib := &cocoonv1.CocoonHibernation{ + ObjectMeta: metav1.ObjectMeta{Name: "hib", Namespace: "ns", Finalizers: []string{finalizerName}}, + Spec: cocoonv1.CocoonHibernationSpec{ + Desire: cocoonv1.HibernationDesireWake, + PodRef: cocoonv1.HibernationPodRef{Name: "demo-0"}, + }, + Status: cocoonv1.CocoonHibernationStatus{Phase: cocoonv1.CocoonHibernationPhaseWaking}, + } + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "demo-0", Namespace: "ns"}, + Status: corev1.PodStatus{ + ContainerStatuses: []corev1.ContainerStatus{{ + State: corev1.ContainerState{Running: &corev1.ContainerStateRunning{}}, + }}, + }, + } + (&meta.VMSpec{VMName: "vk-ns-demo-0", Managed: true}).Apply(pod) + (&meta.VMRuntime{VMID: "vmid-live"}).Apply(pod) + meta.HibernateState(true).Apply(pod) + + scheme := testScheme(t) + cli := ctrlfake.NewClientBuilder(). + WithScheme(scheme). + WithObjects(hib, pod). + WithStatusSubresource(&cocoonv1.CocoonHibernation{}). + Build() + r := &Reconciler{Client: cli, Scheme: scheme, Epoch: &fakeRegistry{}} + + if _, err := r.Reconcile(t.Context(), ctrl.Request{ + NamespacedName: types.NamespacedName{Namespace: "ns", Name: "hib"}, + }); err != nil { + t.Fatalf("Reconcile: %v", err) + } + + var outHib cocoonv1.CocoonHibernation + if err := cli.Get(t.Context(), types.NamespacedName{Namespace: "ns", Name: "hib"}, &outHib); err != nil { + t.Fatalf("get hib: %v", err) + } + if outHib.Status.Phase != cocoonv1.CocoonHibernationPhaseActive { + t.Errorf("phase = %q, want Active (fast-path)", outHib.Status.Phase) + } + + var outPod corev1.Pod + if err := cli.Get(t.Context(), types.NamespacedName{Namespace: "ns", Name: "demo-0"}, &outPod); err != nil { + t.Fatalf("get pod: %v", err) + } + if meta.ReadHibernateState(&outPod) { + t.Error("hibernate annotation must be cleared on the wake fast-path; still true") + } +} + func TestHibernateDeadlineExceeded(t *testing.T) { staleReady := metav1.Condition{ Type: commonk8s.ConditionTypeReady, diff --git a/hibernation/wake.go b/hibernation/wake.go index 30b4f11..4df03a6 100644 --- a/hibernation/wake.go +++ b/hibernation/wake.go @@ -17,6 +17,12 @@ func (r *Reconciler) reconcileWake(ctx context.Context, hib *cocoonv1.CocoonHibe logger := log.WithFunc("hibernation.Reconciler.reconcileWake") r.announceRetryFromFailed(hib, cocoonv1.HibernationDesireWake) + if meta.ReadHibernateState(pod) { + if err := commonk8s.PatchHibernateState(ctx, r.Client, pod, false); err != nil { + return ctrl.Result{}, fmt.Errorf("clear hibernate annotation: %w", err) + } + } + if vmClonedAndRunning(pod) { // Drop snapshot tag (non-fatal; stale tag gets overwritten on next hibernate). if err := r.Epoch.DeleteManifest(ctx, vmName, meta.HibernateSnapshotTag); err != nil { @@ -29,12 +35,6 @@ func (r *Reconciler) reconcileWake(ctx context.Context, hib *cocoonv1.CocoonHibe return ctrl.Result{}, r.setPhase(ctx, hib, cocoonv1.CocoonHibernationPhaseActive, vmName) } - if meta.ReadHibernateState(pod) { - if err := commonk8s.PatchHibernateState(ctx, r.Client, pod, false); err != nil { - return ctrl.Result{}, fmt.Errorf("clear hibernate annotation: %w", err) - } - } - if phaseDeadlineExceeded(hib, cocoonv1.CocoonHibernationPhaseWaking, wakeTimeout) { if r.firstTransitionAt(hib) { observePhaseExit(hib, "timeout")