Skip to content

Commit c7be7f8

Browse files
authored
Allow marking releases stuck in a pending state as failed (#16)
1 parent 5094baf commit c7be7f8

File tree

4 files changed

+84
-11
lines changed

4 files changed

+84
-11
lines changed

pkg/client/actionclient.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ type ActionInterface interface {
6262
Get(name string, opts ...GetOption) (*release.Release, error)
6363
Install(name, namespace string, chrt *chart.Chart, vals map[string]interface{}, opts ...InstallOption) (*release.Release, error)
6464
Upgrade(name, namespace string, chrt *chart.Chart, vals map[string]interface{}, opts ...UpgradeOption) (*release.Release, error)
65+
MarkFailed(release *release.Release, reason string) error
6566
Uninstall(name string, opts ...UninstallOption) (*release.UninstallReleaseResponse, error)
6667
Reconcile(rel *release.Release) error
6768
}
@@ -180,6 +181,14 @@ func (c *actionClient) Upgrade(name, namespace string, chrt *chart.Chart, vals m
180181
return rel, nil
181182
}
182183

184+
func (c *actionClient) MarkFailed(rel *release.Release, reason string) error {
185+
infoCopy := *rel.Info
186+
releaseCopy := *rel
187+
releaseCopy.Info = &infoCopy
188+
releaseCopy.SetStatus(release.StatusFailed, reason)
189+
return c.conf.Releases.Update(&releaseCopy)
190+
}
191+
183192
func (c *actionClient) Uninstall(name string, opts ...UninstallOption) (*release.UninstallReleaseResponse, error) {
184193
uninstall := action.NewUninstall(c.conf)
185194
for _, o := range opts {

pkg/reconciler/internal/conditions/conditions.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ const (
4141
ReasonUpgradeError = status.ConditionReason("UpgradeError")
4242
ReasonReconcileError = status.ConditionReason("ReconcileError")
4343
ReasonUninstallError = status.ConditionReason("UninstallError")
44+
ReasonPendingError = status.ConditionReason("PendingError")
4445
)
4546

4647
func Initialized(stat corev1.ConditionStatus, reason status.ConditionReason, message interface{}) status.Condition {

pkg/reconciler/internal/fake/actionclient.go

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -48,17 +48,19 @@ func (hcg *fakeActionClientGetter) ActionClientFor(_ crclient.Object) (client.Ac
4848
}
4949

5050
type ActionClient struct {
51-
Gets []GetCall
52-
Installs []InstallCall
53-
Upgrades []UpgradeCall
54-
Uninstalls []UninstallCall
55-
Reconciles []ReconcileCall
56-
57-
HandleGet func() (*release.Release, error)
58-
HandleInstall func() (*release.Release, error)
59-
HandleUpgrade func() (*release.Release, error)
60-
HandleUninstall func() (*release.UninstallReleaseResponse, error)
61-
HandleReconcile func() error
51+
Gets []GetCall
52+
Installs []InstallCall
53+
Upgrades []UpgradeCall
54+
MarkFaileds []MarkFailedCall
55+
Uninstalls []UninstallCall
56+
Reconciles []ReconcileCall
57+
58+
HandleGet func() (*release.Release, error)
59+
HandleInstall func() (*release.Release, error)
60+
HandleUpgrade func() (*release.Release, error)
61+
HandleMarkFailed func() error
62+
HandleUninstall func() (*release.UninstallReleaseResponse, error)
63+
HandleReconcile func() error
6264
}
6365

6466
func NewActionClient() ActionClient {
@@ -109,6 +111,11 @@ type UpgradeCall struct {
109111
Opts []client.UpgradeOption
110112
}
111113

114+
type MarkFailedCall struct {
115+
Release *release.Release
116+
Reason string
117+
}
118+
112119
type UninstallCall struct {
113120
Name string
114121
Opts []client.UninstallOption
@@ -133,6 +140,11 @@ func (c *ActionClient) Upgrade(name, namespace string, chrt *chart.Chart, vals m
133140
return c.HandleUpgrade()
134141
}
135142

143+
func (c *ActionClient) MarkFailed(rel *release.Release, reason string) error {
144+
c.MarkFaileds = append(c.MarkFaileds, MarkFailedCall{rel, reason})
145+
return c.HandleMarkFailed()
146+
}
147+
136148
func (c *ActionClient) Uninstall(name string, opts ...client.UninstallOption) (*release.UninstallReleaseResponse, error) {
137149
c.Uninstalls = append(c.Uninstalls, UninstallCall{name, opts})
138150
return c.HandleUninstall()

pkg/reconciler/reconciler.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
758809
func (r *Reconciler) reportOverrideEvents(obj runtime.Object) {
759810
for k, v := range r.overrideValues {
760811
r.eventRecorder.Eventf(obj, "Warning", "ValueOverridden",

0 commit comments

Comments
 (0)