From 1f63b8a40aeb25d2f77214484e9f26beaebd173f Mon Sep 17 00:00:00 2001 From: Stefan Prodan Date: Thu, 9 May 2024 10:08:13 +0300 Subject: [PATCH 1/4] Prevent snapshot drift when upgrading to API v2 Signed-off-by: Stefan Prodan (cherry picked from commit 56b5f14b344cdc739f14164c03cd76b94a348fe2) --- internal/release/digest_test.go | 2 +- internal/release/observation.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/release/digest_test.go b/internal/release/digest_test.go index 3aca5b8eb..c340ca965 100644 --- a/internal/release/digest_test.go +++ b/internal/release/digest_test.go @@ -37,7 +37,7 @@ func TestDigest(t *testing.T) { rel: Observation{ Name: "foo", }, - exp: "sha256:ca8901e499a79368647134cc4f1c2118f8e7ec64c8a4703b281d17fb01acfbed", + exp: "sha256:91b6773f7696d3eb405708a07e2daedc6e69664dabac8e10af7d570d09f947d5", }, } for _, tt := range tests { diff --git a/internal/release/observation.go b/internal/release/observation.go index 48a5bb56d..71ec18613 100644 --- a/internal/release/observation.go +++ b/internal/release/observation.go @@ -80,7 +80,7 @@ type Observation struct { // Namespace is the Kubernetes namespace of the release. Namespace string `json:"namespace"` // OCIDigest is the digest of the OCI artifact that was used to - OCIDigest string `json:"ociDigest"` + OCIDigest string `json:"ociDigest,omitempty"` } // Targets returns if the release matches the given name, namespace and From e0629b79670eb34d400cfcc321bfcacdf583e0db Mon Sep 17 00:00:00 2001 From: Sunny Date: Thu, 9 May 2024 10:58:41 +0000 Subject: [PATCH 2/4] PostRenderersDigest observation improvements Move the post renderers digest set/update code from summarize() to atomic release reconciler in order to update the observation only at the end of a successful reconciliation. summarize() is for summarizing the status conditions and is also called by all the other action sub-reconcilers, which can update the post renderers digest observation too early. Updating the observed post renderers digest at the very end of a reconciliation introduces an issue where a digest mismatch in DetermineReleaseState() could result in the release to get stuck in a loop as even after running an upgrade due to post renderers value, the new observation isn't reflected immediately in the middle of atomic reconciliation. This can be solved by checking post renderers digest value only for new configurations where the object generation and the ready status condition observed generations don't match, in other words when the generation of a configuration has not be processed. This assumes that an upgrade due to any other reason also takes into account the post renderers value and need not be checked separately for the same config generation. Signed-off-by: Sunny (cherry picked from commit 63f7a76319e7692855ec9f92899b021614ecc58a) --- internal/reconcile/atomic_release.go | 11 ++ internal/reconcile/atomic_release_test.go | 225 ++++++++++++++++++++++ internal/reconcile/release.go | 11 -- internal/reconcile/release_test.go | 124 ++---------- internal/reconcile/state.go | 25 ++- internal/reconcile/state_test.go | 48 +++++ 6 files changed, 318 insertions(+), 126 deletions(-) diff --git a/internal/reconcile/atomic_release.go b/internal/reconcile/atomic_release.go index d26324a78..7ae18c986 100644 --- a/internal/reconcile/atomic_release.go +++ b/internal/reconcile/atomic_release.go @@ -38,7 +38,9 @@ import ( v2 "github.com/fluxcd/helm-controller/api/v2" "github.com/fluxcd/helm-controller/internal/action" "github.com/fluxcd/helm-controller/internal/diff" + "github.com/fluxcd/helm-controller/internal/digest" interrors "github.com/fluxcd/helm-controller/internal/errors" + "github.com/fluxcd/helm-controller/internal/postrender" ) // OwnedConditions is a list of Condition types owned by the HelmRelease object. @@ -210,6 +212,15 @@ func (r *AtomicRelease) Reconcile(ctx context.Context, req *Request) error { // written to Ready. summarize(req) + // remove stale post-renderers digest on successful reconciliation. + if conditions.IsReady(req.Object) { + 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() + } + } + return nil } diff --git a/internal/reconcile/atomic_release_test.go b/internal/reconcile/atomic_release_test.go index 108f39154..7081a290d 100644 --- a/internal/reconcile/atomic_release_test.go +++ b/internal/reconcile/atomic_release_test.go @@ -43,7 +43,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" ) @@ -1067,6 +1069,229 @@ func TestAtomicRelease_Reconcile_Scenarios(t *testing.T) { } } +func TestAtomicRelease_Reconcile_PostRenderers_Scenarios(t *testing.T) { + tests := []struct { + name string + releases func(namespace string) []*helmrelease.Release + spec func(spec *v2.HelmReleaseSpec) + values map[string]interface{} + status func(releases []*helmrelease.Release) v2.HelmReleaseStatus + wantDigest string + wantReleaseAction v2.ReleaseAction + }{ + { + name: "addition of post renderers", + releases: func(namespace string) []*helmrelease.Release { + return []*helmrelease.Release{ + testutil.BuildRelease(&helmrelease.MockReleaseOptions{ + Name: mockReleaseName, + Namespace: namespace, + Version: 1, + Status: helmrelease.StatusDeployed, + Chart: testutil.BuildChart(), + }, testutil.ReleaseWithConfig(nil)), + } + }, + spec: func(spec *v2.HelmReleaseSpec) { + spec.PostRenderers = postRenderers + }, + status: func(releases []*helmrelease.Release) v2.HelmReleaseStatus { + return v2.HelmReleaseStatus{ + History: v2.Snapshots{ + release.ObservedToSnapshot(release.ObserveRelease(releases[0])), + }, + Conditions: []metav1.Condition{ + { + Type: meta.ReadyCondition, + Status: metav1.ConditionTrue, + ObservedGeneration: 1, + }, + }, + } + }, + wantDigest: postrender.Digest(digest.Canonical, postRenderers).String(), + wantReleaseAction: v2.ReleaseActionUpgrade, + }, + { + name: "config change and addition of post renderers", + releases: func(namespace string) []*helmrelease.Release { + return []*helmrelease.Release{ + testutil.BuildRelease(&helmrelease.MockReleaseOptions{ + Name: mockReleaseName, + Namespace: namespace, + Version: 1, + Status: helmrelease.StatusDeployed, + Chart: testutil.BuildChart(), + }, testutil.ReleaseWithConfig(nil)), + } + }, + spec: func(spec *v2.HelmReleaseSpec) { + spec.PostRenderers = postRenderers + }, + status: func(releases []*helmrelease.Release) v2.HelmReleaseStatus { + return v2.HelmReleaseStatus{ + History: v2.Snapshots{ + release.ObservedToSnapshot(release.ObserveRelease(releases[0])), + }, + Conditions: []metav1.Condition{ + { + Type: meta.ReadyCondition, + Status: metav1.ConditionTrue, + ObservedGeneration: 1, + }, + }, + } + }, + values: map[string]interface{}{"foo": "baz"}, + wantDigest: postrender.Digest(digest.Canonical, postRenderers).String(), + wantReleaseAction: v2.ReleaseActionUpgrade, + }, + { + name: "existing and new post renderers value", + releases: func(namespace string) []*helmrelease.Release { + return []*helmrelease.Release{ + testutil.BuildRelease(&helmrelease.MockReleaseOptions{ + Name: mockReleaseName, + Namespace: namespace, + Version: 1, + Status: helmrelease.StatusDeployed, + Chart: testutil.BuildChart(), + }, testutil.ReleaseWithConfig(nil)), + } + }, + 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])), + }, + Conditions: []metav1.Condition{ + { + Type: meta.ReadyCondition, + Status: metav1.ConditionTrue, + ObservedGeneration: 1, + }, + }, + ObservedPostRenderersDigest: postrender.Digest(digest.Canonical, postRenderers).String(), + } + }, + wantDigest: postrender.Digest(digest.Canonical, postRenderers2).String(), + wantReleaseAction: v2.ReleaseActionUpgrade, + }, + { + name: "post renderers mismatch remains in sync for processed config", + releases: func(namespace string) []*helmrelease.Release { + return []*helmrelease.Release{ + testutil.BuildRelease(&helmrelease.MockReleaseOptions{ + Name: mockReleaseName, + Namespace: namespace, + Version: 1, + Status: helmrelease.StatusDeployed, + Chart: testutil.BuildChart(), + }, testutil.ReleaseWithConfig(nil)), + } + }, + 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])), + }, + Conditions: []metav1.Condition{ + { + Type: meta.ReadyCondition, + Status: metav1.ConditionTrue, + ObservedGeneration: 2, // This is used to set processed config generation. + }, + }, + ObservedPostRenderersDigest: postrender.Digest(digest.Canonical, postRenderers).String(), + } + }, + wantDigest: postrender.Digest(digest.Canonical, postRenderers2).String(), + wantReleaseAction: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + + namedNS, err := testEnv.CreateNamespace(context.TODO(), mockReleaseNamespace) + g.Expect(err).NotTo(HaveOccurred()) + t.Cleanup(func() { + _ = testEnv.Delete(context.TODO(), namedNS) + }) + releaseNamespace := namedNS.Name + + releases := tt.releases(releaseNamespace) + + obj := &v2.HelmRelease{ + ObjectMeta: metav1.ObjectMeta{ + Name: mockReleaseName, + Namespace: releaseNamespace, + // Set a higher generation value to allow setting + // observations in previous generations. + Generation: 2, + }, + Spec: v2.HelmReleaseSpec{ + ReleaseName: mockReleaseName, + TargetNamespace: releaseNamespace, + StorageNamespace: releaseNamespace, + Timeout: &metav1.Duration{Duration: 100 * time.Millisecond}, + }, + } + + if tt.spec != nil { + tt.spec(&obj.Spec) + } + if tt.status != nil { + obj.Status = tt.status(releases) + } + + getter, err := RESTClientGetterFromManager(testEnv.Manager, obj.GetReleaseNamespace()) + g.Expect(err).ToNot(HaveOccurred()) + + cfg, err := action.NewConfigFactory(getter, + action.WithStorage(action.DefaultStorageDriver, obj.GetStorageNamespace()), + ) + g.Expect(err).ToNot(HaveOccurred()) + + store := helmstorage.Init(cfg.Driver) + for _, r := range releases { + g.Expect(store.Create(r)).To(Succeed()) + } + + // We use a fake client here to allow us to work with a minimal release + // object mock. As the fake client does not perform any validation. + // However, for the Helm storage driver to work, we need a real client + // which is therefore initialized separately above. + client := fake.NewClientBuilder(). + WithScheme(testEnv.Scheme()). + WithObjects(obj). + WithStatusSubresource(&v2.HelmRelease{}). + Build() + patchHelper := patch.NewSerialPatcher(obj, client) + recorder := new(record.FakeRecorder) + + req := &Request{ + Object: obj, + Chart: testutil.BuildChart(), + Values: tt.values, + } + + err = NewAtomicRelease(patchHelper, cfg, recorder, testFieldManager).Reconcile(context.TODO(), req) + g.Expect(err).ToNot(HaveOccurred()) + + g.Expect(obj.Status.ObservedPostRenderersDigest).To(Equal(tt.wantDigest)) + g.Expect(obj.Status.LastAttemptedReleaseAction).To(Equal(tt.wantReleaseAction)) + }) + } +} + func TestAtomicRelease_actionForState(t *testing.T) { tests := []struct { name string diff --git a/internal/reconcile/release.go b/internal/reconcile/release.go index 8e8564e66..269b1a2f8 100644 --- a/internal/reconcile/release.go +++ b/internal/reconcile/release.go @@ -28,8 +28,6 @@ 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" ) @@ -190,15 +188,6 @@ func summarize(req *Request) { Message: conds[0].Message, ObservedGeneration: req.Object.Generation, }) - - // remove stale post-renderers digest - 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() - } - } } // eventMessageWithLog returns an event message composed out of the given diff --git a/internal/reconcile/release_test.go b/internal/reconcile/release_test.go index 0cd320e78..d2cede74f 100644 --- a/internal/reconcile/release_test.go +++ b/internal/reconcile/release_test.go @@ -31,8 +31,6 @@ 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" ) const ( @@ -50,14 +48,14 @@ var ( Kind: "Deployment", Name: "test", }, - Patch: `|- - apiVersion: apps/v1 - kind: Deployment - metadata: - name: test - spec: - replicas: 2 - `, + Patch: ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: test +spec: + replicas: 2 +`, }, }, }, @@ -73,14 +71,14 @@ var ( Kind: "Deployment", Name: "test", }, - Patch: `|- - apiVersion: apps/v1 - kind: Deployment - metadata: - name: test - spec: - replicas: 3 - `, + Patch: ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: test +spec: + replicas: 3 +`, }, }, }, @@ -509,96 +507,6 @@ func Test_summarize(t *testing.T) { }, }, }, - { - 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 { diff --git a/internal/reconcile/state.go b/internal/reconcile/state.go index be255d11c..247a752a7 100644 --- a/internal/reconcile/state.go +++ b/internal/reconcile/state.go @@ -21,6 +21,8 @@ import ( "errors" "fmt" + "github.com/fluxcd/pkg/apis/meta" + "github.com/fluxcd/pkg/runtime/conditions" "github.com/fluxcd/pkg/ssa/jsondiff" "helm.sh/helm/v3/pkg/kube" helmrelease "helm.sh/helm/v3/pkg/release" @@ -143,13 +145,22 @@ func DetermineReleaseState(ctx context.Context, cfg *action.ConfigFactory, req * } } - // Verify if postrender digest has changed - 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 + // Verify if postrender digest has changed if config has not been + // processed. For the processed or partially processed generation, the + // updated observation will only be reflected at the end of a successful + // reconciliation. Comparing here would result the reconciliation to + // get stuck in this check due to a mismatch forever. The value can't + // change without a new generation. Hence, compare the observed digest + // for new generations only. + ready := conditions.Get(req.Object, meta.ReadyCondition) + if ready != nil && ready.ObservedGeneration != req.Object.Generation { + 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: "postrenderers digest has changed"}, nil + } } // For the further determination of test results, we look at the diff --git a/internal/reconcile/state_test.go b/internal/reconcile/state_test.go index bbd844f94..fb51b8648 100644 --- a/internal/reconcile/state_test.go +++ b/internal/reconcile/state_test.go @@ -27,8 +27,10 @@ import ( helmrelease "helm.sh/helm/v3/pkg/release" helmstorage "helm.sh/helm/v3/pkg/storage" helmdriver "helm.sh/helm/v3/pkg/storage/driver" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "github.com/fluxcd/pkg/apis/meta" "github.com/fluxcd/pkg/ssa/jsondiff" ssanormalize "github.com/fluxcd/pkg/ssa/normalize" ssautil "github.com/fluxcd/pkg/ssa/utils" @@ -474,6 +476,13 @@ func Test_DetermineReleaseState(t *testing.T) { release.ObservedToSnapshot(release.ObserveRelease(releases[0])), }, ObservedPostRenderersDigest: postrender.Digest(digest.Canonical, postRenderers).String(), + Conditions: []metav1.Condition{ + { + Type: meta.ReadyCondition, + Status: metav1.ConditionTrue, + ObservedGeneration: 1, + }, + }, } }, chart: testutil.BuildChart(), @@ -482,6 +491,41 @@ func Test_DetermineReleaseState(t *testing.T) { Status: ReleaseStatusOutOfSync, }, }, + { + name: "postRenderers mismatch ignored for processed generation", + 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(), + Conditions: []metav1.Condition{ + { + Type: meta.ReadyCondition, + Status: metav1.ConditionTrue, + ObservedGeneration: 2, + }, + }, + } + }, + chart: testutil.BuildChart(), + values: map[string]interface{}{"foo": "bar"}, + want: ReleaseState{ + Status: ReleaseStatusInSync, + }, + }, } for _, tt := range tests { @@ -495,6 +539,10 @@ func Test_DetermineReleaseState(t *testing.T) { StorageNamespace: mockReleaseNamespace, }, } + // Set a non-zero generation so that old observations can be set on + // the object status. + obj.Generation = 2 + if tt.spec != nil { tt.spec(&obj.Spec) } From 7ccb1cbfb37fe69c24859ef8adeb6147e2fb5877 Mon Sep 17 00:00:00 2001 From: Stefan Prodan Date: Wed, 8 May 2024 13:11:48 +0300 Subject: [PATCH 3/4] Add changelog entry for v1.0.0 Signed-off-by: Stefan Prodan --- CHANGELOG.md | 149 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 149 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ef3df05b5..c10485119 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,154 @@ # Changelog +## 1.0.0 + +**Release date:** 2024-05-08 + +This is the general availability release of helm-controller. From now on, this controller +follows the [Flux release cadence and support pledge](https://fluxcd.io/flux/releases/). + +This release promotes the `HelmRelease` API from `v2beta2` to `v2` (GA), and +comes with new features, improvements and bug fixes. + +In addition, the controller has been updated to Kubernetes v1.30.0, +Helm v3.14.4, and various other dependencies to their latest version +to patch upstream CVEs. + +### Highlights + +The `helm.toolkit.fluxcd.io/v2` API comes with a new field +[`.spec.chartRef`](https://github.com/fluxcd/helm-controller/blob/release-v1.0.0-rc.1/docs/spec/v2/helmreleases.md#chart-reference) +that adds support for referencing `OCIRepository` and `HelmChart` objects in a `HelmRelease`. +When using `.spec.chartRef` instead of `.spec.chart`, the controller allows the reuse +of a Helm chart version across multiple `HelmRelease` resources. + +The notification mechanism has been improved to provide more detailed metadata +in the notification payload. The controller now annotates the Kubernetes events with +the `appVersion` and `version` of the Helm chart, and the `oci digest` of the +chart artifact when available. + +### Helm OCI support + +Starting with this version, the recommended way of referencing Helm charts stored +in container registries is through [OCIRepository](https://fluxcd.io/flux/components/source/ocirepositories/). + +The `OCIRepository` provides more flexibility in managing Helm charts, +as it allows targeting a Helm chart version by `tag`, `semver` or OCI `digest`. +It also provides a way to +[filter semver tags](https://github.com/fluxcd/source-controller/blob/release/v1.3.x/docs/spec/v1beta2/ocirepositories.md#semverfilter-example), +allowing targeting a specific version range e.g. pre-releases only, patch versions, etc. + +Using `OCIRepository` objects instead of `HelmRepository` and `HelmChart` objects +improves the controller's performance and simplifies the debugging process. +If a chart version gets overwritten in the container registry, the controller +will detect the change in the upstream OCI digest and reconcile the `HelmRelease` +resources accordingly. +[Promoting](https://fluxcd.io/flux/use-cases/gh-actions-helm-promotion/) +a Helm chart version to production can be done by pinning the `OCIRepository` +to an immutable digest, ensuring that the chart version is not changed unintentionally. + +Helm OCI example: + +```yaml +apiVersion: source.toolkit.fluxcd.io/v1beta2 +kind: OCIRepository +metadata: + name: podinfo + namespace: default +spec: + interval: 10m + layerSelector: + mediaType: "application/vnd.cncf.helm.chart.content.v1.tar+gzip" + operation: copy + url: oci://ghcr.io/stefanprodan/charts/podinfo + ref: + semver: "*" +--- +apiVersion: helm.toolkit.fluxcd.io/v2 +kind: HelmRelease +metadata: + name: podinfo + namespace: default +spec: + interval: 10m + chartRef: + kind: OCIRepository + name: podinfo +``` + +#### API changes + +The `helm.toolkit.fluxcd.io` CRD contains the following versions: +- v2 (storage version) +- v2beta2 (deprecated) +- v2beta1 (deprecated) + +New optional fields have been added to the `HelmRelease` API: + +- `.spec.chartRef` allows referencing chart artifacts from `OCIRepository` and `HelmChart` objects. +- `.spec.chart.spec.ignoreMissingValuesFiles` allows ignoring missing values files instead of failing to reconcile. + +Deprecated fields have been removed from the `HelmRelease` API: + +- `.spec.chart.spec.valuesFile` replaced by `.spec.chart.spec.valuesFiles` +- `.spec.postRenderers.kustomize.patchesJson6902` replaced by `.spec.postRenderers.kustomize.patches` +- `.spec.postRenderers.kustomize.patchesStrategicMerge` replaced by `.spec.postRenderers.kustomize.patches` +- `.status.lastAppliedRevision` replaced by `.status.history.chartVersion` + +#### Upgrade procedure + +1. Before upgrading the controller, ensure that the `HelmRelease` v2beta2 manifests stored in Git + are not using the deprecated fields. Search for `valuesFile` and replace it with `valuesFiles`, + replace `patchesJson6902` and `patchesStrategicMerge` with `patches`. + Commit and push the changes to the Git repository, then wait for Flux to reconcile the changes. +2. Upgrade the controller and CRDs to v1.0.0 on the cluster using Flux v2.3 release. + Note that helm-controller v1.0.0 requires source-controller v1.3.0. +3. Update the `apiVersion` field of the `HelmRelease` resources to `helm.toolkit.fluxcd.io/v2`, + commit and push the changes to the Git repository. + +Bumping the API version in manifests can be done gradually. +It is advised to not delay this procedure as the beta versions will be removed after 6 months. + +### Full changelog + +Improvements: +- Add the chart app version to status and events metadata + [#968](https://github.com/fluxcd/helm-controller/pull/968) +- Promote HelmRelease API to v2 (GA) + [#963](https://github.com/fluxcd/helm-controller/pull/963) +- Add `.spec.ignoreMissingValuesFiles` to HelmChartTemplate API + [#942](https://github.com/fluxcd/helm-controller/pull/942) +- Update HelmChart API to v1 (GA) + [#962](https://github.com/fluxcd/helm-controller/pull/962) +- Update dependencies to Kubernetes 1.30.0 + [#944](https://github.com/fluxcd/helm-controller/pull/944) +- Add support for HelmChart to `.spec.chartRef` + [#945](https://github.com/fluxcd/helm-controller/pull/945) +- Add support for OCIRepository to `.spec.chartRef` + [#905](https://github.com/fluxcd/helm-controller/pull/905) +- Update dependencies to Kustomize v5.4.0 + [#932](https://github.com/fluxcd/helm-controller/pull/932) +- Add notation verification provider to API + [#930](https://github.com/fluxcd/helm-controller/pull/930) +- Update controller to Helm v3.14.3 and Kubernetes v1.29.0 + [#879](https://github.com/fluxcd/helm-controller/pull/879) +- Update controller-gen to v0.14.0 + [#910](https://github.com/fluxcd/helm-controller/pull/910) + +Fixes: +- Track changes in `.spec.postRenderers` + [#965](https://github.com/fluxcd/helm-controller/pull/965) +- Update Ready condition during drift correction + [#885](https://github.com/fluxcd/helm-controller/pull/885) +- Fix patching on drift detection + [#935](https://github.com/fluxcd/helm-controller/pull/935) +- Use corev1 event type for sending events + [#908](https://github.com/fluxcd/helm-controller/pull/908) +- Reintroduce missing events for helmChart reconciliation failures + [#907](https://github.com/fluxcd/helm-controller/pull/907) +- Remove `genclient:Namespaced` tag + [#901](https://github.com/fluxcd/helm-controller/pull/901) + ## 0.37.4 **Release date:** 2024-02-05 From 93702c7a575aba34369cb29cc54821a6faa49f8c Mon Sep 17 00:00:00 2001 From: Stefan Prodan Date: Wed, 8 May 2024 13:17:15 +0300 Subject: [PATCH 4/4] Release v1.0.0 Signed-off-by: Stefan Prodan --- .goreleaser.yaml | 2 +- config/manager/kustomization.yaml | 2 +- go.mod | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.goreleaser.yaml b/.goreleaser.yaml index e665fc862..6b7177cfb 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -6,7 +6,7 @@ builds: release: extra_files: - glob: config/release/*.yaml - prerelease: "true" + prerelease: "auto" header: | ## Changelog diff --git a/config/manager/kustomization.yaml b/config/manager/kustomization.yaml index ca14db83c..09d0eb65b 100644 --- a/config/manager/kustomization.yaml +++ b/config/manager/kustomization.yaml @@ -5,4 +5,4 @@ resources: images: - name: fluxcd/helm-controller newName: fluxcd/helm-controller - newTag: v0.37.4 + newTag: v1.0.0 diff --git a/go.mod b/go.mod index d9e3e63be..18680f102 100644 --- a/go.mod +++ b/go.mod @@ -17,7 +17,7 @@ replace ( require ( github.com/Masterminds/semver v1.5.0 github.com/fluxcd/cli-utils v0.36.0-flux.7 - github.com/fluxcd/helm-controller/api v0.37.4 + github.com/fluxcd/helm-controller/api v1.0.0 github.com/fluxcd/pkg/apis/acl v0.3.0 github.com/fluxcd/pkg/apis/event v0.9.0 github.com/fluxcd/pkg/apis/kustomize v1.5.0