From 3dde8c26d144580bf1fba5112a0d5f498b35f285 Mon Sep 17 00:00:00 2001 From: Hidde Beydals Date: Wed, 29 Nov 2023 10:14:03 +0100 Subject: [PATCH] Roughly implement `forceAt` and `resetAt` Signed-off-by: Hidde Beydals --- internal/action/reset.go | 17 ++++++++ internal/controller/helmrelease_controller.go | 4 ++ internal/reconcile/atomic_release.go | 39 ++++++++++++++++++- 3 files changed, 59 insertions(+), 1 deletion(-) diff --git a/internal/action/reset.go b/internal/action/reset.go index da0caf69f..ed7b07d68 100644 --- a/internal/action/reset.go +++ b/internal/action/reset.go @@ -17,6 +17,7 @@ limitations under the License. package action import ( + "github.com/fluxcd/pkg/apis/meta" "github.com/opencontainers/go-digest" "helm.sh/helm/v3/pkg/chart" "helm.sh/helm/v3/pkg/chartutil" @@ -29,6 +30,7 @@ const ( differentGenerationReason = "generation differs from last attempt" differentRevisionReason = "chart version differs from last attempt" differentValuesReason = "values differ from last attempt" + resetRequestedReason = "reset requested through annotation" ) // MustResetFailures returns a reason and true if the HelmRelease's status @@ -53,5 +55,20 @@ func MustResetFailures(obj *v2.HelmRelease, chart *chart.Metadata, values chartu return differentValuesReason, true } } + + // TODO(hidde): factor this out. + resetAt, resetOk := obj.ResetAnnotationValue() + reconcileAt, reconcileOk := meta.ReconcileAnnotationValue(obj.GetAnnotations()) + + if resetOk && reconcileOk && resetAt == reconcileAt { + lastHandledReconcile := obj.Status.GetLastHandledReconcileRequest() + lastHandledReset := obj.Status.LastHandledResetAt + + if lastHandledReconcile != reconcileAt && lastHandledReset != resetAt { + obj.Status.LastHandledResetAt = resetAt + return resetRequestedReason, true + } + } + return "", false } diff --git a/internal/controller/helmrelease_controller.go b/internal/controller/helmrelease_controller.go index 1c3c8f6da..d10911653 100644 --- a/internal/controller/helmrelease_controller.go +++ b/internal/controller/helmrelease_controller.go @@ -154,6 +154,10 @@ func (r *HelmReleaseReconciler) Reconcile(ctx context.Context, req ctrl.Request) // Always attempt to patch the object after each reconciliation. defer func() { + if v, ok := meta.ReconcileAnnotationValue(obj.GetAnnotations()); ok { + obj.Status.SetLastHandledReconcileRequest(v) + } + patchOpts := []patch.Option{ patch.WithFieldOwner(r.FieldManager), patch.WithOwnedConditions{Conditions: intreconcile.OwnedConditions}, diff --git a/internal/reconcile/atomic_release.go b/internal/reconcile/atomic_release.go index 3949cc0b2..7d6fa18ff 100644 --- a/internal/reconcile/atomic_release.go +++ b/internal/reconcile/atomic_release.go @@ -174,7 +174,7 @@ func (r *AtomicRelease) Reconcile(ctx context.Context, req *Request) error { } return fmt.Errorf("atomic release canceled: %w", ctx.Err()) default: - // Determine the next action to run based on the current state. + // Determine the current state of the Helm release. log.V(logger.DebugLevel).Info("determining current state of Helm release") state, err := DetermineReleaseState(ctx, r.configFactory, req) if err != nil { @@ -272,6 +272,9 @@ func (r *AtomicRelease) Reconcile(ctx context.Context, req *Request) error { func (r *AtomicRelease) actionForState(ctx context.Context, req *Request, state ReleaseState) (ActionReconciler, error) { log := ctrl.LoggerFrom(ctx) + // Determine whether we may need to force a release action. + mustForce := r.mustForce(req.Object) + switch state.Status { case ReleaseStatusInSync: log.Info("release in-sync with desired state") @@ -290,6 +293,11 @@ func (r *AtomicRelease) actionForState(ctx context.Context, req *Request, state // field, but should be removed in a future release. req.Object.Status.LastAppliedRevision = req.Object.Status.History.Latest().ChartVersion + if mustForce { + log.Info(msgWithReason("forcing upgrade", "force annotation is set")) + return NewUpgrade(r.configFactory, r.eventRecorder), nil + } + return nil, nil case ReleaseStatusLocked: log.Info(msgWithReason("release locked", state.Reason)) @@ -297,6 +305,11 @@ func (r *AtomicRelease) actionForState(ctx context.Context, req *Request, state case ReleaseStatusAbsent: log.Info(msgWithReason("release not installed", state.Reason)) + if mustForce { + log.Info(msgWithReason("forcing install", "force annotation is set")) + return NewInstall(r.configFactory, r.eventRecorder), nil + } + if req.Object.GetInstall().GetRemediation().RetriesExhausted(req.Object) { return nil, fmt.Errorf("%w: cannot install release", ErrExceededMaxRetries) } @@ -360,6 +373,13 @@ func (r *AtomicRelease) actionForState(ctx context.Context, req *Request, state return NewUpgrade(r.configFactory, r.eventRecorder), nil } + // If the force annotation is set, we can attempt to upgrade the release + // without any further checks. + if mustForce { + log.Info(msgWithReason("forcing upgrade", "force annotation is set")) + return NewUpgrade(r.configFactory, r.eventRecorder), nil + } + // We have exhausted the number of retries for the remediation // strategy. if remediation.RetriesExhausted(req.Object) && !remediation.MustRemediateLastFailure() { @@ -398,6 +418,23 @@ func (r *AtomicRelease) actionForState(ctx context.Context, req *Request, state } } +// mustForce returns true if the release must be forced. +func (r *AtomicRelease) mustForce(obj *v2.HelmRelease) bool { + forceAt, forceOk := obj.ForceAnnotationValue() + reconcileAt, reconcileOk := meta.ReconcileAnnotationValue(obj.GetAnnotations()) + + if forceOk && reconcileOk && forceAt == reconcileAt { + lastHandledReconcile := obj.Status.GetLastHandledReconcileRequest() + lastHandledForce := obj.Status.LastHandledForceAt + + if lastHandledReconcile != reconcileAt && lastHandledForce != forceAt { + obj.Status.LastHandledForceAt = forceAt + return true + } + } + return false +} + func (r *AtomicRelease) Name() string { return "atomic-release" }