@@ -114,9 +114,6 @@ func (r *CredentialRotationReconciler) handleStartRotation(ctx context.Context,
114114 return ctrl.Result {RequeueAfter : credRotationRequeueInterval }, nil
115115 }
116116
117- // Generate rotationID
118- rotationID := uuid .New ().String ()
119-
120117 // Get the source Secret
121118 sourceSecret := & corev1.Secret {}
122119 secretName := cluster .ControllerSecretName ()
@@ -127,12 +124,20 @@ func (r *CredentialRotationReconciler) handleStartRotation(ctx context.Context,
127124 return ctrl.Result {}, fmt .Errorf ("failed to get source secret: %w" , err )
128125 }
129126
130- // Generate pending passwords
127+ // Reuse existing rotationID if the Secret already has complete pending
128+ // passwords (crash recovery: Secret was updated but status was not).
129+ rotationID := password .GetRotationID (sourceSecret )
130+ if rotationID == "" {
131+ rotationID = uuid .New ().String ()
132+ }
133+
134+ // Generate pending passwords (idempotent if rotationID matches)
131135 _ , err := password .SetPendingPasswords (sourceSecret , rotationID )
132136 if err != nil {
133137 r .Recorder .Eventf (cr , corev1 .EventTypeWarning , "RotationPendingError" ,
134- "Failed to set pending passwords: %v" , err )
135- return ctrl.Result {}, fmt .Errorf ("failed to set pending passwords: %w" , err )
138+ "Failed to set pending passwords: %v. Manual cleanup required: " +
139+ "see docs/designdoc/credential_rotation_crd.md Recovery Procedures" , err )
140+ return ctrl.Result {RequeueAfter : credRotationRequeueInterval }, nil
136141 }
137142
138143 // Update the source Secret
@@ -167,11 +172,22 @@ func (r *CredentialRotationReconciler) handleRetainedPhase(ctx context.Context,
167172 return ctrl.Result {}, fmt .Errorf ("failed to get source secret: %w" , err )
168173 }
169174
175+ // Verify pending passwords belong to this rotation cycle.
176+ if hasPending , err := password .HasPendingPasswords (sourceSecret , cr .Status .RotationID ); err != nil {
177+ r .Recorder .Eventf (cr , corev1 .EventTypeWarning , "RotationPendingError" ,
178+ "Pending password state inconsistency: %v. Manual cleanup required: " +
179+ "see docs/designdoc/credential_rotation_crd.md Recovery Procedures" , err )
180+ return ctrl.Result {RequeueAfter : credRotationRequeueInterval }, nil
181+ } else if ! hasPending {
182+ r .Recorder .Eventf (cr , corev1 .EventTypeWarning , "MissingRotationPending" ,
183+ "Pending passwords not found in source secret for rotationID %s. Manual cleanup required: " +
184+ "see docs/designdoc/credential_rotation_crd.md Recovery Procedures" , cr .Status .RotationID )
185+ return ctrl.Result {RequeueAfter : credRotationRequeueInterval }, nil
186+ }
187+
170188 // Distribute pending passwords to per-namespace user Secret
171189 pendingPasswd , err := password .NewMySQLPasswordFromPending (sourceSecret )
172190 if err != nil {
173- r .Recorder .Eventf (cr , corev1 .EventTypeWarning , "MissingRotationPending" ,
174- "Pending passwords not found in source secret: %v" , err )
175191 return ctrl.Result {}, fmt .Errorf ("failed to read pending passwords: %w" , err )
176192 }
177193
@@ -282,6 +298,21 @@ func (r *CredentialRotationReconciler) handleDiscardedPhase(ctx context.Context,
282298 return ctrl.Result {}, fmt .Errorf ("failed to get source secret for confirm: %w" , err )
283299 }
284300
301+ // Verify pending passwords are still present before confirming.
302+ // ConfirmPendingPasswords is a no-op when no pending keys exist, which would
303+ // incorrectly finalize the rotation with stale passwords if they were lost.
304+ if hasPending , err := password .HasPendingPasswords (sourceSecret , cr .Status .RotationID ); err != nil {
305+ r .Recorder .Eventf (cr , corev1 .EventTypeWarning , "RotationPendingError" ,
306+ "Pending password state inconsistency during confirm: %v. Manual cleanup required: " +
307+ "see docs/designdoc/credential_rotation_crd.md Recovery Procedures" , err )
308+ return ctrl.Result {RequeueAfter : credRotationRequeueInterval }, nil
309+ } else if ! hasPending {
310+ r .Recorder .Eventf (cr , corev1 .EventTypeWarning , "MissingRotationPending" ,
311+ "Pending passwords lost before confirm for rotationID %s. Manual cleanup required: " +
312+ "see docs/designdoc/credential_rotation_crd.md Recovery Procedures" , cr .Status .RotationID )
313+ return ctrl.Result {RequeueAfter : credRotationRequeueInterval }, nil
314+ }
315+
285316 if err := password .ConfirmPendingPasswords (sourceSecret ); err != nil {
286317 return ctrl.Result {}, fmt .Errorf ("failed to confirm pending passwords: %w" , err )
287318 }
0 commit comments