Skip to content

Commit d0e923a

Browse files
committed
feat(repave): implement LastAppliedRepaveTrigger to track repave annotation state
1 parent 3376358 commit d0e923a

5 files changed

Lines changed: 167 additions & 100 deletions

File tree

database/api/v1alpha1/dbinstance_types.go

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,14 @@ type DBInstanceStatus struct {
310310
// +optional
311311
CurrentImageRevision string `json:"currentImageRevision,omitempty"`
312312

313+
// LastAppliedRepaveTrigger records the value of AnnotationRepaveTrigger
314+
// that was last processed (accepted, rejected, or applied) — mirrors
315+
// Flux's ReconcileRequestAnnotation/LastHandledReconcileAt pattern. The
316+
// annotation itself is never modified by the controller; a repave is
317+
// dispatched only when its current value differs from this field.
318+
// +optional
319+
LastAppliedRepaveTrigger string `json:"lastAppliedRepaveTrigger,omitempty"`
320+
313321
// RestartCount is the cumulative number of VM restarts detected or
314322
// initiated by the controller liveness loop (both planned and unplanned).
315323
// +optional
@@ -488,10 +496,12 @@ const (
488496
// down from a later out-of-band recovery VMI.
489497
AnnotationCrashLoopHaltedVMIUID = "dbaas.opencloud.wso2.com/crash-loop-halted-vmi-uid"
490498

491-
// AnnotationRepaveTrigger, when set to "now", triggers a repave —
492-
// swapping the VM's OS disk onto the catalog's current validated
493-
// revision for its stream. The controller clears the annotation once
494-
// the trigger has been processed (accepted, rejected, or applied).
499+
// AnnotationRepaveTrigger, when its value differs from
500+
// Status.LastAppliedRepaveTrigger, triggers a repave — swapping the
501+
// VM's OS disk onto the catalog's current validated revision for its
502+
// stream. The controller never modifies or clears this annotation; set
503+
// a fresh, unique value (e.g. an RFC3339 timestamp) to trigger a new
504+
// repave, mirroring Flux's reconcile.fluxcd.io/requestedAt convention.
495505
AnnotationRepaveTrigger = "dbaas.opencloud.wso2.com/repave-trigger"
496506

497507
// FinalizerName triggers controller-side teardown of Harvester resources.

database/config/crd/bases/dbaas.opencloud.wso2.com_dbinstances.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -464,6 +464,14 @@ spec:
464464
grafanaUrl:
465465
description: GrafanaURL is the per-instance Grafana dashboard URL.
466466
type: string
467+
lastAppliedRepaveTrigger:
468+
description: |-
469+
LastAppliedRepaveTrigger records the value of AnnotationRepaveTrigger
470+
that was last processed (accepted, rejected, or applied) — mirrors
471+
Flux's ReconcileRequestAnnotation/LastHandledReconcileAt pattern. The
472+
annotation itself is never modified by the controller; a repave is
473+
dispatched only when its current value differs from this field.
474+
type: string
467475
lastKnownVMIUID:
468476
description: |-
469477
LastKnownVMIUID is the UID of the VMI last recorded by the controller.

database/internal/ensure/repave.go

Lines changed: 28 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -39,17 +39,22 @@ func newRepaveStep(deps Dependencies) Step { return &repaveStep{Dependencies: de
3939
func (*repaveStep) Name() string { return "repave" }
4040

4141
// ensureRepave reports baked-image drift (ConditionImageDrift, report-only,
42-
// every pass) and applies a repave when dbaasv1.AnnotationRepaveTrigger is
43-
// "now": cold-halt the VM, swap the OS disk to the catalog's current
44-
// revision, regenerate cloud-init, and let ensurePowerState (ordered after
45-
// this step) restart it — same halt/apply/restart shape as ensureResize, and
46-
// ordered directly before it so the two never fight over the VM's power
47-
// state.
42+
// every pass) and applies a repave when dbaasv1.AnnotationRepaveTrigger's
43+
// value differs from Status.LastAppliedRepaveTrigger: cold-halt the VM, swap
44+
// the OS disk to the catalog's current revision, regenerate cloud-init, and
45+
// let ensurePowerState (ordered after this step) restart it — same
46+
// halt/apply/restart shape as ensureResize, and ordered directly before it so
47+
// the two never fight over the VM's power state. The annotation itself is
48+
// never modified by the controller (Flux ReconcileRequestAnnotation style);
49+
// each accept/reject/apply outcome instead records the value it processed
50+
// into Status.LastAppliedRepaveTrigger through the normal deferred status
51+
// patch, so a new repave only dispatches once the annotation is set to a
52+
// fresh value.
4853
//
4954
// If the catalog can't resolve a stream for databaseDefaults.osVersion (unset,
5055
// unknown, or not yet Validated — see resolveBakedImage), this step no-ops
51-
// entirely: no drift is reported and a trigger annotation is left untouched
52-
// for the next pass to reconsider once the catalog is validated.
56+
// entirely: no drift is reported and the trigger annotation is left
57+
// unexamined for the next pass to reconsider once the catalog is validated.
5358
func (r *repaveStep) Run(ctx context.Context, inst *dbaasv1.DBInstance) Result {
5459
// crash-safety recovery. Runs first, unconditionally,
5560
// regardless of catalog state or image observability: if a prior pass
@@ -148,8 +153,11 @@ func (r *repaveStep) Run(ctx context.Context, inst *dbaasv1.DBInstance) Result {
148153
fmt.Sprintf("VM is on the current image revision %q", stream.Revision))
149154
}
150155

151-
// --- repave dispatch: gated on the trigger annotation ---
152-
if inst.Annotations[dbaasv1.AnnotationRepaveTrigger] != "now" {
156+
// --- repave dispatch: gated on the trigger annotation differing from
157+
// the last value this controller processed. The annotation is never
158+
// modified; Status.LastAppliedRepaveTrigger is what advances instead. ---
159+
triggerValue, hasTrigger := inst.Annotations[dbaasv1.AnnotationRepaveTrigger]
160+
if !hasTrigger || triggerValue == inst.Status.LastAppliedRepaveTrigger {
153161
return Satisfied()
154162
}
155163

@@ -159,19 +167,17 @@ func (r *repaveStep) Run(ctx context.Context, inst *dbaasv1.DBInstance) Result {
159167
// caused itself. Without this, a repave that gets as far as stopping the
160168
// VM can never finish: the next pass sees its own RepaveInProgress=True
161169
// having flipped Phase to Modifying, Terminal-aborts on that self-caused
162-
// change, clears the trigger, and leaves the VM halted on the old image
163-
// with no automatic retry .
170+
// change, records the trigger as handled, and leaves the VM halted on
171+
// the old image with no automatic retry.
164172
if inst.Status.Phase != dbaasv1.StatusAvailable &&
165173
!inst.Status.IsConditionTrue(dbaasv1.ConditionRepaveInProgress) {
166174
msg := "repave requires the instance to be Available"
167-
if err := r.clearTriggerAnnotation(ctx, inst); err != nil {
168-
return Transient(err)
169-
}
175+
inst.Status.LastAppliedRepaveTrigger = triggerValue
170176
// Every other Terminal branch in this package sets a condition
171177
// before returning — Result.Reason/Message are otherwise dropped
172178
// entirely (reconcileInstance only reads ControllerResult/Err), so
173179
// without this a blocked repave leaves no observable trace beyond
174-
// the trigger annotation silently disappearing.
180+
// LastAppliedRepaveTrigger silently catching up.
175181
inst.SetCurrentCondition(dbaasv1.ConditionRepaveInProgress, metav1.ConditionFalse, dbaasv1.ReasonRepaveNotAvailable, msg)
176182
return Terminal(dbaasv1.ReasonRepaveNotAvailable, msg)
177183
}
@@ -182,16 +188,12 @@ func (r *repaveStep) Run(ctx context.Context, inst *dbaasv1.DBInstance) Result {
182188
if !ok {
183189
msg := fmt.Sprintf("engineVersion %q is not available in revision %q; migrate data before repaving",
184190
inst.Spec.EngineVersion, stream.Revision)
185-
if err := r.clearTriggerAnnotation(ctx, inst); err != nil {
186-
return Transient(err)
187-
}
191+
inst.Status.LastAppliedRepaveTrigger = triggerValue
188192
inst.SetCurrentCondition(dbaasv1.ConditionRepaveInProgress, metav1.ConditionFalse, dbaasv1.ReasonRepaveBlockedEOL, msg)
189193
return Terminal(dbaasv1.ReasonRepaveBlockedEOL, msg)
190194
}
191195
if inst.Status.CurrentImageRevision == stream.Revision {
192-
if err := r.clearTriggerAnnotation(ctx, inst); err != nil {
193-
return Transient(err)
194-
}
196+
inst.Status.LastAppliedRepaveTrigger = triggerValue
195197
return Satisfied()
196198
}
197199

@@ -257,44 +259,16 @@ func (r *repaveStep) Run(ctx context.Context, inst *dbaasv1.DBInstance) Result {
257259
return res
258260
}
259261

260-
// Clear the annotation and report Pending; the power step (ordered after
261-
// this one) observes desired-running + declared-Halted and restarts on
262-
// its own — no manual StartVM call needed here.
263-
if err := r.clearTriggerAnnotation(ctx, inst); err != nil {
264-
return Transient(err)
265-
}
262+
// Record the trigger as handled and report Pending; the power step
263+
// (ordered after this one) observes desired-running + declared-Halted
264+
// and restarts on its own — no manual StartVM call needed here.
265+
inst.Status.LastAppliedRepaveTrigger = triggerValue
266266
msg := fmt.Sprintf("applied repave to revision %s", stream.Revision)
267267
inst.SetCurrentCondition(dbaasv1.ConditionRepaveInProgress, metav1.ConditionTrue, dbaasv1.ReasonRepaveApplied, msg)
268268
inst.SetCurrentCondition(dbaasv1.ConditionDatabaseReady, metav1.ConditionFalse, dbaasv1.ReasonRepaveApplied, msg)
269269
return Pending(dbaasv1.ReasonRepaveApplied, msg)
270270
}
271271

272-
// clearTriggerAnnotation removes AnnotationRepaveTrigger once a repave has
273-
// either applied or been rejected. Annotations live in ObjectMeta, not
274-
// Status, so this is a real object Update — separate from, and issued
275-
// before, the single deferred status patch every other write in this file
276-
// participates in.
277-
//
278-
// DBInstance has a status subresource, so the API server (and the fake
279-
// client used in tests) ignore inst.Status in the request and return the
280-
// object with whatever Status was last durably persisted — which
281-
// client-go then decodes into inst, in place. Since nothing this Run() call
282-
// has done to inst.Status is durable yet (only the single deferred status
283-
// patch at the very end of Reconcile persists it), a bare Update here would
284-
// silently wipe out every Status mutation made earlier in this same pass
285-
// (verified empirically, not just inferred). Save and restore it around the
286-
// call.
287-
func (r *repaveStep) clearTriggerAnnotation(ctx context.Context, inst *dbaasv1.DBInstance) error {
288-
if _, ok := inst.Annotations[dbaasv1.AnnotationRepaveTrigger]; !ok {
289-
return nil
290-
}
291-
delete(inst.Annotations, dbaasv1.AnnotationRepaveTrigger)
292-
status := inst.Status
293-
err := r.Update(ctx, inst)
294-
inst.Status = status
295-
return err
296-
}
297-
298272
// regenerateCloudInit rebuilds the cloud-init Secret from durable credential
299273
// Material, mirroring createVM's initial build (vm.go) — DBName/
300274
// MasterUsername/Port/etc. are all immutable (AppliedSpec), so reusing the

0 commit comments

Comments
 (0)