Skip to content

Commit 88bba8f

Browse files
committed
Refactor OnboardingController: statusCfg passed through reconcile chain
With SSA the status config is built once at the top of Reconcile and threaded through to all sub-functions (abortOnboarding, initialOnboarding, smokeTest, completeOnboarding) which mutate it directly. An apply closure is also passed so each function calls apply() when it has determined the desired condition, rather than building and applying the config itself. This removes the applyOnboardingCondition helper and makes the flow fully declarative: compute desired state, then apply once.
1 parent 993a78e commit 88bba8f

1 file changed

Lines changed: 54 additions & 63 deletions

File tree

internal/controller/onboarding_controller.go

Lines changed: 54 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -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

357348
func (r *OnboardingController) deleteTestServers(ctx context.Context, host string) error {

0 commit comments

Comments
 (0)