Skip to content

Commit 2a5a56c

Browse files
committed
Roughly implement forceAt and resetAt
Signed-off-by: Hidde Beydals <hidde@hhh.computer>
1 parent ba74b4a commit 2a5a56c

File tree

4 files changed

+72
-1
lines changed

4 files changed

+72
-1
lines changed

internal/action/reset.go

+12
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ const (
2929
differentGenerationReason = "generation differs from last attempt"
3030
differentRevisionReason = "chart version differs from last attempt"
3131
differentValuesReason = "values differ from last attempt"
32+
resetRequestedReason = "reset requested through annotation"
3233
)
3334

3435
// MustResetFailures returns a reason and true if the HelmRelease's status
@@ -38,6 +39,12 @@ const (
3839
// For example, a change in generation, chart version, or values.
3940
// If no change is detected, an empty string is returned along with false.
4041
func MustResetFailures(obj *v2.HelmRelease, chart *chart.Metadata, values chartutil.Values) (string, bool) {
42+
// Always check if a reset is requested.
43+
// This is done first, so that the HelmReleaseStatus.LastHandledResetAt
44+
// field is updated even if the reset request is not handled due to other
45+
// diverging data.
46+
resetRequested := v2.ShouldHandleResetRequest(obj)
47+
4148
switch {
4249
case obj.Status.LastAttemptedGeneration != obj.Generation:
4350
return differentGenerationReason, true
@@ -53,5 +60,10 @@ func MustResetFailures(obj *v2.HelmRelease, chart *chart.Metadata, values chartu
5360
return differentValuesReason, true
5461
}
5562
}
63+
64+
if resetRequested {
65+
return resetRequestedReason, true
66+
}
67+
5668
return "", false
5769
}

internal/action/reset_test.go

+26
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package action
1919
import (
2020
"testing"
2121

22+
"github.com/fluxcd/pkg/apis/meta"
2223
. "github.com/onsi/gomega"
2324
"helm.sh/helm/v3/pkg/chart"
2425
"helm.sh/helm/v3/pkg/chartutil"
@@ -108,6 +109,31 @@ func TestMustResetFailures(t *testing.T) {
108109
want: true,
109110
wantReason: differentValuesReason,
110111
},
112+
{
113+
name: "on reset request through annotation",
114+
obj: &v2.HelmRelease{
115+
ObjectMeta: metav1.ObjectMeta{
116+
Generation: 1,
117+
Annotations: map[string]string{
118+
meta.ReconcileRequestAnnotation: "a",
119+
v2.ResetRequestAnnotation: "a",
120+
},
121+
},
122+
Status: v2.HelmReleaseStatus{
123+
LastAttemptedGeneration: 1,
124+
LastAttemptedRevision: "1.0.0",
125+
LastAttemptedConfigDigest: "sha256:1dabc4e3cbbd6a0818bd460f3a6c9855bfe95d506c74726bc0f2edb0aecb1f4e",
126+
},
127+
},
128+
chart: &chart.Metadata{
129+
Version: "1.0.0",
130+
},
131+
values: chartutil.Values{
132+
"foo": "bar",
133+
},
134+
want: true,
135+
wantReason: resetRequestedReason,
136+
},
111137
{
112138
name: "without change no reset",
113139
obj: &v2.HelmRelease{

internal/controller/helmrelease_controller.go

+4
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,10 @@ func (r *HelmReleaseReconciler) Reconcile(ctx context.Context, req ctrl.Request)
154154

155155
// Always attempt to patch the object after each reconciliation.
156156
defer func() {
157+
if v, ok := meta.ReconcileAnnotationValue(obj.GetAnnotations()); ok {
158+
obj.Status.SetLastHandledReconcileRequest(v)
159+
}
160+
157161
patchOpts := []patch.Option{
158162
patch.WithFieldOwner(r.FieldManager),
159163
patch.WithOwnedConditions{Conditions: intreconcile.OwnedConditions},

internal/reconcile/atomic_release.go

+30-1
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ func (r *AtomicRelease) Reconcile(ctx context.Context, req *Request) error {
174174
}
175175
return fmt.Errorf("atomic release canceled: %w", ctx.Err())
176176
default:
177-
// Determine the next action to run based on the current state.
177+
// Determine the current state of the Helm release.
178178
log.V(logger.DebugLevel).Info("determining current state of Helm release")
179179
state, err := DetermineReleaseState(ctx, r.configFactory, req)
180180
if err != nil {
@@ -272,6 +272,13 @@ func (r *AtomicRelease) Reconcile(ctx context.Context, req *Request) error {
272272
func (r *AtomicRelease) actionForState(ctx context.Context, req *Request, state ReleaseState) (ActionReconciler, error) {
273273
log := ctrl.LoggerFrom(ctx)
274274

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+
275282
switch state.Status {
276283
case ReleaseStatusInSync:
277284
log.Info("release in-sync with desired state")
@@ -290,6 +297,11 @@ func (r *AtomicRelease) actionForState(ctx context.Context, req *Request, state
290297
// field, but should be removed in a future release.
291298
req.Object.Status.LastAppliedRevision = req.Object.Status.History.Latest().ChartVersion
292299

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+
293305
return nil, nil
294306
case ReleaseStatusLocked:
295307
log.Info(msgWithReason("release locked", state.Reason))
@@ -298,6 +310,11 @@ func (r *AtomicRelease) actionForState(ctx context.Context, req *Request, state
298310
log.Info(msgWithReason("release not installed", state.Reason))
299311

300312
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+
301318
return nil, fmt.Errorf("%w: cannot install release", ErrExceededMaxRetries)
302319
}
303320

@@ -313,6 +330,11 @@ func (r *AtomicRelease) actionForState(ctx context.Context, req *Request, state
313330
log.Info(msgWithReason("release out-of-sync with desired state", state.Reason))
314331

315332
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+
316338
return nil, fmt.Errorf("%w: cannot upgrade release", ErrExceededMaxRetries)
317339
}
318340

@@ -360,6 +382,13 @@ func (r *AtomicRelease) actionForState(ctx context.Context, req *Request, state
360382
return NewUpgrade(r.configFactory, r.eventRecorder), nil
361383
}
362384

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+
363392
// We have exhausted the number of retries for the remediation
364393
// strategy.
365394
if remediation.RetriesExhausted(req.Object) && !remediation.MustRemediateLastFailure() {

0 commit comments

Comments
 (0)