Skip to content

Commit 0850d05

Browse files
misbernerludydoo
authored andcommitted
Allow marking releases stuck in a pending state as failed (#16)
1 parent b2aff59 commit 0850d05

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
@@ -53,6 +53,7 @@ type ActionInterface interface {
5353
Get(name string, opts ...GetOption) (*release.Release, error)
5454
Install(name, namespace string, chrt *chart.Chart, vals map[string]interface{}, opts ...InstallOption) (*release.Release, error)
5555
Upgrade(name, namespace string, chrt *chart.Chart, vals map[string]interface{}, opts ...UpgradeOption) (*release.Release, error)
56+
MarkFailed(release *release.Release, reason string) error
5657
Uninstall(name string, opts ...UninstallOption) (*release.UninstallReleaseResponse, error)
5758
Reconcile(rel *release.Release) error
5859
}
@@ -264,6 +265,14 @@ func (c *actionClient) Upgrade(name, namespace string, chrt *chart.Chart, vals m
264265
return rel, nil
265266
}
266267

268+
func (c *actionClient) MarkFailed(rel *release.Release, reason string) error {
269+
infoCopy := *rel.Info
270+
releaseCopy := *rel
271+
releaseCopy.Info = &infoCopy
272+
releaseCopy.SetStatus(release.StatusFailed, reason)
273+
return c.conf.Releases.Update(&releaseCopy)
274+
}
275+
267276
func (c *actionClient) rollback(name string, opts ...RollbackOption) error {
268277
rollback := action.NewRollback(c.conf)
269278
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
@@ -83,6 +83,7 @@ type Reconciler struct {
8383
skipDependentWatches bool
8484
maxConcurrentReconciles int
8585
reconcilePeriod time.Duration
86+
markFailedAfter time.Duration
8687
maxHistory int
8788
skipPrimaryGVKSchemeRegistration bool
8889
controllerSetupFuncs []ControllerSetupFunc
@@ -363,6 +364,18 @@ func WithMaxReleaseHistory(maxHistory int) Option {
363364
}
364365
}
365366

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+
366379
// WithInstallAnnotations is an Option that configures Install annotations
367380
// to enable custom action.Install fields to be set based on the value of
368381
// 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.
676689
)
677690
return ctrl.Result{}, err
678691
}
692+
if state == statePending {
693+
return r.handlePending(actionClient, rel, &u, log)
694+
}
695+
679696
u.UpdateStatus(updater.EnsureCondition(conditions.Irreconcilable(corev1.ConditionFalse, "", "")))
680697

681698
for _, h := range r.preHooks {
@@ -752,6 +769,7 @@ const (
752769
stateNeedsInstall helmReleaseState = "needs install"
753770
stateNeedsUpgrade helmReleaseState = "needs upgrade"
754771
stateUnchanged helmReleaseState = "unchanged"
772+
statePending helmReleaseState = "pending"
755773
stateError helmReleaseState = "error"
756774
)
757775

@@ -799,6 +817,10 @@ func (r *Reconciler) getReleaseState(client helmclient.ActionInterface, obj meta
799817
return nil, stateNeedsInstall, nil
800818
}
801819

820+
if currentRelease.Info != nil && currentRelease.Info.Status.IsPending() {
821+
return currentRelease, statePending, nil
822+
}
823+
802824
var opts []helmclient.UpgradeOption
803825
if r.maxHistory > 0 {
804826
opts = append(opts, func(u *action.Upgrade) error {
@@ -893,6 +915,35 @@ func (r *Reconciler) doUpgrade(actionClient helmclient.ActionInterface, u *updat
893915
return rel, nil
894916
}
895917

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+
896947
func (r *Reconciler) reportOverrideEvents(obj runtime.Object) {
897948
for k, v := range r.overrideValues {
898949
r.eventRecorder.Eventf(obj, "Warning", "ValueOverridden",

0 commit comments

Comments
 (0)