@@ -78,6 +78,7 @@ type Reconciler struct {
7878 skipDependentWatches bool
7979 maxConcurrentReconciles int
8080 reconcilePeriod time.Duration
81+ markFailedAfter time.Duration
8182 maxHistory int
8283
8384 annotSetupOnce sync.Once
@@ -304,6 +305,18 @@ func WithMaxReleaseHistory(maxHistory int) Option {
304305 }
305306}
306307
308+ // WithMarkFailedAfter specifies the duration after which the reconciler will mark a release in a pending (locked)
309+ // state as false in order to allow rolling forward.
310+ func WithMarkFailedAfter (duration time.Duration ) Option {
311+ return func (r * Reconciler ) error {
312+ if duration < 0 {
313+ return errors .New ("auto-rollback after duration must not be negative" )
314+ }
315+ r .markFailedAfter = duration
316+ return nil
317+ }
318+ }
319+
307320// WithInstallAnnotations is an Option that configures Install annotations
308321// to enable custom action.Install fields to be set based on the value of
309322// annotations found in the custom resource watched by this reconciler.
@@ -553,6 +566,10 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (res ctrl.
553566 )
554567 return ctrl.Result {}, err
555568 }
569+ if state == statePending {
570+ return r .handlePending (actionClient , rel , & u , log )
571+ }
572+
556573 u .UpdateStatus (updater .EnsureCondition (conditions .Irreconcilable (corev1 .ConditionFalse , "" , "" )))
557574
558575 for _ , h := range r .preHooks {
@@ -630,6 +647,7 @@ const (
630647 stateNeedsInstall helmReleaseState = "needs install"
631648 stateNeedsUpgrade helmReleaseState = "needs upgrade"
632649 stateUnchanged helmReleaseState = "unchanged"
650+ statePending helmReleaseState = "pending"
633651 stateError helmReleaseState = "error"
634652)
635653
@@ -678,6 +696,10 @@ func (r *Reconciler) getReleaseState(client helmclient.ActionInterface, obj meta
678696 return nil , stateNeedsInstall , nil
679697 }
680698
699+ if currentRelease .Info != nil && currentRelease .Info .Status .IsPending () {
700+ return currentRelease , statePending , nil
701+ }
702+
681703 var opts []helmclient.UpgradeOption
682704 if r .maxHistory > 0 {
683705 opts = append (opts , func (u * action.Upgrade ) error {
@@ -755,6 +777,35 @@ func (r *Reconciler) doUpgrade(actionClient helmclient.ActionInterface, u *updat
755777 return rel , nil
756778}
757779
780+ func (r * Reconciler ) handlePending (actionClient helmclient.ActionInterface , rel * release.Release , u * updater.Updater , log logr.Logger ) (ctrl.Result , error ) {
781+ err := r .doHandlePending (actionClient , rel , log )
782+ if err == nil {
783+ err = errors .New ("unknown error handling pending release" )
784+ }
785+ u .UpdateStatus (
786+ updater .EnsureCondition (conditions .Irreconcilable (corev1 .ConditionTrue , conditions .ReasonPendingError , err )))
787+ return ctrl.Result {}, err
788+ }
789+
790+ func (r * Reconciler ) doHandlePending (actionClient helmclient.ActionInterface , rel * release.Release , log logr.Logger ) error {
791+ if r .markFailedAfter <= 0 {
792+ return errors .New ("Release is in a pending (locked) state and cannot be modified. User intervention is required." )
793+ }
794+ if rel .Info == nil || rel .Info .LastDeployed .IsZero () {
795+ return errors .New ("Release is in a pending (locked) state and lacks 'last deployed' timestamp. User intervention is required." )
796+ }
797+ if pendingSince := time .Since (rel .Info .LastDeployed .Time ); pendingSince < r .markFailedAfter {
798+ return fmt .Errorf ("Release is in a pending (locked) state and cannot currently be modified. Release will be marked failed to allow a roll-forward in %v." , r .markFailedAfter - pendingSince )
799+ }
800+
801+ log .Info ("Marking release as failed" , "releaseName" , rel .Name )
802+ err := actionClient .MarkFailed (rel , fmt .Sprintf ("operator marked pending (locked) release as failed after state did not change for %v" , r .markFailedAfter ))
803+ if err != nil {
804+ return fmt .Errorf ("Failed to mark pending (locked) release as failed: %w" , err )
805+ }
806+ return fmt .Errorf ("marked release %s as failed to allow upgrade to succeed in next reconcile attempt" , rel .Name )
807+ }
808+
758809func (r * Reconciler ) reportOverrideEvents (obj runtime.Object ) {
759810 for k , v := range r .overrideValues {
760811 r .eventRecorder .Eventf (obj , "Warning" , "ValueOverridden" ,
0 commit comments