@@ -88,14 +88,27 @@ func (r *OnboardingController) Reconcile(ctx context.Context, req ctrl.Request)
8888
8989 computeHost := hv .Name
9090
91+ // Build the status apply config upfront; sub-functions mutate it.
92+ // HypervisorID and ServiceID are always retained so they are never pruned.
93+ statusCfg := apiv1 .HypervisorStatus ().
94+ WithHypervisorID (hv .Status .HypervisorID ).
95+ WithServiceID (hv .Status .ServiceID )
96+ statusCfg .Conditions = utils .ConditionsFromStatus (hv .Status .Conditions )
97+
98+ apply := func () error {
99+ return r .Status ().Apply (ctx ,
100+ apiv1 .Hypervisor (hv .Name , "" ).WithStatus (statusCfg ),
101+ k8sclient .ForceOwnership , k8sclient .FieldOwner (OnboardingControllerName ))
102+ }
103+
91104 // check if lifecycle management is enabled
92105 if ! hv .Spec .LifecycleEnabled {
93- return ctrl.Result {}, r .abortOnboarding (ctx , hv , computeHost , "Aborted due to LifecycleEnabled being false" )
106+ return ctrl.Result {}, r .abortOnboarding (ctx , hv , computeHost , "Aborted due to LifecycleEnabled being false" , statusCfg , apply )
94107 }
95108
96109 // check if hv is terminating
97110 if hv .Spec .Maintenance == kvmv1 .MaintenanceTermination {
98- return ctrl.Result {}, r .abortOnboarding (ctx , hv , computeHost , "Aborted due to MaintenanceTermination" )
111+ return ctrl.Result {}, r .abortOnboarding (ctx , hv , computeHost , "Aborted due to MaintenanceTermination" , statusCfg , apply )
99112 }
100113
101114 // We bail here out, because the openstack api is not the best to poll
@@ -107,54 +120,36 @@ func (r *OnboardingController) Reconcile(ctx context.Context, req ctrl.Request)
107120 }
108121 return ctrl.Result {}, err
109122 }
110- statusCfg := apiv1 .HypervisorStatus ().
111- WithHypervisorID (hypervisorID ).
112- WithServiceID (serviceID )
113- statusCfg .Conditions = utils .ConditionsFromStatus (hv .Status .Conditions )
123+ statusCfg .WithHypervisorID (hypervisorID ).WithServiceID (serviceID )
114124 utils .SetApplyConfigurationStatusCondition (& statusCfg .Conditions ,
115125 * k8sacmetav1 .Condition ().
116126 WithType (kvmv1 .ConditionTypeOnboarding ).
117127 WithStatus (metav1 .ConditionTrue ).
118128 WithReason (kvmv1 .ConditionReasonInitial ).
119129 WithMessage ("Initial onboarding" ))
120- return ctrl.Result {}, r .Status ().Apply (ctx ,
121- apiv1 .Hypervisor (hv .Name , "" ).WithStatus (statusCfg ),
122- k8sclient .ForceOwnership , k8sclient .FieldOwner (OnboardingControllerName ))
130+ return ctrl.Result {}, apply ()
123131 }
124132
125133 // check condition reason
126134 status := meta .FindStatusCondition (hv .Status .Conditions , kvmv1 .ConditionTypeOnboarding )
127135 switch status .Reason {
128136 case kvmv1 .ConditionReasonInitial :
129- return ctrl.Result {}, r .initialOnboarding (ctx , hv )
137+ return ctrl.Result {}, r .initialOnboarding (ctx , hv , statusCfg , apply )
130138 case kvmv1 .ConditionReasonTesting :
131139 if hv .Spec .SkipTests {
132- return r .completeOnboarding (ctx , computeHost , hv )
140+ return r .completeOnboarding (ctx , computeHost , hv , statusCfg , apply )
133141 } else {
134- return r .smokeTest (ctx , hv , computeHost )
142+ return r .smokeTest (ctx , hv , computeHost , statusCfg , apply )
135143 }
136144 case kvmv1 .ConditionReasonHandover :
137- return r .completeOnboarding (ctx , computeHost , hv )
145+ return r .completeOnboarding (ctx , computeHost , hv , statusCfg , apply )
138146 default :
139147 // Nothing to be done
140148 return ctrl.Result {}, nil
141149 }
142150}
143151
144- // applyOnboardingCondition applies a single onboarding condition via SSA,
145- // carrying all existing conditions and scalar fields forward.
146- func (r * OnboardingController ) applyOnboardingCondition (ctx context.Context , hv * kvmv1.Hypervisor , cond k8sacmetav1.ConditionApplyConfiguration ) error {
147- statusCfg := apiv1 .HypervisorStatus ().
148- WithHypervisorID (hv .Status .HypervisorID ).
149- WithServiceID (hv .Status .ServiceID )
150- statusCfg .Conditions = utils .ConditionsFromStatus (hv .Status .Conditions )
151- utils .SetApplyConfigurationStatusCondition (& statusCfg .Conditions , cond )
152- return r .Status ().Apply (ctx ,
153- apiv1 .Hypervisor (hv .Name , "" ).WithStatus (statusCfg ),
154- k8sclient .ForceOwnership , k8sclient .FieldOwner (OnboardingControllerName ))
155- }
156-
157- func (r * OnboardingController ) abortOnboarding (ctx context.Context , hv * kvmv1.Hypervisor , computeHost , message string ) error {
152+ func (r * OnboardingController ) abortOnboarding (ctx context.Context , hv * kvmv1.Hypervisor , computeHost , message string , statusCfg * apiv1.HypervisorStatusApplyConfiguration , apply func () error ) error {
158153 status := meta .FindStatusCondition (hv .Status .Conditions , kvmv1 .ConditionTypeOnboarding )
159154 // Never onboarded
160155 if status == nil {
@@ -169,23 +164,25 @@ func (r *OnboardingController) abortOnboarding(ctx context.Context, hv *kvmv1.Hy
169164 }
170165
171166 if err := r .deleteTestServers (ctx , computeHost ); err != nil {
172- return errors . Join ( err , r . applyOnboardingCondition ( ctx , hv ,
167+ utils . SetApplyConfigurationStatusCondition ( & statusCfg . Conditions ,
173168 * k8sacmetav1 .Condition ().
174169 WithType (kvmv1 .ConditionTypeOnboarding ).
175170 WithStatus (metav1 .ConditionTrue ). // No cleanup, so we are still "onboarding"
176171 WithReason (kvmv1 .ConditionReasonAborted ).
177- WithMessage (err .Error ())))
172+ WithMessage (err .Error ()))
173+ return errors .Join (err , apply ())
178174 }
179175
180- return r . applyOnboardingCondition ( ctx , hv ,
176+ utils . SetApplyConfigurationStatusCondition ( & statusCfg . Conditions ,
181177 * k8sacmetav1 .Condition ().
182178 WithType (kvmv1 .ConditionTypeOnboarding ).
183179 WithStatus (metav1 .ConditionFalse ).
184180 WithReason (kvmv1 .ConditionReasonAborted ).
185181 WithMessage (message ))
182+ return apply ()
186183}
187184
188- func (r * OnboardingController ) initialOnboarding (ctx context.Context , hv * kvmv1.Hypervisor ) error {
185+ func (r * OnboardingController ) initialOnboarding (ctx context.Context , hv * kvmv1.Hypervisor , statusCfg * apiv1. HypervisorStatusApplyConfiguration , apply func () error ) error {
189186 zone , found := hv .Labels [corev1 .LabelTopologyZone ]
190187 if ! found || zone == "" {
191188 return fmt .Errorf ("cannot find availability-zone label %v on node" , corev1 .LabelTopologyZone )
@@ -213,22 +210,33 @@ func (r *OnboardingController) initialOnboarding(ctx context.Context, hv *kvmv1.
213210 return result .Err
214211 }
215212
216- return r . applyOnboardingCondition ( ctx , hv ,
213+ utils . SetApplyConfigurationStatusCondition ( & statusCfg . Conditions ,
217214 * k8sacmetav1 .Condition ().
218215 WithType (kvmv1 .ConditionTypeOnboarding ).
219216 WithStatus (metav1 .ConditionTrue ).
220217 WithReason (kvmv1 .ConditionReasonTesting ).
221218 WithMessage ("Running onboarding tests" ))
219+ return apply ()
222220}
223221
224- func (r * OnboardingController ) smokeTest (ctx context.Context , hv * kvmv1.Hypervisor , host string ) (ctrl.Result , error ) {
222+ func (r * OnboardingController ) smokeTest (ctx context.Context , hv * kvmv1.Hypervisor , host string , statusCfg * apiv1. HypervisorStatusApplyConfiguration , apply func () error ) (ctrl.Result , error ) {
225223 log := logger .FromContext (ctx )
226224 zone := hv .Labels [corev1 .LabelTopologyZone ]
227225 server , err := r .createOrGetTestServer (ctx , zone , host , hv .UID )
228226 if err != nil {
229227 return ctrl.Result {}, fmt .Errorf ("failed to create or get test instance: %w" , err )
230228 }
231229
230+ setTestingCondition := func (message string ) error {
231+ utils .SetApplyConfigurationStatusCondition (& statusCfg .Conditions ,
232+ * k8sacmetav1 .Condition ().
233+ WithType (kvmv1 .ConditionTypeOnboarding ).
234+ WithStatus (metav1 .ConditionTrue ).
235+ WithReason (kvmv1 .ConditionReasonTesting ).
236+ WithMessage (message ))
237+ return apply ()
238+ }
239+
232240 switch server .Status {
233241 case "ERROR" :
234242 // servers.List doesn't provide the fault field, so fetch the server again
@@ -240,12 +248,7 @@ func (r *OnboardingController) smokeTest(ctx context.Context, hv *kvmv1.Hypervis
240248 }
241249
242250 // Set condition back to testing
243- if err = r .applyOnboardingCondition (ctx , hv ,
244- * k8sacmetav1 .Condition ().
245- WithType (kvmv1 .ConditionTypeOnboarding ).
246- WithStatus (metav1 .ConditionTrue ).
247- WithReason (kvmv1 .ConditionReasonTesting ).
248- WithMessage ("Server ended up in error state: " + server .Fault .Message )); err != nil {
251+ if err = setTestingCondition ("Server ended up in error state: " + server .Fault .Message ); err != nil {
249252 return ctrl.Result {}, err
250253 }
251254
@@ -261,25 +264,15 @@ func (r *OnboardingController) smokeTest(ctx context.Context, hv *kvmv1.Hypervis
261264 ShowConsoleOutput (ctx , r .testComputeClient , server .ID , servers.ShowConsoleOutputOpts {Length : 11 }).
262265 Extract ()
263266 if err != nil {
264- if err2 := r .applyOnboardingCondition (ctx , hv ,
265- * k8sacmetav1 .Condition ().
266- WithType (kvmv1 .ConditionTypeOnboarding ).
267- WithStatus (metav1 .ConditionTrue ).
268- WithReason (kvmv1 .ConditionReasonTesting ).
269- WithMessage (fmt .Sprintf ("could not get console output %v" , err ))); err2 != nil {
267+ if err2 := setTestingCondition (fmt .Sprintf ("could not get console output %v" , err )); err2 != nil {
270268 return ctrl.Result {}, err2
271269 }
272270 return ctrl.Result {RequeueAfter : defaultWaitTime }, nil
273271 }
274272
275273 if ! strings .Contains (consoleOutput , server .Name ) {
276274 if ! server .LaunchedAt .IsZero () && r .Clock .Now ().After (server .LaunchedAt .Add (smokeTestTimeout )) {
277- if err2 := r .applyOnboardingCondition (ctx , hv ,
278- * k8sacmetav1 .Condition ().
279- WithType (kvmv1 .ConditionTypeOnboarding ).
280- WithStatus (metav1 .ConditionTrue ).
281- WithReason (kvmv1 .ConditionReasonTesting ).
282- WithMessage (fmt .Sprintf ("timeout waiting for console output since %v" , server .LaunchedAt ))); err2 != nil {
275+ if err2 := setTestingCondition (fmt .Sprintf ("timeout waiting for console output since %v" , server .LaunchedAt )); err2 != nil {
283276 return ctrl.Result {}, err2
284277 }
285278 if err = servers .Delete (ctx , r .testComputeClient , server .ID ).ExtractErr (); err != nil {
@@ -292,25 +285,20 @@ func (r *OnboardingController) smokeTest(ctx context.Context, hv *kvmv1.Hypervis
292285 }
293286
294287 if err = servers .Delete (ctx , r .testComputeClient , server .ID ).ExtractErr (); err != nil {
295- if err2 := r .applyOnboardingCondition (ctx , hv ,
296- * k8sacmetav1 .Condition ().
297- WithType (kvmv1 .ConditionTypeOnboarding ).
298- WithStatus (metav1 .ConditionTrue ).
299- WithReason (kvmv1 .ConditionReasonTesting ).
300- WithMessage (fmt .Sprintf ("failed to terminate instance %v" , err ))); err2 != nil {
288+ if err2 := setTestingCondition (fmt .Sprintf ("failed to terminate instance %v" , err )); err2 != nil {
301289 return ctrl.Result {}, err2
302290 }
303291 return ctrl.Result {RequeueAfter : defaultWaitTime }, nil
304292 }
305293
306- return r .completeOnboarding (ctx , host , hv )
294+ return r .completeOnboarding (ctx , host , hv , statusCfg , apply )
307295
308296 default :
309297 return ctrl.Result {RequeueAfter : defaultWaitTime }, nil
310298 }
311299}
312300
313- func (r * OnboardingController ) completeOnboarding (ctx context.Context , host string , hv * kvmv1.Hypervisor ) (ctrl.Result , error ) {
301+ func (r * OnboardingController ) completeOnboarding (ctx context.Context , host string , hv * kvmv1.Hypervisor , statusCfg * apiv1. HypervisorStatusApplyConfiguration , apply func () error ) (ctrl.Result , error ) {
314302 onboardingCondition := meta .FindStatusCondition (hv .Status .Conditions , kvmv1 .ConditionTypeOnboarding )
315303 if onboardingCondition != nil && onboardingCondition .Reason == kvmv1 .ConditionReasonHandover {
316304 // We're waiting for aggregates and traits controllers to sync
@@ -326,32 +314,35 @@ func (r *OnboardingController) completeOnboarding(ctx context.Context, host stri
326314 return ctrl.Result {}, nil
327315 }
328316
329- return ctrl. Result {}, r . applyOnboardingCondition ( ctx , hv ,
317+ utils . SetApplyConfigurationStatusCondition ( & statusCfg . Conditions ,
330318 * k8sacmetav1 .Condition ().
331319 WithType (kvmv1 .ConditionTypeOnboarding ).
332320 WithStatus (metav1 .ConditionFalse ).
333321 WithReason (kvmv1 .ConditionReasonSucceeded ).
334322 WithMessage ("Onboarding completed" ))
323+ return ctrl.Result {}, apply ()
335324 }
336325
337326 // First time in completeOnboarding - clean up and prepare for aggregate sync
338327 if err := r .deleteTestServers (ctx , host ); err != nil {
339- return ctrl. Result {}, errors . Join ( err , r . applyOnboardingCondition ( ctx , hv ,
328+ utils . SetApplyConfigurationStatusCondition ( & statusCfg . Conditions ,
340329 * k8sacmetav1 .Condition ().
341330 WithType (kvmv1 .ConditionTypeOnboarding ).
342331 WithStatus (metav1 .ConditionTrue ). // No cleanup, so we are still "onboarding"
343332 WithReason (kvmv1 .ConditionReasonAborted ).
344- WithMessage (err .Error ())))
333+ WithMessage (err .Error ()))
334+ return ctrl.Result {}, errors .Join (err , apply ())
345335 }
346336
347337 // Mark onboarding as almost complete, triggers other controllers to do their part
348338 // Patch status to signal aggregates controller
349- return ctrl. Result {}, r . applyOnboardingCondition ( ctx , hv ,
339+ utils . SetApplyConfigurationStatusCondition ( & statusCfg . Conditions ,
350340 * k8sacmetav1 .Condition ().
351341 WithType (kvmv1 .ConditionTypeOnboarding ).
352342 WithStatus (metav1 .ConditionTrue ).
353343 WithReason (kvmv1 .ConditionReasonHandover ).
354344 WithMessage ("Waiting for other controllers to take over" ))
345+ return ctrl.Result {}, apply ()
355346}
356347
357348func (r * OnboardingController ) deleteTestServers (ctx context.Context , host string ) error {
0 commit comments