@@ -2,6 +2,7 @@ package controller
22
33import (
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