Skip to content

Commit 08c64a6

Browse files
committed
feat(controller): ImpVMMigration full state machine — snapshot → restore → delete source
Replace the TODO skeleton with a proper phase-based state machine: - Phase "" → "Pending" (requeue) - Phase "Pending" → validate source VM, select target node, create child ImpVMSnapshot → "Snapshotting" - Phase "Snapshotting" → wait for snapshot TerminatedAt; on Succeeded create target ImpVM from source spec + executionRef → "Restoring"; on failure → "Failed" - Phase "Restoring" → wait for target VM Running, delete source VM → "Succeeded" - Terminal phases (Succeeded/Failed) → no-op Add SnapshotRef and TargetVMName fields to ImpVMMigrationStatus and regenerate CRD manifests. Add two plain-Go unit tests covering the snapshotting wait-for-snapshot and snapshot-failed paths.
1 parent 91adc1e commit 08c64a6

5 files changed

Lines changed: 389 additions & 23 deletions

File tree

api/v1alpha1/impvmmigration_types.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,14 @@ type ImpVMMigrationStatus struct {
3030
// +optional
3131
SelectedNode string `json:"selectedNode,omitempty"`
3232

33+
// SnapshotRef names the child ImpVMSnapshot created for this migration.
34+
// +optional
35+
SnapshotRef string `json:"snapshotRef,omitempty"`
36+
37+
// TargetVMName is the name of the ImpVM created on the target node.
38+
// +optional
39+
TargetVMName string `json:"targetVMName,omitempty"`
40+
3341
// CompletedAt is the time migration completed or failed.
3442
// +optional
3543
CompletedAt *metav1.Time `json:"completedAt,omitempty"`

config/crd/bases/imp.dev_impvmmigrations.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,14 @@ spec:
152152
description: SelectedNode is the node chosen by the scheduler (when
153153
TargetNode was empty).
154154
type: string
155+
snapshotRef:
156+
description: SnapshotRef names the child ImpVMSnapshot created for
157+
this migration.
158+
type: string
159+
targetVMName:
160+
description: TargetVMName is the name of the ImpVM created on the
161+
target node.
162+
type: string
155163
type: object
156164
type: object
157165
served: true

config/rbac/role.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ rules:
2525
resources:
2626
- clusterimpconfigs
2727
- clusterimpnodeprofiles
28+
- impvmtemplates
2829
verbs:
2930
- get
3031
- list
@@ -33,7 +34,10 @@ rules:
3334
- imp.dev
3435
resources:
3536
- impnetworks
37+
- impvmmigrations
3638
- impvms
39+
- impvmsnapshots
40+
- impwarmpools
3741
verbs:
3842
- create
3943
- delete
@@ -46,14 +50,20 @@ rules:
4650
- imp.dev
4751
resources:
4852
- impnetworks/finalizers
53+
- impvmmigrations/finalizers
4954
- impvms/finalizers
55+
- impvmsnapshots/finalizers
56+
- impwarmpools/finalizers
5057
verbs:
5158
- update
5259
- apiGroups:
5360
- imp.dev
5461
resources:
5562
- impnetworks/status
63+
- impvmmigrations/status
5664
- impvms/status
65+
- impvmsnapshots/status
66+
- impwarmpools/status
5767
verbs:
5868
- get
5969
- patch

internal/controller/impvmmigration_controller.go

Lines changed: 209 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package controller
22

33
import (
44
"context"
5+
"time"
56

67
corev1 "k8s.io/api/core/v1"
78
apierrors "k8s.io/apimachinery/pkg/api/errors"
@@ -35,47 +36,234 @@ func (r *ImpVMMigrationReconciler) Reconcile(ctx context.Context, req ctrl.Reque
3536
return ctrl.Result{}, client.IgnoreNotFound(err)
3637
}
3738

38-
if mig.Status.Phase != "" {
39-
return ctrl.Result{}, nil // already initialised
39+
switch mig.Status.Phase {
40+
case "":
41+
return r.handleEmpty(ctx, mig)
42+
case "Pending":
43+
return r.handlePending(ctx, mig)
44+
case "Snapshotting":
45+
return r.handleSnapshotting(ctx, mig)
46+
case "Restoring":
47+
return r.handleRestoring(ctx, mig)
48+
case "Succeeded", "Failed":
49+
return ctrl.Result{}, nil
50+
default:
51+
log.Info("unknown migration phase, ignoring", "phase", mig.Status.Phase)
52+
return ctrl.Result{}, nil
4053
}
54+
}
4155

56+
// handleEmpty transitions Phase="" → "Pending" and requeues.
57+
func (r *ImpVMMigrationReconciler) handleEmpty(ctx context.Context, mig *impv1alpha1.ImpVMMigration) (ctrl.Result, error) {
4258
base := mig.DeepCopy()
4359
mig.Status.Phase = "Pending"
60+
if err := r.Status().Patch(ctx, mig, client.MergeFrom(base)); err != nil {
61+
return ctrl.Result{}, err
62+
}
63+
return ctrl.Result{Requeue: true}, nil
64+
}
4465

45-
// Validate source VM exists
66+
// handlePending validates the source VM, selects a target node, creates a child
67+
// ImpVMSnapshot, then advances to Phase="Snapshotting".
68+
func (r *ImpVMMigrationReconciler) handlePending(ctx context.Context, mig *impv1alpha1.ImpVMMigration) (ctrl.Result, error) {
69+
log := logf.FromContext(ctx)
70+
71+
// Validate source VM exists.
4672
vm := &impv1alpha1.ImpVM{}
4773
err := r.Get(ctx, client.ObjectKey{
48-
Namespace: mig.Spec.SourceVMNamespace, Name: mig.Spec.SourceVMName,
74+
Namespace: mig.Spec.SourceVMNamespace,
75+
Name: mig.Spec.SourceVMName,
4976
}, vm)
5077
if apierrors.IsNotFound(err) {
51-
mig.Status.Phase = "Failed"
52-
mig.Status.Message = "source VM not found"
53-
} else if err != nil {
78+
return r.failMigration(ctx, mig, "source VM not found")
79+
}
80+
if err != nil {
5481
return ctrl.Result{}, err
55-
} else if mig.Spec.TargetNode != "" {
56-
mig.Status.SelectedNode = mig.Spec.TargetNode
57-
} else {
58-
// CPU-compatible node selection
59-
selectedNode, selErr := r.selectMigrationTarget(ctx, vm)
60-
if selErr != nil {
61-
return ctrl.Result{}, selErr
62-
}
63-
if selectedNode == "" {
64-
mig.Status.Phase = "Failed"
65-
mig.Status.Message = "no CPU-compatible node available (NoCPUCompatibleNode)"
82+
}
83+
84+
// Select target node.
85+
if mig.Status.SelectedNode == "" {
86+
if mig.Spec.TargetNode != "" {
87+
base := mig.DeepCopy()
88+
mig.Status.SelectedNode = mig.Spec.TargetNode
89+
if err := r.Status().Patch(ctx, mig, client.MergeFrom(base)); err != nil {
90+
return ctrl.Result{}, err
91+
}
6692
} else {
93+
selectedNode, selErr := r.selectMigrationTarget(ctx, vm)
94+
if selErr != nil {
95+
return ctrl.Result{}, selErr
96+
}
97+
if selectedNode == "" {
98+
return r.failMigration(ctx, mig, "no CPU-compatible node available (NoCPUCompatibleNode)")
99+
}
100+
base := mig.DeepCopy()
67101
mig.Status.SelectedNode = selectedNode
102+
if err := r.Status().Patch(ctx, mig, client.MergeFrom(base)); err != nil {
103+
return ctrl.Result{}, err
104+
}
105+
}
106+
}
107+
108+
// Create child ImpVMSnapshot.
109+
snap := &impv1alpha1.ImpVMSnapshot{}
110+
snap.Namespace = mig.Namespace
111+
snap.Name = "mig-" + mig.Name
112+
snap.Spec = impv1alpha1.ImpVMSnapshotSpec{
113+
SourceVMName: mig.Spec.SourceVMName,
114+
SourceVMNamespace: mig.Spec.SourceVMNamespace,
115+
Storage: impv1alpha1.SnapshotStorageSpec{Type: "node-local"},
116+
}
117+
if err := ctrl.SetControllerReference(mig, snap, r.Scheme); err != nil {
118+
return ctrl.Result{}, err
119+
}
120+
if err := r.Create(ctx, snap); err != nil && !apierrors.IsAlreadyExists(err) {
121+
return ctrl.Result{}, err
122+
}
123+
124+
base := mig.DeepCopy()
125+
mig.Status.Phase = "Snapshotting"
126+
mig.Status.SnapshotRef = snap.Name
127+
if err := r.Status().Patch(ctx, mig, client.MergeFrom(base)); err != nil {
128+
return ctrl.Result{}, err
129+
}
130+
131+
log.Info("ImpVMMigration snapshotting", "name", mig.Name, "snapshot", snap.Name, "targetNode", mig.Status.SelectedNode)
132+
return ctrl.Result{}, nil
133+
}
134+
135+
// handleSnapshotting waits for the child snapshot to reach a terminal state.
136+
// On success it creates the target VM and advances to "Restoring".
137+
// On failure it sets Phase="Failed".
138+
func (r *ImpVMMigrationReconciler) handleSnapshotting(ctx context.Context, mig *impv1alpha1.ImpVMMigration) (ctrl.Result, error) {
139+
snap := &impv1alpha1.ImpVMSnapshot{}
140+
if err := r.Get(ctx, client.ObjectKey{
141+
Namespace: mig.Namespace,
142+
Name: mig.Status.SnapshotRef,
143+
}, snap); err != nil {
144+
if apierrors.IsNotFound(err) {
145+
// Snapshot was deleted; requeue and wait.
146+
return ctrl.Result{RequeueAfter: 10 * time.Second}, nil
147+
}
148+
return ctrl.Result{}, err
149+
}
150+
151+
if snap.Status.TerminatedAt == nil {
152+
// Snapshot still in progress.
153+
return ctrl.Result{RequeueAfter: 10 * time.Second}, nil
154+
}
155+
156+
if snap.Status.Phase != "Succeeded" {
157+
return r.failMigration(ctx, mig, "snapshot failed: phase="+snap.Status.Phase)
158+
}
159+
160+
// Determine the execution reference to pass to the target VM.
161+
var executionRef string
162+
if snap.Status.LastExecutionRef != nil {
163+
executionRef = snap.Status.LastExecutionRef.Name
164+
} else {
165+
executionRef = snap.Name // fallback: use the snapshot itself
166+
}
167+
168+
return r.createTargetVM(ctx, mig, executionRef)
169+
}
170+
171+
// createTargetVM copies the source VM spec to a new ImpVM on the selected node,
172+
// wiring in the snapshot execution reference, then advances to "Restoring".
173+
func (r *ImpVMMigrationReconciler) createTargetVM(ctx context.Context, mig *impv1alpha1.ImpVMMigration, executionRef string) (ctrl.Result, error) {
174+
log := logf.FromContext(ctx)
175+
176+
srcVM := &impv1alpha1.ImpVM{}
177+
if err := r.Get(ctx, client.ObjectKey{
178+
Namespace: mig.Spec.SourceVMNamespace,
179+
Name: mig.Spec.SourceVMName,
180+
}, srcVM); err != nil {
181+
if apierrors.IsNotFound(err) {
182+
return r.failMigration(ctx, mig, "source VM disappeared before target could be created")
68183
}
184+
return ctrl.Result{}, err
185+
}
186+
187+
targetVM := &impv1alpha1.ImpVM{}
188+
targetVM.Namespace = srcVM.Namespace
189+
targetVM.Name = "mig-" + mig.Name + "-target"
190+
targetVM.Spec = *srcVM.Spec.DeepCopy()
191+
targetVM.Spec.NodeName = mig.Status.SelectedNode
192+
targetVM.Spec.SnapshotRef = executionRef
193+
194+
if err := ctrl.SetControllerReference(mig, targetVM, r.Scheme); err != nil {
195+
return ctrl.Result{}, err
196+
}
197+
if err := r.Create(ctx, targetVM); err != nil && !apierrors.IsAlreadyExists(err) {
198+
return ctrl.Result{}, err
69199
}
70200

201+
base := mig.DeepCopy()
202+
mig.Status.Phase = "Restoring"
203+
mig.Status.TargetVMName = targetVM.Name
71204
if err := r.Status().Patch(ctx, mig, client.MergeFrom(base)); err != nil {
72205
return ctrl.Result{}, err
73206
}
74-
log.Info("ImpVMMigration initialised", "name", mig.Name, "phase", mig.Status.Phase,
75-
"targetNode", mig.Status.SelectedNode)
76207

77-
// TODO: Phase 2 impl: pause VM → snapshot → restore on target → delete source.
208+
log.Info("ImpVMMigration restoring", "name", mig.Name, "targetVM", targetVM.Name)
209+
return ctrl.Result{}, nil
210+
}
211+
212+
// handleRestoring waits for the target VM to reach Running, then deletes the source
213+
// VM and marks the migration Succeeded.
214+
func (r *ImpVMMigrationReconciler) handleRestoring(ctx context.Context, mig *impv1alpha1.ImpVMMigration) (ctrl.Result, error) {
215+
log := logf.FromContext(ctx)
78216

217+
targetVM := &impv1alpha1.ImpVM{}
218+
if err := r.Get(ctx, client.ObjectKey{
219+
Namespace: mig.Spec.SourceVMNamespace,
220+
Name: mig.Status.TargetVMName,
221+
}, targetVM); err != nil {
222+
if apierrors.IsNotFound(err) {
223+
return ctrl.Result{RequeueAfter: 10 * time.Second}, nil
224+
}
225+
return ctrl.Result{}, err
226+
}
227+
228+
if targetVM.Status.Phase != impv1alpha1.VMPhaseRunning {
229+
return ctrl.Result{RequeueAfter: 10 * time.Second}, nil
230+
}
231+
232+
// Delete source VM.
233+
srcVM := &impv1alpha1.ImpVM{}
234+
if err := r.Get(ctx, client.ObjectKey{
235+
Namespace: mig.Spec.SourceVMNamespace,
236+
Name: mig.Spec.SourceVMName,
237+
}, srcVM); err == nil {
238+
if delErr := r.Delete(ctx, srcVM); delErr != nil && !apierrors.IsNotFound(delErr) {
239+
return ctrl.Result{}, delErr
240+
}
241+
} else if !apierrors.IsNotFound(err) {
242+
return ctrl.Result{}, err
243+
}
244+
245+
now := metav1.Now()
246+
base := mig.DeepCopy()
247+
mig.Status.Phase = "Succeeded"
248+
mig.Status.CompletedAt = &now
249+
if err := r.Status().Patch(ctx, mig, client.MergeFrom(base)); err != nil {
250+
return ctrl.Result{}, err
251+
}
252+
253+
log.Info("ImpVMMigration succeeded", "name", mig.Name)
254+
return ctrl.Result{}, nil
255+
}
256+
257+
// failMigration patches the migration to Phase="Failed" with the given message.
258+
func (r *ImpVMMigrationReconciler) failMigration(ctx context.Context, mig *impv1alpha1.ImpVMMigration, msg string) (ctrl.Result, error) {
259+
now := metav1.Now()
260+
base := mig.DeepCopy()
261+
mig.Status.Phase = "Failed"
262+
mig.Status.Message = msg
263+
mig.Status.CompletedAt = &now
264+
if err := r.Status().Patch(ctx, mig, client.MergeFrom(base)); err != nil {
265+
return ctrl.Result{}, err
266+
}
79267
return ctrl.Result{}, nil
80268
}
81269

0 commit comments

Comments
 (0)