Skip to content

Commit 17d94c1

Browse files
committed
fix(controller): guard empty SnapshotRef, requeue unscheduled source VM
1 parent 08c64a6 commit 17d94c1

File tree

2 files changed

+45
-0
lines changed

2 files changed

+45
-0
lines changed

internal/controller/impvmmigration_controller.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,10 @@ func (r *ImpVMMigrationReconciler) handlePending(ctx context.Context, mig *impv1
9595
return ctrl.Result{}, selErr
9696
}
9797
if selectedNode == "" {
98+
if vm.Spec.NodeName == "" {
99+
// Source VM not yet scheduled; wait for placement.
100+
return ctrl.Result{RequeueAfter: 10 * time.Second}, nil
101+
}
98102
return r.failMigration(ctx, mig, "no CPU-compatible node available (NoCPUCompatibleNode)")
99103
}
100104
base := mig.DeepCopy()
@@ -136,6 +140,10 @@ func (r *ImpVMMigrationReconciler) handlePending(ctx context.Context, mig *impv1
136140
// On success it creates the target VM and advances to "Restoring".
137141
// On failure it sets Phase="Failed".
138142
func (r *ImpVMMigrationReconciler) handleSnapshotting(ctx context.Context, mig *impv1alpha1.ImpVMMigration) (ctrl.Result, error) {
143+
if mig.Status.SnapshotRef == "" {
144+
return r.failMigration(ctx, mig, "internal error: Snapshotting phase without SnapshotRef")
145+
}
146+
139147
snap := &impv1alpha1.ImpVMSnapshot{}
140148
if err := r.Get(ctx, client.ObjectKey{
141149
Namespace: mig.Namespace,

internal/controller/impvmmigration_controller_test.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,43 @@ func TestMigrationReconciler_snapshotFailed_setsFailedPhase(t *testing.T) {
159159
assert.Equal(t, "Failed", updated.Status.Phase)
160160
}
161161

162+
func TestMigrationReconciler_requeueWhenSourceVMUnscheduled(t *testing.T) {
163+
// Source VM has no NodeName yet → should requeue, not fail
164+
mig := &impv1alpha1.ImpVMMigration{}
165+
mig.Name, mig.Namespace = "mig-unsched", "default"
166+
mig.Spec.SourceVMName = "vm-unsched"
167+
mig.Spec.SourceVMNamespace = "default"
168+
mig.Status.Phase = "Pending"
169+
170+
vm := &impv1alpha1.ImpVM{}
171+
vm.Name, vm.Namespace = "vm-unsched", "default"
172+
// NodeName is empty — VM not yet scheduled
173+
174+
scheme := newMigrationTestScheme(t)
175+
c := fake.NewClientBuilder().WithScheme(scheme).WithObjects(mig, vm).WithStatusSubresource(mig).Build()
176+
// Push status into the fake store.
177+
migWithStatus := mig.DeepCopy()
178+
require.NoError(t, c.Status().Update(context.Background(), migWithStatus))
179+
180+
r := &ImpVMMigrationReconciler{Client: c, Scheme: scheme}
181+
182+
res, err := r.Reconcile(context.Background(), reconcile.Request{
183+
NamespacedName: types.NamespacedName{Name: "mig-unsched", Namespace: "default"},
184+
})
185+
if err != nil {
186+
t.Fatalf("unexpected error: %v", err)
187+
}
188+
if res.RequeueAfter == 0 && !res.Requeue {
189+
t.Error("expected requeue when source VM has no NodeName")
190+
}
191+
192+
var updated impv1alpha1.ImpVMMigration
193+
_ = c.Get(context.Background(), types.NamespacedName{Name: "mig-unsched", Namespace: "default"}, &updated)
194+
if updated.Status.Phase == "Failed" {
195+
t.Error("migration should not be Failed when source VM is simply unscheduled")
196+
}
197+
}
198+
162199
func newMigrationReconciler() *ImpVMMigrationReconciler {
163200
return &ImpVMMigrationReconciler{
164201
Client: k8sClient,

0 commit comments

Comments
 (0)