From 99a87b9e9f36d6f9e7c34e9a4f6fab1ce0830bef Mon Sep 17 00:00:00 2001 From: Daniil Antoshin Date: Thu, 2 Jul 2026 13:49:01 +0200 Subject: [PATCH 1/5] fix(vm): start VM after restore for all run policies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After a restore the VM is stopped during the maintenance window (its KVVMI and KVVM are deleted). Bringing it back up relied on a side effect: the recreated KVVM getting RunStrategy=Always at creation time. That path is racy — if the first post-restore KVVMI does not survive (e.g. a broken first boot after the root->non-root virt-launcher migration) the run strategy is already flipped to Manual and nothing starts the VM again, leaving it stuck in Stopped even for AlwaysOnUnlessStoppedManually. Make the restart intent explicit and independent of KVVM recreation timing: capture whether the VM was running before entering the restore maintenance window and store it as an annotation on the VirtualMachine (survives KVVM deletion). The power-state handler consumes this intent via checkNeedStartVM for all run policies and clears it once the VM has been started. Signed-off-by: Daniil Antoshin --- .../pkg/common/annotations/annotations.go | 5 +++++ .../vm/internal/sync_power_state.go | 14 ++++++++++-- .../vm/internal/sync_power_state_test.go | 22 +++++++++++++++++++ .../internal/step/enter_maintenance_step.go | 20 +++++++++++++++++ 4 files changed, 59 insertions(+), 2 deletions(-) diff --git a/images/virtualization-artifact/pkg/common/annotations/annotations.go b/images/virtualization-artifact/pkg/common/annotations/annotations.go index b72fa48663..c5172f0abd 100644 --- a/images/virtualization-artifact/pkg/common/annotations/annotations.go +++ b/images/virtualization-artifact/pkg/common/annotations/annotations.go @@ -107,6 +107,11 @@ const ( // AnnVMRestartRequested is an annotation on KVVM that represents a request to restart a virtual machine. AnnVMRestartRequested = AnnAPIGroupV + "/vm-restart-requested" + // AnnVMStartRequestedAfterRestore is an annotation on VirtualMachine indicating the VM was running before a + // restore operation and must be started again once restore completes. It is kept on the VM (not the KVVM, + // which is deleted during the restore maintenance window) so the start intent survives KVVM recreation. + AnnVMStartRequestedAfterRestore = AnnAPIGroupV + "/start-requested-after-restore" + // AnnVMOPWorkloadUpdate is an annotation on vmop that represents a vmop created by workload-updater controller. AnnVMOPWorkloadUpdate = AnnAPIGroupV + "/workload-update" AnnVMOPWorkloadUpdateImage = AnnAPIGroupV + "/workload-update-image" diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/sync_power_state.go b/images/virtualization-artifact/pkg/controller/vm/internal/sync_power_state.go index fbb1143601..230abb27f0 100644 --- a/images/virtualization-artifact/pkg/controller/vm/internal/sync_power_state.go +++ b/images/virtualization-artifact/pkg/controller/vm/internal/sync_power_state.go @@ -383,8 +383,11 @@ func (h *SyncPowerStateHandler) checkNeedStartVM( isConfigurationApplied bool, runPolicy v1alpha2.RunPolicy, ) bool { + startAfterRestore := s.VirtualMachine().Changed().GetAnnotations()[annotations.AnnVMStartRequestedAfterRestore] == "true" if isConfigurationApplied && - (kvvm.Annotations[annotations.AnnVMStartRequested] == "true" || kvvm.Annotations[annotations.AnnVMRestartRequested] == "true") { + (kvvm.Annotations[annotations.AnnVMStartRequested] == "true" || + kvvm.Annotations[annotations.AnnVMRestartRequested] == "true" || + startAfterRestore) { h.recordStartEventf(ctx, s.VirtualMachine().Current(), "Start initiated by controller for %v policy", runPolicy) return true } @@ -413,7 +416,14 @@ func (h *SyncPowerStateHandler) start( return err } - return kvvmutil.RemoveRestartAnnotation(ctx, h.client, kvvm) + if err := kvvmutil.RemoveRestartAnnotation(ctx, h.client, kvvm); err != nil { + return err + } + + // The VM has been started after restore; clear the intent so it does not re-trigger on a later manual stop. + delete(s.VirtualMachine().Changed().Annotations, annotations.AnnVMStartRequestedAfterRestore) + + return nil } func (h *SyncPowerStateHandler) restart( diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/sync_power_state_test.go b/images/virtualization-artifact/pkg/controller/vm/internal/sync_power_state_test.go index cb58c52ea9..5700de70fd 100644 --- a/images/virtualization-artifact/pkg/controller/vm/internal/sync_power_state_test.go +++ b/images/virtualization-artifact/pkg/controller/vm/internal/sync_power_state_test.go @@ -206,6 +206,28 @@ var _ = Describe("Test action getters for different run policy", func() { ) Expect(action).To(Equal(Nothing)) }) + + It("should return start action when VM is marked to start after restore", func() { + vm.Annotations = map[string]string{annotations.AnnVMStartRequestedAfterRestore: "true"} + _, _, vmState = setupEnvironment(vm, kvvm, kvvmi, vmPod) + + action := handler.handleManualPolicy( + ctx, vmState, kvvm, nil, true, powerstate.ShutdownInfo{}, + ) + + Expect(action).To(Equal(Start)) + }) + + It("should return nothing when marked to start after restore but configuration is not applied", func() { + vm.Annotations = map[string]string{annotations.AnnVMStartRequestedAfterRestore: "true"} + _, _, vmState = setupEnvironment(vm, kvvm, kvvmi, vmPod) + + action := handler.handleManualPolicy( + ctx, vmState, kvvm, nil, false, powerstate.ShutdownInfo{}, + ) + + Expect(action).To(Equal(Nothing)) + }) }) Context("handleAlwaysOnPolicy", func() { diff --git a/images/virtualization-artifact/pkg/controller/vmop/snapshot/internal/step/enter_maintenance_step.go b/images/virtualization-artifact/pkg/controller/vmop/snapshot/internal/step/enter_maintenance_step.go index 1a9a47cabd..034c7036da 100644 --- a/images/virtualization-artifact/pkg/controller/vmop/snapshot/internal/step/enter_maintenance_step.go +++ b/images/virtualization-artifact/pkg/controller/vmop/snapshot/internal/step/enter_maintenance_step.go @@ -27,6 +27,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/reconcile" + "github.com/deckhouse/virtualization-controller/pkg/common/annotations" "github.com/deckhouse/virtualization-controller/pkg/common/object" "github.com/deckhouse/virtualization-controller/pkg/controller/conditions" "github.com/deckhouse/virtualization-controller/pkg/eventrecord" @@ -84,6 +85,25 @@ func (s EnterMaintenanceStep) Take(ctx context.Context, vmop *v1alpha2.VirtualMa return nil, nil } + // Capture whether the VM was running before restore stops it. Restore must not silently leave a + // running VM stopped: the start intent is stored on the VM so it survives the KVVM deletion during + // maintenance and is consumed by the power-state handler once restore completes (see checkNeedStartVM). + if vm.Status.Phase == v1alpha2.MachineRunning || vm.Status.Phase == v1alpha2.MachinePending { + if vm.Annotations == nil { + vm.Annotations = make(map[string]string) + } + if vm.Annotations[annotations.AnnVMStartRequestedAfterRestore] != "true" { + vm.Annotations[annotations.AnnVMStartRequestedAfterRestore] = "true" + if err = s.client.Update(ctx, vm); err != nil { + if apierrors.IsConflict(err) { + return &reconcile.Result{}, nil + } + s.recorder.Event(vmop, corev1.EventTypeWarning, v1alpha2.ReasonErrVMOPFailed, "Failed to mark VM for start after restore: "+err.Error()) + return &reconcile.Result{}, err + } + } + } + conditions.SetCondition( conditions.NewConditionBuilder(vmcondition.TypeMaintenance). Generation(vm.GetGeneration()). From caf411a3621d443c083a0966b4e463fc6b015797 Mon Sep 17 00:00:00 2001 From: Daniil Antoshin Date: Thu, 2 Jul 2026 20:07:16 +0200 Subject: [PATCH 2/5] fix(vm): preserve start-after-restore intent through ProcessRestore EnterMaintenance records the start-requested-after-restore intent on the live VM, but the ProcessRestore step overwrites vm.Annotations wholesale with the annotations from the snapshot template, dropping the intent. As a result the power-state handler never saw it and the VM stayed stopped after restore (reproduced deterministically for the Manual run policy). Preserve the intent annotation across the annotation overwrite in ProcessRestore, same as the AnnVMOPRestore marker. Signed-off-by: Daniil Antoshin --- .../service/restorer/restorers/vm_restorer.go | 8 ++++++ .../restorer/restorers/vm_restorer_test.go | 28 +++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/images/virtualization-artifact/pkg/controller/service/restorer/restorers/vm_restorer.go b/images/virtualization-artifact/pkg/controller/service/restorer/restorers/vm_restorer.go index b7b73e4614..dd9797282a 100644 --- a/images/virtualization-artifact/pkg/controller/service/restorer/restorers/vm_restorer.go +++ b/images/virtualization-artifact/pkg/controller/service/restorer/restorers/vm_restorer.go @@ -193,10 +193,18 @@ func (v *VirtualMachineHandler) ProcessRestore(ctx context.Context) error { vm.Annotations = make(map[string]string) } + // EnterMaintenance records the pre-restore power-state intent on the live VM so it survives the KVVM + // deletion. The snapshot template does not carry it, so preserve it across the full annotation overwrite + // below; otherwise the VM would never be started again after restore. + startIntent := vm.Annotations[annotations.AnnVMStartRequestedAfterRestore] + vm.Spec = v.vm.Spec vm.Labels = v.vm.Labels vm.Annotations = v.vm.Annotations vm.Annotations[annotations.AnnVMOPRestore] = v.restoreUID + if startIntent != "" { + vm.Annotations[annotations.AnnVMStartRequestedAfterRestore] = startIntent + } updErr := v.client.Update(ctx, vm) if updErr != nil { diff --git a/images/virtualization-artifact/pkg/controller/service/restorer/restorers/vm_restorer_test.go b/images/virtualization-artifact/pkg/controller/service/restorer/restorers/vm_restorer_test.go index 8e54f3076e..29c3d323de 100644 --- a/images/virtualization-artifact/pkg/controller/service/restorer/restorers/vm_restorer_test.go +++ b/images/virtualization-artifact/pkg/controller/service/restorer/restorers/vm_restorer_test.go @@ -337,6 +337,34 @@ var _ = Describe("VirtualMachineRestorer", func() { }), ) + It("preserves the start-requested-after-restore intent across the annotation overwrite", func() { + liveVM := vm.DeepCopy() + liveVM.Annotations = map[string]string{ + annotations.AnnVMStartRequestedAfterRestore: "true", + } + // Different spec so ProcessRestore takes the update path (full annotation overwrite). + liveVM.Spec.RunPolicy = v1alpha2.ManualPolicy + + var updatedVM *v1alpha2.VirtualMachine + ic := interceptor.Funcs{ + Update: func(_ context.Context, c client.WithWatch, obj client.Object, opts ...client.UpdateOption) error { + if got, ok := obj.(*v1alpha2.VirtualMachine); ok { + updatedVM = got.DeepCopy() + } + return c.Update(ctx, obj, opts...) + }, + } + fc, err := testutil.NewFakeClientWithInterceptorWithObjects(ic, liveVM) + Expect(err).ToNot(HaveOccurred()) + + handler := NewVirtualMachineHandler(fc, vm, restoreUID, v1alpha2.SnapshotOperationModeStrict) + Expect(handler.ProcessRestore(ctx)).To(Succeed()) + + Expect(updatedVM).ToNot(BeNil()) + Expect(updatedVM.Annotations).To(HaveKeyWithValue(annotations.AnnVMStartRequestedAfterRestore, "true")) + Expect(updatedVM.Annotations).To(HaveKeyWithValue(annotations.AnnVMOPRestore, restoreUID)) + }) + Describe("Override", func() { var rules []v1alpha2.NameReplacement From 2b66f1c2b91b8ff957c78537d25679ba27a91d9b Mon Sep 17 00:00:00 2001 From: Daniil Antoshin Date: Thu, 2 Jul 2026 20:12:07 +0200 Subject: [PATCH 3/5] fix(vm): keep VM stopped after restore when it was stopped manually Symmetrically to the start-after-restore intent, capture the stopped power state before restore and honor it. The maintenance window deletes the KVVM, dropping the implicit "stopped manually" run-strategy state; recreating the KVVM for AlwaysOnUnlessStoppedManually would otherwise start the VM (RunStrategy=Always on create), violating the "unless stopped manually" contract. EnterMaintenance now records keep-stopped-after-restore for a stopped VM, ProcessRestore preserves it across the annotation overwrite (like the start intent), and createKVVM forces RunStrategy=Manual for AlwaysOnUnlessStoppedManually so the VM stays stopped. Normal VM creation without the intent keeps RunStrategy=Always and starts as before. Signed-off-by: Daniil Antoshin --- .../pkg/common/annotations/annotations.go | 6 +++ .../service/restorer/restorers/vm_restorer.go | 14 +++-- .../restorer/restorers/vm_restorer_test.go | 54 ++++++++++--------- .../pkg/controller/vm/internal/sync_kvvm.go | 12 +++++ .../controller/vm/internal/sync_kvvm_test.go | 36 +++++++++++++ .../internal/step/enter_maintenance_step.go | 31 ++++++----- 6 files changed, 111 insertions(+), 42 deletions(-) diff --git a/images/virtualization-artifact/pkg/common/annotations/annotations.go b/images/virtualization-artifact/pkg/common/annotations/annotations.go index c5172f0abd..0e9734332a 100644 --- a/images/virtualization-artifact/pkg/common/annotations/annotations.go +++ b/images/virtualization-artifact/pkg/common/annotations/annotations.go @@ -112,6 +112,12 @@ const ( // which is deleted during the restore maintenance window) so the start intent survives KVVM recreation. AnnVMStartRequestedAfterRestore = AnnAPIGroupV + "/start-requested-after-restore" + // AnnVMKeepStoppedAfterRestore is an annotation on VirtualMachine indicating the VM was stopped before a + // restore operation and must remain stopped once restore completes. It is needed because the maintenance + // window deletes the KVVM, and recreating it for the AlwaysOnUnlessStoppedManually policy would otherwise + // implicitly start the VM (RunStrategy=Always on create), breaking the "unless stopped manually" contract. + AnnVMKeepStoppedAfterRestore = AnnAPIGroupV + "/keep-stopped-after-restore" + // AnnVMOPWorkloadUpdate is an annotation on vmop that represents a vmop created by workload-updater controller. AnnVMOPWorkloadUpdate = AnnAPIGroupV + "/workload-update" AnnVMOPWorkloadUpdateImage = AnnAPIGroupV + "/workload-update-image" diff --git a/images/virtualization-artifact/pkg/controller/service/restorer/restorers/vm_restorer.go b/images/virtualization-artifact/pkg/controller/service/restorer/restorers/vm_restorer.go index dd9797282a..cfa9922172 100644 --- a/images/virtualization-artifact/pkg/controller/service/restorer/restorers/vm_restorer.go +++ b/images/virtualization-artifact/pkg/controller/service/restorer/restorers/vm_restorer.go @@ -19,6 +19,7 @@ package restorer import ( "context" "fmt" + "maps" "k8s.io/apimachinery/pkg/api/equality" apierrors "k8s.io/apimachinery/pkg/api/errors" @@ -195,16 +196,19 @@ func (v *VirtualMachineHandler) ProcessRestore(ctx context.Context) error { // EnterMaintenance records the pre-restore power-state intent on the live VM so it survives the KVVM // deletion. The snapshot template does not carry it, so preserve it across the full annotation overwrite - // below; otherwise the VM would never be started again after restore. - startIntent := vm.Annotations[annotations.AnnVMStartRequestedAfterRestore] + // below; otherwise the VM would be left in the wrong power state after restore. + powerStateIntents := map[string]string{} + for _, k := range []string{annotations.AnnVMStartRequestedAfterRestore, annotations.AnnVMKeepStoppedAfterRestore} { + if val, ok := vm.Annotations[k]; ok { + powerStateIntents[k] = val + } + } vm.Spec = v.vm.Spec vm.Labels = v.vm.Labels vm.Annotations = v.vm.Annotations vm.Annotations[annotations.AnnVMOPRestore] = v.restoreUID - if startIntent != "" { - vm.Annotations[annotations.AnnVMStartRequestedAfterRestore] = startIntent - } + maps.Copy(vm.Annotations, powerStateIntents) updErr := v.client.Update(ctx, vm) if updErr != nil { diff --git a/images/virtualization-artifact/pkg/controller/service/restorer/restorers/vm_restorer_test.go b/images/virtualization-artifact/pkg/controller/service/restorer/restorers/vm_restorer_test.go index 29c3d323de..52964aee12 100644 --- a/images/virtualization-artifact/pkg/controller/service/restorer/restorers/vm_restorer_test.go +++ b/images/virtualization-artifact/pkg/controller/service/restorer/restorers/vm_restorer_test.go @@ -337,33 +337,37 @@ var _ = Describe("VirtualMachineRestorer", func() { }), ) - It("preserves the start-requested-after-restore intent across the annotation overwrite", func() { - liveVM := vm.DeepCopy() - liveVM.Annotations = map[string]string{ - annotations.AnnVMStartRequestedAfterRestore: "true", - } - // Different spec so ProcessRestore takes the update path (full annotation overwrite). - liveVM.Spec.RunPolicy = v1alpha2.ManualPolicy - - var updatedVM *v1alpha2.VirtualMachine - ic := interceptor.Funcs{ - Update: func(_ context.Context, c client.WithWatch, obj client.Object, opts ...client.UpdateOption) error { - if got, ok := obj.(*v1alpha2.VirtualMachine); ok { - updatedVM = got.DeepCopy() - } - return c.Update(ctx, obj, opts...) - }, - } - fc, err := testutil.NewFakeClientWithInterceptorWithObjects(ic, liveVM) - Expect(err).ToNot(HaveOccurred()) + DescribeTable("preserves the pre-restore power-state intent across the annotation overwrite", + func(intentKey string) { + liveVM := vm.DeepCopy() + liveVM.Annotations = map[string]string{ + intentKey: "true", + } + // Different spec so ProcessRestore takes the update path (full annotation overwrite). + liveVM.Spec.RunPolicy = v1alpha2.ManualPolicy + + var updatedVM *v1alpha2.VirtualMachine + ic := interceptor.Funcs{ + Update: func(_ context.Context, c client.WithWatch, obj client.Object, opts ...client.UpdateOption) error { + if got, ok := obj.(*v1alpha2.VirtualMachine); ok { + updatedVM = got.DeepCopy() + } + return c.Update(ctx, obj, opts...) + }, + } + fc, err := testutil.NewFakeClientWithInterceptorWithObjects(ic, liveVM) + Expect(err).ToNot(HaveOccurred()) - handler := NewVirtualMachineHandler(fc, vm, restoreUID, v1alpha2.SnapshotOperationModeStrict) - Expect(handler.ProcessRestore(ctx)).To(Succeed()) + handler := NewVirtualMachineHandler(fc, vm, restoreUID, v1alpha2.SnapshotOperationModeStrict) + Expect(handler.ProcessRestore(ctx)).To(Succeed()) - Expect(updatedVM).ToNot(BeNil()) - Expect(updatedVM.Annotations).To(HaveKeyWithValue(annotations.AnnVMStartRequestedAfterRestore, "true")) - Expect(updatedVM.Annotations).To(HaveKeyWithValue(annotations.AnnVMOPRestore, restoreUID)) - }) + Expect(updatedVM).ToNot(BeNil()) + Expect(updatedVM.Annotations).To(HaveKeyWithValue(intentKey, "true")) + Expect(updatedVM.Annotations).To(HaveKeyWithValue(annotations.AnnVMOPRestore, restoreUID)) + }, + Entry("start-requested-after-restore", annotations.AnnVMStartRequestedAfterRestore), + Entry("keep-stopped-after-restore", annotations.AnnVMKeepStoppedAfterRestore), + ) Describe("Override", func() { var rules []v1alpha2.NameReplacement diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/sync_kvvm.go b/images/virtualization-artifact/pkg/controller/vm/internal/sync_kvvm.go index 6041ad9a6c..a08bc6d464 100644 --- a/images/virtualization-artifact/pkg/controller/vm/internal/sync_kvvm.go +++ b/images/virtualization-artifact/pkg/controller/vm/internal/sync_kvvm.go @@ -414,6 +414,18 @@ func (h *SyncKvvmHandler) createKVVM(ctx context.Context, s state.VirtualMachine return fmt.Errorf("failed to make the internal virtual machine: %w", err) } + // The VM was stopped before restore and must stay stopped. Only AlwaysOnUnlessStoppedManually would start + // on KVVM (re)creation via RunStrategy=Always, so override it to Manual to honor the "unless stopped + // manually" contract. The one-shot intent is cleared here regardless of policy. + changed := s.VirtualMachine().Changed() + if changed.GetAnnotations()[annotations.AnnVMKeepStoppedAfterRestore] == "true" { + if changed.Spec.RunPolicy == v1alpha2.AlwaysOnUnlessStoppedManually { + runStrategy := virtv1.RunStrategyManual + kvvm.Spec.RunStrategy = &runStrategy + } + delete(changed.Annotations, annotations.AnnVMKeepStoppedAfterRestore) + } + err = h.client.Create(ctx, kvvm) if err != nil { if k8serrors.IsAlreadyExists(err) { diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/sync_kvvm_test.go b/images/virtualization-artifact/pkg/controller/vm/internal/sync_kvvm_test.go index f79e78f8d2..d40ab146d2 100644 --- a/images/virtualization-artifact/pkg/controller/vm/internal/sync_kvvm_test.go +++ b/images/virtualization-artifact/pkg/controller/vm/internal/sync_kvvm_test.go @@ -31,6 +31,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" vmbuilder "github.com/deckhouse/virtualization-controller/pkg/builder/vm" + "github.com/deckhouse/virtualization-controller/pkg/common/annotations" "github.com/deckhouse/virtualization-controller/pkg/common/network" "github.com/deckhouse/virtualization-controller/pkg/common/testutil" "github.com/deckhouse/virtualization-controller/pkg/controller/conditions" @@ -285,6 +286,41 @@ var _ = Describe("SyncKvvmHandler", func() { } } + Context("keep-stopped-after-restore intent on KVVM creation", func() { + It("keeps AlwaysOnUnlessStoppedManually VM stopped and clears the intent", func() { + vm := makeVM(v1alpha2.MachineStopped) + vm.Spec.RunPolicy = v1alpha2.AlwaysOnUnlessStoppedManually + vm.Annotations = map[string]string{annotations.AnnVMKeepStoppedAfterRestore: "true"} + + fakeClient, reconcileObj, vmState = setupEnvironment(vm, makeVMIP(), makeVMClass()) + + reconcile() + + kvvm := &virtv1.VirtualMachine{} + Expect(fakeClient.Get(ctx, client.ObjectKeyFromObject(vm), kvvm)).To(Succeed()) + Expect(kvvm.Spec.RunStrategy).NotTo(BeNil()) + Expect(*kvvm.Spec.RunStrategy).To(Equal(virtv1.RunStrategyManual)) + + newVM := &v1alpha2.VirtualMachine{} + Expect(fakeClient.Get(ctx, client.ObjectKeyFromObject(vm), newVM)).To(Succeed()) + Expect(newVM.Annotations).NotTo(HaveKey(annotations.AnnVMKeepStoppedAfterRestore)) + }) + + It("starts AlwaysOnUnlessStoppedManually VM on create without the keep-stopped intent", func() { + vm := makeVM(v1alpha2.MachineStopped) + vm.Spec.RunPolicy = v1alpha2.AlwaysOnUnlessStoppedManually + + fakeClient, reconcileObj, vmState = setupEnvironment(vm, makeVMIP(), makeVMClass()) + + reconcile() + + kvvm := &virtv1.VirtualMachine{} + Expect(fakeClient.Get(ctx, client.ObjectKeyFromObject(vm), kvvm)).To(Succeed()) + Expect(kvvm.Spec.RunStrategy).NotTo(BeNil()) + Expect(*kvvm.Spec.RunStrategy).To(Equal(virtv1.RunStrategyAlways)) + }) + }) + DescribeTable("AwaitingRestart Condition Tests", func(phase v1alpha2.MachinePhase, needChange bool, expectedStatus metav1.ConditionStatus, expectedExistence bool) { ip := makeVMIP() diff --git a/images/virtualization-artifact/pkg/controller/vmop/snapshot/internal/step/enter_maintenance_step.go b/images/virtualization-artifact/pkg/controller/vmop/snapshot/internal/step/enter_maintenance_step.go index 034c7036da..31b3700740 100644 --- a/images/virtualization-artifact/pkg/controller/vmop/snapshot/internal/step/enter_maintenance_step.go +++ b/images/virtualization-artifact/pkg/controller/vmop/snapshot/internal/step/enter_maintenance_step.go @@ -85,22 +85,29 @@ func (s EnterMaintenanceStep) Take(ctx context.Context, vmop *v1alpha2.VirtualMa return nil, nil } - // Capture whether the VM was running before restore stops it. Restore must not silently leave a - // running VM stopped: the start intent is stored on the VM so it survives the KVVM deletion during - // maintenance and is consumed by the power-state handler once restore completes (see checkNeedStartVM). - if vm.Status.Phase == v1alpha2.MachineRunning || vm.Status.Phase == v1alpha2.MachinePending { + // Preserve the VM power state across restore. The maintenance window deletes the KVVM (and with it the + // implicit run-strategy state), so capture whether the VM was running or stopped before restore and store it + // as an annotation. ProcessRestore preserves it across the annotation overwrite, and it is consumed once + // restore completes: a running VM is started again (see checkNeedStartVM), a stopped VM is kept stopped + // (see createKVVM for the AlwaysOnUnlessStoppedManually policy). + var powerStateAnn string + switch vm.Status.Phase { + case v1alpha2.MachineRunning, v1alpha2.MachinePending: + powerStateAnn = annotations.AnnVMStartRequestedAfterRestore + case v1alpha2.MachineStopped: + powerStateAnn = annotations.AnnVMKeepStoppedAfterRestore + } + if powerStateAnn != "" && vm.Annotations[powerStateAnn] != "true" { if vm.Annotations == nil { vm.Annotations = make(map[string]string) } - if vm.Annotations[annotations.AnnVMStartRequestedAfterRestore] != "true" { - vm.Annotations[annotations.AnnVMStartRequestedAfterRestore] = "true" - if err = s.client.Update(ctx, vm); err != nil { - if apierrors.IsConflict(err) { - return &reconcile.Result{}, nil - } - s.recorder.Event(vmop, corev1.EventTypeWarning, v1alpha2.ReasonErrVMOPFailed, "Failed to mark VM for start after restore: "+err.Error()) - return &reconcile.Result{}, err + vm.Annotations[powerStateAnn] = "true" + if err = s.client.Update(ctx, vm); err != nil { + if apierrors.IsConflict(err) { + return &reconcile.Result{}, nil } + s.recorder.Event(vmop, corev1.EventTypeWarning, v1alpha2.ReasonErrVMOPFailed, "Failed to record VM power state for restore: "+err.Error()) + return &reconcile.Result{}, err } } From 4a40e6e8ff71900c6e9e491e2f4314dff5503bdd Mon Sep 17 00:00:00 2001 From: Daniil Antoshin Date: Thu, 2 Jul 2026 20:16:36 +0200 Subject: [PATCH 4/5] refactor(vm): use a single restore power-state annotation Replace the two boolean annotations (start-requested-after-restore and keep-stopped-after-restore) with a single restore-power-state annotation whose value is a MachinePhase string ("Running" or "Stopped"). The two intents are mutually exclusive, so one key makes that explicit by construction, drops a constant, and simplifies the ProcessRestore preservation to a single key. Signed-off-by: Daniil Antoshin --- .../pkg/common/annotations/annotations.go | 17 +++++++---------- .../service/restorer/restorers/vm_restorer.go | 18 +++++++----------- .../restorer/restorers/vm_restorer_test.go | 12 ++++++------ .../pkg/controller/vm/internal/sync_kvvm.go | 4 ++-- .../controller/vm/internal/sync_kvvm_test.go | 4 ++-- .../controller/vm/internal/sync_power_state.go | 4 ++-- .../vm/internal/sync_power_state_test.go | 4 ++-- .../internal/step/enter_maintenance_step.go | 10 +++++----- 8 files changed, 33 insertions(+), 40 deletions(-) diff --git a/images/virtualization-artifact/pkg/common/annotations/annotations.go b/images/virtualization-artifact/pkg/common/annotations/annotations.go index 0e9734332a..37e8746a33 100644 --- a/images/virtualization-artifact/pkg/common/annotations/annotations.go +++ b/images/virtualization-artifact/pkg/common/annotations/annotations.go @@ -107,16 +107,13 @@ const ( // AnnVMRestartRequested is an annotation on KVVM that represents a request to restart a virtual machine. AnnVMRestartRequested = AnnAPIGroupV + "/vm-restart-requested" - // AnnVMStartRequestedAfterRestore is an annotation on VirtualMachine indicating the VM was running before a - // restore operation and must be started again once restore completes. It is kept on the VM (not the KVVM, - // which is deleted during the restore maintenance window) so the start intent survives KVVM recreation. - AnnVMStartRequestedAfterRestore = AnnAPIGroupV + "/start-requested-after-restore" - - // AnnVMKeepStoppedAfterRestore is an annotation on VirtualMachine indicating the VM was stopped before a - // restore operation and must remain stopped once restore completes. It is needed because the maintenance - // window deletes the KVVM, and recreating it for the AlwaysOnUnlessStoppedManually policy would otherwise - // implicitly start the VM (RunStrategy=Always on create), breaking the "unless stopped manually" contract. - AnnVMKeepStoppedAfterRestore = AnnAPIGroupV + "/keep-stopped-after-restore" + // AnnVMRestorePowerState is an annotation on VirtualMachine that records the VM power state captured before a + // restore operation, so restore restores it. It is kept on the VM (not the KVVM, which is deleted during the + // restore maintenance window) so it survives KVVM recreation. The value is a MachinePhase string: + // "Running" means start the VM again after restore (see checkNeedStartVM); "Stopped" means keep it stopped, + // which for AlwaysOnUnlessStoppedManually requires overriding the implicit RunStrategy=Always on KVVM create + // (see createKVVM) to honor the "unless stopped manually" contract. + AnnVMRestorePowerState = AnnAPIGroupV + "/restore-power-state" // AnnVMOPWorkloadUpdate is an annotation on vmop that represents a vmop created by workload-updater controller. AnnVMOPWorkloadUpdate = AnnAPIGroupV + "/workload-update" diff --git a/images/virtualization-artifact/pkg/controller/service/restorer/restorers/vm_restorer.go b/images/virtualization-artifact/pkg/controller/service/restorer/restorers/vm_restorer.go index cfa9922172..768bd63063 100644 --- a/images/virtualization-artifact/pkg/controller/service/restorer/restorers/vm_restorer.go +++ b/images/virtualization-artifact/pkg/controller/service/restorer/restorers/vm_restorer.go @@ -19,7 +19,6 @@ package restorer import ( "context" "fmt" - "maps" "k8s.io/apimachinery/pkg/api/equality" apierrors "k8s.io/apimachinery/pkg/api/errors" @@ -194,21 +193,18 @@ func (v *VirtualMachineHandler) ProcessRestore(ctx context.Context) error { vm.Annotations = make(map[string]string) } - // EnterMaintenance records the pre-restore power-state intent on the live VM so it survives the KVVM - // deletion. The snapshot template does not carry it, so preserve it across the full annotation overwrite - // below; otherwise the VM would be left in the wrong power state after restore. - powerStateIntents := map[string]string{} - for _, k := range []string{annotations.AnnVMStartRequestedAfterRestore, annotations.AnnVMKeepStoppedAfterRestore} { - if val, ok := vm.Annotations[k]; ok { - powerStateIntents[k] = val - } - } + // EnterMaintenance records the pre-restore power state on the live VM so it survives the KVVM deletion. + // The snapshot template does not carry it, so preserve it across the full annotation overwrite below; + // otherwise the VM would be left in the wrong power state after restore. + powerState, hasPowerState := vm.Annotations[annotations.AnnVMRestorePowerState] vm.Spec = v.vm.Spec vm.Labels = v.vm.Labels vm.Annotations = v.vm.Annotations vm.Annotations[annotations.AnnVMOPRestore] = v.restoreUID - maps.Copy(vm.Annotations, powerStateIntents) + if hasPowerState { + vm.Annotations[annotations.AnnVMRestorePowerState] = powerState + } updErr := v.client.Update(ctx, vm) if updErr != nil { diff --git a/images/virtualization-artifact/pkg/controller/service/restorer/restorers/vm_restorer_test.go b/images/virtualization-artifact/pkg/controller/service/restorer/restorers/vm_restorer_test.go index 52964aee12..9e3ad39176 100644 --- a/images/virtualization-artifact/pkg/controller/service/restorer/restorers/vm_restorer_test.go +++ b/images/virtualization-artifact/pkg/controller/service/restorer/restorers/vm_restorer_test.go @@ -337,11 +337,11 @@ var _ = Describe("VirtualMachineRestorer", func() { }), ) - DescribeTable("preserves the pre-restore power-state intent across the annotation overwrite", - func(intentKey string) { + DescribeTable("preserves the pre-restore power state across the annotation overwrite", + func(powerState string) { liveVM := vm.DeepCopy() liveVM.Annotations = map[string]string{ - intentKey: "true", + annotations.AnnVMRestorePowerState: powerState, } // Different spec so ProcessRestore takes the update path (full annotation overwrite). liveVM.Spec.RunPolicy = v1alpha2.ManualPolicy @@ -362,11 +362,11 @@ var _ = Describe("VirtualMachineRestorer", func() { Expect(handler.ProcessRestore(ctx)).To(Succeed()) Expect(updatedVM).ToNot(BeNil()) - Expect(updatedVM.Annotations).To(HaveKeyWithValue(intentKey, "true")) + Expect(updatedVM.Annotations).To(HaveKeyWithValue(annotations.AnnVMRestorePowerState, powerState)) Expect(updatedVM.Annotations).To(HaveKeyWithValue(annotations.AnnVMOPRestore, restoreUID)) }, - Entry("start-requested-after-restore", annotations.AnnVMStartRequestedAfterRestore), - Entry("keep-stopped-after-restore", annotations.AnnVMKeepStoppedAfterRestore), + Entry("running", string(v1alpha2.MachineRunning)), + Entry("stopped", string(v1alpha2.MachineStopped)), ) Describe("Override", func() { diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/sync_kvvm.go b/images/virtualization-artifact/pkg/controller/vm/internal/sync_kvvm.go index a08bc6d464..84e76c2fa7 100644 --- a/images/virtualization-artifact/pkg/controller/vm/internal/sync_kvvm.go +++ b/images/virtualization-artifact/pkg/controller/vm/internal/sync_kvvm.go @@ -418,12 +418,12 @@ func (h *SyncKvvmHandler) createKVVM(ctx context.Context, s state.VirtualMachine // on KVVM (re)creation via RunStrategy=Always, so override it to Manual to honor the "unless stopped // manually" contract. The one-shot intent is cleared here regardless of policy. changed := s.VirtualMachine().Changed() - if changed.GetAnnotations()[annotations.AnnVMKeepStoppedAfterRestore] == "true" { + if changed.GetAnnotations()[annotations.AnnVMRestorePowerState] == string(v1alpha2.MachineStopped) { if changed.Spec.RunPolicy == v1alpha2.AlwaysOnUnlessStoppedManually { runStrategy := virtv1.RunStrategyManual kvvm.Spec.RunStrategy = &runStrategy } - delete(changed.Annotations, annotations.AnnVMKeepStoppedAfterRestore) + delete(changed.Annotations, annotations.AnnVMRestorePowerState) } err = h.client.Create(ctx, kvvm) diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/sync_kvvm_test.go b/images/virtualization-artifact/pkg/controller/vm/internal/sync_kvvm_test.go index d40ab146d2..9ce93175e5 100644 --- a/images/virtualization-artifact/pkg/controller/vm/internal/sync_kvvm_test.go +++ b/images/virtualization-artifact/pkg/controller/vm/internal/sync_kvvm_test.go @@ -290,7 +290,7 @@ var _ = Describe("SyncKvvmHandler", func() { It("keeps AlwaysOnUnlessStoppedManually VM stopped and clears the intent", func() { vm := makeVM(v1alpha2.MachineStopped) vm.Spec.RunPolicy = v1alpha2.AlwaysOnUnlessStoppedManually - vm.Annotations = map[string]string{annotations.AnnVMKeepStoppedAfterRestore: "true"} + vm.Annotations = map[string]string{annotations.AnnVMRestorePowerState: string(v1alpha2.MachineStopped)} fakeClient, reconcileObj, vmState = setupEnvironment(vm, makeVMIP(), makeVMClass()) @@ -303,7 +303,7 @@ var _ = Describe("SyncKvvmHandler", func() { newVM := &v1alpha2.VirtualMachine{} Expect(fakeClient.Get(ctx, client.ObjectKeyFromObject(vm), newVM)).To(Succeed()) - Expect(newVM.Annotations).NotTo(HaveKey(annotations.AnnVMKeepStoppedAfterRestore)) + Expect(newVM.Annotations).NotTo(HaveKey(annotations.AnnVMRestorePowerState)) }) It("starts AlwaysOnUnlessStoppedManually VM on create without the keep-stopped intent", func() { diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/sync_power_state.go b/images/virtualization-artifact/pkg/controller/vm/internal/sync_power_state.go index 230abb27f0..55777c8888 100644 --- a/images/virtualization-artifact/pkg/controller/vm/internal/sync_power_state.go +++ b/images/virtualization-artifact/pkg/controller/vm/internal/sync_power_state.go @@ -383,7 +383,7 @@ func (h *SyncPowerStateHandler) checkNeedStartVM( isConfigurationApplied bool, runPolicy v1alpha2.RunPolicy, ) bool { - startAfterRestore := s.VirtualMachine().Changed().GetAnnotations()[annotations.AnnVMStartRequestedAfterRestore] == "true" + startAfterRestore := s.VirtualMachine().Changed().GetAnnotations()[annotations.AnnVMRestorePowerState] == string(v1alpha2.MachineRunning) if isConfigurationApplied && (kvvm.Annotations[annotations.AnnVMStartRequested] == "true" || kvvm.Annotations[annotations.AnnVMRestartRequested] == "true" || @@ -421,7 +421,7 @@ func (h *SyncPowerStateHandler) start( } // The VM has been started after restore; clear the intent so it does not re-trigger on a later manual stop. - delete(s.VirtualMachine().Changed().Annotations, annotations.AnnVMStartRequestedAfterRestore) + delete(s.VirtualMachine().Changed().Annotations, annotations.AnnVMRestorePowerState) return nil } diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/sync_power_state_test.go b/images/virtualization-artifact/pkg/controller/vm/internal/sync_power_state_test.go index 5700de70fd..561b28e7f2 100644 --- a/images/virtualization-artifact/pkg/controller/vm/internal/sync_power_state_test.go +++ b/images/virtualization-artifact/pkg/controller/vm/internal/sync_power_state_test.go @@ -208,7 +208,7 @@ var _ = Describe("Test action getters for different run policy", func() { }) It("should return start action when VM is marked to start after restore", func() { - vm.Annotations = map[string]string{annotations.AnnVMStartRequestedAfterRestore: "true"} + vm.Annotations = map[string]string{annotations.AnnVMRestorePowerState: string(v1alpha2.MachineRunning)} _, _, vmState = setupEnvironment(vm, kvvm, kvvmi, vmPod) action := handler.handleManualPolicy( @@ -219,7 +219,7 @@ var _ = Describe("Test action getters for different run policy", func() { }) It("should return nothing when marked to start after restore but configuration is not applied", func() { - vm.Annotations = map[string]string{annotations.AnnVMStartRequestedAfterRestore: "true"} + vm.Annotations = map[string]string{annotations.AnnVMRestorePowerState: string(v1alpha2.MachineRunning)} _, _, vmState = setupEnvironment(vm, kvvm, kvvmi, vmPod) action := handler.handleManualPolicy( diff --git a/images/virtualization-artifact/pkg/controller/vmop/snapshot/internal/step/enter_maintenance_step.go b/images/virtualization-artifact/pkg/controller/vmop/snapshot/internal/step/enter_maintenance_step.go index 31b3700740..f5c90ae011 100644 --- a/images/virtualization-artifact/pkg/controller/vmop/snapshot/internal/step/enter_maintenance_step.go +++ b/images/virtualization-artifact/pkg/controller/vmop/snapshot/internal/step/enter_maintenance_step.go @@ -90,18 +90,18 @@ func (s EnterMaintenanceStep) Take(ctx context.Context, vmop *v1alpha2.VirtualMa // as an annotation. ProcessRestore preserves it across the annotation overwrite, and it is consumed once // restore completes: a running VM is started again (see checkNeedStartVM), a stopped VM is kept stopped // (see createKVVM for the AlwaysOnUnlessStoppedManually policy). - var powerStateAnn string + var powerState string switch vm.Status.Phase { case v1alpha2.MachineRunning, v1alpha2.MachinePending: - powerStateAnn = annotations.AnnVMStartRequestedAfterRestore + powerState = string(v1alpha2.MachineRunning) case v1alpha2.MachineStopped: - powerStateAnn = annotations.AnnVMKeepStoppedAfterRestore + powerState = string(v1alpha2.MachineStopped) } - if powerStateAnn != "" && vm.Annotations[powerStateAnn] != "true" { + if powerState != "" && vm.Annotations[annotations.AnnVMRestorePowerState] != powerState { if vm.Annotations == nil { vm.Annotations = make(map[string]string) } - vm.Annotations[powerStateAnn] = "true" + vm.Annotations[annotations.AnnVMRestorePowerState] = powerState if err = s.client.Update(ctx, vm); err != nil { if apierrors.IsConflict(err) { return &reconcile.Result{}, nil From 5dfcef3bef8ba43faa08b4dc48b66446e8c19131 Mon Sep 17 00:00:00 2001 From: Daniil Antoshin Date: Thu, 2 Jul 2026 20:20:14 +0200 Subject: [PATCH 5/5] refactor(vm): consume restore power-state intent only in createKVVM Instead of teaching the power-state handler to read the VM-level intent and clear it, convert it on the freshly created KVVM: "Running" becomes the regular AnnVMStartRequested annotation (reusing the existing start + retry machinery), "Stopped" overrides RunStrategy to Manual for AlwaysOnUnlessStoppedManually. The power-state handler is left untouched, and all consumption lives in one place. Signed-off-by: Daniil Antoshin --- .../pkg/controller/vm/internal/sync_kvvm.go | 13 +++++++---- .../controller/vm/internal/sync_kvvm_test.go | 20 ++++++++++++++++- .../vm/internal/sync_power_state.go | 14 ++---------- .../vm/internal/sync_power_state_test.go | 22 ------------------- 4 files changed, 30 insertions(+), 39 deletions(-) diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/sync_kvvm.go b/images/virtualization-artifact/pkg/controller/vm/internal/sync_kvvm.go index 84e76c2fa7..735c9203e7 100644 --- a/images/virtualization-artifact/pkg/controller/vm/internal/sync_kvvm.go +++ b/images/virtualization-artifact/pkg/controller/vm/internal/sync_kvvm.go @@ -414,11 +414,16 @@ func (h *SyncKvvmHandler) createKVVM(ctx context.Context, s state.VirtualMachine return fmt.Errorf("failed to make the internal virtual machine: %w", err) } - // The VM was stopped before restore and must stay stopped. Only AlwaysOnUnlessStoppedManually would start - // on KVVM (re)creation via RunStrategy=Always, so override it to Manual to honor the "unless stopped - // manually" contract. The one-shot intent is cleared here regardless of policy. + // Restore the pre-restore power state captured by EnterMaintenance onto the freshly (re)created KVVM. + // "Running" is turned into the regular start-request annotation so the existing power-state machinery + // starts the VM and retries on a failed first boot. "Stopped" must override the implicit RunStrategy=Always + // that AlwaysOnUnlessStoppedManually gets on create, to honor the "unless stopped manually" contract. changed := s.VirtualMachine().Changed() - if changed.GetAnnotations()[annotations.AnnVMRestorePowerState] == string(v1alpha2.MachineStopped) { + switch changed.GetAnnotations()[annotations.AnnVMRestorePowerState] { + case string(v1alpha2.MachineRunning): + annotations.AddAnnotation(kvvm, annotations.AnnVMStartRequested, "true") + delete(changed.Annotations, annotations.AnnVMRestorePowerState) + case string(v1alpha2.MachineStopped): if changed.Spec.RunPolicy == v1alpha2.AlwaysOnUnlessStoppedManually { runStrategy := virtv1.RunStrategyManual kvvm.Spec.RunStrategy = &runStrategy diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/sync_kvvm_test.go b/images/virtualization-artifact/pkg/controller/vm/internal/sync_kvvm_test.go index 9ce93175e5..5524bb1237 100644 --- a/images/virtualization-artifact/pkg/controller/vm/internal/sync_kvvm_test.go +++ b/images/virtualization-artifact/pkg/controller/vm/internal/sync_kvvm_test.go @@ -286,7 +286,25 @@ var _ = Describe("SyncKvvmHandler", func() { } } - Context("keep-stopped-after-restore intent on KVVM creation", func() { + Context("restore-power-state intent on KVVM creation", func() { + It("requests start for a VM that was running before restore and clears the intent", func() { + vm := makeVM(v1alpha2.MachineStopped) + vm.Spec.RunPolicy = v1alpha2.ManualPolicy + vm.Annotations = map[string]string{annotations.AnnVMRestorePowerState: string(v1alpha2.MachineRunning)} + + fakeClient, reconcileObj, vmState = setupEnvironment(vm, makeVMIP(), makeVMClass()) + + reconcile() + + kvvm := &virtv1.VirtualMachine{} + Expect(fakeClient.Get(ctx, client.ObjectKeyFromObject(vm), kvvm)).To(Succeed()) + Expect(kvvm.Annotations).To(HaveKeyWithValue(annotations.AnnVMStartRequested, "true")) + + newVM := &v1alpha2.VirtualMachine{} + Expect(fakeClient.Get(ctx, client.ObjectKeyFromObject(vm), newVM)).To(Succeed()) + Expect(newVM.Annotations).NotTo(HaveKey(annotations.AnnVMRestorePowerState)) + }) + It("keeps AlwaysOnUnlessStoppedManually VM stopped and clears the intent", func() { vm := makeVM(v1alpha2.MachineStopped) vm.Spec.RunPolicy = v1alpha2.AlwaysOnUnlessStoppedManually diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/sync_power_state.go b/images/virtualization-artifact/pkg/controller/vm/internal/sync_power_state.go index 55777c8888..fbb1143601 100644 --- a/images/virtualization-artifact/pkg/controller/vm/internal/sync_power_state.go +++ b/images/virtualization-artifact/pkg/controller/vm/internal/sync_power_state.go @@ -383,11 +383,8 @@ func (h *SyncPowerStateHandler) checkNeedStartVM( isConfigurationApplied bool, runPolicy v1alpha2.RunPolicy, ) bool { - startAfterRestore := s.VirtualMachine().Changed().GetAnnotations()[annotations.AnnVMRestorePowerState] == string(v1alpha2.MachineRunning) if isConfigurationApplied && - (kvvm.Annotations[annotations.AnnVMStartRequested] == "true" || - kvvm.Annotations[annotations.AnnVMRestartRequested] == "true" || - startAfterRestore) { + (kvvm.Annotations[annotations.AnnVMStartRequested] == "true" || kvvm.Annotations[annotations.AnnVMRestartRequested] == "true") { h.recordStartEventf(ctx, s.VirtualMachine().Current(), "Start initiated by controller for %v policy", runPolicy) return true } @@ -416,14 +413,7 @@ func (h *SyncPowerStateHandler) start( return err } - if err := kvvmutil.RemoveRestartAnnotation(ctx, h.client, kvvm); err != nil { - return err - } - - // The VM has been started after restore; clear the intent so it does not re-trigger on a later manual stop. - delete(s.VirtualMachine().Changed().Annotations, annotations.AnnVMRestorePowerState) - - return nil + return kvvmutil.RemoveRestartAnnotation(ctx, h.client, kvvm) } func (h *SyncPowerStateHandler) restart( diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/sync_power_state_test.go b/images/virtualization-artifact/pkg/controller/vm/internal/sync_power_state_test.go index 561b28e7f2..cb58c52ea9 100644 --- a/images/virtualization-artifact/pkg/controller/vm/internal/sync_power_state_test.go +++ b/images/virtualization-artifact/pkg/controller/vm/internal/sync_power_state_test.go @@ -206,28 +206,6 @@ var _ = Describe("Test action getters for different run policy", func() { ) Expect(action).To(Equal(Nothing)) }) - - It("should return start action when VM is marked to start after restore", func() { - vm.Annotations = map[string]string{annotations.AnnVMRestorePowerState: string(v1alpha2.MachineRunning)} - _, _, vmState = setupEnvironment(vm, kvvm, kvvmi, vmPod) - - action := handler.handleManualPolicy( - ctx, vmState, kvvm, nil, true, powerstate.ShutdownInfo{}, - ) - - Expect(action).To(Equal(Start)) - }) - - It("should return nothing when marked to start after restore but configuration is not applied", func() { - vm.Annotations = map[string]string{annotations.AnnVMRestorePowerState: string(v1alpha2.MachineRunning)} - _, _, vmState = setupEnvironment(vm, kvvm, kvvmi, vmPod) - - action := handler.handleManualPolicy( - ctx, vmState, kvvm, nil, false, powerstate.ShutdownInfo{}, - ) - - Expect(action).To(Equal(Nothing)) - }) }) Context("handleAlwaysOnPolicy", func() {