@@ -39,17 +39,22 @@ func newRepaveStep(deps Dependencies) Step { return &repaveStep{Dependencies: de
3939func (* 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.
5358func (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