@@ -174,7 +174,7 @@ func (r *AtomicRelease) Reconcile(ctx context.Context, req *Request) error {
174
174
}
175
175
return fmt .Errorf ("atomic release canceled: %w" , ctx .Err ())
176
176
default :
177
- // Determine the next action to run based on the current state .
177
+ // Determine the current state of the Helm release .
178
178
log .V (logger .DebugLevel ).Info ("determining current state of Helm release" )
179
179
state , err := DetermineReleaseState (ctx , r .configFactory , req )
180
180
if err != nil {
@@ -272,6 +272,13 @@ func (r *AtomicRelease) Reconcile(ctx context.Context, req *Request) error {
272
272
func (r * AtomicRelease ) actionForState (ctx context.Context , req * Request , state ReleaseState ) (ActionReconciler , error ) {
273
273
log := ctrl .LoggerFrom (ctx )
274
274
275
+ // Determine whether we may need to force a release action.
276
+ // We do this before determining the next action to run, as otherwise we may
277
+ // end up running a Helm upgrade (due to e.g. ReleaseStatusUnmanaged) and
278
+ // then forcing an upgrade (due to the release now being in
279
+ // ReleaseStatusInSync with a yet unhandled force request).
280
+ forceRequested := v2 .ShouldHandleForceRequest (req .Object )
281
+
275
282
switch state .Status {
276
283
case ReleaseStatusInSync :
277
284
log .Info ("release in-sync with desired state" )
@@ -290,6 +297,11 @@ func (r *AtomicRelease) actionForState(ctx context.Context, req *Request, state
290
297
// field, but should be removed in a future release.
291
298
req .Object .Status .LastAppliedRevision = req .Object .Status .History .Latest ().ChartVersion
292
299
300
+ if forceRequested {
301
+ log .Info (msgWithReason ("forcing upgrade for in-sync release" , "force requested through annotation" ))
302
+ return NewUpgrade (r .configFactory , r .eventRecorder ), nil
303
+ }
304
+
293
305
return nil , nil
294
306
case ReleaseStatusLocked :
295
307
log .Info (msgWithReason ("release locked" , state .Reason ))
@@ -298,6 +310,11 @@ func (r *AtomicRelease) actionForState(ctx context.Context, req *Request, state
298
310
log .Info (msgWithReason ("release not installed" , state .Reason ))
299
311
300
312
if req .Object .GetInstall ().GetRemediation ().RetriesExhausted (req .Object ) {
313
+ if forceRequested {
314
+ log .Info (msgWithReason ("forcing install while out of retries" , "force requested through annotation" ))
315
+ return NewInstall (r .configFactory , r .eventRecorder ), nil
316
+ }
317
+
301
318
return nil , fmt .Errorf ("%w: cannot install release" , ErrExceededMaxRetries )
302
319
}
303
320
@@ -313,6 +330,11 @@ func (r *AtomicRelease) actionForState(ctx context.Context, req *Request, state
313
330
log .Info (msgWithReason ("release out-of-sync with desired state" , state .Reason ))
314
331
315
332
if req .Object .GetUpgrade ().GetRemediation ().RetriesExhausted (req .Object ) {
333
+ if forceRequested {
334
+ log .Info (msgWithReason ("forcing upgrade while out of retries" , "force requested through annotation" ))
335
+ return NewInstall (r .configFactory , r .eventRecorder ), nil
336
+ }
337
+
316
338
return nil , fmt .Errorf ("%w: cannot upgrade release" , ErrExceededMaxRetries )
317
339
}
318
340
@@ -360,6 +382,13 @@ func (r *AtomicRelease) actionForState(ctx context.Context, req *Request, state
360
382
return NewUpgrade (r .configFactory , r .eventRecorder ), nil
361
383
}
362
384
385
+ // If the force annotation is set, we can attempt to upgrade the release
386
+ // without any further checks.
387
+ if forceRequested {
388
+ log .Info (msgWithReason ("forcing upgrade for failed release" , "force requested through annotation" ))
389
+ return NewUpgrade (r .configFactory , r .eventRecorder ), nil
390
+ }
391
+
363
392
// We have exhausted the number of retries for the remediation
364
393
// strategy.
365
394
if remediation .RetriesExhausted (req .Object ) && ! remediation .MustRemediateLastFailure () {
0 commit comments