diff --git a/api/v2/helmrelease_types.go b/api/v2/helmrelease_types.go
index 688db28d0..5698ea148 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
diff --git a/api/v2beta1/helmrelease_types.go b/api/v2beta1/helmrelease_types.go
index 6b241bced..0557cdf58 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.
diff --git a/api/v2beta2/helmrelease_types.go b/api/v2beta2/helmrelease_types.go
index 03c693e48..589b331fd 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
diff --git a/config/crd/bases/helm.toolkit.fluxcd.io_helmreleases.yaml b/config/crd/bases/helm.toolkit.fluxcd.io_helmreleases.yaml
index 685bb6e3e..7be94e22e 100644
--- a/config/crd/bases/helm.toolkit.fluxcd.io_helmreleases.yaml
+++ b/config/crd/bases/helm.toolkit.fluxcd.io_helmreleases.yaml
@@ -1160,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
@@ -2394,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
@@ -3677,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 075a53055..8f928a6cb 100644
--- a/docs/api/v2/helm.md
+++ b/docs/api/v2/helm.md
@@ -1434,6 +1434,19 @@ int64
+
lastAttemptedGeneration
int64
diff --git a/docs/spec/v2/helmreleases.md b/docs/spec/v2/helmreleases.md
index be91e993e..85e90ddba 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.
+### 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.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
+it should trigger a Helm upgrade.
+
### Last Attempted Config Digest
The helm-controller reports the digest for the [values](#values) it last
diff --git a/internal/controller/helmrelease_controller.go b/internal/controller/helmrelease_controller.go
index 083a44528..3b210256a 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"
@@ -352,14 +353,17 @@ 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")
}
+ r.adoptPostRenderersStatus(obj)
}
// If the release target configuration has changed, we need to uninstall the
@@ -646,6 +650,20 @@ func (r *HelmReleaseReconciler) adoptLegacyRelease(ctx context.Context, getter g
return nil
}
+// 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
+ }
+
+ // 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.ObservedPostRenderersDigest == "" {
+ obj.Status.ObservedPostRenderersDigest = 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..c2b66c3a5 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,157 @@ 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())
+
+ 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.ObservedPostRenderersDigest = 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.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.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..66855808b 100644
--- a/internal/postrender/build.go
+++ b/internal/postrender/build.go
@@ -17,6 +17,9 @@ limitations under the License.
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"
@@ -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/release.go b/internal/reconcile/release.go
index 049eaf421..8e8564e66 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,15 @@ 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 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.go b/internal/reconcile/state.go
index b3bf7d03e..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
@@ -141,6 +143,15 @@ 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
+ }
+
// 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`.
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 {
|