@@ -83,6 +83,7 @@ type Reconciler struct {
83
83
skipDependentWatches bool
84
84
maxConcurrentReconciles int
85
85
reconcilePeriod time.Duration
86
+ markFailedAfter time.Duration
86
87
maxHistory int
87
88
skipPrimaryGVKSchemeRegistration bool
88
89
controllerSetupFuncs []ControllerSetupFunc
@@ -363,6 +364,18 @@ func WithMaxReleaseHistory(maxHistory int) Option {
363
364
}
364
365
}
365
366
367
+ // WithMarkFailedAfter specifies the duration after which the reconciler will mark a release in a pending (locked)
368
+ // state as false in order to allow rolling forward.
369
+ func WithMarkFailedAfter (duration time.Duration ) Option {
370
+ return func (r * Reconciler ) error {
371
+ if duration < 0 {
372
+ return errors .New ("auto-rollback after duration must not be negative" )
373
+ }
374
+ r .markFailedAfter = duration
375
+ return nil
376
+ }
377
+ }
378
+
366
379
// WithInstallAnnotations is an Option that configures Install annotations
367
380
// to enable custom action.Install fields to be set based on the value of
368
381
// annotations found in the custom resource watched by this reconciler.
@@ -676,6 +689,10 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (res ctrl.
676
689
)
677
690
return ctrl.Result {}, err
678
691
}
692
+ if state == statePending {
693
+ return r .handlePending (actionClient , rel , & u , log )
694
+ }
695
+
679
696
u .UpdateStatus (updater .EnsureCondition (conditions .Irreconcilable (corev1 .ConditionFalse , "" , "" )))
680
697
681
698
for _ , h := range r .preHooks {
@@ -752,6 +769,7 @@ const (
752
769
stateNeedsInstall helmReleaseState = "needs install"
753
770
stateNeedsUpgrade helmReleaseState = "needs upgrade"
754
771
stateUnchanged helmReleaseState = "unchanged"
772
+ statePending helmReleaseState = "pending"
755
773
stateError helmReleaseState = "error"
756
774
)
757
775
@@ -799,6 +817,10 @@ func (r *Reconciler) getReleaseState(client helmclient.ActionInterface, obj meta
799
817
return nil , stateNeedsInstall , nil
800
818
}
801
819
820
+ if currentRelease .Info != nil && currentRelease .Info .Status .IsPending () {
821
+ return currentRelease , statePending , nil
822
+ }
823
+
802
824
var opts []helmclient.UpgradeOption
803
825
if r .maxHistory > 0 {
804
826
opts = append (opts , func (u * action.Upgrade ) error {
@@ -893,6 +915,35 @@ func (r *Reconciler) doUpgrade(actionClient helmclient.ActionInterface, u *updat
893
915
return rel , nil
894
916
}
895
917
918
+ func (r * Reconciler ) handlePending (actionClient helmclient.ActionInterface , rel * release.Release , u * updater.Updater , log logr.Logger ) (ctrl.Result , error ) {
919
+ err := r .doHandlePending (actionClient , rel , log )
920
+ if err == nil {
921
+ err = errors .New ("unknown error handling pending release" )
922
+ }
923
+ u .UpdateStatus (
924
+ updater .EnsureCondition (conditions .Irreconcilable (corev1 .ConditionTrue , conditions .ReasonPendingError , err )))
925
+ return ctrl.Result {}, err
926
+ }
927
+
928
+ func (r * Reconciler ) doHandlePending (actionClient helmclient.ActionInterface , rel * release.Release , log logr.Logger ) error {
929
+ if r .markFailedAfter <= 0 {
930
+ return errors .New ("Release is in a pending (locked) state and cannot be modified. User intervention is required." )
931
+ }
932
+ if rel .Info == nil || rel .Info .LastDeployed .IsZero () {
933
+ return errors .New ("Release is in a pending (locked) state and lacks 'last deployed' timestamp. User intervention is required." )
934
+ }
935
+ if pendingSince := time .Since (rel .Info .LastDeployed .Time ); pendingSince < r .markFailedAfter {
936
+ 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 )
937
+ }
938
+
939
+ log .Info ("Marking release as failed" , "releaseName" , rel .Name )
940
+ err := actionClient .MarkFailed (rel , fmt .Sprintf ("operator marked pending (locked) release as failed after state did not change for %v" , r .markFailedAfter ))
941
+ if err != nil {
942
+ return fmt .Errorf ("Failed to mark pending (locked) release as failed: %w" , err )
943
+ }
944
+ return fmt .Errorf ("marked release %s as failed to allow upgrade to succeed in next reconcile attempt" , rel .Name )
945
+ }
946
+
896
947
func (r * Reconciler ) reportOverrideEvents (obj runtime.Object ) {
897
948
for k , v := range r .overrideValues {
898
949
r .eventRecorder .Eventf (obj , "Warning" , "ValueOverridden" ,
0 commit comments