From aa70ac2b316a2956d9052ed20427e3dc35152742 Mon Sep 17 00:00:00 2001 From: Soule BA Date: Sun, 5 May 2024 21:52:52 +0200 Subject: [PATCH 1/4] fix: detect changes in spec.postRenderers Signed-off-by: Soule BA --- api/v2/helmrelease_types.go | 5 + api/v2beta1/helmrelease_types.go | 5 + api/v2beta2/helmrelease_types.go | 5 + .../helm.toolkit.fluxcd.io_helmreleases.yaml | 15 ++ docs/api/v2/helm.md | 13 ++ internal/controller/helmrelease_controller.go | 32 +++- .../controller/helmrelease_controller_test.go | 159 ++++++++++++++++++ internal/postrender/build.go | 12 ++ internal/reconcile/reconcile.go | 3 + internal/reconcile/state.go | 5 + 10 files changed, 251 insertions(+), 3 deletions(-) diff --git a/api/v2/helmrelease_types.go b/api/v2/helmrelease_types.go index 688db28d0..50a132bae 100644 --- a/api/v2/helmrelease_types.go +++ b/api/v2/helmrelease_types.go @@ -1008,6 +1008,11 @@ type HelmReleaseStatus struct { // +optional LastAttemptedConfigDigest string `json:"lastAttemptedConfigDigest,omitempty"` + // LastAttemptedPostRenderersDigest is the digest for the post-renderers of + // the last reconciliation attempt. + // +optional + LastAttemptedPostRenderersDigest string `json:"lastAttemptedPostRenderersDigest,omitempty"` + // LastHandledForceAt holds the value of the most recent force request // value, so a change of the annotation value can be detected. // +optional diff --git a/api/v2beta1/helmrelease_types.go b/api/v2beta1/helmrelease_types.go index 6b241bced..691fa2e4b 100644 --- a/api/v2beta1/helmrelease_types.go +++ b/api/v2beta1/helmrelease_types.go @@ -950,6 +950,11 @@ type HelmReleaseStatus struct { // +optional LastAttemptedConfigDigest string `json:"lastAttemptedConfigDigest,omitempty"` + // LastAttemptedPostRenderersDigest is the digest for the post-renderers of + // the last reconciliation attempt. + // +optional + LastAttemptedPostRenderersDigest string `json:"lastAttemptedPostRenderersDigest,omitempty"` + // LastAttemptedReleaseAction is the last release action performed for this // HelmRelease. It is used to determine the active remediation strategy. // diff --git a/api/v2beta2/helmrelease_types.go b/api/v2beta2/helmrelease_types.go index 03c693e48..22f88670e 100644 --- a/api/v2beta2/helmrelease_types.go +++ b/api/v2beta2/helmrelease_types.go @@ -1034,6 +1034,11 @@ type HelmReleaseStatus struct { // +optional LastAttemptedConfigDigest string `json:"lastAttemptedConfigDigest,omitempty"` + // LastAttemptedPostRenderersDigest is the digest for the post-renderers of + // the last reconciliation attempt. + // +optional + LastAttemptedPostRenderersDigest string `json:"lastAttemptedPostRenderersDigest,omitempty"` + // LastHandledForceAt holds the value of the most recent force request // value, so a change of the annotation value can be detected. // +optional diff --git a/config/crd/bases/helm.toolkit.fluxcd.io_helmreleases.yaml b/config/crd/bases/helm.toolkit.fluxcd.io_helmreleases.yaml index 685bb6e3e..9ea45d357 100644 --- a/config/crd/bases/helm.toolkit.fluxcd.io_helmreleases.yaml +++ b/config/crd/bases/helm.toolkit.fluxcd.io_helmreleases.yaml @@ -1110,6 +1110,11 @@ spec: to reconcile. format: int64 type: integer + lastAttemptedPostRenderersDigest: + description: |- + LastAttemptedPostRenderersDigest is the digest for the post-renderers of + the last reconciliation attempt. + type: string lastAttemptedReleaseAction: description: |- LastAttemptedReleaseAction is the last release action performed for this @@ -2344,6 +2349,11 @@ spec: by v2beta1 HelmReleases. format: int64 type: integer + lastAttemptedPostRenderersDigest: + description: |- + LastAttemptedPostRenderersDigest is the digest for the post-renderers of + the last reconciliation attempt. + type: string lastAttemptedReleaseAction: description: |- LastAttemptedReleaseAction is the last release action performed for this @@ -3627,6 +3637,11 @@ spec: to reconcile. format: int64 type: integer + lastAttemptedPostRenderersDigest: + description: |- + LastAttemptedPostRenderersDigest is the digest for the post-renderers of + the last reconciliation attempt. + type: string lastAttemptedReleaseAction: description: |- LastAttemptedReleaseAction is the last release action performed for this diff --git a/docs/api/v2/helm.md b/docs/api/v2/helm.md index 075a53055..11e6765d4 100644 --- a/docs/api/v2/helm.md +++ b/docs/api/v2/helm.md @@ -1623,6 +1623,19 @@ string +lastAttemptedPostRenderersDigest
+ +string + + + +(Optional) +

LastAttemptedPostRenderersDigest is the digest for the post-renderers of +the last reconciliation attempt.

+ + + + lastHandledForceAt
string diff --git a/internal/controller/helmrelease_controller.go b/internal/controller/helmrelease_controller.go index 083a44528..667500c44 100644 --- a/internal/controller/helmrelease_controller.go +++ b/internal/controller/helmrelease_controller.go @@ -67,6 +67,7 @@ import ( "github.com/fluxcd/helm-controller/internal/features" "github.com/fluxcd/helm-controller/internal/kube" "github.com/fluxcd/helm-controller/internal/loader" + "github.com/fluxcd/helm-controller/internal/postrender" intpredicates "github.com/fluxcd/helm-controller/internal/predicates" intreconcile "github.com/fluxcd/helm-controller/internal/reconcile" "github.com/fluxcd/helm-controller/internal/release" @@ -360,6 +361,7 @@ func (r *HelmReleaseReconciler) reconcileRelease(ctx context.Context, patchHelpe if err := r.adoptLegacyRelease(ctx, getter, obj); err != nil { log.Error(err, "failed to adopt v2beta1 release state") } + r.adoptPostRenderersStatus(obj) } // If the release target configuration has changed, we need to uninstall the @@ -391,6 +393,15 @@ func (r *HelmReleaseReconciler) reconcileRelease(ctx context.Context, patchHelpe obj.Status.LastAttemptedRevisionDigest = ociDigest obj.Status.LastAttemptedConfigDigest = chartutil.DigestValues(digest.Canonical, values).String() obj.Status.LastAttemptedValuesChecksum = "" + // Keep track of the post-renderers digest used during the last reconciliation. + // This is used to determine if the post-renderers have changed. + oldPostRenderersDigest := obj.Status.LastAttemptedPostRenderersDigest + // remove stale post-renderers digest + obj.Status.LastAttemptedPostRenderersDigest = "" + if obj.Spec.PostRenderers != nil { + // Update the post-renderers digest if the post-renderers exist. + obj.Status.LastAttemptedPostRenderersDigest = postrender.Digest(digest.Canonical, obj.Spec.PostRenderers).String() + } obj.Status.LastReleaseRevision = 0 // Construct config factory for any further Helm actions. @@ -409,9 +420,10 @@ func (r *HelmReleaseReconciler) reconcileRelease(ctx context.Context, patchHelpe // Off we go! if err = intreconcile.NewAtomicRelease(patchHelper, cfg, r.EventRecorder, r.FieldManager).Reconcile(ctx, &intreconcile.Request{ - Object: obj, - Chart: loadedChart, - Values: values, + Object: obj, + Chart: loadedChart, + Values: values, + PreviousPostrendersDigest: oldPostRenderersDigest, }); err != nil { if errors.Is(err, intreconcile.ErrMustRequeue) { return ctrl.Result{Requeue: true}, nil @@ -646,6 +658,20 @@ func (r *HelmReleaseReconciler) adoptLegacyRelease(ctx context.Context, getter g return nil } +// adoptPostRenderersStatus attempts to set obj.Status.LastAttemptedPostRenderersDigest +// for v1beta1 and v1beta2 HelmReleases. +func (*HelmReleaseReconciler) adoptPostRenderersStatus(obj *v2.HelmRelease) { + if obj.GetGeneration() != obj.Status.ObservedGeneration { + return + } + + // if we have a reconciled object with PostRenderers not reflected in the + // status, we need to update the status. + if obj.Spec.PostRenderers != nil && obj.Status.LastAttemptedPostRenderersDigest == "" { + obj.Status.LastAttemptedPostRenderersDigest = postrender.Digest(digest.Canonical, obj.Spec.PostRenderers).String() + } +} + func (r *HelmReleaseReconciler) buildRESTClientGetter(ctx context.Context, obj *v2.HelmRelease) (genericclioptions.RESTClientGetter, error) { opts := []kube.Option{ kube.WithNamespace(obj.GetReleaseNamespace()), diff --git a/internal/controller/helmrelease_controller_test.go b/internal/controller/helmrelease_controller_test.go index a7921a358..ebde22e79 100644 --- a/internal/controller/helmrelease_controller_test.go +++ b/internal/controller/helmrelease_controller_test.go @@ -60,9 +60,11 @@ import ( "github.com/fluxcd/helm-controller/internal/chartutil" "github.com/fluxcd/helm-controller/internal/features" "github.com/fluxcd/helm-controller/internal/kube" + "github.com/fluxcd/helm-controller/internal/postrender" intreconcile "github.com/fluxcd/helm-controller/internal/reconcile" "github.com/fluxcd/helm-controller/internal/release" "github.com/fluxcd/helm-controller/internal/testutil" + "github.com/fluxcd/pkg/apis/kustomize" ) func TestHelmReleaseReconciler_reconcileRelease(t *testing.T) { @@ -1161,6 +1163,163 @@ func TestHelmReleaseReconciler_reconcileReleaseFromHelmChartSource(t *testing.T) *conditions.FalseCondition(meta.ReadyCondition, v2.ArtifactFailedReason, "Source not ready"), })) }) + + t.Run("reports postrenderer changes", func(t *testing.T) { + g := NewWithT(t) + + patches := ` +- target: + version: v1 + kind: ConfigMap + name: cm + patch: | + - op: add + path: /metadata/annotations/foo + value: bar +` + + patches2 := ` +- target: + version: v1 + kind: ConfigMap + name: cm + patch: | + - op: add + path: /metadata/annotations/foo2 + value: bar2 +` + + var targeted []kustomize.Patch + err := yaml.Unmarshal([]byte(patches), &targeted) + g.Expect(err).ToNot(HaveOccurred()) + + // Create HelmChart mock. + chartMock := testutil.BuildChart() + chartArtifact, err := testutil.SaveChartAsArtifact(chartMock, digest.SHA256, testServer.URL(), testServer.Root()) + g.Expect(err).ToNot(HaveOccurred()) + // copy the artifact to mutate the revision + ociArtifact := chartArtifact.DeepCopy() + ociArtifact.Revision += "@" + chartArtifact.Digest + + ns, err := testEnv.CreateNamespace(context.TODO(), "mock") + g.Expect(err).ToNot(HaveOccurred()) + t.Cleanup(func() { + _ = testEnv.Delete(context.TODO(), ns) + }) + + hc := &sourcev1.HelmChart{ + ObjectMeta: metav1.ObjectMeta{ + Name: "chart", + Namespace: ns.Name, + Generation: 1, + }, + Spec: sourcev1.HelmChartSpec{ + Chart: "testdata/test-helmrepo", + Version: "0.1.0", + SourceRef: sourcev1.LocalHelmChartSourceReference{ + Kind: sourcev1.HelmRepositoryKind, + Name: "test-helmrepo", + }, + }, + Status: sourcev1.HelmChartStatus{ + ObservedGeneration: 1, + Artifact: chartArtifact, + Conditions: []metav1.Condition{ + { + Type: meta.ReadyCondition, + Status: metav1.ConditionTrue, + }, + }, + }, + } + + // Create a test Helm release storage mock. + rls := testutil.BuildRelease(&helmrelease.MockReleaseOptions{ + Name: "release", + Namespace: ns.Name, + Version: 1, + Chart: chartMock, + Status: helmrelease.StatusDeployed, + }) + + obj := &v2.HelmRelease{ + ObjectMeta: metav1.ObjectMeta{ + Name: "release", + Namespace: ns.Name, + }, + Spec: v2.HelmReleaseSpec{ + ChartRef: &v2.CrossNamespaceSourceReference{ + Kind: sourcev1.HelmChartKind, + Name: hc.Name, + }, + PostRenderers: []v2.PostRenderer{ + { + Kustomize: &v2.Kustomize{ + Patches: targeted, + }, + }, + }, + }, + Status: v2.HelmReleaseStatus{ + StorageNamespace: ns.Name, + History: v2.Snapshots{ + release.ObservedToSnapshot(release.ObserveRelease(rls)), + }, + HelmChart: hc.Namespace + "/" + hc.Name, + }, + } + + obj.Status.LastAttemptedPostRenderersDigest = postrender.Digest(digest.Canonical, obj.Spec.PostRenderers).String() + obj.Status.LastAttemptedConfigDigest = chartutil.DigestValues(digest.Canonical, chartMock.Values).String() + + c := fake.NewClientBuilder(). + WithScheme(NewTestScheme()). + WithStatusSubresource(&v2.HelmRelease{}). + WithObjects(hc, obj). + Build() + + r := &HelmReleaseReconciler{ + Client: c, + GetClusterConfig: GetTestClusterConfig, + EventRecorder: record.NewFakeRecorder(32), + } + + //Store the Helm release mock in the test namespace. + getter, err := r.buildRESTClientGetter(context.TODO(), obj) + g.Expect(err).ToNot(HaveOccurred()) + + cfg, err := action.NewConfigFactory(getter, action.WithStorage(helmdriver.SecretsDriverName, obj.Status.StorageNamespace)) + g.Expect(err).ToNot(HaveOccurred()) + + store := helmstorage.Init(cfg.Driver) + g.Expect(store.Create(rls)).To(Succeed()) + + // update the postrenderers + err = yaml.Unmarshal([]byte(patches2), &targeted) + g.Expect(err).ToNot(HaveOccurred()) + obj.Spec.PostRenderers[0].Kustomize.Patches = targeted + + _, err = r.reconcileRelease(context.TODO(), patch.NewSerialPatcher(obj, r.Client), obj) + g.Expect(err).ToNot(HaveOccurred()) + + // Verify attempted values are set. + g.Expect(obj.Status.LastAttemptedGeneration).To(Equal(obj.Generation)) + g.Expect(obj.Status.LastAttemptedPostRenderersDigest).To(Equal(postrender.Digest(digest.Canonical, obj.Spec.PostRenderers).String())) + + // verify upgrade succeeded + g.Expect(obj.Status.Conditions).To(conditions.MatchConditions([]metav1.Condition{ + *conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingWithRetryReason, "Helm upgrade succeeded for release %s with chart %s", + fmt.Sprintf("%s/%s.v%d", rls.Namespace, rls.Name, rls.Version+1), fmt.Sprintf("%s@%s", chartMock.Name(), + chartMock.Metadata.Version)), + *conditions.TrueCondition(meta.ReadyCondition, v2.UpgradeSucceededReason, "Helm upgrade succeeded for release %s with chart %s", + fmt.Sprintf("%s/%s.v%d", rls.Namespace, rls.Name, rls.Version+1), fmt.Sprintf("%s@%s", chartMock.Name(), + chartMock.Metadata.Version)), + *conditions.TrueCondition(v2.ReleasedCondition, v2.UpgradeSucceededReason, "Helm upgrade succeeded for release %s with chart %s", + fmt.Sprintf("%s/%s.v%d", rls.Namespace, rls.Name, rls.Version+1), fmt.Sprintf("%s@%s", chartMock.Name(), + chartMock.Metadata.Version)), + })) + + }) } func TestHelmReleaseReconciler_reconcileReleaseFromOCIRepositorySource(t *testing.T) { diff --git a/internal/postrender/build.go b/internal/postrender/build.go index bac4ea48b..96ea800e9 100644 --- a/internal/postrender/build.go +++ b/internal/postrender/build.go @@ -17,9 +17,12 @@ limitations under the License. package postrender import ( + "encoding/json" + helmpostrender "helm.sh/helm/v3/pkg/postrender" v2 "github.com/fluxcd/helm-controller/api/v2" + "github.com/opencontainers/go-digest" ) // BuildPostRenderers creates the post-renderer instances from a HelmRelease @@ -43,3 +46,12 @@ func BuildPostRenderers(rel *v2.HelmRelease) helmpostrender.PostRenderer { } return NewCombined(renderers...) } + +func Digest(algo digest.Algorithm, postrenders []v2.PostRenderer) digest.Digest { + digester := algo.Digester() + enc := json.NewEncoder(digester.Hash()) + if err := enc.Encode(postrenders); err != nil { + return "" + } + return digester.Digest() +} diff --git a/internal/reconcile/reconcile.go b/internal/reconcile/reconcile.go index 6c51dd1cc..353dee0ff 100644 --- a/internal/reconcile/reconcile.go +++ b/internal/reconcile/reconcile.go @@ -83,6 +83,9 @@ type Request struct { // Values is the Helm chart values to be used for the installation or // upgrade. Values helmchartutil.Values + // PreviousPostrendersDigest is the digest of the post-renderers that were used + // during the last reconciliation. + PreviousPostrendersDigest string } // ActionReconciler is an interface which defines the methods that a reconciler diff --git a/internal/reconcile/state.go b/internal/reconcile/state.go index b3bf7d03e..ea8b2504c 100644 --- a/internal/reconcile/state.go +++ b/internal/reconcile/state.go @@ -141,6 +141,11 @@ func DetermineReleaseState(ctx context.Context, cfg *action.ConfigFactory, req * } } + // Verify if postrender digest has changed + if req.PreviousPostrendersDigest != req.Object.Status.LastAttemptedPostRenderersDigest { + return ReleaseState{Status: ReleaseStatusOutOfSync, Reason: "postrender digest has changed"}, nil + } + // For the further determination of test results, we look at the // observed state of the object. As tests can be run manually by // users running e.g. `helm test`. From e58a102de97e14a189acb11fd728151c076c6d79 Mon Sep 17 00:00:00 2001 From: Soule BA Date: Tue, 7 May 2024 11:56:46 +0200 Subject: [PATCH 2/4] document lastAttemptedPostRenderersDigest behavior Signed-off-by: Soule BA --- docs/spec/v2/helmreleases.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/spec/v2/helmreleases.md b/docs/spec/v2/helmreleases.md index be91e993e..137a22f5b 100644 --- a/docs/spec/v2/helmreleases.md +++ b/docs/spec/v2/helmreleases.md @@ -1675,6 +1675,16 @@ attempted to perform a Helm install or upgrade with in the The digest is used to determine if the controller should reset the [failure counters](#failure-counters) due to a change in the values. +### Last Attempted Post Renderers Digest + +The helm-controller reports the digest for the [post renderers](#post-renderers) +it last attempted to perform a Helm install or upgrade with in the +`.status.lastAttemptedPostRenderersDigest` field. + +This field is used by the controller to determine if a deployed Helm release +is in sync with the HelmRelease `spec.PostRenderers` configuration and whether +it should trigger a Helm upgrade. + ### Last Attempted Revision The helm-controller reports the revision of the Helm chart it last attempted From 4069ad47e9e2703845bf3aca585ca17815175f0b Mon Sep 17 00:00:00 2001 From: Soule BA Date: Tue, 7 May 2024 14:17:17 +0200 Subject: [PATCH 3/4] A better implementation for observing Postrenderers This implementation update `.status.ObervedPostRenderersDigest` after a successful reconciliation if `spec.postRenderers` is not nil. Changes to `spec.postRenderers` are detected for `deployed` releases. `Failed` and `Uninstalled` releases are detected earlier in the reconciliation process. Signed-off-by: Soule BA --- api/v2/helmrelease_types.go | 10 +++---- api/v2beta1/helmrelease_types.go | 10 +++---- api/v2beta2/helmrelease_types.go | 10 +++---- .../helm.toolkit.fluxcd.io_helmreleases.yaml | 30 +++++++++---------- docs/api/v2/helm.md | 26 ++++++++-------- docs/spec/v2/helmreleases.md | 19 ++++++------ internal/controller/helmrelease_controller.go | 24 +++++---------- .../controller/helmrelease_controller_test.go | 7 ++--- internal/reconcile/reconcile.go | 3 -- internal/reconcile/release.go | 11 +++++++ internal/reconcile/state.go | 8 ++++- 11 files changed, 80 insertions(+), 78 deletions(-) diff --git a/api/v2/helmrelease_types.go b/api/v2/helmrelease_types.go index 50a132bae..08b1379a0 100644 --- a/api/v2/helmrelease_types.go +++ b/api/v2/helmrelease_types.go @@ -933,6 +933,11 @@ type HelmReleaseStatus struct { // +optional ObservedGeneration int64 `json:"observedGeneration,omitempty"` + // ObservedPostRenderersDigest is the digest for the post-renderers of + // the last successful reconciliation attempt. + // +optional + ObservedPostRenderersDigest string `json:"ObservedPostRenderersDigest,omitempty"` + // LastAttemptedGeneration is the last generation the controller attempted // to reconcile. // +optional @@ -1008,11 +1013,6 @@ type HelmReleaseStatus struct { // +optional LastAttemptedConfigDigest string `json:"lastAttemptedConfigDigest,omitempty"` - // LastAttemptedPostRenderersDigest is the digest for the post-renderers of - // the last reconciliation attempt. - // +optional - LastAttemptedPostRenderersDigest string `json:"lastAttemptedPostRenderersDigest,omitempty"` - // LastHandledForceAt holds the value of the most recent force request // value, so a change of the annotation value can be detected. // +optional diff --git a/api/v2beta1/helmrelease_types.go b/api/v2beta1/helmrelease_types.go index 691fa2e4b..b72f1a18b 100644 --- a/api/v2beta1/helmrelease_types.go +++ b/api/v2beta1/helmrelease_types.go @@ -875,6 +875,11 @@ type HelmReleaseStatus struct { // +optional ObservedGeneration int64 `json:"observedGeneration,omitempty"` + // ObservedPostRenderersDigest is the digest for the post-renderers of + // the last successful reconciliation attempt. + // +optional + ObservedPostRenderersDigest string `json:"ObservedPostRenderersDigest,omitempty"` + meta.ReconcileRequestStatus `json:",inline"` // Conditions holds the conditions for the HelmRelease. @@ -950,11 +955,6 @@ type HelmReleaseStatus struct { // +optional LastAttemptedConfigDigest string `json:"lastAttemptedConfigDigest,omitempty"` - // LastAttemptedPostRenderersDigest is the digest for the post-renderers of - // the last reconciliation attempt. - // +optional - LastAttemptedPostRenderersDigest string `json:"lastAttemptedPostRenderersDigest,omitempty"` - // LastAttemptedReleaseAction is the last release action performed for this // HelmRelease. It is used to determine the active remediation strategy. // diff --git a/api/v2beta2/helmrelease_types.go b/api/v2beta2/helmrelease_types.go index 22f88670e..df1fc25b6 100644 --- a/api/v2beta2/helmrelease_types.go +++ b/api/v2beta2/helmrelease_types.go @@ -953,6 +953,11 @@ type HelmReleaseStatus struct { // +optional ObservedGeneration int64 `json:"observedGeneration,omitempty"` + // ObservedPostRenderersDigest is the digest for the post-renderers of + // the last successful reconciliation attempt. + // +optional + ObservedPostRenderersDigest string `json:"ObservedPostRenderersDigest,omitempty"` + // LastAttemptedGeneration is the last generation the controller attempted // to reconcile. // +optional @@ -1034,11 +1039,6 @@ type HelmReleaseStatus struct { // +optional LastAttemptedConfigDigest string `json:"lastAttemptedConfigDigest,omitempty"` - // LastAttemptedPostRenderersDigest is the digest for the post-renderers of - // the last reconciliation attempt. - // +optional - LastAttemptedPostRenderersDigest string `json:"lastAttemptedPostRenderersDigest,omitempty"` - // LastHandledForceAt holds the value of the most recent force request // value, so a change of the annotation value can be detected. // +optional diff --git a/config/crd/bases/helm.toolkit.fluxcd.io_helmreleases.yaml b/config/crd/bases/helm.toolkit.fluxcd.io_helmreleases.yaml index 9ea45d357..25860c95c 100644 --- a/config/crd/bases/helm.toolkit.fluxcd.io_helmreleases.yaml +++ b/config/crd/bases/helm.toolkit.fluxcd.io_helmreleases.yaml @@ -906,6 +906,11 @@ spec: observedGeneration: -1 description: HelmReleaseStatus defines the observed state of a HelmRelease. properties: + ObservedPostRenderersDigest: + description: |- + ObservedPostRenderersDigest is the digest for the post-renderers of + the last successful reconciliation attempt. + type: string conditions: description: Conditions holds the conditions for the HelmRelease. items: @@ -1110,11 +1115,6 @@ spec: to reconcile. format: int64 type: integer - lastAttemptedPostRenderersDigest: - description: |- - LastAttemptedPostRenderersDigest is the digest for the post-renderers of - the last reconciliation attempt. - type: string lastAttemptedReleaseAction: description: |- LastAttemptedReleaseAction is the last release action performed for this @@ -2129,6 +2129,11 @@ spec: observedGeneration: -1 description: HelmReleaseStatus defines the observed state of a HelmRelease. properties: + ObservedPostRenderersDigest: + description: |- + ObservedPostRenderersDigest is the digest for the post-renderers of + the last successful reconciliation attempt. + type: string conditions: description: Conditions holds the conditions for the HelmRelease. items: @@ -2349,11 +2354,6 @@ spec: by v2beta1 HelmReleases. format: int64 type: integer - lastAttemptedPostRenderersDigest: - description: |- - LastAttemptedPostRenderersDigest is the digest for the post-renderers of - the last reconciliation attempt. - type: string lastAttemptedReleaseAction: description: |- LastAttemptedReleaseAction is the last release action performed for this @@ -3427,6 +3427,11 @@ spec: observedGeneration: -1 description: HelmReleaseStatus defines the observed state of a HelmRelease. properties: + ObservedPostRenderersDigest: + description: |- + ObservedPostRenderersDigest is the digest for the post-renderers of + the last successful reconciliation attempt. + type: string conditions: description: Conditions holds the conditions for the HelmRelease. items: @@ -3637,11 +3642,6 @@ spec: to reconcile. format: int64 type: integer - lastAttemptedPostRenderersDigest: - description: |- - LastAttemptedPostRenderersDigest is the digest for the post-renderers of - the last reconciliation attempt. - type: string lastAttemptedReleaseAction: description: |- LastAttemptedReleaseAction is the last release action performed for this diff --git a/docs/api/v2/helm.md b/docs/api/v2/helm.md index 11e6765d4..ec8bd14e9 100644 --- a/docs/api/v2/helm.md +++ b/docs/api/v2/helm.md @@ -1434,6 +1434,19 @@ int64 +ObservedPostRenderersDigest
+ +string + + + +(Optional) +

ObservedPostRenderersDigest is the digest for the post-renderers of +the last successful reconciliation attempt.

+ + + + lastAttemptedGeneration
int64 @@ -1623,19 +1636,6 @@ string -lastAttemptedPostRenderersDigest
- -string - - - -(Optional) -

LastAttemptedPostRenderersDigest is the digest for the post-renderers of -the last reconciliation attempt.

- - - - lastHandledForceAt
string diff --git a/docs/spec/v2/helmreleases.md b/docs/spec/v2/helmreleases.md index 137a22f5b..5104e19de 100644 --- a/docs/spec/v2/helmreleases.md +++ b/docs/spec/v2/helmreleases.md @@ -1666,6 +1666,16 @@ The helm-controller reports an observed generation in the HelmRelease's `.metadata.generation` which resulted in either a [ready state](#ready-helmrelease), or stalled due to error it can not recover from without human intervention. +### Oberved Post Renderers Digest + +The helm-controller reports the digest for the [post renderers](#post-renderers) +it last rendered the Helm chart with in the for a successful Helm install or +upgrade in the `.status.ObervedPostRenderersDigest` field. + +This field is used by the controller to determine if a deployed Helm release +is in sync with the HelmRelease `spec.PostRenderers` configuration and whether +it should trigger a Helm upgrade. + ### Last Attempted Config Digest The helm-controller reports the digest for the [values](#values) it last @@ -1675,15 +1685,6 @@ attempted to perform a Helm install or upgrade with in the The digest is used to determine if the controller should reset the [failure counters](#failure-counters) due to a change in the values. -### Last Attempted Post Renderers Digest - -The helm-controller reports the digest for the [post renderers](#post-renderers) -it last attempted to perform a Helm install or upgrade with in the -`.status.lastAttemptedPostRenderersDigest` field. - -This field is used by the controller to determine if a deployed Helm release -is in sync with the HelmRelease `spec.PostRenderers` configuration and whether -it should trigger a Helm upgrade. ### Last Attempted Revision diff --git a/internal/controller/helmrelease_controller.go b/internal/controller/helmrelease_controller.go index 667500c44..9d0220e7c 100644 --- a/internal/controller/helmrelease_controller.go +++ b/internal/controller/helmrelease_controller.go @@ -393,15 +393,6 @@ func (r *HelmReleaseReconciler) reconcileRelease(ctx context.Context, patchHelpe obj.Status.LastAttemptedRevisionDigest = ociDigest obj.Status.LastAttemptedConfigDigest = chartutil.DigestValues(digest.Canonical, values).String() obj.Status.LastAttemptedValuesChecksum = "" - // Keep track of the post-renderers digest used during the last reconciliation. - // This is used to determine if the post-renderers have changed. - oldPostRenderersDigest := obj.Status.LastAttemptedPostRenderersDigest - // remove stale post-renderers digest - obj.Status.LastAttemptedPostRenderersDigest = "" - if obj.Spec.PostRenderers != nil { - // Update the post-renderers digest if the post-renderers exist. - obj.Status.LastAttemptedPostRenderersDigest = postrender.Digest(digest.Canonical, obj.Spec.PostRenderers).String() - } obj.Status.LastReleaseRevision = 0 // Construct config factory for any further Helm actions. @@ -420,10 +411,9 @@ func (r *HelmReleaseReconciler) reconcileRelease(ctx context.Context, patchHelpe // Off we go! if err = intreconcile.NewAtomicRelease(patchHelper, cfg, r.EventRecorder, r.FieldManager).Reconcile(ctx, &intreconcile.Request{ - Object: obj, - Chart: loadedChart, - Values: values, - PreviousPostrendersDigest: oldPostRenderersDigest, + Object: obj, + Chart: loadedChart, + Values: values, }); err != nil { if errors.Is(err, intreconcile.ErrMustRequeue) { return ctrl.Result{Requeue: true}, nil @@ -658,8 +648,8 @@ func (r *HelmReleaseReconciler) adoptLegacyRelease(ctx context.Context, getter g return nil } -// adoptPostRenderersStatus attempts to set obj.Status.LastAttemptedPostRenderersDigest -// for v1beta1 and v1beta2 HelmReleases. +// adoptPostRenderersStatus attempts to set obj.Status.ObservedPostRenderersDigest +// for v2beta1 and v2beta2 HelmReleases. func (*HelmReleaseReconciler) adoptPostRenderersStatus(obj *v2.HelmRelease) { if obj.GetGeneration() != obj.Status.ObservedGeneration { return @@ -667,8 +657,8 @@ func (*HelmReleaseReconciler) adoptPostRenderersStatus(obj *v2.HelmRelease) { // if we have a reconciled object with PostRenderers not reflected in the // status, we need to update the status. - if obj.Spec.PostRenderers != nil && obj.Status.LastAttemptedPostRenderersDigest == "" { - obj.Status.LastAttemptedPostRenderersDigest = postrender.Digest(digest.Canonical, obj.Spec.PostRenderers).String() + if obj.Spec.PostRenderers != nil && obj.Status.ObservedPostRenderersDigest == "" { + obj.Status.ObservedPostRenderersDigest = postrender.Digest(digest.Canonical, obj.Spec.PostRenderers).String() } } diff --git a/internal/controller/helmrelease_controller_test.go b/internal/controller/helmrelease_controller_test.go index ebde22e79..d998a6ef6 100644 --- a/internal/controller/helmrelease_controller_test.go +++ b/internal/controller/helmrelease_controller_test.go @@ -1269,7 +1269,7 @@ func TestHelmReleaseReconciler_reconcileReleaseFromHelmChartSource(t *testing.T) }, } - obj.Status.LastAttemptedPostRenderersDigest = postrender.Digest(digest.Canonical, obj.Spec.PostRenderers).String() + obj.Status.ObservedPostRenderersDigest = postrender.Digest(digest.Canonical, obj.Spec.PostRenderers).String() obj.Status.LastAttemptedConfigDigest = chartutil.DigestValues(digest.Canonical, chartMock.Values).String() c := fake.NewClientBuilder(). @@ -1304,13 +1304,10 @@ func TestHelmReleaseReconciler_reconcileReleaseFromHelmChartSource(t *testing.T) // Verify attempted values are set. g.Expect(obj.Status.LastAttemptedGeneration).To(Equal(obj.Generation)) - g.Expect(obj.Status.LastAttemptedPostRenderersDigest).To(Equal(postrender.Digest(digest.Canonical, obj.Spec.PostRenderers).String())) + g.Expect(obj.Status.ObservedPostRenderersDigest).To(Equal(postrender.Digest(digest.Canonical, obj.Spec.PostRenderers).String())) // verify upgrade succeeded g.Expect(obj.Status.Conditions).To(conditions.MatchConditions([]metav1.Condition{ - *conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingWithRetryReason, "Helm upgrade succeeded for release %s with chart %s", - fmt.Sprintf("%s/%s.v%d", rls.Namespace, rls.Name, rls.Version+1), fmt.Sprintf("%s@%s", chartMock.Name(), - chartMock.Metadata.Version)), *conditions.TrueCondition(meta.ReadyCondition, v2.UpgradeSucceededReason, "Helm upgrade succeeded for release %s with chart %s", fmt.Sprintf("%s/%s.v%d", rls.Namespace, rls.Name, rls.Version+1), fmt.Sprintf("%s@%s", chartMock.Name(), chartMock.Metadata.Version)), diff --git a/internal/reconcile/reconcile.go b/internal/reconcile/reconcile.go index 353dee0ff..6c51dd1cc 100644 --- a/internal/reconcile/reconcile.go +++ b/internal/reconcile/reconcile.go @@ -83,9 +83,6 @@ type Request struct { // Values is the Helm chart values to be used for the installation or // upgrade. Values helmchartutil.Values - // PreviousPostrendersDigest is the digest of the post-renderers that were used - // during the last reconciliation. - PreviousPostrendersDigest string } // ActionReconciler is an interface which defines the methods that a reconciler diff --git a/internal/reconcile/release.go b/internal/reconcile/release.go index 049eaf421..75dc2c3a7 100644 --- a/internal/reconcile/release.go +++ b/internal/reconcile/release.go @@ -28,6 +28,8 @@ import ( v2 "github.com/fluxcd/helm-controller/api/v2" "github.com/fluxcd/helm-controller/internal/action" + "github.com/fluxcd/helm-controller/internal/digest" + "github.com/fluxcd/helm-controller/internal/postrender" "github.com/fluxcd/helm-controller/internal/release" "github.com/fluxcd/helm-controller/internal/storage" ) @@ -137,6 +139,8 @@ func observeRelease(observed observedReleases) storage.ObserveFunc { // tests are not enabled, and excluding it when failures must be ignored. // // If Ready=True, any Stalled condition is removed. +// +// The ObservedPostRenderersDigest is updated if the post-renderers exist. func summarize(req *Request) { var sumConds = []string{v2.RemediatedCondition, v2.ReleasedCondition} if req.Object.GetTest().Enable && !req.Object.GetTest().IgnoreFailures { @@ -186,6 +190,13 @@ func summarize(req *Request) { Message: conds[0].Message, ObservedGeneration: req.Object.Generation, }) + + // remove stale post-renderers digest + req.Object.Status.ObservedPostRenderersDigest = "" + if req.Object.Spec.PostRenderers != nil { + // Update the post-renderers digest if the post-renderers exist. + req.Object.Status.ObservedPostRenderersDigest = postrender.Digest(digest.Canonical, req.Object.Spec.PostRenderers).String() + } } // eventMessageWithLog returns an event message composed out of the given diff --git a/internal/reconcile/state.go b/internal/reconcile/state.go index ea8b2504c..be255d11c 100644 --- a/internal/reconcile/state.go +++ b/internal/reconcile/state.go @@ -27,7 +27,9 @@ import ( ctrl "sigs.k8s.io/controller-runtime" "github.com/fluxcd/helm-controller/internal/action" + "github.com/fluxcd/helm-controller/internal/digest" interrors "github.com/fluxcd/helm-controller/internal/errors" + "github.com/fluxcd/helm-controller/internal/postrender" ) // ReleaseStatus represents the status of a Helm release as determined by @@ -142,7 +144,11 @@ func DetermineReleaseState(ctx context.Context, cfg *action.ConfigFactory, req * } // Verify if postrender digest has changed - if req.PreviousPostrendersDigest != req.Object.Status.LastAttemptedPostRenderersDigest { + var postrenderersDigest string + if req.Object.Spec.PostRenderers != nil { + postrenderersDigest = postrender.Digest(digest.Canonical, req.Object.Spec.PostRenderers).String() + } + if postrenderersDigest != req.Object.Status.ObservedPostRenderersDigest { return ReleaseState{Status: ReleaseStatusOutOfSync, Reason: "postrender digest has changed"}, nil } From 4b6febf48c139e90ae069352ff0f69f651d13899 Mon Sep 17 00:00:00 2001 From: Soule BA Date: Tue, 7 May 2024 15:18:18 +0200 Subject: [PATCH 4/4] Add test cases for Summarize() and DetermineReleaseState() Signed-off-by: Soule BA --- api/v2/helmrelease_types.go | 2 +- api/v2beta1/helmrelease_types.go | 2 +- api/v2beta2/helmrelease_types.go | 2 +- .../helm.toolkit.fluxcd.io_helmreleases.yaml | 30 +- docs/api/v2/helm.md | 2 +- docs/spec/v2/helmreleases.md | 7 +- internal/controller/helmrelease_controller.go | 10 +- .../controller/helmrelease_controller_test.go | 3 - internal/postrender/build.go | 2 +- internal/reconcile/release.go | 10 +- internal/reconcile/release_test.go | 793 +++++++++++------- internal/reconcile/state_test.go | 30 + 12 files changed, 548 insertions(+), 345 deletions(-) diff --git a/api/v2/helmrelease_types.go b/api/v2/helmrelease_types.go index 08b1379a0..5698ea148 100644 --- a/api/v2/helmrelease_types.go +++ b/api/v2/helmrelease_types.go @@ -936,7 +936,7 @@ type HelmReleaseStatus struct { // ObservedPostRenderersDigest is the digest for the post-renderers of // the last successful reconciliation attempt. // +optional - ObservedPostRenderersDigest string `json:"ObservedPostRenderersDigest,omitempty"` + ObservedPostRenderersDigest string `json:"observedPostRenderersDigest,omitempty"` // LastAttemptedGeneration is the last generation the controller attempted // to reconcile. diff --git a/api/v2beta1/helmrelease_types.go b/api/v2beta1/helmrelease_types.go index b72f1a18b..0557cdf58 100644 --- a/api/v2beta1/helmrelease_types.go +++ b/api/v2beta1/helmrelease_types.go @@ -878,7 +878,7 @@ type HelmReleaseStatus struct { // ObservedPostRenderersDigest is the digest for the post-renderers of // the last successful reconciliation attempt. // +optional - ObservedPostRenderersDigest string `json:"ObservedPostRenderersDigest,omitempty"` + ObservedPostRenderersDigest string `json:"observedPostRenderersDigest,omitempty"` meta.ReconcileRequestStatus `json:",inline"` diff --git a/api/v2beta2/helmrelease_types.go b/api/v2beta2/helmrelease_types.go index df1fc25b6..589b331fd 100644 --- a/api/v2beta2/helmrelease_types.go +++ b/api/v2beta2/helmrelease_types.go @@ -956,7 +956,7 @@ type HelmReleaseStatus struct { // ObservedPostRenderersDigest is the digest for the post-renderers of // the last successful reconciliation attempt. // +optional - ObservedPostRenderersDigest string `json:"ObservedPostRenderersDigest,omitempty"` + ObservedPostRenderersDigest string `json:"observedPostRenderersDigest,omitempty"` // LastAttemptedGeneration is the last generation the controller attempted // to reconcile. diff --git a/config/crd/bases/helm.toolkit.fluxcd.io_helmreleases.yaml b/config/crd/bases/helm.toolkit.fluxcd.io_helmreleases.yaml index 25860c95c..7be94e22e 100644 --- a/config/crd/bases/helm.toolkit.fluxcd.io_helmreleases.yaml +++ b/config/crd/bases/helm.toolkit.fluxcd.io_helmreleases.yaml @@ -906,11 +906,6 @@ spec: observedGeneration: -1 description: HelmReleaseStatus defines the observed state of a HelmRelease. properties: - ObservedPostRenderersDigest: - description: |- - ObservedPostRenderersDigest is the digest for the post-renderers of - the last successful reconciliation attempt. - type: string conditions: description: Conditions holds the conditions for the HelmRelease. items: @@ -1165,6 +1160,11 @@ spec: description: ObservedGeneration is the last observed generation. format: int64 type: integer + observedPostRenderersDigest: + description: |- + ObservedPostRenderersDigest is the digest for the post-renderers of + the last successful reconciliation attempt. + type: string storageNamespace: description: |- StorageNamespace is the namespace of the Helm release storage for the @@ -2129,11 +2129,6 @@ spec: observedGeneration: -1 description: HelmReleaseStatus defines the observed state of a HelmRelease. properties: - ObservedPostRenderersDigest: - description: |- - ObservedPostRenderersDigest is the digest for the post-renderers of - the last successful reconciliation attempt. - type: string conditions: description: Conditions holds the conditions for the HelmRelease. items: @@ -2404,6 +2399,11 @@ spec: description: ObservedGeneration is the last observed generation. format: int64 type: integer + observedPostRenderersDigest: + description: |- + ObservedPostRenderersDigest is the digest for the post-renderers of + the last successful reconciliation attempt. + type: string storageNamespace: description: |- StorageNamespace is the namespace of the Helm release storage for the @@ -3427,11 +3427,6 @@ spec: observedGeneration: -1 description: HelmReleaseStatus defines the observed state of a HelmRelease. properties: - ObservedPostRenderersDigest: - description: |- - ObservedPostRenderersDigest is the digest for the post-renderers of - the last successful reconciliation attempt. - type: string conditions: description: Conditions holds the conditions for the HelmRelease. items: @@ -3692,6 +3687,11 @@ spec: description: ObservedGeneration is the last observed generation. format: int64 type: integer + observedPostRenderersDigest: + description: |- + ObservedPostRenderersDigest is the digest for the post-renderers of + the last successful reconciliation attempt. + type: string storageNamespace: description: |- StorageNamespace is the namespace of the Helm release storage for the diff --git a/docs/api/v2/helm.md b/docs/api/v2/helm.md index ec8bd14e9..8f928a6cb 100644 --- a/docs/api/v2/helm.md +++ b/docs/api/v2/helm.md @@ -1434,7 +1434,7 @@ int64 -ObservedPostRenderersDigest
+observedPostRenderersDigest
string diff --git a/docs/spec/v2/helmreleases.md b/docs/spec/v2/helmreleases.md index 5104e19de..85e90ddba 100644 --- a/docs/spec/v2/helmreleases.md +++ b/docs/spec/v2/helmreleases.md @@ -1666,14 +1666,14 @@ The helm-controller reports an observed generation in the HelmRelease's `.metadata.generation` which resulted in either a [ready state](#ready-helmrelease), or stalled due to error it can not recover from without human intervention. -### Oberved Post Renderers Digest +### Observed Post Renderers Digest The helm-controller reports the digest for the [post renderers](#post-renderers) it last rendered the Helm chart with in the for a successful Helm install or -upgrade in the `.status.ObervedPostRenderersDigest` field. +upgrade in the `.status.observedPostRenderersDigest` field. This field is used by the controller to determine if a deployed Helm release -is in sync with the HelmRelease `spec.PostRenderers` configuration and whether +is in sync with the HelmRelease `spec.postRenderers` configuration and whether it should trigger a Helm upgrade. ### Last Attempted Config Digest @@ -1685,7 +1685,6 @@ attempted to perform a Helm install or upgrade with in the The digest is used to determine if the controller should reset the [failure counters](#failure-counters) due to a change in the values. - ### Last Attempted Revision The helm-controller reports the revision of the Helm chart it last attempted diff --git a/internal/controller/helmrelease_controller.go b/internal/controller/helmrelease_controller.go index 9d0220e7c..3b210256a 100644 --- a/internal/controller/helmrelease_controller.go +++ b/internal/controller/helmrelease_controller.go @@ -353,11 +353,13 @@ func (r *HelmReleaseReconciler) reconcileRelease(ctx context.Context, patchHelpe conditions.MarkUnknown(obj, meta.ReadyCondition, meta.ProgressingReason, "reconciliation in progress") } - // Attempt to adopt "legacy" v2beta1 release state on a best-effort basis. - // If this fails, the controller will fall back to performing an upgrade - // to settle on the desired state. - // TODO(hidde): remove this in a future release. + // Keep feature flagged code paths separate from the main reconciliation + // logic to ensure easy removal when the feature flag is removed. if ok, _ := features.Enabled(features.AdoptLegacyReleases); ok { + // Attempt to adopt "legacy" v2beta1 release state on a best-effort basis. + // If this fails, the controller will fall back to performing an upgrade + // to settle on the desired state. + // TODO(hidde): remove this in a future release. if err := r.adoptLegacyRelease(ctx, getter, obj); err != nil { log.Error(err, "failed to adopt v2beta1 release state") } diff --git a/internal/controller/helmrelease_controller_test.go b/internal/controller/helmrelease_controller_test.go index d998a6ef6..c2b66c3a5 100644 --- a/internal/controller/helmrelease_controller_test.go +++ b/internal/controller/helmrelease_controller_test.go @@ -1197,9 +1197,6 @@ func TestHelmReleaseReconciler_reconcileReleaseFromHelmChartSource(t *testing.T) chartMock := testutil.BuildChart() chartArtifact, err := testutil.SaveChartAsArtifact(chartMock, digest.SHA256, testServer.URL(), testServer.Root()) g.Expect(err).ToNot(HaveOccurred()) - // copy the artifact to mutate the revision - ociArtifact := chartArtifact.DeepCopy() - ociArtifact.Revision += "@" + chartArtifact.Digest ns, err := testEnv.CreateNamespace(context.TODO(), "mock") g.Expect(err).ToNot(HaveOccurred()) diff --git a/internal/postrender/build.go b/internal/postrender/build.go index 96ea800e9..66855808b 100644 --- a/internal/postrender/build.go +++ b/internal/postrender/build.go @@ -19,10 +19,10 @@ package postrender import ( "encoding/json" + "github.com/opencontainers/go-digest" helmpostrender "helm.sh/helm/v3/pkg/postrender" v2 "github.com/fluxcd/helm-controller/api/v2" - "github.com/opencontainers/go-digest" ) // BuildPostRenderers creates the post-renderer instances from a HelmRelease diff --git a/internal/reconcile/release.go b/internal/reconcile/release.go index 75dc2c3a7..8e8564e66 100644 --- a/internal/reconcile/release.go +++ b/internal/reconcile/release.go @@ -192,10 +192,12 @@ func summarize(req *Request) { }) // remove stale post-renderers digest - req.Object.Status.ObservedPostRenderersDigest = "" - if req.Object.Spec.PostRenderers != nil { - // Update the post-renderers digest if the post-renderers exist. - req.Object.Status.ObservedPostRenderersDigest = postrender.Digest(digest.Canonical, req.Object.Spec.PostRenderers).String() + if conditions.Get(req.Object, meta.ReadyCondition).Status == metav1.ConditionTrue { + req.Object.Status.ObservedPostRenderersDigest = "" + if req.Object.Spec.PostRenderers != nil { + // Update the post-renderers digest if the post-renderers exist. + req.Object.Status.ObservedPostRenderersDigest = postrender.Digest(digest.Canonical, req.Object.Spec.PostRenderers).String() + } } } diff --git a/internal/reconcile/release_test.go b/internal/reconcile/release_test.go index 9c050cf1e..0cd320e78 100644 --- a/internal/reconcile/release_test.go +++ b/internal/reconcile/release_test.go @@ -25,11 +25,14 @@ import ( "helm.sh/helm/v3/pkg/chart" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "github.com/fluxcd/pkg/apis/kustomize" "github.com/fluxcd/pkg/apis/meta" "github.com/fluxcd/pkg/runtime/conditions" v2 "github.com/fluxcd/helm-controller/api/v2" "github.com/fluxcd/helm-controller/internal/action" + "github.com/fluxcd/helm-controller/internal/digest" + "github.com/fluxcd/helm-controller/internal/postrender" ) const ( @@ -37,67 +40,121 @@ const ( mockReleaseNamespace = "mock-ns" ) +var ( + postRenderers = []v2.PostRenderer{ + { + Kustomize: &v2.Kustomize{ + Patches: []kustomize.Patch{ + { + Target: &kustomize.Selector{ + Kind: "Deployment", + Name: "test", + }, + Patch: `|- + apiVersion: apps/v1 + kind: Deployment + metadata: + name: test + spec: + replicas: 2 + `, + }, + }, + }, + }, + } + + postRenderers2 = []v2.PostRenderer{ + { + Kustomize: &v2.Kustomize{ + Patches: []kustomize.Patch{ + { + Target: &kustomize.Selector{ + Kind: "Deployment", + Name: "test", + }, + Patch: `|- + apiVersion: apps/v1 + kind: Deployment + metadata: + name: test + spec: + replicas: 3 + `, + }, + }, + }, + }, + } +) + func Test_summarize(t *testing.T) { tests := []struct { - name string - generation int64 - spec *v2.HelmReleaseSpec - conditions []metav1.Condition - expect []metav1.Condition + name string + generation int64 + spec *v2.HelmReleaseSpec + status v2.HelmReleaseStatus + expectedStatus *v2.HelmReleaseStatus }{ { name: "summarize conditions", generation: 1, - conditions: []metav1.Condition{ - { - Type: v2.ReleasedCondition, - Status: metav1.ConditionTrue, - Reason: v2.InstallSucceededReason, - Message: "Install complete", - ObservedGeneration: 1, - }, - { - Type: v2.TestSuccessCondition, - Status: metav1.ConditionFalse, - Reason: v2.TestFailedReason, - Message: "test hook(s) failure", - ObservedGeneration: 1, - }, - }, - expect: []metav1.Condition{ - { - Type: meta.ReadyCondition, - Status: metav1.ConditionTrue, - Reason: v2.InstallSucceededReason, - Message: "Install complete", - ObservedGeneration: 1, - }, - { - Type: v2.ReleasedCondition, - Status: metav1.ConditionTrue, - Reason: v2.InstallSucceededReason, - Message: "Install complete", - ObservedGeneration: 1, + status: v2.HelmReleaseStatus{ + Conditions: []metav1.Condition{ + { + Type: v2.ReleasedCondition, + Status: metav1.ConditionTrue, + Reason: v2.InstallSucceededReason, + Message: "Install complete", + ObservedGeneration: 1, + }, + { + Type: v2.TestSuccessCondition, + Status: metav1.ConditionFalse, + Reason: v2.TestFailedReason, + Message: "test hook(s) failure", + ObservedGeneration: 1, + }, + }, + }, + expectedStatus: &v2.HelmReleaseStatus{ + Conditions: []metav1.Condition{ + { + Type: meta.ReadyCondition, + Status: metav1.ConditionTrue, + Reason: v2.InstallSucceededReason, + Message: "Install complete", + ObservedGeneration: 1, + }, + { + Type: v2.ReleasedCondition, + Status: metav1.ConditionTrue, + Reason: v2.InstallSucceededReason, + Message: "Install complete", + ObservedGeneration: 1, + }, }, }, }, { name: "with tests enabled", generation: 1, - conditions: []metav1.Condition{ - { - Type: v2.ReleasedCondition, - Status: metav1.ConditionTrue, - Reason: v2.InstallSucceededReason, - Message: "Install complete", - ObservedGeneration: 1, - }, - { - Type: v2.TestSuccessCondition, - Status: metav1.ConditionTrue, - Reason: v2.TestSucceededReason, - Message: "test hook(s) succeeded", - ObservedGeneration: 1, + status: v2.HelmReleaseStatus{ + Conditions: []metav1.Condition{ + { + Type: v2.ReleasedCondition, + Status: metav1.ConditionTrue, + Reason: v2.InstallSucceededReason, + Message: "Install complete", + ObservedGeneration: 1, + }, + { + Type: v2.TestSuccessCondition, + Status: metav1.ConditionTrue, + Reason: v2.TestSucceededReason, + Message: "test hook(s) succeeded", + ObservedGeneration: 1, + }, }, }, spec: &v2.HelmReleaseSpec{ @@ -105,47 +162,51 @@ func Test_summarize(t *testing.T) { Enable: true, }, }, - expect: []metav1.Condition{ - { - Type: meta.ReadyCondition, - Status: metav1.ConditionTrue, - Reason: v2.TestSucceededReason, - Message: "test hook(s) succeeded", - ObservedGeneration: 1, - }, - { - Type: v2.ReleasedCondition, - Status: metav1.ConditionTrue, - Reason: v2.InstallSucceededReason, - Message: "Install complete", - ObservedGeneration: 1, - }, - { - Type: v2.TestSuccessCondition, - Status: metav1.ConditionTrue, - Reason: v2.TestSucceededReason, - Message: "test hook(s) succeeded", - ObservedGeneration: 1, + expectedStatus: &v2.HelmReleaseStatus{ + Conditions: []metav1.Condition{ + { + Type: meta.ReadyCondition, + Status: metav1.ConditionTrue, + Reason: v2.TestSucceededReason, + Message: "test hook(s) succeeded", + ObservedGeneration: 1, + }, + { + Type: v2.ReleasedCondition, + Status: metav1.ConditionTrue, + Reason: v2.InstallSucceededReason, + Message: "Install complete", + ObservedGeneration: 1, + }, + { + Type: v2.TestSuccessCondition, + Status: metav1.ConditionTrue, + Reason: v2.TestSucceededReason, + Message: "test hook(s) succeeded", + ObservedGeneration: 1, + }, }, }, }, { name: "with tests enabled and failure tests", generation: 1, - conditions: []metav1.Condition{ - { - Type: v2.ReleasedCondition, - Status: metav1.ConditionTrue, - Reason: v2.InstallSucceededReason, - Message: "Install complete", - ObservedGeneration: 1, - }, - { - Type: v2.TestSuccessCondition, - Status: metav1.ConditionFalse, - Reason: v2.TestFailedReason, - Message: "test hook(s) failure", - ObservedGeneration: 1, + status: v2.HelmReleaseStatus{ + Conditions: []metav1.Condition{ + { + Type: v2.ReleasedCondition, + Status: metav1.ConditionTrue, + Reason: v2.InstallSucceededReason, + Message: "Install complete", + ObservedGeneration: 1, + }, + { + Type: v2.TestSuccessCondition, + Status: metav1.ConditionFalse, + Reason: v2.TestFailedReason, + Message: "test hook(s) failure", + ObservedGeneration: 1, + }, }, }, spec: &v2.HelmReleaseSpec{ @@ -153,46 +214,50 @@ func Test_summarize(t *testing.T) { Enable: true, }, }, - expect: []metav1.Condition{ - { - Type: meta.ReadyCondition, - Status: metav1.ConditionFalse, - Reason: v2.TestFailedReason, - Message: "test hook(s) failure", - ObservedGeneration: 1, - }, - { - Type: v2.ReleasedCondition, - Status: metav1.ConditionTrue, - Reason: v2.InstallSucceededReason, - Message: "Install complete", - ObservedGeneration: 1, - }, - { - Type: v2.TestSuccessCondition, - Status: metav1.ConditionFalse, - Reason: v2.TestFailedReason, - Message: "test hook(s) failure", - ObservedGeneration: 1, + expectedStatus: &v2.HelmReleaseStatus{ + Conditions: []metav1.Condition{ + { + Type: meta.ReadyCondition, + Status: metav1.ConditionFalse, + Reason: v2.TestFailedReason, + Message: "test hook(s) failure", + ObservedGeneration: 1, + }, + { + Type: v2.ReleasedCondition, + Status: metav1.ConditionTrue, + Reason: v2.InstallSucceededReason, + Message: "Install complete", + ObservedGeneration: 1, + }, + { + Type: v2.TestSuccessCondition, + Status: metav1.ConditionFalse, + Reason: v2.TestFailedReason, + Message: "test hook(s) failure", + ObservedGeneration: 1, + }, }, }, }, { name: "with test hooks enabled and pending tests", - conditions: []metav1.Condition{ - { - Type: v2.ReleasedCondition, - Status: metav1.ConditionTrue, - Reason: v2.InstallSucceededReason, - Message: "Install complete", - ObservedGeneration: 1, - }, - { - Type: v2.TestSuccessCondition, - Status: metav1.ConditionUnknown, - Reason: "AwaitingTests", - Message: "Release is awaiting tests", - ObservedGeneration: 1, + status: v2.HelmReleaseStatus{ + Conditions: []metav1.Condition{ + { + Type: v2.ReleasedCondition, + Status: metav1.ConditionTrue, + Reason: v2.InstallSucceededReason, + Message: "Install complete", + ObservedGeneration: 1, + }, + { + Type: v2.TestSuccessCondition, + Status: metav1.ConditionUnknown, + Reason: "AwaitingTests", + Message: "Release is awaiting tests", + ObservedGeneration: 1, + }, }, }, spec: &v2.HelmReleaseSpec{ @@ -200,54 +265,58 @@ func Test_summarize(t *testing.T) { Enable: true, }, }, - expect: []metav1.Condition{ - { - Type: meta.ReadyCondition, - Status: metav1.ConditionUnknown, - Reason: "AwaitingTests", - Message: "Release is awaiting tests", - ObservedGeneration: 1, - }, - { - Type: v2.ReleasedCondition, - Status: metav1.ConditionTrue, - Reason: v2.InstallSucceededReason, - Message: "Install complete", - ObservedGeneration: 1, - }, - { - Type: v2.TestSuccessCondition, - Status: metav1.ConditionUnknown, - Reason: "AwaitingTests", - Message: "Release is awaiting tests", - ObservedGeneration: 1, + expectedStatus: &v2.HelmReleaseStatus{ + Conditions: []metav1.Condition{ + { + Type: meta.ReadyCondition, + Status: metav1.ConditionUnknown, + Reason: "AwaitingTests", + Message: "Release is awaiting tests", + ObservedGeneration: 1, + }, + { + Type: v2.ReleasedCondition, + Status: metav1.ConditionTrue, + Reason: v2.InstallSucceededReason, + Message: "Install complete", + ObservedGeneration: 1, + }, + { + Type: v2.TestSuccessCondition, + Status: metav1.ConditionUnknown, + Reason: "AwaitingTests", + Message: "Release is awaiting tests", + ObservedGeneration: 1, + }, }, }, }, { name: "with remediation failure", generation: 1, - conditions: []metav1.Condition{ - { - Type: v2.ReleasedCondition, - Status: metav1.ConditionTrue, - Reason: v2.InstallSucceededReason, - Message: "Install complete", - ObservedGeneration: 1, - }, - { - Type: v2.TestSuccessCondition, - Status: metav1.ConditionFalse, - Reason: v2.TestFailedReason, - Message: "test hook(s) failure", - ObservedGeneration: 1, - }, - { - Type: v2.RemediatedCondition, - Status: metav1.ConditionFalse, - Reason: v2.UninstallFailedReason, - Message: "Uninstall failure", - ObservedGeneration: 1, + status: v2.HelmReleaseStatus{ + Conditions: []metav1.Condition{ + { + Type: v2.ReleasedCondition, + Status: metav1.ConditionTrue, + Reason: v2.InstallSucceededReason, + Message: "Install complete", + ObservedGeneration: 1, + }, + { + Type: v2.TestSuccessCondition, + Status: metav1.ConditionFalse, + Reason: v2.TestFailedReason, + Message: "test hook(s) failure", + ObservedGeneration: 1, + }, + { + Type: v2.RemediatedCondition, + Status: metav1.ConditionFalse, + Reason: v2.UninstallFailedReason, + Message: "Uninstall failure", + ObservedGeneration: 1, + }, }, }, spec: &v2.HelmReleaseSpec{ @@ -255,112 +324,122 @@ func Test_summarize(t *testing.T) { Enable: true, }, }, - expect: []metav1.Condition{ - { - Type: meta.ReadyCondition, - Status: metav1.ConditionFalse, - Reason: v2.UninstallFailedReason, - Message: "Uninstall failure", - ObservedGeneration: 1, - }, - { - Type: v2.ReleasedCondition, - Status: metav1.ConditionTrue, - Reason: v2.InstallSucceededReason, - Message: "Install complete", - ObservedGeneration: 1, - }, - { - Type: v2.TestSuccessCondition, - Status: metav1.ConditionFalse, - Reason: v2.TestFailedReason, - Message: "test hook(s) failure", - ObservedGeneration: 1, - }, - { - Type: v2.RemediatedCondition, - Status: metav1.ConditionFalse, - Reason: v2.UninstallFailedReason, - Message: "Uninstall failure", - ObservedGeneration: 1, + expectedStatus: &v2.HelmReleaseStatus{ + Conditions: []metav1.Condition{ + { + Type: meta.ReadyCondition, + Status: metav1.ConditionFalse, + Reason: v2.UninstallFailedReason, + Message: "Uninstall failure", + ObservedGeneration: 1, + }, + { + Type: v2.ReleasedCondition, + Status: metav1.ConditionTrue, + Reason: v2.InstallSucceededReason, + Message: "Install complete", + ObservedGeneration: 1, + }, + { + Type: v2.TestSuccessCondition, + Status: metav1.ConditionFalse, + Reason: v2.TestFailedReason, + Message: "test hook(s) failure", + ObservedGeneration: 1, + }, + { + Type: v2.RemediatedCondition, + Status: metav1.ConditionFalse, + Reason: v2.UninstallFailedReason, + Message: "Uninstall failure", + ObservedGeneration: 1, + }, }, }, }, { name: "with remediation success", generation: 1, - conditions: []metav1.Condition{ - { - Type: v2.ReleasedCondition, - Status: metav1.ConditionFalse, - Reason: v2.UpgradeFailedReason, - Message: "Upgrade failure", - ObservedGeneration: 1, - }, - { - Type: v2.RemediatedCondition, - Status: metav1.ConditionTrue, - Reason: v2.RollbackSucceededReason, - Message: "Uninstall complete", - ObservedGeneration: 1, - }, - }, - expect: []metav1.Condition{ - { - Type: meta.ReadyCondition, - Status: metav1.ConditionFalse, - Reason: v2.RollbackSucceededReason, - Message: "Uninstall complete", - ObservedGeneration: 1, - }, - { - Type: v2.ReleasedCondition, - Status: metav1.ConditionFalse, - Reason: v2.UpgradeFailedReason, - Message: "Upgrade failure", - ObservedGeneration: 1, - }, - { - Type: v2.RemediatedCondition, - Status: metav1.ConditionTrue, - Reason: v2.RollbackSucceededReason, - Message: "Uninstall complete", - ObservedGeneration: 1, + status: v2.HelmReleaseStatus{ + Conditions: []metav1.Condition{ + { + Type: v2.ReleasedCondition, + Status: metav1.ConditionFalse, + Reason: v2.UpgradeFailedReason, + Message: "Upgrade failure", + ObservedGeneration: 1, + }, + { + Type: v2.RemediatedCondition, + Status: metav1.ConditionTrue, + Reason: v2.RollbackSucceededReason, + Message: "Uninstall complete", + ObservedGeneration: 1, + }, + }, + }, + expectedStatus: &v2.HelmReleaseStatus{ + Conditions: []metav1.Condition{ + { + Type: meta.ReadyCondition, + Status: metav1.ConditionFalse, + Reason: v2.RollbackSucceededReason, + Message: "Uninstall complete", + ObservedGeneration: 1, + }, + { + Type: v2.ReleasedCondition, + Status: metav1.ConditionFalse, + Reason: v2.UpgradeFailedReason, + Message: "Upgrade failure", + ObservedGeneration: 1, + }, + { + Type: v2.RemediatedCondition, + Status: metav1.ConditionTrue, + Reason: v2.RollbackSucceededReason, + Message: "Uninstall complete", + ObservedGeneration: 1, + }, }, }, }, { name: "with stale ready", generation: 1, - conditions: []metav1.Condition{ - { - Type: meta.ReadyCondition, - Status: metav1.ConditionFalse, - Reason: "ChartNotFound", - Message: "chart not found", - }, - { - Type: v2.ReleasedCondition, - Status: metav1.ConditionTrue, - Reason: v2.UpgradeSucceededReason, - Message: "Upgrade finished", - ObservedGeneration: 1, - }, - }, - expect: []metav1.Condition{ - { - Type: meta.ReadyCondition, - Status: metav1.ConditionTrue, - Reason: v2.UpgradeSucceededReason, - Message: "Upgrade finished", - ObservedGeneration: 1, - }, - { - Type: v2.ReleasedCondition, - Status: metav1.ConditionTrue, - Reason: v2.UpgradeSucceededReason, - Message: "Upgrade finished", - ObservedGeneration: 1, + status: v2.HelmReleaseStatus{ + Conditions: []metav1.Condition{ + { + Type: meta.ReadyCondition, + Status: metav1.ConditionFalse, + Reason: "ChartNotFound", + Message: "chart not found", + }, + { + Type: v2.ReleasedCondition, + Status: metav1.ConditionTrue, + Reason: v2.UpgradeSucceededReason, + Message: "Upgrade finished", + ObservedGeneration: 1, + }, + }, + }, + expectedStatus: &v2.HelmReleaseStatus{ + Conditions: []metav1.Condition{ + { + Type: meta.ReadyCondition, + Status: metav1.ConditionTrue, + Reason: v2.UpgradeSucceededReason, + Message: "Upgrade finished", + ObservedGeneration: 1, + }, + { + Type: v2.ReleasedCondition, + Status: metav1.ConditionTrue, + Reason: v2.UpgradeSucceededReason, + Message: "Upgrade finished", + ObservedGeneration: 1, + }, }, }, }, @@ -372,61 +451,156 @@ func Test_summarize(t *testing.T) { Enable: true, }, }, - conditions: []metav1.Condition{ - { - Type: v2.ReleasedCondition, - Status: metav1.ConditionTrue, - Reason: v2.UpgradeSucceededReason, - Message: "Upgrade finished", - ObservedGeneration: 4, - }, - { - Type: v2.RemediatedCondition, - Status: metav1.ConditionTrue, - Reason: v2.RollbackSucceededReason, - Message: "Rollback finished", - ObservedGeneration: 3, - }, - { - Type: v2.TestSuccessCondition, - Status: metav1.ConditionFalse, - Reason: v2.TestFailedReason, - Message: "test hook(s) failure", - ObservedGeneration: 2, - }, - }, - expect: []metav1.Condition{ - { - Type: meta.ReadyCondition, - Status: metav1.ConditionTrue, - Reason: v2.UpgradeSucceededReason, - Message: "Upgrade finished", - ObservedGeneration: 5, - }, - { - Type: v2.ReleasedCondition, - Status: metav1.ConditionTrue, - Reason: v2.UpgradeSucceededReason, - Message: "Upgrade finished", - ObservedGeneration: 4, - }, - { - Type: v2.RemediatedCondition, - Status: metav1.ConditionTrue, - Reason: v2.RollbackSucceededReason, - Message: "Rollback finished", - ObservedGeneration: 3, - }, - { - Type: v2.TestSuccessCondition, - Status: metav1.ConditionFalse, - Reason: v2.TestFailedReason, - Message: "test hook(s) failure", - ObservedGeneration: 2, + status: v2.HelmReleaseStatus{ + Conditions: []metav1.Condition{ + { + Type: v2.ReleasedCondition, + Status: metav1.ConditionTrue, + Reason: v2.UpgradeSucceededReason, + Message: "Upgrade finished", + ObservedGeneration: 4, + }, + { + Type: v2.RemediatedCondition, + Status: metav1.ConditionTrue, + Reason: v2.RollbackSucceededReason, + Message: "Rollback finished", + ObservedGeneration: 3, + }, + { + Type: v2.TestSuccessCondition, + Status: metav1.ConditionFalse, + Reason: v2.TestFailedReason, + Message: "test hook(s) failure", + ObservedGeneration: 2, + }, + }, + }, + expectedStatus: &v2.HelmReleaseStatus{ + Conditions: []metav1.Condition{ + { + Type: meta.ReadyCondition, + Status: metav1.ConditionTrue, + Reason: v2.UpgradeSucceededReason, + Message: "Upgrade finished", + ObservedGeneration: 5, + }, + { + Type: v2.ReleasedCondition, + Status: metav1.ConditionTrue, + Reason: v2.UpgradeSucceededReason, + Message: "Upgrade finished", + ObservedGeneration: 4, + }, + { + Type: v2.RemediatedCondition, + Status: metav1.ConditionTrue, + Reason: v2.RollbackSucceededReason, + Message: "Rollback finished", + ObservedGeneration: 3, + }, + { + Type: v2.TestSuccessCondition, + Status: metav1.ConditionFalse, + Reason: v2.TestFailedReason, + Message: "test hook(s) failure", + ObservedGeneration: 2, + }, }, }, }, + { + name: "with postrender", + generation: 1, + status: v2.HelmReleaseStatus{ + Conditions: []metav1.Condition{ + { + Type: v2.ReleasedCondition, + Status: metav1.ConditionTrue, + Reason: v2.InstallSucceededReason, + Message: "Install complete", + ObservedGeneration: 1, + }, + }, + ObservedPostRenderersDigest: postrender.Digest(digest.Canonical, postRenderers).String(), + }, + spec: &v2.HelmReleaseSpec{ + PostRenderers: postRenderers2, + }, + expectedStatus: &v2.HelmReleaseStatus{ + Conditions: []metav1.Condition{ + { + Type: meta.ReadyCondition, + Status: metav1.ConditionTrue, + Reason: v2.InstallSucceededReason, + Message: "Install complete", + ObservedGeneration: 1, + }, + { + Type: v2.ReleasedCondition, + Status: metav1.ConditionTrue, + Reason: v2.InstallSucceededReason, + Message: "Install complete", + ObservedGeneration: 1, + }, + }, + ObservedPostRenderersDigest: postrender.Digest(digest.Canonical, postRenderers2).String(), + }, + }, + { + name: "with PostRenderers and Remediaction success", + generation: 1, + status: v2.HelmReleaseStatus{ + Conditions: []metav1.Condition{ + { + Type: v2.ReleasedCondition, + Status: metav1.ConditionFalse, + Reason: v2.UpgradeFailedReason, + Message: "Upgrade failure", + ObservedGeneration: 1, + }, + { + Type: v2.RemediatedCondition, + Status: metav1.ConditionTrue, + Reason: v2.RollbackSucceededReason, + Message: "Uninstall complete", + ObservedGeneration: 1, + }, + }, + ObservedPostRenderersDigest: postrender.Digest(digest.Canonical, postRenderers).String(), + }, + spec: &v2.HelmReleaseSpec{ + PostRenderers: postRenderers2, + }, + expectedStatus: &v2.HelmReleaseStatus{ + Conditions: []metav1.Condition{ + { + Type: meta.ReadyCondition, + Status: metav1.ConditionFalse, + Reason: v2.RollbackSucceededReason, + Message: "Uninstall complete", + ObservedGeneration: 1, + }, + { + Type: v2.ReleasedCondition, + Status: metav1.ConditionFalse, + Reason: v2.UpgradeFailedReason, + Message: "Upgrade failure", + ObservedGeneration: 1, + }, + { + Type: v2.RemediatedCondition, + Status: metav1.ConditionTrue, + Reason: v2.RollbackSucceededReason, + Message: "Uninstall complete", + ObservedGeneration: 1, + }, + }, + ObservedPostRenderersDigest: postrender.Digest(digest.Canonical, postRenderers).String(), + }, + }, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { g := NewWithT(t) @@ -435,16 +609,15 @@ func Test_summarize(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Generation: tt.generation, }, - Status: v2.HelmReleaseStatus{ - Conditions: tt.conditions, - }, + Status: tt.status, } if tt.spec != nil { obj.Spec = *tt.spec.DeepCopy() } summarize(&Request{Object: obj}) - g.Expect(obj.Status.Conditions).To(conditions.MatchConditions(tt.expect)) + g.Expect(obj.Status.Conditions).To(conditions.MatchConditions(tt.expectedStatus.Conditions)) + g.Expect(obj.Status.ObservedPostRenderersDigest).To(Equal(tt.expectedStatus.ObservedPostRenderersDigest)) }) } } diff --git a/internal/reconcile/state_test.go b/internal/reconcile/state_test.go index 2a53980c2..bbd844f94 100644 --- a/internal/reconcile/state_test.go +++ b/internal/reconcile/state_test.go @@ -35,7 +35,9 @@ import ( v2 "github.com/fluxcd/helm-controller/api/v2" "github.com/fluxcd/helm-controller/internal/action" + "github.com/fluxcd/helm-controller/internal/digest" "github.com/fluxcd/helm-controller/internal/kube" + "github.com/fluxcd/helm-controller/internal/postrender" "github.com/fluxcd/helm-controller/internal/release" "github.com/fluxcd/helm-controller/internal/testutil" ) @@ -452,6 +454,34 @@ func Test_DetermineReleaseState(t *testing.T) { Status: ReleaseStatusOutOfSync, }, }, + { + name: "postRenderers changed", + releases: []*helmrelease.Release{ + testutil.BuildRelease(&helmrelease.MockReleaseOptions{ + Name: mockReleaseName, + Namespace: mockReleaseNamespace, + Version: 1, + Status: helmrelease.StatusDeployed, + Chart: testutil.BuildChart(), + }, testutil.ReleaseWithConfig(map[string]interface{}{"foo": "bar"})), + }, + spec: func(spec *v2.HelmReleaseSpec) { + spec.PostRenderers = postRenderers2 + }, + status: func(releases []*helmrelease.Release) v2.HelmReleaseStatus { + return v2.HelmReleaseStatus{ + History: v2.Snapshots{ + release.ObservedToSnapshot(release.ObserveRelease(releases[0])), + }, + ObservedPostRenderersDigest: postrender.Digest(digest.Canonical, postRenderers).String(), + } + }, + chart: testutil.BuildChart(), + values: map[string]interface{}{"foo": "bar"}, + want: ReleaseState{ + Status: ReleaseStatusOutOfSync, + }, + }, } for _, tt := range tests {