diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml
index 595ed6ba4..7a2170e53 100644
--- a/.github/dependabot.yaml
+++ b/.github/dependabot.yaml
@@ -1,16 +1,39 @@
version: 2
updates:
+ - package-ecosystem: "gomod"
+ directory: "/"
+ labels: ["dependencies"]
+ schedule:
+ interval: "daily"
+ groups:
+ go-deps:
+ patterns:
+ - "*"
+ allow:
+ - dependency-type: "direct"
+ ignore:
+ # Kubernetes deps are updated by fluxcd/pkg/runtime
+ - dependency-name: "k8s.io/*"
+ - dependency-name: "sigs.k8s.io/*"
+ - dependency-name: "github.com/go-logr/*"
+ # jsondiff is updated by fluxcd/pkg/ssa
+ - dependency-name: "github.com/wI2L/jsondiff"
+ # OCI deps are updated by fluxcd/pkg/oci
+ - dependency-name: "github.com/google/go-containerregistry*"
+ - dependency-name: "github.com/opencontainers/*"
+ # Helm deps are updated by fluxcd/pkg/helmtestserver
+ - dependency-name: "helm.sh/helm/*"
+ - dependency-name: "github.com/Masterminds/semver/*"
+ # Flux APIs are updated at release time
+ - dependency-name: "github.com/fluxcd/helm-controller/api"
+ - dependency-name: "github.com/fluxcd/source-controller/api"
- package-ecosystem: "github-actions"
directory: "/"
labels: ["area/ci", "dependencies"]
- schedule:
- # By default, this will be on a monday.
- interval: "weekly"
groups:
- # Group all updates together, so that they are all applied in a single PR.
- # Grouped updates are currently in beta and is subject to change.
- # xref: https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#groups
ci:
patterns:
- "*"
+ schedule:
+ interval: "monthly"
diff --git a/.github/labels.yaml b/.github/labels.yaml
index 278402b32..ae521970b 100644
--- a/.github/labels.yaml
+++ b/.github/labels.yaml
@@ -13,8 +13,21 @@
- name: area/oci
description: OCI related issues and pull requests
color: '#c739ff'
-
-# TODO: enable this when we have a release/v1.0.x branch
-#- name: backport:release/v1.0.x
-# description: To be backported to release/v1.0.x
-# color: '#ffd700'
+- name: backport:release/v1.0.x
+ description: To be backported to release/v1.0.x
+ color: '#ffd700'
+- name: backport:release/v1.1.x
+ description: To be backported to release/v1.1.x
+ color: '#ffd700'
+- name: backport:release/v1.2.x
+ description: To be backported to release/v1.2.x
+ color: '#ffd700'
+- name: backport:release/v1.3.x
+ description: To be backported to release/v1.3.x
+ color: '#ffd700'
+- name: backport:release/v1.4.x
+ description: To be backported to release/v1.4.x
+ color: '#ffd700'
+- name: backport:release/v1.5.x
+ description: To be backported to release/v1.5.x
+ color: '#ffd700'
diff --git a/.github/workflows/backport.yaml b/.github/workflows/backport.yaml
index c0c1609b3..17743c8be 100644
--- a/.github/workflows/backport.yaml
+++ b/.github/workflows/backport.yaml
@@ -1,34 +1,12 @@
name: backport
-
on:
pull_request_target:
types: [closed, labeled]
-
-permissions:
- contents: read
-
jobs:
- pull-request:
- runs-on: ubuntu-latest
+ backport:
permissions:
- contents: write
- pull-requests: write
- if: github.event.pull_request.state == 'closed' && github.event.pull_request.merged && (github.event_name != 'labeled' || startsWith('backport:', github.event.label.name))
- steps:
- - name: Checkout
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0
- with:
- ref: ${{ github.event.pull_request.head.sha }}
- - name: Create backport PRs
- uses: korthout/backport-action@bd68141f079bd036e45ea8149bc9d174d5a04703 # v1.4.0
- # xref: https://github.com/korthout/backport-action#inputs
- with:
- # Use token to allow workflows to be triggered for the created PR
- github_token: ${{ secrets.BOT_GITHUB_TOKEN }}
- # Match labels with a pattern `backport:`
- label_pattern: '^backport:([^ ]+)$'
- # A bit shorter pull-request title than the default
- pull_title: '[${target_branch}] ${pull_title}'
- # Simpler PR description than default
- pull_description: |-
- Automated backport to `${target_branch}`, triggered by a label in #${pull_number}.
+ contents: write # for reading and creating branches.
+ pull-requests: write # for creating pull requests against release branches.
+ uses: fluxcd/gha-workflows/.github/workflows/backport.yaml@v0.9.0
+ secrets:
+ github-token: ${{ secrets.BOT_GITHUB_TOKEN }}
diff --git a/.github/workflows/cifuzz.yaml b/.github/workflows/cifuzz.yaml
index 27be6ca67..ac96de1af 100644
--- a/.github/workflows/cifuzz.yaml
+++ b/.github/workflows/cifuzz.yaml
@@ -4,22 +4,15 @@ on:
branches:
- "main"
- "release/**"
-
-permissions:
- contents: read # for actions/checkout to fetch code
-
jobs:
smoketest:
runs-on: ubuntu-latest
+ permissions:
+ contents: read # for reading the repository code.
steps:
- - name: Checkout
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0
- - name: Setup Go
- uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0
- with:
- go-version: 1.20.x
- cache-dependency-path: |
- **/go.sum
- **/go.mod
- - name: Smoke test Fuzzers
- run: make fuzz-smoketest
+ - name: Test suite setup
+ uses: fluxcd/gha-workflows/.github/actions/setup-kubernetes@v0.9.0
+ with:
+ go-version: 1.26.x
+ - name: Smoke test Fuzzers
+ run: make fuzz-smoketest
diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml
index 4eb0e822e..f3b338d56 100644
--- a/.github/workflows/e2e.yaml
+++ b/.github/workflows/e2e.yaml
@@ -6,71 +6,25 @@ on:
branches:
- "main"
- "release/**"
-
-permissions:
- contents: read # for actions/checkout to fetch code
-
jobs:
kind:
runs-on: ubuntu-latest
+ permissions:
+ contents: read # for reading the repository code.
steps:
- - name: Checkout
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0
- - name: Setup QEMU
- uses: docker/setup-qemu-action@2b82ce82d56a2a04d2637cd93a637ae1b359c0a7 # v2.2.0
- - name: Setup Docker Buildx
- id: buildx
- uses: docker/setup-buildx-action@885d1462b80bc1c1c7f0b00334ad271f09369c55 # v2.10.0
- with:
- buildkitd-flags: "--debug"
- - name: Cache Docker layers
- uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 # v3.3.2
- id: cache
- with:
- path: /tmp/.buildx-cache
- key: ${{ runner.os }}-buildx-ghcache-${{ github.sha }}
- restore-keys: |
- ${{ runner.os }}-buildx-ghcache-
- - name: Setup Go
- uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0
+ - name: Test suite setup
+ uses: fluxcd/gha-workflows/.github/actions/setup-kubernetes@v0.9.0
with:
- go-version: 1.20.x
- cache-dependency-path: |
- **/go.sum
- **/go.mod
- - name: Setup Kubernetes
- uses: helm/kind-action@dda0770415bac9fc20092cacbc54aa298604d140 # v1.8.0
- with:
- version: v0.20.0
- cluster_name: kind
- node_image: kindest/node:v1.27.3@sha256:3966ac761ae0136263ffdb6cfd4db23ef8a83cba8a463690e98317add2c9ba72
- - name: Setup Helm
- uses: fluxcd/pkg/actions/helm@main
- - name: Setup Kustomize
- uses: fluxcd/pkg/actions/kustomize@main
+ go-version: 1.26.x
- name: Run tests
run: make test
- name: Check if working tree is dirty
- run: |
- if [[ $(git diff --stat) != '' ]]; then
- git --no-pager diff
- echo 'run make test and commit changes'
- exit 1
- fi
+ run: make verify
- name: Build container image
run: |
make docker-build IMG=test/helm-controller:latest \
BUILD_PLATFORMS=linux/amd64 \
- BUILD_ARGS="--cache-from=type=local,src=/tmp/.buildx-cache \
- --cache-to=type=local,dest=/tmp/.buildx-cache-new,mode=max \
- --load"
- - # Temp fix
- # https://github.com/docker/build-push-action/issues/252
- # https://github.com/moby/buildkit/issues/1896
- name: Move cache
- run: |
- rm -rf /tmp/.buildx-cache
- mv /tmp/.buildx-cache-new /tmp/.buildx-cache
+ BUILD_ARGS="--load"
- name: Load test image
run: kind load docker-image test/helm-controller:latest
- name: Install CRDs
@@ -91,6 +45,14 @@ jobs:
kubectl -n helm-system rollout status deploy/helm-controller --timeout=1m
env:
KUBEBUILDER_ASSETS: ${{ github.workspace }}/kubebuilder/bin
+ - name: Test samples
+ run: |
+ kubectl create ns samples
+ kubectl -n samples apply -f config/samples
+ kubectl -n samples wait hr/podinfo-ocirepository --for=condition=ready --timeout=4m
+ kubectl -n samples wait hr/podinfo-gitrepository --for=condition=ready --timeout=4m
+ kubectl -n samples wait hr/podinfo-helmrepository --for=condition=ready --timeout=4m
+ kubectl delete ns samples
- name: Install sources
run: |
kubectl -n helm-system apply -f config/testdata/sources
@@ -98,9 +60,593 @@ jobs:
run: |
kubectl -n helm-system apply -f config/testdata/podinfo
kubectl -n helm-system wait helmreleases/podinfo --for=condition=ready --timeout=4m
+
+ # Inventory tracking enables drift detection and garbage collection.
+ # Ensure it captures managed objects from the Helm release.
+ INVENTORY=$(kubectl -n helm-system get helmrelease/podinfo -o jsonpath='{.status.inventory.entries}')
+ INVENTORY_COUNT=$(echo "$INVENTORY" | jq 'length')
+ if [ "$INVENTORY_COUNT" -lt 1 ]; then
+ echo "Expected inventory entries, got $INVENTORY_COUNT"
+ exit 1
+ fi
+ # Deployment is a primary workload resource; its presence confirms
+ # that the inventory correctly tracks resources from the rendered manifests.
+ if ! echo "$INVENTORY" | jq -e '.[] | select(.id | contains("_Deployment"))' > /dev/null; then
+ echo "Expected Deployment in inventory"
+ echo "Inventory: $INVENTORY"
+ exit 1
+ fi
+
kubectl -n helm-system wait helmreleases/podinfo-git --for=condition=ready --timeout=4m
kubectl -n helm-system wait helmreleases/podinfo-oci --for=condition=ready --timeout=4m
kubectl -n helm-system delete -f config/testdata/podinfo
+ - name: Run Job with TTL test
+ run: |
+ # This test verifies that the wait logic correctly handles Jobs with
+ # ttlSecondsAfterFinished that get garbage-collected after completion.
+ # Without the fix, the wait would fail with NotFound error.
+ kubectl -n helm-system apply -f config/testdata/job-ttl
+ kubectl -n helm-system wait helmreleases/job-ttl --for=condition=ready --timeout=4m
+ kubectl -n helm-system delete -f config/testdata/job-ttl
+ - name: Run client-side apply upgrade test
+ run: |
+ set -euo pipefail
+
+ test_name=no-server-side-apply
+ deploy_name=$test_name-podinfo
+
+ kubectl -n helm-system apply -f config/testdata/server-side-apply/no-ssa.yaml
+ kubectl -n helm-system wait helmreleases/$test_name --for=condition=ready --timeout=4m
+
+ # Capture managed fields before upgrade.
+ echo ">>> Checking managed fields after install"
+ MANAGED_FIELDS_BEFORE=$(kubectl -n helm-system get deployment $deploy_name --show-managed-fields -o jsonpath='{.metadata.managedFields}')
+
+ # Ensure we got managed fields data.
+ FIELD_COUNT_BEFORE=$(echo "$MANAGED_FIELDS_BEFORE" | jq 'length')
+ if [ "$FIELD_COUNT_BEFORE" -lt 1 ]; then
+ echo "ERROR: No managed fields found on deployment"
+ exit 1
+ fi
+ echo "Found $FIELD_COUNT_BEFORE managed field entries"
+
+ # Show all managers and their operations.
+ echo "Managers before upgrade:"
+ echo "$MANAGED_FIELDS_BEFORE" | jq -r '.[] | " - \(.manager): \(.operation)"'
+
+ # Verify helm-controller used Update (not Apply) after install.
+ HELM_UPDATE_COUNT_BEFORE=$(echo "$MANAGED_FIELDS_BEFORE" | jq '[.[] | select(.manager | test("helm")) | select(.operation == "Update")] | length')
+ HELM_APPLY_COUNT_BEFORE=$(echo "$MANAGED_FIELDS_BEFORE" | jq '[.[] | select(.manager | test("helm")) | select(.operation == "Apply")] | length')
+ echo "helm-controller operations: Update=$HELM_UPDATE_COUNT_BEFORE, Apply=$HELM_APPLY_COUNT_BEFORE"
+
+ if [ "$HELM_UPDATE_COUNT_BEFORE" -lt 1 ]; then
+ echo "ERROR: Expected helm-controller to use Update operation"
+ exit 1
+ fi
+ if [ "$HELM_APPLY_COUNT_BEFORE" != "0" ]; then
+ echo "ERROR: Unexpected Apply operation from helm-controller"
+ exit 1
+ fi
+ echo "PASS: Install used CSA (Update operation)"
+
+ # Trigger upgrade by changing values.
+ echo ">>> Triggering upgrade via patch (expecting CSA)"
+ TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
+ kubectl -n helm-system patch helmrelease/$test_name --type=merge -p '{"spec":{"values":{"podAnnotations":{"upgrade-timestamp":"'$TIMESTAMP'"}}}}'
+
+ # Wait for the upgrade to complete (revision count should be 2).
+ echo -n ">>> Waiting for upgrade"
+ count=0
+ until [ '2' == "$(helm -n helm-system history -o json $test_name 2>/dev/null | jq 'length')" ]; do
+ echo -n '.'
+ sleep 5
+ count=$((count + 1))
+ if [[ ${count} -eq 24 ]]; then
+ echo ' No more retries left!'
+ exit 1
+ fi
+ done
+ echo ' done'
+
+ kubectl -n helm-system wait helmreleases/$test_name --for=condition=ready --timeout=4m
+
+ # Verify the pod annotation was applied.
+ POD_ANNOTATION=$(kubectl -n helm-system get deployment $deploy_name -o jsonpath='{.spec.template.metadata.annotations.upgrade-timestamp}')
+ echo "Pod annotation upgrade-timestamp: $POD_ANNOTATION"
+ if [ "$POD_ANNOTATION" != "$TIMESTAMP" ]; then
+ echo "ERROR: Pod annotation not updated, upgrade may not have occurred"
+ exit 1
+ fi
+
+ # Capture managed fields after upgrade.
+ echo ">>> Checking managed fields after upgrade"
+ MANAGED_FIELDS_AFTER=$(kubectl -n helm-system get deployment $deploy_name --show-managed-fields -o jsonpath='{.metadata.managedFields}')
+
+ # Show all managers and their operations.
+ echo "Managers after upgrade:"
+ echo "$MANAGED_FIELDS_AFTER" | jq -r '.[] | " - \(.manager): \(.operation)"'
+
+ # Verify helm-controller used Update (not Apply) after upgrade.
+ HELM_UPDATE_COUNT_AFTER=$(echo "$MANAGED_FIELDS_AFTER" | jq '[.[] | select(.manager | test("helm")) | select(.operation == "Update")] | length')
+ HELM_APPLY_COUNT_AFTER=$(echo "$MANAGED_FIELDS_AFTER" | jq '[.[] | select(.manager | test("helm")) | select(.operation == "Apply")] | length')
+ echo "helm-controller operations: Update=$HELM_UPDATE_COUNT_AFTER, Apply=$HELM_APPLY_COUNT_AFTER"
+
+ if [ "$HELM_UPDATE_COUNT_AFTER" -lt 1 ]; then
+ echo "ERROR: Expected helm-controller to use Update operation"
+ exit 1
+ fi
+ if [ "$HELM_APPLY_COUNT_AFTER" != "0" ]; then
+ echo "ERROR: Unexpected Apply operation from helm-controller"
+ exit 1
+ fi
+ echo "PASS: Upgrade used CSA (Update operation)"
+
+ kubectl -n helm-system delete -f config/testdata/server-side-apply/no-ssa.yaml
+ - name: Run SSA install with CSA upgrade test
+ run: |
+ set -euo pipefail
+
+ test_name=ssa-install-no-ssa-upgrade
+ deploy_name=$test_name-podinfo
+
+ kubectl -n helm-system apply -f config/testdata/server-side-apply/$test_name.yaml
+ kubectl -n helm-system wait helmreleases/$test_name --for=condition=ready --timeout=4m
+
+ # Capture managed fields after install.
+ echo ">>> Checking managed fields after install (expecting SSA)"
+ MANAGED_FIELDS_INSTALL=$(kubectl -n helm-system get deployment $deploy_name --show-managed-fields -o jsonpath='{.metadata.managedFields}')
+
+ FIELD_COUNT=$(echo "$MANAGED_FIELDS_INSTALL" | jq 'length')
+ if [ "$FIELD_COUNT" -lt 1 ]; then
+ echo "ERROR: No managed fields found on deployment"
+ exit 1
+ fi
+ echo "Found $FIELD_COUNT managed field entries"
+
+ echo "Managers after install:"
+ echo "$MANAGED_FIELDS_INSTALL" | jq -r '.[] | " - \(.manager): \(.operation)"'
+
+ # Show fields managed by helm-controller.
+ echo "Fields managed by helm-controller:"
+ echo "$MANAGED_FIELDS_INSTALL" | jq -r '.[] | select(.manager | test("helm")) | "\(.operation): \([.fieldsV1 | paths(type == "object" and length == 0)] | length) fields"'
+
+ # Verify helm-controller used Apply (SSA) after install.
+ HELM_APPLY_COUNT=$(echo "$MANAGED_FIELDS_INSTALL" | jq '[.[] | select(.manager | test("helm")) | select(.operation == "Apply")] | length')
+ HELM_UPDATE_COUNT=$(echo "$MANAGED_FIELDS_INSTALL" | jq '[.[] | select(.manager | test("helm")) | select(.operation == "Update")] | length')
+ echo "helm-controller operations: Apply=$HELM_APPLY_COUNT, Update=$HELM_UPDATE_COUNT"
+
+ if [ "$HELM_APPLY_COUNT" -lt 1 ]; then
+ echo "ERROR: Expected helm-controller to use Apply operation for SSA install"
+ exit 1
+ fi
+ echo "PASS: Install used SSA (Apply operation)"
+
+ # Trigger upgrade by changing values.
+ echo ">>> Triggering upgrade via patch (expecting CSA)"
+ TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
+ kubectl -n helm-system patch helmrelease/$test_name --type=merge -p '{"spec":{"values":{"podAnnotations":{"upgrade-timestamp":"'$TIMESTAMP'"}}}}'
+
+ # Wait for the upgrade to complete.
+ echo -n ">>> Waiting for upgrade"
+ count=0
+ until [ '2' == "$(helm -n helm-system history -o json $test_name 2>/dev/null | jq 'length')" ]; do
+ echo -n '.'
+ sleep 5
+ count=$((count + 1))
+ if [[ ${count} -eq 24 ]]; then
+ echo ' No more retries left!'
+ echo "DEBUG: Helm history:"
+ helm -n helm-system history $test_name || true
+ echo "DEBUG: HelmRelease status:"
+ kubectl -n helm-system get helmrelease/$test_name -o jsonpath='{.status}' | jq . || true
+ exit 1
+ fi
+ done
+ echo ' done'
+
+ kubectl -n helm-system wait helmreleases/$test_name --for=condition=ready --timeout=4m
+
+ # Verify the pod annotation was applied.
+ POD_ANNOTATION=$(kubectl -n helm-system get deployment $deploy_name -o jsonpath='{.spec.template.metadata.annotations.upgrade-timestamp}')
+ echo "Pod annotation upgrade-timestamp: $POD_ANNOTATION"
+ if [ "$POD_ANNOTATION" != "$TIMESTAMP" ]; then
+ echo "ERROR: Pod annotation not updated, upgrade may not have occurred"
+ exit 1
+ fi
+
+ # Capture managed fields after upgrade.
+ echo ">>> Checking managed fields after upgrade (expecting CSA)"
+ MANAGED_FIELDS_UPGRADE=$(kubectl -n helm-system get deployment $deploy_name --show-managed-fields -o jsonpath='{.metadata.managedFields}')
+
+ echo "Managers after upgrade:"
+ echo "$MANAGED_FIELDS_UPGRADE" | jq -r '.[] | " - \(.manager): \(.operation)"'
+
+ # Show fields managed by each helm-controller entry.
+ echo "Fields managed by helm-controller:"
+ echo "$MANAGED_FIELDS_UPGRADE" | jq -r '.[] | select(.manager | test("helm")) | if .operation == "Apply" then "\(.operation): \([.fieldsV1 | paths(type == "object" and length == 0)] | length) fields" else "\(.operation): \([.fieldsV1 | paths(type == "object" and length == 0) | map(tostring) | join(".")] | sort | join(", "))" end'
+
+ # Verify helm-controller used Update (CSA) after upgrade.
+ HELM_APPLY_COUNT=$(echo "$MANAGED_FIELDS_UPGRADE" | jq '[.[] | select(.manager | test("helm")) | select(.operation == "Apply")] | length')
+ HELM_UPDATE_COUNT=$(echo "$MANAGED_FIELDS_UPGRADE" | jq '[.[] | select(.manager | test("helm")) | select(.operation == "Update")] | length')
+ echo "helm-controller operations: Apply=$HELM_APPLY_COUNT, Update=$HELM_UPDATE_COUNT"
+
+ if [ "$HELM_UPDATE_COUNT" -lt 1 ]; then
+ echo "ERROR: Expected helm-controller to use Update operation for CSA upgrade"
+ exit 1
+ fi
+ echo "PASS: Upgrade used CSA (Update operation)"
+
+ kubectl -n helm-system delete -f config/testdata/server-side-apply/$test_name.yaml
+ - name: Run CSA install with SSA upgrade test
+ run: |
+ set -euo pipefail
+
+ test_name=no-ssa-install-ssa-upgrade
+ deploy_name=$test_name-podinfo
+
+ kubectl -n helm-system apply -f config/testdata/server-side-apply/$test_name.yaml
+ kubectl -n helm-system wait helmreleases/$test_name --for=condition=ready --timeout=4m
+
+ # Capture managed fields after install.
+ echo ">>> Checking managed fields after install (expecting CSA)"
+ MANAGED_FIELDS_INSTALL=$(kubectl -n helm-system get deployment $deploy_name --show-managed-fields -o jsonpath='{.metadata.managedFields}')
+
+ FIELD_COUNT=$(echo "$MANAGED_FIELDS_INSTALL" | jq 'length')
+ if [ "$FIELD_COUNT" -lt 1 ]; then
+ echo "ERROR: No managed fields found on deployment"
+ exit 1
+ fi
+ echo "Found $FIELD_COUNT managed field entries"
+
+ echo "Managers after install:"
+ echo "$MANAGED_FIELDS_INSTALL" | jq -r '.[] | " - \(.manager): \(.operation)"'
+
+ # Show fields managed by helm-controller.
+ echo "Fields managed by helm-controller:"
+ echo "$MANAGED_FIELDS_INSTALL" | jq -r '.[] | select(.manager | test("helm")) | "\(.operation): \([.fieldsV1 | paths(type == "object" and length == 0)] | length) fields"'
+
+ # Verify helm-controller used Update (CSA) after install.
+ HELM_APPLY_COUNT=$(echo "$MANAGED_FIELDS_INSTALL" | jq '[.[] | select(.manager | test("helm")) | select(.operation == "Apply")] | length')
+ HELM_UPDATE_COUNT=$(echo "$MANAGED_FIELDS_INSTALL" | jq '[.[] | select(.manager | test("helm")) | select(.operation == "Update")] | length')
+ echo "helm-controller operations: Apply=$HELM_APPLY_COUNT, Update=$HELM_UPDATE_COUNT"
+
+ if [ "$HELM_UPDATE_COUNT" -lt 1 ]; then
+ echo "ERROR: Expected helm-controller to use Update operation for CSA install"
+ exit 1
+ fi
+ if [ "$HELM_APPLY_COUNT" != "0" ]; then
+ echo "ERROR: Unexpected Apply operation from helm-controller"
+ exit 1
+ fi
+ echo "PASS: Install used CSA (Update operation)"
+
+ # Trigger upgrade by changing values.
+ echo ">>> Triggering upgrade via patch (expecting SSA)"
+ TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
+ kubectl -n helm-system patch helmrelease/$test_name --type=merge -p '{"spec":{"values":{"podAnnotations":{"upgrade-timestamp":"'$TIMESTAMP'"}}}}'
+
+ # Wait for the upgrade to complete.
+ echo -n ">>> Waiting for upgrade"
+ count=0
+ until [ '2' == "$(helm -n helm-system history -o json $test_name 2>/dev/null | jq 'length')" ]; do
+ echo -n '.'
+ sleep 5
+ count=$((count + 1))
+ if [[ ${count} -eq 24 ]]; then
+ echo ' No more retries left!'
+ exit 1
+ fi
+ done
+ echo ' done'
+
+ kubectl -n helm-system wait helmreleases/$test_name --for=condition=ready --timeout=4m
+
+ # Verify the pod annotation was applied.
+ POD_ANNOTATION=$(kubectl -n helm-system get deployment $deploy_name -o jsonpath='{.spec.template.metadata.annotations.upgrade-timestamp}')
+ echo "Pod annotation upgrade-timestamp: $POD_ANNOTATION"
+ if [ "$POD_ANNOTATION" != "$TIMESTAMP" ]; then
+ echo "ERROR: Pod annotation not updated, upgrade may not have occurred"
+ exit 1
+ fi
+
+ # Capture managed fields after upgrade.
+ echo ">>> Checking managed fields after upgrade (expecting SSA)"
+ MANAGED_FIELDS_UPGRADE=$(kubectl -n helm-system get deployment $deploy_name --show-managed-fields -o jsonpath='{.metadata.managedFields}')
+
+ echo "Managers after upgrade:"
+ echo "$MANAGED_FIELDS_UPGRADE" | jq -r '.[] | " - \(.manager): \(.operation)"'
+
+ # Show fields managed by helm-controller.
+ echo "Fields managed by helm-controller:"
+ echo "$MANAGED_FIELDS_UPGRADE" | jq -r '.[] | select(.manager | test("helm")) | if .operation == "Apply" then "\(.operation): \([.fieldsV1 | paths(type == "object" and length == 0)] | length) fields" else "\(.operation): \([.fieldsV1 | paths(type == "object" and length == 0) | map(tostring) | join(".")] | sort | join(", "))" end'
+
+ # Verify helm-controller used Apply (SSA) after upgrade.
+ HELM_APPLY_COUNT=$(echo "$MANAGED_FIELDS_UPGRADE" | jq '[.[] | select(.manager | test("helm")) | select(.operation == "Apply")] | length')
+ HELM_UPDATE_COUNT=$(echo "$MANAGED_FIELDS_UPGRADE" | jq '[.[] | select(.manager | test("helm")) | select(.operation == "Update")] | length')
+ echo "helm-controller operations: Apply=$HELM_APPLY_COUNT, Update=$HELM_UPDATE_COUNT"
+
+ if [ "$HELM_APPLY_COUNT" -lt 1 ]; then
+ echo "ERROR: Expected helm-controller to use Apply operation for SSA upgrade"
+ exit 1
+ fi
+ echo "PASS: Upgrade used SSA (Apply operation)"
+
+ kubectl -n helm-system delete -f config/testdata/server-side-apply/$test_name.yaml
+ - name: Run SSA to CSA field removal test
+ run: |
+ set -euo pipefail
+
+ # This test verifies the behavior when switching from SSA to CSA and
+ # removing a field from values. The field WILL be removed because
+ # Helm tracks field ownership and removes fields that are no longer
+ # in the rendered manifests, regardless of the apply method.
+
+ test_name=ssa-to-csa-field-removal
+ deploy_name=$test_name-podinfo
+
+ kubectl -n helm-system apply -f config/testdata/server-side-apply/$test_name.yaml
+ kubectl -n helm-system wait helmreleases/$test_name --for=condition=ready --timeout=4m
+
+ # Verify install used SSA.
+ echo ">>> Checking managed fields after install (expecting SSA)"
+ MANAGED_FIELDS_INSTALL=$(kubectl -n helm-system get deployment $deploy_name --show-managed-fields -o jsonpath='{.metadata.managedFields}')
+
+ FIELD_COUNT=$(echo "$MANAGED_FIELDS_INSTALL" | jq 'length')
+ if [ "$FIELD_COUNT" -lt 1 ]; then
+ echo "ERROR: No managed fields found on deployment"
+ exit 1
+ fi
+ echo "Found $FIELD_COUNT managed field entries"
+
+ echo "Managers after install:"
+ echo "$MANAGED_FIELDS_INSTALL" | jq -r '.[] | " - \(.manager): \(.operation)"'
+
+ # Show fields managed by helm-controller.
+ echo "Fields managed by helm-controller:"
+ HELM_FIELD_COUNT_BEFORE=$(echo "$MANAGED_FIELDS_INSTALL" | jq '[.[] | select(.manager | test("helm")) | [.fieldsV1 | paths(type == "object" and length == 0)] | length] | add')
+ echo "$MANAGED_FIELDS_INSTALL" | jq -r '.[] | select(.manager | test("helm")) | "\(.operation): \([.fieldsV1 | paths(type == "object" and length == 0)] | length) fields"'
+
+ HELM_APPLY_COUNT=$(echo "$MANAGED_FIELDS_INSTALL" | jq '[.[] | select(.manager | test("helm")) | select(.operation == "Apply")] | length')
+ HELM_UPDATE_COUNT=$(echo "$MANAGED_FIELDS_INSTALL" | jq '[.[] | select(.manager | test("helm")) | select(.operation == "Update")] | length')
+ echo "helm-controller operations: Apply=$HELM_APPLY_COUNT, Update=$HELM_UPDATE_COUNT"
+
+ if [ "$HELM_APPLY_COUNT" -lt 1 ]; then
+ echo "ERROR: Expected helm-controller to use Apply operation for SSA install"
+ exit 1
+ fi
+ echo "PASS: Install used SSA (Apply operation)"
+
+ # Verify the SSA-owned annotation is present after install.
+ echo ">>> Verifying SSA-owned annotation after install"
+ SSA_ANNOTATION=$(kubectl -n helm-system get deployment $deploy_name -o jsonpath='{.spec.template.metadata.annotations.ssa-owned-field}')
+ echo "Pod annotation ssa-owned-field: $SSA_ANNOTATION"
+ if [ "$SSA_ANNOTATION" != "this-should-persist-after-csa-upgrade" ]; then
+ echo "ERROR: Expected ssa-owned-field annotation to be present after install"
+ exit 1
+ fi
+ echo "PASS: SSA-owned annotation present after install"
+
+ # Trigger upgrade by REMOVING the podAnnotations entirely.
+ # This simulates someone removing a field from their HelmRelease values.
+ echo ">>> Triggering upgrade by removing podAnnotations from values"
+ kubectl -n helm-system patch helmrelease/$test_name --type=merge -p '{"spec":{"values":{"podAnnotations":null}}}'
+
+ # Wait for the upgrade to complete.
+ echo -n ">>> Waiting for upgrade"
+ count=0
+ until [ '2' == "$(helm -n helm-system history -o json $test_name 2>/dev/null | jq 'length')" ]; do
+ echo -n '.'
+ sleep 5
+ count=$((count + 1))
+ if [[ ${count} -eq 24 ]]; then
+ echo ' No more retries left!'
+ echo "DEBUG: Helm history:"
+ helm -n helm-system history $test_name || true
+ echo "DEBUG: HelmRelease status:"
+ kubectl -n helm-system get helmrelease/$test_name -o jsonpath='{.status}' | jq . || true
+ exit 1
+ fi
+ done
+ echo ' done'
+
+ kubectl -n helm-system wait helmreleases/$test_name --for=condition=ready --timeout=4m
+
+ # Check managed fields after upgrade.
+ echo ">>> Checking managed fields after upgrade"
+ MANAGED_FIELDS_UPGRADE=$(kubectl -n helm-system get deployment $deploy_name --show-managed-fields -o jsonpath='{.metadata.managedFields}')
+
+ echo "Managers after upgrade:"
+ echo "$MANAGED_FIELDS_UPGRADE" | jq -r '.[] | " - \(.manager): \(.operation)"'
+
+ # Show fields managed by helm-controller.
+ echo "Fields managed by helm-controller:"
+ HELM_FIELD_COUNT_AFTER=$(echo "$MANAGED_FIELDS_UPGRADE" | jq '[.[] | select(.manager | test("helm")) | [.fieldsV1 | paths(type == "object" and length == 0)] | length] | add')
+ echo "$MANAGED_FIELDS_UPGRADE" | jq -r '.[] | select(.manager | test("helm")) | "\(.operation): \([.fieldsV1 | paths(type == "object" and length == 0)] | length) fields"'
+
+ HELM_APPLY_COUNT=$(echo "$MANAGED_FIELDS_UPGRADE" | jq '[.[] | select(.manager | test("helm")) | select(.operation == "Apply")] | length')
+ HELM_UPDATE_COUNT=$(echo "$MANAGED_FIELDS_UPGRADE" | jq '[.[] | select(.manager | test("helm")) | select(.operation == "Update")] | length')
+ echo "helm-controller operations: Apply=$HELM_APPLY_COUNT, Update=$HELM_UPDATE_COUNT"
+
+ # Verify field count decreased (annotation was removed).
+ echo ">>> Verifying field count decreased"
+ echo "Field count before: $HELM_FIELD_COUNT_BEFORE"
+ echo "Field count after: $HELM_FIELD_COUNT_AFTER"
+ if [ "$HELM_FIELD_COUNT_AFTER" -ge "$HELM_FIELD_COUNT_BEFORE" ]; then
+ echo "ERROR: Expected field count to decrease after removing podAnnotations"
+ exit 1
+ fi
+ echo "PASS: Field count decreased from $HELM_FIELD_COUNT_BEFORE to $HELM_FIELD_COUNT_AFTER"
+
+ # THIS IS THE KEY ASSERTION: The field should be REMOVED because Helm
+ # tracks field ownership and removes fields no longer in the manifests.
+ echo ">>> Verifying SSA-owned field was removed after upgrade"
+ SSA_ANNOTATION_AFTER=$(kubectl -n helm-system get deployment $deploy_name -o jsonpath='{.spec.template.metadata.annotations.ssa-owned-field}')
+ echo "Pod annotation ssa-owned-field after upgrade: '$SSA_ANNOTATION_AFTER'"
+
+ if [ -n "$SSA_ANNOTATION_AFTER" ]; then
+ echo "FAIL: SSA-owned field was NOT removed after upgrade"
+ echo "Expected the field to be removed when podAnnotations is removed from values."
+ exit 1
+ fi
+
+ echo "PASS: SSA-owned field was removed after upgrade"
+ echo ""
+ echo "This confirms that removing a field from HelmRelease values WILL"
+ echo "remove it from the actual object, even when switching from SSA to CSA."
+ echo "Helm properly tracks field ownership and cleans up removed fields."
+
+ kubectl -n helm-system delete -f config/testdata/server-side-apply/$test_name.yaml
+ - name: Run server-side apply test
+ run: |
+ set -euo pipefail
+
+ test_name=server-side-apply
+ deploy_name=$test_name-podinfo
+
+ kubectl -n helm-system apply -f config/testdata/$test_name/install.yaml
+ kubectl -n helm-system wait helmreleases/$test_name --for=condition=ready --timeout=4m
+
+ # Verify the release is deployed with SSA via Helm secret.
+ echo ">>> Checking Helm release secret after install"
+ APPLY_METHOD=$(kubectl -n helm-system get secret sh.helm.release.v1.$test_name.v1 -o jsonpath='{.data.release}' | base64 -d | base64 -d | gunzip | jq -r '.apply_method')
+ echo "Helm release apply_method: $APPLY_METHOD"
+ if [ "$APPLY_METHOD" != "ssa" ]; then
+ echo "ERROR: Unexpected apply method: $APPLY_METHOD (expected: ssa)"
+ exit 1
+ fi
+
+ # Verify SSA via managed fields on deployment.
+ echo ">>> Checking managed fields after install"
+ MANAGED_FIELDS=$(kubectl -n helm-system get deployment $deploy_name --show-managed-fields -o jsonpath='{.metadata.managedFields}')
+ echo "Managers after install:"
+ echo "$MANAGED_FIELDS" | jq -r '.[] | " - \(.manager): \(.operation)"'
+
+ HELM_APPLY_COUNT=$(echo "$MANAGED_FIELDS" | jq '[.[] | select(.manager | test("helm")) | select(.operation == "Apply")] | length')
+ echo "helm-controller Apply operations: $HELM_APPLY_COUNT"
+ if [ "$HELM_APPLY_COUNT" -lt 1 ]; then
+ echo "ERROR: Expected helm-controller to use Apply operation for SSA"
+ exit 1
+ fi
+ echo "PASS: Install used SSA (Apply operation)"
+
+ # Upgrade with SSA.
+ echo ">>> Applying upgrade manifest"
+ kubectl -n helm-system apply -f config/testdata/$test_name/upgrade.yaml
+ kubectl -n helm-system wait helmreleases/$test_name --for=condition=ready --timeout=4m
+
+ # Validate release was upgraded.
+ REVISION_COUNT=$(helm -n helm-system history -o json $test_name | jq 'length')
+ echo "Helm revision count: $REVISION_COUNT"
+ if [ "$REVISION_COUNT" != 2 ]; then
+ echo "ERROR: Unexpected revision count: $REVISION_COUNT (expected: 2)"
+ exit 1
+ fi
+
+ # Verify SSA via managed fields after upgrade.
+ echo ">>> Checking managed fields after upgrade"
+ MANAGED_FIELDS=$(kubectl -n helm-system get deployment $deploy_name --show-managed-fields -o jsonpath='{.metadata.managedFields}')
+ echo "Managers after upgrade:"
+ echo "$MANAGED_FIELDS" | jq -r '.[] | " - \(.manager): \(.operation)"'
+
+ HELM_APPLY_COUNT=$(echo "$MANAGED_FIELDS" | jq '[.[] | select(.manager | test("helm")) | select(.operation == "Apply")] | length')
+ echo "helm-controller Apply operations: $HELM_APPLY_COUNT"
+ if [ "$HELM_APPLY_COUNT" -lt 1 ]; then
+ echo "ERROR: Expected helm-controller to use Apply operation for SSA"
+ exit 1
+ fi
+ echo "PASS: Upgrade used SSA (Apply operation)"
+
+ kubectl -n helm-system delete -f config/testdata/$test_name/install.yaml
+ - name: Run server-side apply rollback test
+ run: |
+ set -euo pipefail
+
+ test_name=server-side-apply-rollback
+ deploy_name=$test_name-podinfo
+
+ kubectl -n helm-system apply -f config/testdata/server-side-apply/rollback-install.yaml
+ kubectl -n helm-system wait helmreleases/$test_name --for=condition=ready --timeout=4m
+
+ # Verify the release is deployed with SSA via Helm secret.
+ echo ">>> Checking Helm release secret after install"
+ APPLY_METHOD=$(kubectl -n helm-system get secret sh.helm.release.v1.$test_name.v1 -o jsonpath='{.data.release}' | base64 -d | base64 -d | gunzip | jq -r '.apply_method')
+ echo "Helm release apply_method: $APPLY_METHOD"
+ if [ "$APPLY_METHOD" != "ssa" ]; then
+ echo "ERROR: Unexpected apply method: $APPLY_METHOD (expected: ssa)"
+ exit 1
+ fi
+
+ # Verify SSA via managed fields on deployment.
+ echo ">>> Checking managed fields after install"
+ MANAGED_FIELDS=$(kubectl -n helm-system get deployment $deploy_name --show-managed-fields -o jsonpath='{.metadata.managedFields}')
+ echo "Managers after install:"
+ echo "$MANAGED_FIELDS" | jq -r '.[] | " - \(.manager): \(.operation)"'
+
+ HELM_APPLY_COUNT=$(echo "$MANAGED_FIELDS" | jq '[.[] | select(.manager | test("helm")) | select(.operation == "Apply")] | length')
+ echo "helm-controller Apply operations: $HELM_APPLY_COUNT"
+ if [ "$HELM_APPLY_COUNT" -lt 1 ]; then
+ echo "ERROR: Expected helm-controller to use Apply operation for SSA"
+ exit 1
+ fi
+ echo "PASS: Install used SSA (Apply operation)"
+
+ # Upgrade with failing config to trigger rollback.
+ echo ">>> Applying failing upgrade to trigger rollback"
+ kubectl -n helm-system apply -f config/testdata/server-side-apply/rollback-upgrade.yaml
+ echo -n ">>> Waiting for rollback"
+ count=0
+ until [ 'true' == "$( kubectl -n helm-system get helmrelease/$test_name -o json | jq '.status.conditions | map( { (.type): .status } ) | add | .Released=="False" and .Ready=="False" and .Remediated=="True"' )" ]; do
+ echo -n '.'
+ sleep 5
+ count=$((count + 1))
+ if [[ ${count} -eq 24 ]]; then
+ echo ' No more retries left!'
+ exit 1
+ fi
+ done
+ echo ' done'
+
+ # Validate rollback happened (revision 3 = rollback to 1).
+ echo ">>> Checking Helm history after rollback"
+ HISTORY=$(helm -n helm-system history -o json $test_name)
+ REVISION_COUNT=$(echo "$HISTORY" | jq 'length')
+ echo "Helm revision count: $REVISION_COUNT"
+ if [ "$REVISION_COUNT" != 3 ]; then
+ echo "ERROR: Unexpected revision count: $REVISION_COUNT (expected: 3)"
+ exit 1
+ fi
+ LAST_REVISION_DESCRIPTION=$(echo "$HISTORY" | jq -r 'last | .description')
+ echo "Last revision description: $LAST_REVISION_DESCRIPTION"
+ if [ "$LAST_REVISION_DESCRIPTION" != "Rollback to 1" ]; then
+ echo "ERROR: Unexpected last revision description: $LAST_REVISION_DESCRIPTION (expected: Rollback to 1)"
+ exit 1
+ fi
+ echo "PASS: Rollback occurred (revision 3 = Rollback to 1)"
+
+ # Verify the rollback release used SSA via Helm secret.
+ echo ">>> Checking Helm release secret after rollback"
+ APPLY_METHOD=$(kubectl -n helm-system get secret sh.helm.release.v1.$test_name.v3 -o jsonpath='{.data.release}' | base64 -d | base64 -d | gunzip | jq -r '.apply_method')
+ echo "Helm release apply_method: $APPLY_METHOD"
+ if [ "$APPLY_METHOD" != "ssa" ]; then
+ echo "ERROR: Unexpected apply method after rollback: $APPLY_METHOD (expected: ssa)"
+ exit 1
+ fi
+
+ # Verify SSA via managed fields after rollback.
+ echo ">>> Checking managed fields after rollback"
+ MANAGED_FIELDS=$(kubectl -n helm-system get deployment $deploy_name --show-managed-fields -o jsonpath='{.metadata.managedFields}')
+ echo "Managers after rollback:"
+ echo "$MANAGED_FIELDS" | jq -r '.[] | " - \(.manager): \(.operation)"'
+
+ HELM_APPLY_COUNT=$(echo "$MANAGED_FIELDS" | jq '[.[] | select(.manager | test("helm")) | select(.operation == "Apply")] | length')
+ echo "helm-controller Apply operations: $HELM_APPLY_COUNT"
+ if [ "$HELM_APPLY_COUNT" -lt 1 ]; then
+ echo "ERROR: Expected helm-controller to use Apply operation for SSA"
+ exit 1
+ fi
+ echo "PASS: Rollback used SSA (Apply operation)"
+
+ kubectl -n helm-system delete -f config/testdata/server-side-apply/rollback-install.yaml
- name: Run dependency tests
run: |
kubectl -n helm-system apply -f config/testdata/dependencies
@@ -138,6 +684,16 @@ jobs:
kubectl -n install-create-target-ns get deployment install-create-target-ns-install-create-target-ns-podinfo
kubectl -n helm-system delete -f config/testdata/install-create-target-ns
+ - name: Run install from helmChart test
+ run: |
+ kubectl -n helm-system apply -f config/testdata/install-from-hc-source
+ kubectl -n helm-system wait helmreleases/podinfo-from-hc --for=condition=ready --timeout=4m
+ kubectl -n helm-system delete -f config/testdata/install-from-hc-source
+ - name: Run install from ocirepo test
+ run: |
+ kubectl -n helm-system apply -f config/testdata/install-from-ocirepo-source
+ kubectl -n helm-system wait helmreleases/podinfo-from-ocirepo --for=condition=ready --timeout=4m
+ kubectl -n helm-system delete -f config/testdata/install-from-ocirepo-source
- name: Run install fail test
run: |
test_name=install-fail
@@ -169,7 +725,7 @@ jobs:
kubectl -n helm-system apply -f config/testdata/$test_name
echo -n ">>> Waiting for expected conditions"
count=0
- until [ 'true' == "$( kubectl -n helm-system get helmrelease/$test_name -o json | jq '.status.conditions | map( { (.type): .status } ) | add | .Released=="False" and .TestSuccess=="False" and .Ready=="False"' )" ]; do
+ until [ 'true' == "$( kubectl -n helm-system get helmrelease/$test_name -o json | jq '.status.conditions | map( { (.type): .status } ) | add | .Released=="True" and .TestSuccess=="False" and .Ready=="False"' )" ]; do
echo -n '.'
sleep 5
count=$((count + 1))
@@ -213,7 +769,7 @@ jobs:
fi
kubectl -n helm-system delete -f config/testdata/$test_name
- - name: Run install fail with remedition test
+ - name: Run install fail with remediation test
run: |
test_name=install-fail-remediate
kubectl -n helm-system apply -f config/testdata/$test_name
@@ -230,21 +786,22 @@ jobs:
done
echo ' done'
- # Ensure release does not exist (was uninstalled).
- HISTORY=$(helm -n helm-system history $test_name 2>&1; exit 0)
- if [ "$HISTORY" != 'Error: release: not found' ]; then
- echo -e "Unexpected release history: $HISTORY"
+ # Ensure release was uninstalled.
+ RELEASE_STATUS=$(helm -n helm-system history $test_name -o json | jq -r 'if length == 1 then .[0].status else empty end')
+ if [ "$RELEASE_STATUS" != "uninstalled" ]; then
+ echo -e "Unexpected release status: $RELEASE_STATUS"
exit 1
fi
kubectl -n helm-system delete -f config/testdata/$test_name
+ helm -n helm-system delete $test_name
- name: Run install fail with retry test
run: |
test_name=install-fail-retry
kubectl -n helm-system apply -f config/testdata/$test_name
echo -n ">>> Waiting for expected conditions"
count=0
- until [ 'true' == "$( kubectl -n helm-system get helmrelease/$test_name -o json | jq '.status.installFailures == 2 and ( .status.conditions | map( { (.type): .status } ) | add | .Released=="False" and .Ready=="False" )' )" ]; do
+ until [ 'true' == "$( kubectl -n helm-system get helmrelease/$test_name -o json | jq '.status.installFailures == 2 and ( .status.conditions | map( { (.type): .status } ) | add | .Released=="False" and .Ready=="False" and .Stalled=="True" )' )" ]; do
echo -n '.'
sleep 5
count=$((count + 1))
@@ -290,7 +847,7 @@ jobs:
kubectl -n helm-system apply -f config/testdata/$test_name/upgrade.yaml
echo -n ">>> Waiting for expected conditions"
count=0
- until [ 'true' == "$( kubectl -n helm-system get helmrelease/$test_name -o json | jq '.status.conditions | map( { (.type): .status } ) | add | .Released=="False" and .Ready=="False"' )" ]; do
+ until [ 'true' == "$( kubectl -n helm-system get helmrelease/$test_name -o json | jq '.status.conditions | map( { (.type): .status } ) | add | .Released=="False" and .Ready=="False" and .Stalled=="True"' )" ]; do
echo -n '.'
sleep 5
count=$((count + 1))
@@ -336,7 +893,7 @@ jobs:
kubectl -n helm-system apply -f config/testdata/$test_name/upgrade.yaml
echo -n ">>> Waiting for expected conditions"
count=0
- until [ 'true' == "$( kubectl -n helm-system get helmrelease/$test_name -o json | jq '.status.conditions | map( { (.type): .status } ) | add | .Released=="False" and .TestSuccess=="False" and .Ready=="False"' )" ]; do
+ until [ 'true' == "$( kubectl -n helm-system get helmrelease/$test_name -o json | jq '.status.conditions | map( { (.type): .status } ) | add | .Released=="True" and .TestSuccess=="False" and .Ready=="False" and .Stalled=="True"' )" ]; do
echo -n '.'
sleep 5
count=$((count + 1))
@@ -457,6 +1014,45 @@ jobs:
fi
kubectl delete -n helm-system -f config/testdata/$test_name/install.yaml
+ - name: Run upgrade from ocirepo source
+ run: |
+ test_name=upgrade-from-ocirepo-source
+ kubectl -n helm-system apply -f config/testdata/$test_name/install.yaml
+ echo -n ">>> Waiting for expected conditions"
+ count=0
+ until [ 'true' == "$( kubectl -n helm-system get helmrelease/$test_name -o json | jq '.status.conditions | map( { (.type): .status } ) | add | .Released=="True" and .Ready=="True"' )" ]; do
+ echo -n '.'
+ sleep 5
+ count=$((count + 1))
+ if [[ ${count} -eq 24 ]]; then
+ echo ' No more retries left!'
+ exit 1
+ fi
+ done
+ echo ' done'
+
+ # Validate release was installed.
+ REVISION_COUNT=$(helm -n helm-system history -o json $test_name | jq 'length')
+ if [ "$REVISION_COUNT" != 1 ]; then
+ echo -e "Unexpected revision count: $REVISION_COUNT"
+ exit 1
+ fi
+
+ kubectl -n helm-system apply -f config/testdata/$test_name/upgrade.yaml
+ echo -n ">>> Waiting for expected conditions"
+ count=0
+ until [ 'true' == "$( kubectl -n helm-system get helmrelease/$test_name -o json | jq '.status.conditions | map( { (.type): .status } ) | add | .Released=="True" and .Ready=="True"' )" ]; do
+ echo -n '.'
+ sleep 5
+ count=$((count + 1))
+ if [[ ${count} -eq 24 ]]; then
+ echo ' No more retries left!'
+ exit 1
+ fi
+ done
+ echo ' done'
+
+ kubectl delete -n helm-system -f config/testdata/$test_name/install.yaml
- name: Run upgrade fail with uninstall remediation strategy test
run: |
test_name=upgrade-fail-remediate-uninstall
@@ -537,10 +1133,11 @@ jobs:
kubectl -n delete-ns wait helmreleases/podinfo --for=condition=ready --timeout=2m
kubectl delete ns delete-ns 1>/dev/null 2>&1 &
echo -n ">>> Waiting for namespace to be deleted"
- if kubectl wait --for=delete namespace delete-ns --timeout=3m; then
+ if kubectl wait --for=delete namespace delete-ns --timeout=5m; then
echo ' Namespace deleted successfully'
else
echo ' Timed out waiting for namespace to be deleted'
+ kubectl get all -n delete-ns
exit 1
fi
- name: Run post-renderer-kustomize test
@@ -558,37 +1155,40 @@ jobs:
exit 1
fi
kubectl -n helm-system delete -f config/testdata/post-renderer-kustomize
- - name: Boostrap CRDs Upgrade Tests
- if: ${{ startsWith(github.ref, 'refs/tags/') || startsWith(github.ref, 'refs/heads/') }}
+ - name: Bootstrap Tests Using Local Helm Chart
+ if: ${{ startsWith(github.ref, 'refs/tags/') || startsWith(github.ref, 'refs/heads/') || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) }}
run: |
- REF=${{ github.ref }}
- if echo "$REF" | grep 'refs/tags/'; then
- TYPE=tag
- REF=${REF#refs/tags/}
- else
+ if [ "${{ github.event_name }}" = "pull_request" ]; then
TYPE=branch
- if echo "$REF" | grep 'refs/pull/'; then
- REF=${REF#refs/pull/}
+ REF="${{ github.head_ref }}"
+ else
+ REF=${{ github.ref }}
+ if echo "$REF" | grep 'refs/tags/'; then
+ TYPE=tag
+ REF=${REF#refs/tags/}
else
+ TYPE=branch
REF=${REF#refs/heads/}
fi
fi
- echo "$HEAD_REF,$CURR_REF -> $REF of type $TYPE"
+ echo "REF=$REF of type $TYPE"
echo "helm install --namespace default --set $TYPE=$REF --set url=https://github.com/${{ github.repository }} this config/testdata/charts/crds/bootstrap"
helm install --namespace default --set $TYPE=$REF --set url=https://github.com/${{ github.repository }} this config/testdata/charts/crds/bootstrap
kubectl -n default apply -f config/testdata/crds-upgrade/init
kubectl -n default wait helmreleases/crds-upgrade-test --for=condition=ready --timeout=2m
- name: CRDs Upgrade Test Create
- if: ${{ startsWith(github.ref, 'refs/tags/') || startsWith(github.ref, 'refs/heads/') }}
+ if: ${{ startsWith(github.ref, 'refs/tags/') || startsWith(github.ref, 'refs/heads/') || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) }}
run: |
kubectl -n default apply -f config/testdata/crds-upgrade/create
kubectl -n default wait helmreleases/crds-upgrade-test --for=condition=ready --timeout=2m
- name: CRDs Upgrade Test CreateReplace
- if: ${{ startsWith(github.ref, 'refs/tags/') || startsWith(github.ref, 'refs/heads/') }}
+ if: ${{ startsWith(github.ref, 'refs/tags/') || startsWith(github.ref, 'refs/heads/') || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) }}
run: |
kubectl -n default apply -f config/testdata/crds-upgrade/create-replace
kubectl -n default wait helmreleases/crds-upgrade-test --for=condition=ready --timeout=2m
- name: Logs
+ if: always()
+ continue-on-error: true
run: |
kubectl -n helm-system logs deploy/source-controller
kubectl -n helm-system logs deploy/helm-controller
@@ -600,5 +1200,3 @@ jobs:
kubectl -n helm-system get helmreleases -oyaml || true
kubectl -n helm-system get all
helm ls -n helm-system --all
- kubectl -n helm-system logs deploy/source-controller || true
- kubectl -n helm-system logs deploy/helm-controller || true
diff --git a/.github/workflows/nightly.yaml b/.github/workflows/nightly.yaml
deleted file mode 100644
index 4a83abc6b..000000000
--- a/.github/workflows/nightly.yaml
+++ /dev/null
@@ -1,35 +0,0 @@
-name: nightly
-on:
- schedule:
- - cron: '0 0 * * *'
- workflow_dispatch:
-
-permissions:
- contents: read # for actions/checkout to fetch code
-
-env:
- REPOSITORY: ${{ github.repository }}
-
-jobs:
- build:
- runs-on: ubuntu-latest
- steps:
- - name: Checkout
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0
- - name: Setup QEMU
- uses: docker/setup-qemu-action@2b82ce82d56a2a04d2637cd93a637ae1b359c0a7 # v2.2.0
- - name: Setup Docker Buildx
- id: buildx
- uses: docker/setup-buildx-action@885d1462b80bc1c1c7f0b00334ad271f09369c55 # v2.10.0
- with:
- buildkitd-flags: "--debug"
- - name: Build multi-arch container image
- uses: docker/build-push-action@0a97817b6ade9f46837855d676c4cca3a2471fc9 # v4.2.1
- with:
- push: false
- builder: ${{ steps.buildx.outputs.name }}
- context: .
- file: ./Dockerfile
- platforms: linux/amd64,linux/arm/v7,linux/arm64
- tags: |
- ${{ env.REPOSITORY }}:nightly
diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml
new file mode 100644
index 000000000..8812bb661
--- /dev/null
+++ b/.github/workflows/release.yaml
@@ -0,0 +1,66 @@
+name: release
+on:
+ push:
+ tags:
+ - 'v*'
+ workflow_dispatch:
+ inputs:
+ tag:
+ description: 'image tag prefix'
+ default: 'rc'
+ required: true
+jobs:
+ release:
+ permissions:
+ contents: write # for creating the GitHub release.
+ id-token: write # for creating OIDC tokens for signing.
+ packages: write # for pushing and signing container images.
+ uses: fluxcd/gha-workflows/.github/workflows/controller-release.yaml@v0.9.0
+ with:
+ controller: ${{ github.event.repository.name }}
+ release-candidate-prefix: ${{ github.event.inputs.tag }}
+ secrets:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ dockerhub-token: ${{ secrets.DOCKER_FLUXCD_PASSWORD }}
+ release-provenance:
+ needs: [release]
+ permissions:
+ actions: read # for detecting the Github Actions environment.
+ id-token: write # for creating OIDC tokens for signing.
+ contents: write # for uploading attestations to GitHub releases.
+ if: startsWith(github.ref, 'refs/tags/v')
+ uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.1.0
+ with:
+ provenance-name: "provenance.intoto.jsonl"
+ base64-subjects: "${{ needs.release.outputs.release-digests }}"
+ upload-assets: true
+ dockerhub-provenance:
+ needs: [release]
+ permissions:
+ contents: read # for reading the repository code.
+ actions: read # for detecting the Github Actions environment.
+ id-token: write # for creating OIDC tokens for signing.
+ packages: write # for uploading attestations.
+ if: startsWith(github.ref, 'refs/tags/v')
+ uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v2.1.0
+ with:
+ image: ${{ needs.release.outputs.image-name }}
+ digest: ${{ needs.release.outputs.image-digest }}
+ registry-username: ${{ github.repository_owner == 'fluxcd' && 'fluxcdbot' || github.repository_owner }}
+ secrets:
+ registry-password: ${{ secrets.DOCKER_FLUXCD_PASSWORD }}
+ ghcr-provenance:
+ needs: [release]
+ permissions:
+ contents: read # for reading the repository code.
+ actions: read # for detecting the Github Actions environment.
+ id-token: write # for creating OIDC tokens for signing.
+ packages: write # for uploading attestations.
+ if: startsWith(github.ref, 'refs/tags/v')
+ uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v2.1.0
+ with:
+ image: ghcr.io/${{ needs.release.outputs.image-name }}
+ digest: ${{ needs.release.outputs.image-digest }}
+ registry-username: fluxcdbot # not necessary for ghcr.io
+ secrets:
+ registry-password: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
deleted file mode 100644
index 69df8dc91..000000000
--- a/.github/workflows/release.yml
+++ /dev/null
@@ -1,160 +0,0 @@
-name: release
-on:
- push:
- tags:
- - 'v*'
- workflow_dispatch:
- inputs:
- tag:
- description: 'image tag prefix'
- default: 'preview'
- required: true
-
-permissions:
- contents: read
-
-env:
- CONTROLLER: ${{ github.event.repository.name }}
-
-jobs:
- release:
- outputs:
- hashes: ${{ steps.slsa.outputs.hashes }}
- image_url: ${{ steps.slsa.outputs.image_url }}
- image_digest: ${{ steps.slsa.outputs.image_digest }}
- runs-on: ubuntu-latest
- permissions:
- contents: write # for creating the GitHub release.
- id-token: write # for creating OIDC tokens for signing.
- packages: write # for pushing and signing container images.
- steps:
- - name: Checkout
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0
- - name: Setup Kustomize
- uses: fluxcd/pkg/actions/kustomize@main
- - name: Prepare
- id: prep
- run: |
- VERSION="${{ github.event.inputs.tag }}-${GITHUB_SHA::8}"
- if [[ $GITHUB_REF == refs/tags/* ]]; then
- VERSION=${GITHUB_REF/refs\/tags\//}
- fi
- echo "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_OUTPUT
- echo "VERSION=${VERSION}" >> $GITHUB_OUTPUT
- - name: Setup QEMU
- uses: docker/setup-qemu-action@2b82ce82d56a2a04d2637cd93a637ae1b359c0a7 # v2.2.0
- - name: Setup Docker Buildx
- id: buildx
- uses: docker/setup-buildx-action@885d1462b80bc1c1c7f0b00334ad271f09369c55 # v2.10.0
- - name: Login to GitHub Container Registry
- uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc # v2.2.0
- with:
- registry: ghcr.io
- username: fluxcdbot
- password: ${{ secrets.GHCR_TOKEN }}
- - name: Login to Docker Hub
- uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc # v2.2.0
- with:
- username: fluxcdbot
- password: ${{ secrets.DOCKER_FLUXCD_PASSWORD }}
- - name: Generate images meta
- id: meta
- uses: docker/metadata-action@818d4b7b91585d195f67373fd9cb0332e31a7175 # v4.6.0
- with:
- images: |
- fluxcd/${{ env.CONTROLLER }}
- ghcr.io/fluxcd/${{ env.CONTROLLER }}
- tags: |
- type=raw,value=${{ steps.prep.outputs.VERSION }}
- - name: Publish images
- id: build-push
- uses: docker/build-push-action@0a97817b6ade9f46837855d676c4cca3a2471fc9 # v4.2.1
- with:
- sbom: true
- provenance: true
- push: true
- builder: ${{ steps.buildx.outputs.name }}
- context: .
- file: ./Dockerfile
- platforms: linux/amd64,linux/arm/v7,linux/arm64
- tags: ${{ steps.meta.outputs.tags }}
- labels: ${{ steps.meta.outputs.labels }}
- - uses: sigstore/cosign-installer@11086d25041f77fe8fe7b9ea4e48e3b9192b8f19 # v3.1.2
- - name: Sign images
- env:
- COSIGN_EXPERIMENTAL: 1
- run: |
- cosign sign --yes fluxcd/${{ env.CONTROLLER }}@${{ steps.build-push.outputs.digest }}
- cosign sign --yes ghcr.io/fluxcd/${{ env.CONTROLLER }}@${{ steps.build-push.outputs.digest }}
- - name: Generate release artifacts
- if: startsWith(github.ref, 'refs/tags/v')
- run: |
- mkdir -p config/release
- kustomize build ./config/crd > ./config/release/${{ env.CONTROLLER }}.crds.yaml
- kustomize build ./config/manager > ./config/release/${{ env.CONTROLLER }}.deployment.yaml
- - uses: anchore/sbom-action/download-syft@78fc58e266e87a38d4194b2137a3d4e9bcaf7ca1 # v0.14.3
- - name: Create release and SBOM
- id: run-goreleaser
- if: startsWith(github.ref, 'refs/tags/v')
- uses: goreleaser/goreleaser-action@5fdedb94abba051217030cc86d4523cf3f02243d # v4.6.0
- with:
- version: latest
- args: release --clean --skip-validate
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- - name: Generate SLSA metadata
- id: slsa
- env:
- ARTIFACTS: "${{ steps.run-goreleaser.outputs.artifacts }}"
- run: |
- hashes=$(echo -E $ARTIFACTS | jq --raw-output '.[] | {name, "digest": (.extra.Digest // .extra.Checksum)} | select(.digest) | {digest} + {name} | join(" ") | sub("^sha256:";"")' | base64 -w0)
- echo "hashes=$hashes" >> $GITHUB_OUTPUT
-
- image_url=fluxcd/${{ env.CONTROLLER }}:${{ steps.prep.outputs.version }}
- echo "image_url=$image_url" >> $GITHUB_OUTPUT
-
- image_digest=${{ steps.build-push.outputs.digest }}
- echo "image_digest=$image_digest" >> $GITHUB_OUTPUT
-
- release-provenance:
- needs: [release]
- permissions:
- actions: read # for detecting the Github Actions environment.
- id-token: write # for creating OIDC tokens for signing.
- contents: write # for uploading attestations to GitHub releases.
- if: startsWith(github.ref, 'refs/tags/v')
- uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v1.9.0
- with:
- provenance-name: "provenance.intoto.jsonl"
- base64-subjects: "${{ needs.release.outputs.hashes }}"
- upload-assets: true
-
- dockerhub-provenance:
- needs: [release]
- permissions:
- actions: read # for detecting the Github Actions environment.
- id-token: write # for creating OIDC tokens for signing.
- packages: write # for uploading attestations.
- if: startsWith(github.ref, 'refs/tags/v')
- uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v1.9.0
- with:
- image: ${{ needs.release.outputs.image_url }}
- digest: ${{ needs.release.outputs.image_digest }}
- registry-username: fluxcdbot
- secrets:
- registry-password: ${{ secrets.DOCKER_FLUXCD_PASSWORD }}
-
- ghcr-provenance:
- needs: [release]
- permissions:
- actions: read # for detecting the Github Actions environment.
- id-token: write # for creating OIDC tokens for signing.
- packages: write # for uploading attestations.
- if: startsWith(github.ref, 'refs/tags/v')
- uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v1.9.0
- with:
- image: ghcr.io/${{ needs.release.outputs.image_url }}
- digest: ${{ needs.release.outputs.image_digest }}
- registry-username: fluxcdbot
- secrets:
- registry-password: ${{ secrets.GHCR_TOKEN }}
diff --git a/.github/workflows/scan.yaml b/.github/workflows/scan.yaml
index cdec5e2cd..2a7f7eef8 100644
--- a/.github/workflows/scan.yaml
+++ b/.github/workflows/scan.yaml
@@ -1,51 +1,17 @@
name: scan
on:
push:
- branches: [ "main", "release/**" ]
+ branches: [ main ]
pull_request:
- branches: [ "main", "release/**" ]
+ branches: [ main ]
schedule:
- cron: '18 10 * * 3'
-
-permissions:
- contents: read # for actions/checkout to fetch code
- security-events: write # for codeQL to write security events
-
jobs:
- fossa:
- name: FOSSA
- runs-on: ubuntu-latest
- steps:
- - name: Checkout
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0
- - name: Run FOSSA scan and upload build data
- uses: fossa-contrib/fossa-action@6728dc6fe9a068c648d080c33829ffbe56565023 # v2.0.0
- with:
- # FOSSA Push-Only API Token
- fossa-api-key: 5ee8bf422db1471e0bcf2bcb289185de
- github-token: ${{ github.token }}
-
- codeql:
- name: CodeQL
- runs-on: ubuntu-latest
- steps:
- - name: Checkout
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0
- - name: Setup Go
- uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0
- with:
- go-version: 1.20.x
- cache-dependency-path: |
- **/go.sum
- **/go.mod
- - name: Initialize CodeQL
- uses: github/codeql-action/init@00e563ead9f72a8461b24876bee2d0c2e8bd2ee8 # v2.21.5
- with:
- languages: go
- # xref: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
- # xref: https://codeql.github.com/codeql-query-help/go/
- queries: security-and-quality
- - name: Autobuild
- uses: github/codeql-action/autobuild@00e563ead9f72a8461b24876bee2d0c2e8bd2ee8 # v2.21.5
- - name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@00e563ead9f72a8461b24876bee2d0c2e8bd2ee8 # v2.21.5
+ analyze:
+ permissions:
+ contents: read # for reading the repository code.
+ security-events: write # for uploading the CodeQL analysis results.
+ uses: fluxcd/gha-workflows/.github/workflows/code-scan.yaml@v0.9.0
+ secrets:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ fossa-token: ${{ secrets.FOSSA_TOKEN }}
diff --git a/.github/workflows/sync-labels.yaml b/.github/workflows/sync-labels.yaml
index 171444689..5d48b6bf7 100644
--- a/.github/workflows/sync-labels.yaml
+++ b/.github/workflows/sync-labels.yaml
@@ -6,23 +6,11 @@ on:
- main
paths:
- .github/labels.yaml
-
-permissions:
- contents: read
-
jobs:
- labels:
- name: Run sync
- runs-on: ubuntu-latest
+ sync-labels:
permissions:
- issues: write
- steps:
- - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0
- - uses: EndBug/label-sync@da00f2c11fdb78e4fae44adac2fdd713778ea3e8 # v2.3.2
- with:
- # Configuration file
- config-file: |
- https://raw.githubusercontent.com/fluxcd/community/main/.github/standard-labels.yaml
- .github/labels.yaml
- # Strictly declarative
- delete-other-labels: true
+ contents: read # for reading the labels file.
+ issues: write # for creating and updating labels.
+ uses: fluxcd/gha-workflows/.github/workflows/labels-sync.yaml@v0.9.0
+ secrets:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/upgrade-fluxcd-pkg.yaml b/.github/workflows/upgrade-fluxcd-pkg.yaml
new file mode 100644
index 000000000..659fd30ad
--- /dev/null
+++ b/.github/workflows/upgrade-fluxcd-pkg.yaml
@@ -0,0 +1,10 @@
+name: upgrade-fluxcd-pkg
+
+on:
+ workflow_dispatch:
+
+jobs:
+ upgrade-fluxcd-pkg:
+ uses: fluxcd/gha-workflows/.github/workflows/upgrade-fluxcd-pkg.yaml@v0.9.0
+ secrets:
+ github-token: ${{ secrets.BOT_GITHUB_TOKEN }}
diff --git a/.goreleaser.yaml b/.goreleaser.yaml
index e665fc862..681698929 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
@@ -23,7 +23,7 @@ release:
To verify the images and their provenance (SLSA level 3), please see the [security documentation](https://fluxcd.io/flux/security/).
changelog:
- skip: true
+ disable: true
checksum:
extra_files:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3cc0421e2..2dcc2b9d9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,902 @@
# Changelog
+## 1.5.3
+
+**Release date:** 2026-03-16
+
+This patch release fixes templating errors for charts that include
+`---` in the content, e.g. YAML separators, embedded scripts, CAs
+inside ConfigMaps, etc. Some of the errors that could be encountered
+due to this issue are:
+
+- `invalid document separator: ---apiVersion: v1`
+- `wrong node kind`
+
+Fixes:
+- Fix multi-doc parser of `---` for post renderers
+ [#1442](https://github.com/fluxcd/helm-controller/pull/1442)
+
+## 1.5.2
+
+**Release date:** 2026-03-12
+
+This patch release fixes reconciliation queue behavior for source watch events
+while a HelmRelease is already reconciling the watched revision. It also comes
+with Helm 4.1.3, which fixes a Go templates bug where the YAML document separator
+`---` could be concatenated to `apiVersion` as `---apiVersion`, and introduces
+the `DefaultToRetryOnFailure` feature gate to improve the experience when
+`CancelHealthCheckOnNewRevision` is enabled by ensuring canceled HelmReleases
+do not get stuck when no retry strategy is configured.
+
+Fixes:
+- Fix enqueing the same revision while reconciling
+ [#1430](https://github.com/fluxcd/helm-controller/pull/1430)
+
+Improvements:
+- Introduce `DefaultToRetryOnFailure` feature gate
+ [#1431](https://github.com/fluxcd/helm-controller/pull/1431)
+- Update fluxcd/pkg dependencies
+ [#1436](https://github.com/fluxcd/helm-controller/pull/1436)
+
+## 1.5.1
+
+**Release date:** 2026-02-27
+
+This patch release fixes health check logic for StatefulSets during
+rolling updates when the Pods are Pending/Unschedulable.
+
+Fixes:
+- Fix health check logic for StatefulSets during rolling updates
+ [#1424](https://github.com/fluxcd/helm-controller/pull/1424)
+
+Improvements:
+- Add note about MTTR only for kstatus
+ [#1416](https://github.com/fluxcd/helm-controller/pull/1416)
+- Remove no longer needed workaround for Flux 2.8
+ [#1420](https://github.com/fluxcd/helm-controller/pull/1420)
+
+## 1.5.0
+
+**Release date:** 2026-02-20
+
+This minor release comes with Helm v4 support, server-side apply for
+Helm releases, and various bug fixes and improvements.
+
+⚠️ The `v2beta2` APIs were removed. Before upgrading the CRDs, Flux users
+must run [`flux migrate`](https://github.com/fluxcd/flux2/pull/5473) to
+migrate the cluster storage off `v2beta2`.
+
+### HelmRelease
+
+The controller now uses Helm v4, and, with this change, new default
+behaviors are being introduced (breaking changes) to keep Flux and
+Helm aligned:
+
+- Apply method is now defaulting to server-side apply for new HelmReleases.
+- Health checks now default to using kstatus for assessing readiness and
+failures of applied resources.
+
+Those defaults can be changed back to Helm v3's defaults by setting the
+feature gate `UseHelm3Defaults`. Alternatively, fine-tuning the apply
+and health check methods is also possible on a per-HelmRelease basis by
+using the following fields:
+
+- `.spec.install.serverSideApply` (boolean, default defined by `UseHelm3Defaults`)
+- `.spec.upgrade.serverSideApply` (`enabled`, `disabled` or `auto`, defaults to `auto`)
+- `.spec.rollback.serverSideApply` (`enabled`, `disabled` or `auto`, defaults to `auto`)
+- `.spec.waitStrategy.name` (`poller` or `legacy`, default defined by `UseHelm3Defaults`)
+
+Note that Helm persists the apply method in the release storage, hence
+why the `auto` value is an option for upgrade and rollback actions. When
+set to `auto`, the controller will reuse the apply method used in the last
+successful release revision as recorded in the Helm storage, defaulting
+to client-side apply. This means that existing HelmReleases will continue
+to use client-side apply until their `.spec` is updated with
+`.spec.{upgrade|rollback}.serverSideApply: enabled`.
+
+The `poller` health check strategy uses kstatus to check the status
+of applied resources, while the `legacy` strategy uses Helm v3's
+built-in health checking behavior.
+
+The controller now can be configured to cancel in-progress health checks when a new
+reconciliation request is received, reducing the mean time to recovery (MTTR) in case
+of failed deployments. This feature is enabled by the `CancelHealthCheckOnNewRevision`
+feature gate. Note that enabling this feature gate will not cancel apply operations,
+and will only cancel health checks for managed resources. Waiting for Helm hooks and
+tests will not be cancelled. Note also that this feature is only available with the
+`poller` health check strategy.
+
+Still on the health check subject, custom health checks via CEL expressions
+are now supported for HelmRelease via the `.spec.healthCheckExprs` field,
+similar to the Kustomization API. Please see the
+[CEL cheatsheet](https://fluxcd.io/flux/cheatsheets/cel-healthchecks/)
+for more information.
+
+The `--override-manager=` flag has been added for server-side apply drift
+detection and correction. This flag can be passed multiple times. Note that drift
+detection and correction in helm-controller is completely unrelated to Helm v4's
+server-side apply support, and was implemented long before Helm v4 was released.
+
+The `DirectSourceFetch` feature gate has been introduced for bypassing the cache
+when fetching source objects on reconciliations.
+
+For improved observability, inventory tracking has been added via
+`.status.inventory`. Hooks and tests are not tracked in this field.
+Only resources present in the Helm storage and CRDs are tracked.
+
+Also for improved observability, the controller now tracks the action (`install`,
+`upgrade`, `rollback`, `uninstall`, `uninstall-remediation`) in snapshots:
+`.status.history[].action`.
+
+### General updates
+
+In addition, the Kubernetes dependencies have been updated to v1.35.0,
+Kustomize has been updated to v5.8.1 and the controller is now built
+with Go 1.26.
+
+Fixes:
+- Fix state when configuration set back to current state following upgrade failure
+ [#1369](https://github.com/fluxcd/helm-controller/pull/1369)
+- Fix waiting and erroring out on garbage-collected Jobs
+ [#1402](https://github.com/fluxcd/helm-controller/pull/1402)
+- Fix controller not reconciling conditions for in-sync release
+ [#1411](https://github.com/fluxcd/helm-controller/pull/1411)
+- Fix postRenderers not causing new upgrade when applied during ongoing upgrade
+ [#1412](https://github.com/fluxcd/helm-controller/pull/1412)
+
+Improvements:
+- Upgrade Helm to v4
+ [#1383](https://github.com/fluxcd/helm-controller/pull/1383)
+ [#1403](https://github.com/fluxcd/helm-controller/pull/1403)
+- Add ServerSideApply field to HelmRelease API
+ [#1384](https://github.com/fluxcd/helm-controller/pull/1384)
+- Add `.status.inventory` to track managed objects
+ [#1385](https://github.com/fluxcd/helm-controller/pull/1385)
+- Add support for custom health checks via CEL expressions
+ [#1389](https://github.com/fluxcd/helm-controller/pull/1389)
+- Add `--override-manager` flag for server-side apply drift detection
+ [#1365](https://github.com/fluxcd/helm-controller/pull/1365)
+- Reduce the mean time to recovery (MTTR) in case of failed deployments
+ [#1392](https://github.com/fluxcd/helm-controller/pull/1392)
+- Track action in snapshots
+ [#1399](https://github.com/fluxcd/helm-controller/pull/1399)
+- Add `DirectSourceFetch` feature gate to bypass cache for source objects
+ [#1407](https://github.com/fluxcd/helm-controller/pull/1407)
+- Remove deprecated APIs in group `helm.toolkit.fluxcd.io/v2beta2`
+ [#1404](https://github.com/fluxcd/helm-controller/pull/1404)
+- Remove adoption of resources in old API versions
+ [#1396](https://github.com/fluxcd/helm-controller/pull/1396)
+- Remove duplicated struct json tag
+ [#1377](https://github.com/fluxcd/helm-controller/pull/1377)
+- Various dependency updates
+ [#1395](https://github.com/fluxcd/helm-controller/pull/1395)
+ [#1406](https://github.com/fluxcd/helm-controller/pull/1406)
+ [#1408](https://github.com/fluxcd/helm-controller/pull/1408)
+ [#1410](https://github.com/fluxcd/helm-controller/pull/1410)
+
+## 1.4.5
+
+**Release date:** 2025-11-27
+
+This patch release fixes the HelmRelease `.status.history`
+filling up etcd when the `RetryOnFailure` strategy is used.
+
+Fixes:
+- Fix history truncation logic for RetryOnFailure
+ [#1360](https://github.com/fluxcd/helm-controller/pull/1360)
+
+## 1.4.4
+
+**Release date:** 2025-11-19
+
+This patch release fixes the error `no URLLoader registered` and
+Azure Workload Identity in Azure China Cloud. It also adds a
+feature gate to disable the ConfigMap and Secret watchers,
+`DisableConfigWatchers`.
+
+Improvements:
+- Add feature gate for disabling config watchers
+ [#1353](https://github.com/fluxcd/helm-controller/pull/1353)
+- Upgrade k8s to 1.34.2, c-r to 0.22.4 and helm to 3.19.2
+ [#1350](https://github.com/fluxcd/helm-controller/pull/1350)
+- Upgrade Helm to 3.19.1
+ [#1346](https://github.com/fluxcd/helm-controller/pull/1346)
+
+## 1.4.3
+
+**Release date:** 2025-10-28
+
+This patch release comes with various fixes and improvements.
+
+Fixes:
+- Fix status reporting for RetryOnFailure strategy
+ [#1338](https://github.com/fluxcd/helm-controller/pull/1338)
+
+Improvements:
+- Allow fetching charts from a local source-watcher
+ [#1341](https://github.com/fluxcd/helm-controller/pull/1341)
+
+## 1.4.2
+
+**Release date:** 2025-10-08
+
+This patch release comes with various dependency updates.
+
+The controller is now built with Go 1.25.2 which includes
+fixes for vulnerabilities in the Go stdlib:
+[CVE-2025-58183](https://github.com/golang/go/issues/75677),
+[CVE-2025-58188](https://github.com/golang/go/issues/75675)
+and many others. The full list of security fixes can be found
+[here](https://groups.google.com/g/golang-announce/c/4Emdl2iQ_bI/m/qZN5nc-mBgAJ).
+
+Improvements:
+- Update dependencies to Kubernetes v1.34.1 and Go 1.25.2
+ [#1329](https://github.com/fluxcd/helm-controller/pull/1329)
+
+## 1.4.1
+
+**Release date:** 2025-10-06
+
+This patch release fixes the controller setting the `Ready`
+condition to `Unknown` redundantly during reconciliation.
+
+Fixes:
+- Remove redundant Ready condition setter
+ [#1323](https://github.com/fluxcd/helm-controller/pull/1323)
+- Fix docs example for kubeconfig workload identity
+ [#1315](https://github.com/fluxcd/helm-controller/pull/1315)
+
+## 1.4.0
+
+**Release date:** 2025-09-25
+
+This minor release comes with various bug fixes and improvements.
+
+⚠️ The `v2beta1` APIs were removed. Before upgrading the CRDs, Flux users
+must run [`flux migrate`](https://github.com/fluxcd/flux2/pull/5473) to
+migrate the cluster storage off `v2beta1`.
+
+The controller now supports ExternalArtifact Helm chart sources
+under the feature gate `ExternalArtifact`.
+
+A new `RetryOnFailure` strategy has been added for automatic
+retries on Helm release failures.
+
+Dependencies can now be evaluated using CEL expressions via the new
+`readyExpr` field, providing more flexible and powerful dependency
+readiness checks.
+
+Support for workload identity authentication has been added for remote clusters.
+This is supported both at the controller and object levels. For object-level,
+enable the feature gate `ObjectLevelWorkloadIdentity`.
+
+In addition, the Kubernetes dependencies have been updated to v1.34, Helm has
+been updated to v3.19 and various other controller dependencies have been
+updated to their latest version. The controller is now built with Go 1.25.
+
+Fixes:
+- Fix continuous drift due to unstable hashing of values
+ [#1267](https://github.com/fluxcd/helm-controller/pull/1267)
+- Fix watch index conflict between HelmChart and OCIRepository kinds
+ [#1266](https://github.com/fluxcd/helm-controller/pull/1266)
+- Fix requeue interval for SourceNotReady
+ [#1276](https://github.com/fluxcd/helm-controller/pull/1276)
+
+Improvements:
+- [RFC-0010] Add workload identity auth for remote clusters
+ [#1249](https://github.com/fluxcd/helm-controller/pull/1249)
+- [RFC-0010] Support all Azure clouds for remote clusters
+ [#1262](https://github.com/fluxcd/helm-controller/pull/1262)
+- [RFC-0010] Add multi-tenancy lockdown for kubeconfig
+ [#1284](https://github.com/fluxcd/helm-controller/pull/1284)
+- [RFC-0010] Add object-level configuration validation
+ [#1286](https://github.com/fluxcd/helm-controller/pull/1286)
+- [RFC-0012] Add ExternalArtifact feature gate and reconciliation support
+ [#1293](https://github.com/fluxcd/helm-controller/pull/1293)
+- [RFC-0012] Add support for ExternalArtifact revision with digest
+ [#1296](https://github.com/fluxcd/helm-controller/pull/1296)
+- Remove deprecated `helm.toolkit.fluxcd.io/v2beta1` API group
+ [#1280](https://github.com/fluxcd/helm-controller/pull/1280)
+- Add RetryOnFailure lifecycle management strategy
+ [#1281](https://github.com/fluxcd/helm-controller/pull/1281)
+- Add CEL expressions for dependency readiness checks with `readyExpr` field
+ [#1271](https://github.com/fluxcd/helm-controller/pull/1271)
+- Add label selector for watching ConfigMaps and Secrets
+ [#1258](https://github.com/fluxcd/helm-controller/pull/1258)
+- Add common labels and annotations support with Kustomize post-renderer
+ [#1223](https://github.com/fluxcd/helm-controller/pull/1223)
+- Record last Helm release action duration in status
+ [#1282](https://github.com/fluxcd/helm-controller/pull/1282)
+- CI improvements with fluxcd/gha-workflows
+ [#1305](https://github.com/fluxcd/helm-controller/pull/1305)
+ [#1307](https://github.com/fluxcd/helm-controller/pull/1307)
+- Various dependency updates
+ [#1304](https://github.com/fluxcd/helm-controller/pull/1304)
+ [#1247](https://github.com/fluxcd/helm-controller/pull/1247)
+ [#1297](https://github.com/fluxcd/helm-controller/pull/1297)
+
+## 1.3.0
+
+**Release date:** 2025-05-28
+
+This minor release comes with various bug fixes and improvements.
+
+The controller now supports the `DisableChartDigestTracking` feature gate,
+which allows disabling appending the digest of OCI Helm charts to the
+chart version. This is useful for charts that do not follow Helm's
+recommendation of using the app version instead of the chart version
+as a label in the manifests.
+
+In addition, the Kubernetes dependencies have been updated to v1.33, Helm has
+been updated to v3.17.3 and various other controller dependencies have been
+updated to their latest version. The controller is now built with Go 1.24.
+
+Fixes:
+- Fix returning wrong error value in Kubernetes HTTP client
+ [#1188](https://github.com/fluxcd/helm-controller/pull/1188)
+
+Improvements:
+- Add `DisableChartDigestTracking` feature gate
+ [#1212](https://github.com/fluxcd/helm-controller/pull/1212)
+- Various dependency updates
+ [#1227](https://github.com/fluxcd/helm-controller/pull/1227)
+ [#1221](https://github.com/fluxcd/helm-controller/pull/1221)
+ [#1220](https://github.com/fluxcd/helm-controller/pull/1220)
+ [#1218](https://github.com/fluxcd/helm-controller/pull/1218)
+ [#1206](https://github.com/fluxcd/helm-controller/pull/1206)
+ [#1209](https://github.com/fluxcd/helm-controller/pull/1209)
+ [#1204](https://github.com/fluxcd/helm-controller/pull/1204)
+
+## 1.2.0
+
+**Release date:** 2025-02-19
+
+This minor release comes with various bug fixes and improvements.
+
+In addition, the Kubernetes dependencies have been updated to v1.32.1, Helm has
+been updated to v3.17.1 and various other controller dependencies have been
+updated to their latest version.
+
+Fixes:
+- Replace _ with + when verifying the chart version matches the OCI artifact tag
+ [#1102](https://github.com/fluxcd/helm-controller/pull/1102)
+- fix: handle "leader changed" errors
+ [#1084](https://github.com/fluxcd/helm-controller/pull/1084)
+- Make `ValuesReference` an alias for backwards compat
+ [#1126](https://github.com/fluxcd/helm-controller/pull/1126)
+- Fix install and upgrade applying subchart CRDs when condition is false
+ [#1123](https://github.com/fluxcd/helm-controller/pull/1123)
+- fix: use HelmRelease max history for rollback remediation
+ [#1169](https://github.com/fluxcd/helm-controller/pull/1169)
+
+Improvements:
+- Refactor values composition to use pkg/chartutil
+ [#1122](https://github.com/fluxcd/helm-controller/pull/1122)
+- docs: Rendering the final Values locally
+ [#1127](https://github.com/fluxcd/helm-controller/pull/1127)
+- Add disableTakeOwnership to Helm install/upgrade actions
+ [#1140](https://github.com/fluxcd/helm-controller/pull/1140)
+- Various dependency updates
+ [#1103](https://github.com/fluxcd/helm-controller/pull/1103)
+ [#1121](https://github.com/fluxcd/helm-controller/pull/1121)
+ [#1129](https://github.com/fluxcd/helm-controller/pull/1129)
+ [#1142](https://github.com/fluxcd/helm-controller/pull/1142)
+ [#1160](https://github.com/fluxcd/helm-controller/pull/1160)
+ [#1158](https://github.com/fluxcd/helm-controller/pull/1158)
+ [#1165](https://github.com/fluxcd/helm-controller/pull/1165)
+ [#1168](https://github.com/fluxcd/helm-controller/pull/1168)
+ [#1171](https://github.com/fluxcd/helm-controller/pull/1171)
+ [#1167](https://github.com/fluxcd/helm-controller/pull/1167)
+ [#1173](https://github.com/fluxcd/helm-controller/pull/1173)
+ [#1170](https://github.com/fluxcd/helm-controller/pull/1170)
+
+## 1.1.0
+
+**Release date:** 2024-09-26
+
+This minor release comes with various bug fixes and improvements.
+
+The chart [values schema](https://helm.sh/docs/topics/charts/#schema-files)
+validation can now be disabled for install and upgrade actions by setting
+`disableSchemaValidation` under `.spec.install` and `.spec.upgrade` of a
+`HelmRelease` object.
+
+HelmReleases that result in failure during uninstall will now be retried until
+the uninstall succeeds without any error. See [handling failed
+uninstall](https://fluxcd.io/flux/components/helm/helmreleases/#handling-failed-uninstall)
+docs for various remediations based on the cause of the failure.
+
+helm-controller in [sharded
+deployment](https://fluxcd.io/flux/installation/configuration/sharding/)
+configuration now supports cross-shard dependency check. This allows a
+HelmRelease to depend on other HelmReleases managed by different controller
+shards.
+
+In addition, the Kubernetes dependencies have been updated to v1.31.1, Helm has
+been updated to v3.16.1 and various other controller dependencies have been
+updated to their latest version. The controller is now built with Go 1.23.
+
+Fixes:
+- fix: remove digest check to never ignore helm uninstall errors
+ [#1024](https://github.com/fluxcd/helm-controller/pull/1024)
+- Allow overwriting inline values with targetPath
+ [#1060](https://github.com/fluxcd/helm-controller/pull/1060)
+- Fix incorrect use of format strings with the conditions package
+ [#1025](https://github.com/fluxcd/helm-controller/pull/1025)
+- Re-enable logging json patch on StatusDrifted
+ [#1010](https://github.com/fluxcd/helm-controller/pull/1010)
+- Ignore 'v' version prefix in OCI artifact and Helm chart
+ [#990](https://github.com/fluxcd/helm-controller/pull/990)
+- doc: fix HelmRelease default value for .spec.upgrade.crds
+ [#986](https://github.com/fluxcd/helm-controller/pull/986)
+
+Improvements:
+- Allow cross-shard dependency check
+ [#1070](https://github.com/fluxcd/helm-controller/pull/1070)
+- Add disableSchemaValidation to Helm install/upgrade actions
+ [#1068](https://github.com/fluxcd/helm-controller/pull/1068)
+- Update Helm to v3.16.1 and enable the adoption of existing resources
+ [#1062](https://github.com/fluxcd/helm-controller/pull/1062)
+- Build with Go 1.23
+ [#1049](https://github.com/fluxcd/helm-controller/pull/1049)
+- Various dependency updates
+ [#987](https://github.com/fluxcd/helm-controller/pull/987)
+ [#991](https://github.com/fluxcd/helm-controller/pull/991)
+ [#994](https://github.com/fluxcd/helm-controller/pull/994)
+ [#1004](https://github.com/fluxcd/helm-controller/pull/1004)
+ [#1046](https://github.com/fluxcd/helm-controller/pull/1046)
+ [#1048](https://github.com/fluxcd/helm-controller/pull/1048)
+ [#1052](https://github.com/fluxcd/helm-controller/pull/1052)
+ [#1064](https://github.com/fluxcd/helm-controller/pull/1064)
+ [#1072](https://github.com/fluxcd/helm-controller/pull/1072)
+ [#1073](https://github.com/fluxcd/helm-controller/pull/1073)
+
+## 1.0.1
+
+**Release date:** 2024-05-10
+
+This patch release fixes a backwards compatibility issue that could occur when trying
+to move from the `v2beta1` to `v2` API while specifing `.spec.chartRef`.
+
+Fixes:
+- Fix: Allow upgrading from v2beta1 to v2 (GA)
+ [#982](https://github.com/fluxcd/helm-controller/pull/982)
+- Fix: Make HelmChartTemplate a pointer in .spec.chart
+ [#980](https://github.com/fluxcd/helm-controller/pull/980)
+
+## 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
+
+This prerelease comes with improvements in the HelmRelease status reporting.
+After recovering from a reconciliation failure, sometimes the status may show
+stale conditions which could be misleading. This has been fixed by ensuring that
+the stale failure conditions get updated after failure recovery.
+
+Improvements:
+- Remove stale Ready=False conditions value to show more accurate status
+ [#884](https://github.com/fluxcd/helm-controller/pull/884)
+- Dependency update
+ [#886](https://github.com/fluxcd/helm-controller/pull/886)
+
+## 0.37.3
+
+**Release date:** 2024-02-01
+
+This prerelease comes with an update to the Kubernetes dependencies to
+v1.28.6 and various other dependencies have been updated to their latest version
+to patch upstream CVEs.
+
+In addition, the controller is now built with Go 1.21.
+
+Improvements:
+- ci: Enable dependabot gomod updates
+ [#874](https://github.com/fluxcd/helm-controller/pull/874)
+- Update Go to 1.21
+ [#872](https://github.com/fluxcd/helm-controller/pull/872)
+- Various dependency updates
+ [#882](https://github.com/fluxcd/helm-controller/pull/882)
+ [#877](https://github.com/fluxcd/helm-controller/pull/877)
+ [#876](https://github.com/fluxcd/helm-controller/pull/876)
+ [#871](https://github.com/fluxcd/helm-controller/pull/871)
+ [#867](https://github.com/fluxcd/helm-controller/pull/867)
+ [#865](https://github.com/fluxcd/helm-controller/pull/865)
+ [#862](https://github.com/fluxcd/helm-controller/pull/862)
+ [#860](https://github.com/fluxcd/helm-controller/pull/860)
+
+## 0.37.2
+
+This prerelease fixes a bug that resulted in the controller not being able to
+properly watch HelmRelease resources with specific labels.
+
+Fixes:
+- Properly configure namespace selector
+ [#858](https://github.com/fluxcd/helm-controller/pull/858)
+
+Improvements:
+- build(deps): bump golang.org/x/crypto from 0.16.0 to 0.17.0
+ [#856](https://github.com/fluxcd/helm-controller/pull/856)
+
+## 0.37.1
+
+This prerelease fixes a backwards compatibility issue that could occur when
+trying to move from the `v2beta1` to `v2beta2` API while enabling drift
+detection.
+
+In addition, logging has been improved to provide faster feedback on any
+HTTP errors encountered while fetching HelmChart artifacts, and the controller
+will now set the `Stalled` condition as soon as it detects to be out of retries
+without having to wait for the next reconciliation.
+
+Lastly, Helm has been updated to v3.13.3.
+
+Fixes:
+- loader: allow overwrite of URL hostname again
+ [#844](https://github.com/fluxcd/helm-controller/pull/844)
+- api: ensure backwards compatibility v2beta1
+ [#851](https://github.com/fluxcd/helm-controller/pull/851)
+
+Improvements:
+- loader: log HTTP errors to provide faster feedback
+ [#845](https://github.com/fluxcd/helm-controller/pull/845)
+- Update runtime to v0.43.3
+ [#846](https://github.com/fluxcd/helm-controller/pull/846)
+- Early stall condition detection after remediation
+ [#848](https://github.com/fluxcd/helm-controller/pull/848)
+- Update Helm to v3.13.3
+ [#849](https://github.com/fluxcd/helm-controller/pull/849)
+
+## 0.37.0
+
+**Release date:** 2023-12-12
+
+This prerelease promotes the `HelmRelease` API from `v2beta1` to `v2beta2`.
+The promotion of the API is accompanied by a number of new features and bug
+fixes. Refer to the highlights section below for more information.
+
+In addition to the API promotion, this prerelease updates the controller
+dependencies to their latest versions. Making the controller compatible with
+Kubernetes v1.28.x, while updating the Helm library to v3.13.2, and the builtin
+version of Kustomize used for post-rendering to v5.3.0.
+
+Lastly, the base controller image has been updated to Alpine v3.19.
+
+### Highlights
+
+#### API changes
+
+The upgrade is backwards compatible, and the controller will continue to
+reconcile `HelmRelease` resources of the `v2beta1` API without requiring any
+changes. However, making use of the new features requires upgrading the API
+version.
+
+- Drift detection and correction is now enabled on a per-release basis using
+ the `.spec.driftDetection.mode` field. Refer to the [drift detection section](https://github.com/fluxcd/helm-controller/blob/v0.37.0/docs/spec/v2beta2/helmreleases.md#drift-detection)
+ in the `v2beta2` specification for more information.
+- Ignoring specific fields during drift detection and correction is now
+ supported using the `.spec.driftDetection.ignore` field. Refer to the
+ [ignore rules section](https://github.com/fluxcd/helm-controller/blob/v0.37.0/docs/spec/v2beta2/helmreleases.md#ignore-rules)
+ in the `v2beta2` specification to learn more.
+- Helm tests can now be selectively run using the `.spec.test.filters` field.
+ Refer to the [test filters section](https://github.com/fluxcd/helm-controller/blob/v0.37.0/docs/spec/v2beta2/helmreleases.md#filtering-tests)
+ in the `v2beta2` specification for more details.
+- The controller now offers proper integration with [`kstatus`](https://github.com/kubernetes-sigs/cli-utils/blob/master/pkg/kstatus/README.md)
+ and sets `Reconciling` and `Stalled` conditions. See the [Conditions section](https://github.com/fluxcd/helm-controller/blob/v0.37.0/docs/spec/v2beta2/helmreleases.md#conditions)
+ in the `v2beta2` specification to read more about the conditions.
+- The `.spec.maxHistory` default value has been lowered from `10` to `5` to
+ increase the controller's performance.
+- A history of metadata from Helm releases up to the previous successful release
+ is now available in the `.status.history` field. This includes any Helm test
+ results when enabled.
+- The `.patchesStrategicMerge` and `.patchesJson6902` Kustomize post-rendering
+ fields have been deprecated in favor of `.patches`.
+- A `status.lastAttemptedConfigDigest` field has been introduced to track the
+ last attempted configuration digest using a hash of the composed values.
+- A `.status.lastAttemptedReleaseAction` field has been introduced to accurately
+ determine the active remediation strategy.
+- The `.status.lastHandledForceAt` and `.status.lastHandledResetAt` fields have
+ been introduced to track the last time a force upgrade or reset was handled.
+ This to accomadate newly introduced annotations to force upgrades and resets.
+- The `.status.lastAppliedRevision` and `.status.lastReleaseRevision` fields
+ have been deprecated in favor of `.status.history`.
+- The `.status.lastAttemptedValuesChecksum` has been deprecated in favor of
+ `.status.lastAttemptedConfigDigest`.
+
+Although the `v2beta1` API is still supported, it is recommended to upgrade to
+the `v2beta2` API as soon as possible. The `v2beta1` API will be removed after
+6 months.
+
+To upgrade to the `v2beta2` API, update the `apiVersion` field of your
+`HelmRelease` resources to `helm.toolkit.fluxcd.io/v2beta2` after updating the
+controller and Custom Resource Definitions.
+
+#### Other notable improvements
+
+- The reconciliation model of the controller has been improved to be able to
+ better determine the state a Helm release is in. An example of this is that
+ enabling Helm tests will not require a Helm upgrade to be run, but instead
+ will run immediately if the release is in a `deployed` state already.
+- The controller will detect Helm releases in a `pending-install`, `pending-upgrade`
+ or `pending-rollback` state, and wil forcefully unlock the release (to a
+ `failed` state) to allow the controller to reattempt the release.
+- When drift correction is enabled, the controller will now attempt to correct
+ drift it detects by creating and patching Kubernetes resources instead of
+ running a Helm upgrade.
+- The controller emits more detailed Kubernetes Events after running a Helm
+ action. In addition, the controller will now emit a Kubernetes Event when
+ a Helm release is uninstalled.
+- The controller provides richer Condition messages before and after running a
+ Helm action.
+- Changes to a HelmRelease `.spec` which require a Helm uninstall for the
+ changes to be successfully applied are now detected. For example, a change in
+ `.spec.targetNamespace` or `.spec.releaseName`.
+- When the release name exceeds the maximum length of 53 characters, the
+ controller will now truncate the release name to 40 characters and append a
+ short SHA256 hash of the release name prefixed with a `-` to ensure the
+ release name is unique.
+- New annotations have been introduced to force a Helm upgrade or to reset the
+ number of retries for a release. Refer to the [forcing a release](https://github.com/fluxcd/helm-controller/blob/v0.37.0/docs/spec/v2beta2/helmreleases.md#forcing-a-release)
+ and [resetting remediation retries](https://github.com/fluxcd/helm-controller/blob/v0.37.0/docs/spec/v2beta2/helmreleases.md#resetting-remediation-retries)
+ sections in the `v2beta2` specification for more information.
+- The digest algorithm used to calculate the digest of the composed values and
+ hash of the release object can now be configured using the `--snapshot-digest-algo`
+ controller flag. The default value is `sha256`.
+- When the `HelmChart` resource for a `HelmRelease` is not `Ready`, the
+ Conditions of the `HelmRelease` will now contain more detailed information
+ about the reason.
+
+To get a full overview of all changes, and see examples of the new features.
+Please refer to the [v2beta2 specification](https://github.com/fluxcd/helm-controller/blob/v0.37.0/docs/spec/v2beta2/helmreleases.md).
+
+### Full changelog
+
+Improvements:
+- Update dependencies
+ [#791](https://github.com/fluxcd/helm-controller/pull/791)
+ [#792](https://github.com/fluxcd/helm-controller/pull/792)
+ [#799](https://github.com/fluxcd/helm-controller/pull/799)
+ [#812](https://github.com/fluxcd/helm-controller/pull/812)
+- Update source-controller dependency to v1.2.1
+ [#793](https://github.com/fluxcd/helm-controller/pull/793)
+ [#835](https://github.com/fluxcd/helm-controller/pull/835)
+- Rework `HelmRelease` reconciliation logic
+ [#738](https://github.com/fluxcd/helm-controller/pull/738)
+ [#816](https://github.com/fluxcd/helm-controller/pull/816)
+ [#825](https://github.com/fluxcd/helm-controller/pull/825)
+ [#829](https://github.com/fluxcd/helm-controller/pull/829)
+ [#830](https://github.com/fluxcd/helm-controller/pull/830)
+ [#833](https://github.com/fluxcd/helm-controller/pull/833)
+ [#836](https://github.com/fluxcd/helm-controller/pull/836)
+- Update Kubernetes 1.28.x, Helm v3.13.2 and Kustomize v5.3.0
+ [#817](https://github.com/fluxcd/helm-controller/pull/817)
+ [#839](https://github.com/fluxcd/helm-controller/pull/839)
+- Allow configuration of drift detection on `HelmRelease`
+ [#815](https://github.com/fluxcd/helm-controller/pull/815)
+- Allow configuration of snapshot digest algorithm
+ [#818](https://github.com/fluxcd/helm-controller/pull/818)
+- Remove obsolete code and tidy things
+ [#819](https://github.com/fluxcd/helm-controller/pull/819)
+- Add deprecation warning to v2beta1 API
+ [#821](https://github.com/fluxcd/helm-controller/pull/821)
+- Correct cluster drift using patches
+ [#822](https://github.com/fluxcd/helm-controller/pull/822)
+- Introduce `forceAt` and `resetAt` annotations
+ [#823](https://github.com/fluxcd/helm-controller/pull/823)
+- doc/spec: document `v2beta2` API
+ [#828](https://github.com/fluxcd/helm-controller/pull/828)
+- api: deprecate stategic merge and JSON 6902 patches
+ [#832](https://github.com/fluxcd/helm-controller/pull/832)
+- controller: enrich "HelmChart not ready" messages
+ [#834](https://github.com/fluxcd/helm-controller/pull/834)
+- build: update Alpine to 3.19
+ [#838](https://github.com/fluxcd/helm-controller/pull/838)
+
+## 0.36.2
+
+**Release date:** 2023-10-11
+
+This prerelease contains an improvement to retry the reconciliation of a
+`HelmRelease` as soon as the chart is available in storage, instead of waiting
+for the next reconciliation interval. Which is particularly useful when the
+source-controller has just been upgraded.
+
+In addition, it fixes a bug in which the controller would not properly label
+Custom Resource Definitions.
+
+Fixes:
+- runner: ensure CRDs are properly labeled
+ [#781](https://github.com/fluxcd/helm-controller/pull/781)
+- fix: retry failed releases when charts are available in storage
+ [#785](https://github.com/fluxcd/helm-controller/pull/785)
+
+Improvements:
+- Address typo in documentation
+ [#777](https://github.com/fluxcd/helm-controller/pull/777)
+- Update CI dependencies
+ [#783](https://github.com/fluxcd/helm-controller/pull/783)
+ [#786](https://github.com/fluxcd/helm-controller/pull/786)
+- Address miscellaneous issues throughout code base
+ [#788](https://github.com/fluxcd/helm-controller/pull/788)
+
+## 0.36.1
+
+**Release date:** 2023-09-18
+
+This prerelease addresses a regression in which the captured Helm logs used in
+a failure event would not include Helm's Kubernetes client logs, making it more
+difficult to reason about e.g. timeout errors.
+
+In addition, it contains a fix for the default service account used for the
+(experimental) differ, and dependency updates of several dependencies.
+
+Fixes:
+- runner: address regression in captured Helm logs
+ [#767](https://github.com/fluxcd/helm-controller/pull/767)
+- Check source for nil artifact before loading chart
+ [#768](https://github.com/fluxcd/helm-controller/pull/768)
+- controller: use `DefaultServiceAccount` in differ
+ [#774](https://github.com/fluxcd/helm-controller/pull/774)
+
+Improvements:
+- build(deps): bump the ci group dependencies
+ [#761](https://github.com/fluxcd/helm-controller/pull/761)
+ [#762](https://github.com/fluxcd/helm-controller/pull/762)
+ [#766](https://github.com/fluxcd/helm-controller/pull/766)
+ [#773](https://github.com/fluxcd/helm-controller/pull/773)
+- build(deps): bump github.com/cyphar/filepath-securejoin from 0.2.3 to 0.2.4
+ [#764](https://github.com/fluxcd/helm-controller/pull/764)
+- Update source-controller to v1.1.1
+ [#775](https://github.com/fluxcd/helm-controller/pull/775)
+
## 0.36.0
**Release date:** 2023-08-23
diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md
index 207802570..fd525a31f 100644
--- a/DEVELOPMENT.md
+++ b/DEVELOPMENT.md
@@ -15,7 +15,7 @@ There are a number of dependencies required to be able to run the controller and
In addition to the above, the following dependencies are also used by some of the `make` targets:
-- `controller-gen` (v0.7.0)
+- `controller-gen` (v0.19.0)
- `gen-crd-api-reference-docs` (v0.3.0)
- `setup-envtest` (latest)
@@ -24,7 +24,7 @@ If any of the above dependencies are not present on your system, the first invoc
## How to run the test suite
Prerequisites:
-* Go >= 1.20
+* Go >= 1.25
You can run the test suite by simply doing
diff --git a/Dockerfile b/Dockerfile
index 3abbec75c..a23b65314 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,10 +1,10 @@
-ARG GO_VERSION=1.20
-ARG XX_VERSION=1.2.1
+ARG GO_VERSION=1.26
+ARG XX_VERSION=1.9.0
FROM --platform=$BUILDPLATFORM tonistiigi/xx:${XX_VERSION} AS xx
# Docker buildkit multi-arch build requires golang alpine
-FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-alpine as builder
+FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-alpine AS builder
# Copy the build utilities.
COPY --from=xx / /
@@ -31,7 +31,7 @@ COPY internal/ internal/
ENV CGO_ENABLED=0
RUN xx-go build -trimpath -a -o helm-controller main.go
-FROM alpine:3.18
+FROM alpine:3.23
RUN apk add --no-cache ca-certificates \
&& update-ca-certificates
diff --git a/Makefile b/Makefile
index ee5511adc..7f1065a8d 100644
--- a/Makefile
+++ b/Makefile
@@ -19,6 +19,9 @@ GOBIN=$(shell go env GOBIN)
endif
export PATH:=$(GOBIN):${PATH}
+# Allows for defining additional Go test args, e.g. '-tags integration'.
+GO_TEST_ARGS ?=
+
# Allows for defining additional Docker buildx arguments, e.g. '--push'.
BUILD_ARGS ?= --load
# Architectures to build images for.
@@ -27,16 +30,29 @@ BUILD_PLATFORMS ?= linux/amd64
# Architecture to use envtest with
ENVTEST_ARCH ?= amd64
+# Paths to download the CRD dependency to.
+CRD_DEP_ROOT ?= $(BUILD_DIR)/config/crd/bases
+
+# Keep a record of the version of the downloaded source CRDs. It is used to
+# detect and download new CRDs when the SOURCE_VER changes.
+SOURCE_VER ?= $(shell go list -m all | grep github.com/fluxcd/source-controller/api | awk '{print $$2}')
+SOURCE_CRD_VER = $(CRD_DEP_ROOT)/.src-crd-$(SOURCE_VER)
+
+# HelmChart source CRD.
+HELMCHART_SOURCE_CRD ?= $(CRD_DEP_ROOT)/source.toolkit.fluxcd.io_helmcharts.yaml
+OCIREPO_CRD ?= $(CRD_DEP_ROOT)/source.toolkit.fluxcd.io_ocirepositories.yaml
+EA_CRD ?= $(CRD_DEP_ROOT)/source.toolkit.fluxcd.io_externalartifacts.yaml
+
# API (doc) generation utilities
-CONTROLLER_GEN_VERSION ?= v0.12.0
+CONTROLLER_GEN_VERSION ?= v0.19.0
GEN_API_REF_DOCS_VERSION ?= e327d0730470cbd61b06300f81c5fcf91c23c113
all: manager
# Run tests
KUBEBUILDER_ASSETS?="$(shell $(ENVTEST) --arch=$(ENVTEST_ARCH) use -i $(ENVTEST_KUBERNETES_VERSION) --bin-dir=$(ENVTEST_ASSETS_DIR) -p path)"
-test: tidy generate fmt vet manifests api-docs install-envtest
- KUBEBUILDER_ASSETS=$(KUBEBUILDER_ASSETS) go test ./... -coverprofile cover.out
+test: tidy generate fmt vet manifests api-docs install-envtest download-crd-deps
+ KUBEBUILDER_ASSETS=$(KUBEBUILDER_ASSETS) go test ./... $(GO_TEST_ARGS) -coverprofile cover.out
cd api; go test ./... -coverprofile cover.out
# Build manager binary
@@ -81,12 +97,12 @@ manifests: controller-gen
# Generate API reference documentation
api-docs: gen-crd-api-reference-docs
- $(GEN_CRD_API_REFERENCE_DOCS) -api-dir=./api/v2beta1 -config=./hack/api-docs/config.json -template-dir=./hack/api-docs/template -out-file=./docs/api/v2beta1/helm.md
+ $(GEN_CRD_API_REFERENCE_DOCS) -api-dir=./api/v2 -config=./hack/api-docs/config.json -template-dir=./hack/api-docs/template -out-file=./docs/api/v2/helm.md
# Run go mod tidy
tidy:
- cd api; rm -f go.sum; go mod tidy -compat=1.20
- rm -f go.sum; go mod tidy -compat=1.20
+ cd api; rm -f go.sum; go mod tidy -compat=1.26
+ rm -f go.sum; go mod tidy -compat=1.26
# Run go fmt against code
fmt:
@@ -102,6 +118,14 @@ vet:
generate: controller-gen
cd api; $(CONTROLLER_GEN) object:headerFile="../hack/boilerplate.go.txt" paths="./..."
+# Verify that the working directory is clean
+verify: fmt
+ @if [ ! "$$(git status --porcelain --untracked-files=no)" = "" ]; then \
+ echo "working directory is dirty:"; \
+ git --no-pager diff; \
+ exit 1; \
+ fi
+
# Build the docker image
docker-build:
docker buildx build \
@@ -113,6 +137,30 @@ docker-build:
docker-push:
docker push ${IMG}
+# Delete previously downloaded CRDs and record the new version of the source
+# CRDs.
+$(SOURCE_CRD_VER):
+ rm -f $(CRD_DEP_ROOT)/.src-crd*
+ mkdir -p $(CRD_DEP_ROOT)
+ $(MAKE) cleanup-crd-deps
+ touch $(SOURCE_CRD_VER)
+
+$(HELMCHART_SOURCE_CRD):
+ curl -s https://raw.githubusercontent.com/fluxcd/source-controller/${SOURCE_VER}/config/crd/bases/source.toolkit.fluxcd.io_helmcharts.yaml > $(HELMCHART_SOURCE_CRD)
+
+$(OCIREPO_CRD):
+ curl -s https://raw.githubusercontent.com/fluxcd/source-controller/${SOURCE_VER}/config/crd/bases/source.toolkit.fluxcd.io_ocirepositories.yaml -o $(OCIREPO_CRD)
+
+$(EA_CRD):
+ curl -s https://raw.githubusercontent.com/fluxcd/source-controller/${SOURCE_VER}/config/crd/bases/source.toolkit.fluxcd.io_externalartifacts.yaml -o $(EA_CRD)
+
+# Download the CRDs the controller depends on
+download-crd-deps: $(SOURCE_CRD_VER) $(HELMCHART_SOURCE_CRD) $(OCIREPO_CRD) $(EA_CRD)
+
+# Delete the downloaded CRD dependencies.
+cleanup-crd-deps:
+ rm -f $(HELMCHART_SOURCE_CRD)
+
# Find or download controller-gen
CONTROLLER_GEN = $(GOBIN)/controller-gen
.PHONY: controller-gen
diff --git a/PROJECT b/PROJECT
index 4b09ffd52..43da20f17 100644
--- a/PROJECT
+++ b/PROJECT
@@ -3,5 +3,6 @@ repo: github.com/fluxcd/helm-controller
resources:
- group: helm
kind: HelmRelease
- version: v2beta1
+ version: v2
+storageVersion: v2
version: "2"
diff --git a/README.md b/README.md
index 1e9ff704c..4fea29207 100644
--- a/README.md
+++ b/README.md
@@ -24,7 +24,7 @@ operator.
* Supports `HelmChart` artifacts produced from `HelmRepository`,
`GitRepository` and `Bucket` sources
* Fetches artifacts produced by [source-controller][] from `HelmChart`
- objects
+ and `OCIRepository` objects
* Watches `HelmChart` objects for revision changes (including semver
ranges for charts from `HelmRepository` sources)
* Performs automated Helm actions, including Helm tests, rollbacks and
@@ -38,17 +38,18 @@ operator.
[notification-controller][])
* Built-in Kustomize compatible Helm post renderer, providing support
for strategic merge, JSON 6902 and images patches
+* Supports detecting and correcting in-cluster changes compared to the desired
+ state of the Helm release
## Guides
-* [Get started with GitOps Toolkit](https://fluxcd.io/flux/get-started/)
+* [Get started with Flux](https://fluxcd.io/flux/get-started/)
* [Manage Helm Releases](https://fluxcd.io/flux/guides/helmreleases/)
* [Setup Notifications](https://fluxcd.io/flux/guides/notifications/)
-## Specifications
+## Specification
-* [API](docs/spec/v2beta1/README.md)
-* [Controller](docs/spec/README.md)
+[HelmRelease API](docs/spec/v2/helmreleases.md)
[source-controller]: https://github.com/fluxcd/source-controller
[notification-controller]: https://github.com/fluxcd/notification-controller
diff --git a/api/go.mod b/api/go.mod
index 6b90ba4b0..6120fcd9a 100644
--- a/api/go.mod
+++ b/api/go.mod
@@ -1,31 +1,32 @@
module github.com/fluxcd/helm-controller/api
-go 1.20
+go 1.25.0
require (
- github.com/fluxcd/pkg/apis/kustomize v1.1.1
- github.com/fluxcd/pkg/apis/meta v1.1.2
- k8s.io/apiextensions-apiserver v0.27.4
- k8s.io/apimachinery v0.27.4
- sigs.k8s.io/controller-runtime v0.15.1
+ github.com/fluxcd/pkg/apis/kustomize v1.16.0
+ github.com/fluxcd/pkg/apis/meta v1.26.0
+ k8s.io/apiextensions-apiserver v0.35.2
+ k8s.io/apimachinery v0.35.2
+ sigs.k8s.io/controller-runtime v0.23.1
+ sigs.k8s.io/yaml v1.6.0
)
require (
- github.com/go-logr/logr v1.2.4 // indirect
- github.com/gogo/protobuf v1.3.2 // indirect
- github.com/google/gofuzz v1.2.0 // indirect
+ github.com/fxamacker/cbor/v2 v2.9.0 // indirect
+ github.com/go-logr/logr v1.4.3 // indirect
github.com/json-iterator/go v1.1.12 // indirect
- github.com/kr/pretty v0.3.1 // indirect
+ github.com/kr/text v0.2.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
- github.com/modern-go/reflect2 v1.0.2 // indirect
- github.com/rogpeppe/go-internal v1.11.0 // indirect
- golang.org/x/net v0.10.0 // indirect
- golang.org/x/text v0.9.0 // indirect
- gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
+ github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
+ github.com/x448/float16 v0.8.4 // indirect
+ go.yaml.in/yaml/v2 v2.4.3 // indirect
+ golang.org/x/net v0.49.0 // indirect
+ golang.org/x/text v0.33.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
- gopkg.in/yaml.v2 v2.4.0 // indirect
- k8s.io/klog/v2 v2.90.1 // indirect
- k8s.io/utils v0.0.0-20230209194617-a36077c30491 // indirect
- sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
- sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect
+ k8s.io/klog/v2 v2.130.1 // indirect
+ k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect
+ k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect
+ sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
+ sigs.k8s.io/randfill v1.0.0 // indirect
+ sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 // indirect
)
diff --git a/api/go.sum b/api/go.sum
index 4e4ab3fb0..3103a058a 100644
--- a/api/go.sum
+++ b/api/go.sum
@@ -1,105 +1,94 @@
+github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
+github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/fluxcd/pkg/apis/kustomize v1.1.1 h1:MSGn4z0R9PptmoPFHnx2nEZ8Jtl1sKfw0cuDQY2HYwM=
-github.com/fluxcd/pkg/apis/kustomize v1.1.1/go.mod h1:0pCu0ecIY+ZM0iE/hOHYwCMZ3b0SpBrjJ1SH3FFyYdE=
-github.com/fluxcd/pkg/apis/meta v1.1.2 h1:Unjo7hxadtB2dvGpeFqZZUdsjpRA08YYSBb7dF2WIAM=
-github.com/fluxcd/pkg/apis/meta v1.1.2/go.mod h1:BHQyRHCskGMEDf6kDGbgQ+cyiNpUHbLsCOsaMYM2maI=
-github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
-github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
-github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
-github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
-github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
-github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
-github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
+github.com/fluxcd/pkg/apis/kustomize v1.16.0 h1:PhWXEhqQqsisIpwp1/wHvTvo+MO+GGzsBPoN0ZnRE3Y=
+github.com/fluxcd/pkg/apis/kustomize v1.16.0/go.mod h1:IZOy4CCtR/hxMGb7erK1RfbGnczVv4/dRBoVD37AywI=
+github.com/fluxcd/pkg/apis/meta v1.26.0 h1:dxP1FfBpTCYso6odzRcltVnnRuBb2VyhhgV0VX9YbUE=
+github.com/fluxcd/pkg/apis/meta v1.26.0/go.mod h1:c7o6mJGLCMvNrfdinGZehkrdZuFT9vZdZNrn66DtVD0=
+github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
+github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
+github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
+github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
+github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
+github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
-github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
-github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
-github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec=
+github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8=
+github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
-github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
-github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
-github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
-github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
-github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
-github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
-github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q=
-github.com/onsi/gomega v1.27.7 h1:fVih9JD6ogIiHUN6ePK7HJidyEDpWGVB5mzM7cWNXoU=
-github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
+github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8=
+github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns=
+github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo=
+github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A=
+github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
-github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
-github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
-github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
+github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
+github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
+github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
+github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
-github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
-github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
-github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
-golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
-golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
-golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
-golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
-golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
-golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
-golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
-golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.9.1 h1:8WMNJAz3zrtPmnYC7ISf5dEn3MT0gY7jBJfw27yrrLo=
-golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
+github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
+github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
+github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
+go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
+go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
+go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
+go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
+golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
+golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
+golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
+golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
+golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
+golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
+golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
+golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
+golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
+golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
+golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
+golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
-gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
-gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
-k8s.io/api v0.27.4 h1:0pCo/AN9hONazBKlNUdhQymmnfLRbSZjd5H5H3f0bSs=
-k8s.io/apiextensions-apiserver v0.27.4 h1:ie1yZG4nY/wvFMIR2hXBeSVq+HfNzib60FjnBYtPGSs=
-k8s.io/apiextensions-apiserver v0.27.4/go.mod h1:KHZaDr5H9IbGEnSskEUp/DsdXe1hMQ7uzpQcYUFt2bM=
-k8s.io/apimachinery v0.27.4 h1:CdxflD4AF61yewuid0fLl6bM4a3q04jWel0IlP+aYjs=
-k8s.io/apimachinery v0.27.4/go.mod h1:XNfZ6xklnMCOGGFNqXG7bUrQCoR04dh/E7FprV6pb+E=
-k8s.io/klog/v2 v2.90.1 h1:m4bYOKall2MmOiRaR1J+We67Do7vm9KiQVlT96lnHUw=
-k8s.io/klog/v2 v2.90.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0=
-k8s.io/utils v0.0.0-20230209194617-a36077c30491 h1:r0BAOLElQnnFhE/ApUsg3iHdVYYPBjNSSOMowRZxxsY=
-k8s.io/utils v0.0.0-20230209194617-a36077c30491/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
-sigs.k8s.io/controller-runtime v0.15.1 h1:9UvgKD4ZJGcj24vefUFgZFP3xej/3igL9BsOUTb/+4c=
-sigs.k8s.io/controller-runtime v0.15.1/go.mod h1:7ngYvp1MLT+9GeZ+6lH3LOlcHkp/+tzA/fmHa4iq9kk=
-sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo=
-sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
-sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE=
-sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E=
-sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+k8s.io/api v0.35.2 h1:tW7mWc2RpxW7HS4CoRXhtYHSzme1PN1UjGHJ1bdrtdw=
+k8s.io/api v0.35.2/go.mod h1:7AJfqGoAZcwSFhOjcGM7WV05QxMMgUaChNfLTXDRE60=
+k8s.io/apiextensions-apiserver v0.35.2 h1:iyStXHoJZsUXPh/nFAsjC29rjJWdSgUmG1XpApE29c0=
+k8s.io/apiextensions-apiserver v0.35.2/go.mod h1:OdyGvcO1FtMDWQ+rRh/Ei3b6X3g2+ZDHd0MSRGeS8rU=
+k8s.io/apimachinery v0.35.2 h1:NqsM/mmZA7sHW02JZ9RTtk3wInRgbVxL8MPfzSANAK8=
+k8s.io/apimachinery v0.35.2/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns=
+k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
+k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
+k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE=
+k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ=
+k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck=
+k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
+sigs.k8s.io/controller-runtime v0.23.1 h1:TjJSM80Nf43Mg21+RCy3J70aj/W6KyvDtOlpKf+PupE=
+sigs.k8s.io/controller-runtime v0.23.1/go.mod h1:B6COOxKptp+YaUT5q4l6LqUJTRpizbgf9KSRNdQGns0=
+sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=
+sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
+sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
+sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
+sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 h1:2WOzJpHUBVrrkDjU4KBT8n5LDcj824eX0I5UKcgeRUs=
+sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
+sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
+sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
diff --git a/api/v2/annotations.go b/api/v2/annotations.go
new file mode 100644
index 000000000..6abaa013c
--- /dev/null
+++ b/api/v2/annotations.go
@@ -0,0 +1,57 @@
+/*
+Copyright 2024 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package v2
+
+import "github.com/fluxcd/pkg/apis/meta"
+
+const (
+ // ForceRequestAnnotation is the annotation used for triggering a one-off forced
+ // Helm release, even when there are no new changes in the HelmRelease.
+ // The value is interpreted as a token, and must equal the value of
+ // meta.ReconcileRequestAnnotation in order to trigger a release.
+ ForceRequestAnnotation string = meta.ForceRequestAnnotation
+
+ // ResetRequestAnnotation is the annotation used for resetting the failure counts
+ // of a HelmRelease, so that it can be retried again.
+ // The value is interpreted as a token, and must equal the value of
+ // meta.ReconcileRequestAnnotation in order to reset the failure counts.
+ ResetRequestAnnotation string = "reconcile.fluxcd.io/resetAt"
+)
+
+// ShouldHandleResetRequest returns true if the HelmRelease has a reset request
+// annotation, and the value of the annotation matches the value of the
+// meta.ReconcileRequestAnnotation annotation.
+//
+// To ensure that the reset request is handled only once, the value of
+// HelmReleaseStatus.LastHandledResetAt is updated to match the value of the
+// reset request annotation (even if the reset request is not handled because
+// the value of the meta.ReconcileRequestAnnotation annotation does not match).
+func ShouldHandleResetRequest(obj *HelmRelease) bool {
+ return meta.HandleAnnotationRequest(obj, ResetRequestAnnotation, &obj.Status.LastHandledResetAt)
+}
+
+// ShouldHandleForceRequest returns true if the HelmRelease has a force request
+// annotation, and the value of the annotation matches the value of the
+// meta.ReconcileRequestAnnotation annotation.
+//
+// To ensure that the force request is handled only once, the value of
+// HelmReleaseStatus.LastHandledForceAt is updated to match the value of the
+// force request annotation (even if the force request is not handled because
+// the value of the meta.ReconcileRequestAnnotation annotation does not match).
+func ShouldHandleForceRequest(obj *HelmRelease) bool {
+ return meta.ShouldHandleForceRequest(obj)
+}
diff --git a/api/v2/annotations_test.go b/api/v2/annotations_test.go
new file mode 100644
index 000000000..c605b1841
--- /dev/null
+++ b/api/v2/annotations_test.go
@@ -0,0 +1,50 @@
+/*
+Copyright 2024 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package v2
+
+import (
+ "testing"
+
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+ "github.com/fluxcd/pkg/apis/meta"
+)
+
+func TestShouldHandleResetRequest(t *testing.T) {
+ obj := &HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Annotations: map[string]string{
+ meta.ReconcileRequestAnnotation: "b",
+ ResetRequestAnnotation: "b",
+ },
+ },
+ Status: HelmReleaseStatus{
+ LastHandledResetAt: "a",
+ ReconcileRequestStatus: meta.ReconcileRequestStatus{
+ LastHandledReconcileAt: "a",
+ },
+ },
+ }
+
+ if !ShouldHandleResetRequest(obj) {
+ t.Error("ShouldHandleResetRequest() = false")
+ }
+
+ if obj.Status.LastHandledResetAt != "b" {
+ t.Error("ShouldHandleResetRequest did not update LastHandledResetAt")
+ }
+}
diff --git a/api/v2/condition_types.go b/api/v2/condition_types.go
new file mode 100644
index 000000000..ed8252e9a
--- /dev/null
+++ b/api/v2/condition_types.go
@@ -0,0 +1,94 @@
+/*
+Copyright 2024 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package v2
+
+const (
+ // ReleasedCondition represents the status of the last release attempt
+ // (install/upgrade/test) against the latest desired state.
+ ReleasedCondition string = "Released"
+
+ // TestSuccessCondition represents the status of the last test attempt against
+ // the latest desired state.
+ TestSuccessCondition string = "TestSuccess"
+
+ // RemediatedCondition represents the status of the last remediation attempt
+ // (uninstall/rollback) due to a failure of the last release attempt against the
+ // latest desired state.
+ RemediatedCondition string = "Remediated"
+
+ // DriftedCondition represents the status of the Helm release drift detection,
+ // indicating that the deployed release has drifted from the desired state.
+ DriftedCondition string = "Drifted"
+)
+
+const (
+ // InstallSucceededReason represents the fact that the Helm install for the
+ // HelmRelease succeeded.
+ InstallSucceededReason string = "InstallSucceeded"
+
+ // InstallFailedReason represents the fact that the Helm install for the
+ // HelmRelease failed.
+ InstallFailedReason string = "InstallFailed"
+
+ // UpgradeSucceededReason represents the fact that the Helm upgrade for the
+ // HelmRelease succeeded.
+ UpgradeSucceededReason string = "UpgradeSucceeded"
+
+ // UpgradeFailedReason represents the fact that the Helm upgrade for the
+ // HelmRelease failed.
+ UpgradeFailedReason string = "UpgradeFailed"
+
+ // TestSucceededReason represents the fact that the Helm tests for the
+ // HelmRelease succeeded.
+ TestSucceededReason string = "TestSucceeded"
+
+ // TestFailedReason represents the fact that the Helm tests for the HelmRelease
+ // failed.
+ TestFailedReason string = "TestFailed"
+
+ // RollbackSucceededReason represents the fact that the Helm rollback for the
+ // HelmRelease succeeded.
+ RollbackSucceededReason string = "RollbackSucceeded"
+
+ // RollbackFailedReason represents the fact that the Helm test for the
+ // HelmRelease failed.
+ RollbackFailedReason string = "RollbackFailed"
+
+ // UninstallSucceededReason represents the fact that the Helm uninstall for the
+ // HelmRelease succeeded.
+ UninstallSucceededReason string = "UninstallSucceeded"
+
+ // UninstallFailedReason represents the fact that the Helm uninstall for the
+ // HelmRelease failed.
+ UninstallFailedReason string = "UninstallFailed"
+
+ // ArtifactFailedReason represents the fact that the artifact download for the
+ // HelmRelease failed.
+ ArtifactFailedReason string = "ArtifactFailed"
+
+ // DependencyNotReadyReason represents the fact that
+ // one of the dependencies is not ready.
+ DependencyNotReadyReason string = "DependencyNotReady"
+
+ // DriftDetectedReason represents the fact that drift has been detected in the
+ // Helm release compared to the expected state.
+ DriftDetectedReason string = "DriftDetected"
+
+ // NoDriftDetectedReason represents the fact that no drift has been detected in
+ // the Helm release compared to the expected state.
+ NoDriftDetectedReason string = "NoDriftDetected"
+)
diff --git a/api/v2/doc.go b/api/v2/doc.go
new file mode 100644
index 000000000..e04280738
--- /dev/null
+++ b/api/v2/doc.go
@@ -0,0 +1,20 @@
+/*
+Copyright 2024 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Package v2 contains API Schema definitions for the helm v2 API group
+// +kubebuilder:object:generate=true
+// +groupName=helm.toolkit.fluxcd.io
+package v2
diff --git a/api/v2/groupversion_info.go b/api/v2/groupversion_info.go
new file mode 100644
index 000000000..352331bd9
--- /dev/null
+++ b/api/v2/groupversion_info.go
@@ -0,0 +1,33 @@
+/*
+Copyright 2024 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package v2
+
+import (
+ "k8s.io/apimachinery/pkg/runtime/schema"
+ "sigs.k8s.io/controller-runtime/pkg/scheme"
+)
+
+var (
+ // GroupVersion is group version used to register these objects
+ GroupVersion = schema.GroupVersion{Group: "helm.toolkit.fluxcd.io", Version: "v2"}
+
+ // SchemeBuilder is used to add go types to the GroupVersionKind scheme
+ SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion}
+
+ // AddToScheme adds the types in this group-version to the given scheme.
+ AddToScheme = SchemeBuilder.AddToScheme
+)
diff --git a/api/v2/helmrelease_types.go b/api/v2/helmrelease_types.go
new file mode 100644
index 000000000..9495fc537
--- /dev/null
+++ b/api/v2/helmrelease_types.go
@@ -0,0 +1,1599 @@
+/*
+Copyright 2024 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package v2
+
+import (
+ "strings"
+ "time"
+
+ apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/types"
+ "sigs.k8s.io/yaml"
+
+ "github.com/fluxcd/pkg/apis/kustomize"
+ "github.com/fluxcd/pkg/apis/meta"
+)
+
+const (
+ // HelmReleaseKind is the kind in string format.
+ HelmReleaseKind = "HelmRelease"
+ // HelmReleaseFinalizer is set on a HelmRelease when it is first handled by
+ // the controller, and removed when this object is deleted.
+ HelmReleaseFinalizer = "finalizers.fluxcd.io"
+)
+
+const (
+ // defaultMaxHistory is the default number of Helm release versions to keep.
+ defaultMaxHistory = 5
+)
+
+// HelmReleaseSpec defines the desired state of a Helm release.
+// +kubebuilder:validation:XValidation:rule="(has(self.chart) && !has(self.chartRef)) || (!has(self.chart) && has(self.chartRef))", message="either chart or chartRef must be set"
+type HelmReleaseSpec struct {
+ // Chart defines the template of the v1.HelmChart that should be created
+ // for this HelmRelease.
+ // +optional
+ Chart *HelmChartTemplate `json:"chart,omitempty"`
+
+ // ChartRef holds a reference to a source controller resource containing the
+ // Helm chart artifact.
+ // +optional
+ ChartRef *CrossNamespaceSourceReference `json:"chartRef,omitempty"`
+
+ // Interval at which to reconcile the Helm release.
+ // +kubebuilder:validation:Type=string
+ // +kubebuilder:validation:Pattern="^([0-9]+(\\.[0-9]+)?(ms|s|m|h))+$"
+ // +required
+ Interval metav1.Duration `json:"interval"`
+
+ // KubeConfig for reconciling the HelmRelease on a remote cluster.
+ // When used in combination with HelmReleaseSpec.ServiceAccountName,
+ // forces the controller to act on behalf of that Service Account at the
+ // target cluster.
+ // If the --default-service-account flag is set, its value will be used as
+ // a controller level fallback for when HelmReleaseSpec.ServiceAccountName
+ // is empty.
+ // +optional
+ KubeConfig *meta.KubeConfigReference `json:"kubeConfig,omitempty"`
+
+ // Suspend tells the controller to suspend reconciliation for this HelmRelease,
+ // it does not apply to already started reconciliations. Defaults to false.
+ // +optional
+ Suspend bool `json:"suspend,omitempty"`
+
+ // ReleaseName used for the Helm release. Defaults to a composition of
+ // '[TargetNamespace-]Name'.
+ // +kubebuilder:validation:MinLength=1
+ // +kubebuilder:validation:MaxLength=53
+ // +kubebuilder:validation:Optional
+ // +optional
+ ReleaseName string `json:"releaseName,omitempty"`
+
+ // TargetNamespace to target when performing operations for the HelmRelease.
+ // Defaults to the namespace of the HelmRelease.
+ // +kubebuilder:validation:MinLength=1
+ // +kubebuilder:validation:MaxLength=63
+ // +kubebuilder:validation:Optional
+ // +optional
+ TargetNamespace string `json:"targetNamespace,omitempty"`
+
+ // StorageNamespace used for the Helm storage.
+ // Defaults to the namespace of the HelmRelease.
+ // +kubebuilder:validation:MinLength=1
+ // +kubebuilder:validation:MaxLength=63
+ // +kubebuilder:validation:Optional
+ // +optional
+ StorageNamespace string `json:"storageNamespace,omitempty"`
+
+ // DependsOn may contain a DependencyReference slice with
+ // references to HelmRelease resources that must be ready before this HelmRelease
+ // can be reconciled.
+ // +optional
+ DependsOn []DependencyReference `json:"dependsOn,omitempty"`
+
+ // Timeout is the time to wait for any individual Kubernetes operation (like Jobs
+ // for hooks) during the performance of a Helm action. Defaults to '5m0s'.
+ // +kubebuilder:validation:Type=string
+ // +kubebuilder:validation:Pattern="^([0-9]+(\\.[0-9]+)?(ms|s|m|h))+$"
+ // +optional
+ Timeout *metav1.Duration `json:"timeout,omitempty"`
+
+ // MaxHistory is the number of revisions saved by Helm for this HelmRelease.
+ // Use '0' for an unlimited number of revisions; defaults to '5'.
+ // +optional
+ MaxHistory *int `json:"maxHistory,omitempty"`
+
+ // The name of the Kubernetes service account to impersonate
+ // when reconciling this HelmRelease.
+ // +kubebuilder:validation:MinLength=1
+ // +kubebuilder:validation:MaxLength=253
+ // +optional
+ ServiceAccountName string `json:"serviceAccountName,omitempty"`
+
+ // PersistentClient tells the controller to use a persistent Kubernetes
+ // client for this release. When enabled, the client will be reused for the
+ // duration of the reconciliation, instead of being created and destroyed
+ // for each (step of a) Helm action.
+ //
+ // This can improve performance, but may cause issues with some Helm charts
+ // that for example do create Custom Resource Definitions during installation
+ // outside Helm's CRD lifecycle hooks, which are then not observed to be
+ // available by e.g. post-install hooks.
+ //
+ // If not set, it defaults to true.
+ //
+ // +optional
+ PersistentClient *bool `json:"persistentClient,omitempty"`
+
+ // DriftDetection holds the configuration for detecting and handling
+ // differences between the manifest in the Helm storage and the resources
+ // currently existing in the cluster.
+ // +optional
+ DriftDetection *DriftDetection `json:"driftDetection,omitempty"`
+
+ // Install holds the configuration for Helm install actions for this HelmRelease.
+ // +optional
+ Install *Install `json:"install,omitempty"`
+
+ // Upgrade holds the configuration for Helm upgrade actions for this HelmRelease.
+ // +optional
+ Upgrade *Upgrade `json:"upgrade,omitempty"`
+
+ // Test holds the configuration for Helm test actions for this HelmRelease.
+ // +optional
+ Test *Test `json:"test,omitempty"`
+
+ // Rollback holds the configuration for Helm rollback actions for this HelmRelease.
+ // +optional
+ Rollback *Rollback `json:"rollback,omitempty"`
+
+ // Uninstall holds the configuration for Helm uninstall actions for this HelmRelease.
+ // +optional
+ Uninstall *Uninstall `json:"uninstall,omitempty"`
+
+ // ValuesFrom holds references to resources containing Helm values for this HelmRelease,
+ // and information about how they should be merged.
+ ValuesFrom []ValuesReference `json:"valuesFrom,omitempty"`
+
+ // Values holds the values for this Helm release.
+ // +optional
+ Values *apiextensionsv1.JSON `json:"values,omitempty"`
+
+ // CommonMetadata specifies the common labels and annotations that are
+ // applied to all resources. Any existing label or annotation will be
+ // overridden if its key matches a common one.
+ // +optional
+ CommonMetadata *CommonMetadata `json:"commonMetadata,omitempty"`
+
+ // PostRenderers holds an array of Helm PostRenderers, which will be applied in order
+ // of their definition.
+ // +optional
+ PostRenderers []PostRenderer `json:"postRenderers,omitempty"`
+
+ // WaitStrategy defines Helm's wait strategy for waiting for applied
+ // resources to become ready.
+ // +optional
+ WaitStrategy *WaitStrategy `json:"waitStrategy,omitempty"`
+
+ // HealthCheckExprs is a list of healthcheck expressions for evaluating the
+ // health of custom resources using Common Expression Language (CEL).
+ // The expressions are evaluated only when the specific Helm action
+ // taking place has wait enabled, i.e. DisableWait is false, and the
+ // 'poller' WaitStrategy is used.
+ // +optional
+ HealthCheckExprs []kustomize.CustomHealthCheck `json:"healthCheckExprs,omitempty"`
+}
+
+// +kubebuilder:object:generate=false
+
+type ValuesReference = meta.ValuesReference
+
+// Kustomize Helm PostRenderer specification.
+type Kustomize struct {
+ // Strategic merge and JSON patches, defined as inline YAML objects,
+ // capable of targeting objects based on kind, label and annotation selectors.
+ // +optional
+ Patches []kustomize.Patch `json:"patches,omitempty"`
+
+ // Images is a list of (image name, new name, new tag or digest)
+ // for changing image names, tags or digests. This can also be achieved with a
+ // patch, but this operator is simpler to specify.
+ // +optional
+ Images []kustomize.Image `json:"images,omitempty"`
+}
+
+// CommonMetadata defines the common labels and annotations.
+type CommonMetadata struct {
+ // Annotations to be added to the object's metadata.
+ // +optional
+ Annotations map[string]string `json:"annotations,omitempty"`
+
+ // Labels to be added to the object's metadata.
+ // +optional
+ Labels map[string]string `json:"labels,omitempty"`
+}
+
+// PostRenderer contains a Helm PostRenderer specification.
+type PostRenderer struct {
+ // Kustomization to apply as PostRenderer.
+ // +optional
+ Kustomize *Kustomize `json:"kustomize,omitempty"`
+}
+
+// DriftDetectionMode represents the modes in which a controller can detect and
+// handle differences between the manifest in the Helm storage and the resources
+// currently existing in the cluster.
+type DriftDetectionMode string
+
+var (
+ // DriftDetectionEnabled instructs the controller to actively detect any
+ // changes between the manifest in the Helm storage and the resources
+ // currently existing in the cluster.
+ // If any differences are detected, the controller will automatically
+ // correct the cluster state by performing a Helm upgrade.
+ DriftDetectionEnabled DriftDetectionMode = "enabled"
+
+ // DriftDetectionWarn instructs the controller to actively detect any
+ // changes between the manifest in the Helm storage and the resources
+ // currently existing in the cluster.
+ // If any differences are detected, the controller will emit a warning
+ // without automatically correcting the cluster state.
+ DriftDetectionWarn DriftDetectionMode = "warn"
+
+ // DriftDetectionDisabled instructs the controller to skip detection of
+ // differences entirely.
+ // This is the default behavior, and the controller will not actively
+ // detect or respond to differences between the manifest in the Helm
+ // storage and the resources currently existing in the cluster.
+ DriftDetectionDisabled DriftDetectionMode = "disabled"
+)
+
+var (
+ // DriftDetectionMetadataKey is the label or annotation key used to disable
+ // the diffing of an object.
+ DriftDetectionMetadataKey = GroupVersion.Group + "/driftDetection"
+ // DriftDetectionDisabledValue is the value used to disable the diffing of
+ // an object using DriftDetectionMetadataKey.
+ DriftDetectionDisabledValue = "disabled"
+)
+
+// IgnoreRule defines a rule to selectively disregard specific changes during
+// the drift detection process.
+type IgnoreRule struct {
+ // Paths is a list of JSON Pointer (RFC 6901) paths to be excluded from
+ // consideration in a Kubernetes object.
+ // +required
+ Paths []string `json:"paths"`
+
+ // Target is a selector for specifying Kubernetes objects to which this
+ // rule applies.
+ // If Target is not set, the Paths will be ignored for all Kubernetes
+ // objects within the manifest of the Helm release.
+ // +optional
+ Target *kustomize.Selector `json:"target,omitempty"`
+}
+
+// DriftDetection defines the strategy for performing differential analysis and
+// provides a way to define rules for ignoring specific changes during this
+// process.
+type DriftDetection struct {
+ // Mode defines how differences should be handled between the Helm manifest
+ // and the manifest currently applied to the cluster.
+ // If not explicitly set, it defaults to DiffModeDisabled.
+ // +kubebuilder:validation:Enum=enabled;warn;disabled
+ // +optional
+ Mode DriftDetectionMode `json:"mode,omitempty"`
+
+ // Ignore contains a list of rules for specifying which changes to ignore
+ // during diffing.
+ // +optional
+ Ignore []IgnoreRule `json:"ignore,omitempty"`
+}
+
+// GetMode returns the DiffMode set on the Diff, or DiffModeDisabled if not
+// set.
+func (d DriftDetection) GetMode() DriftDetectionMode {
+ if d.Mode == "" {
+ return DriftDetectionDisabled
+ }
+ return d.Mode
+}
+
+// MustDetectChanges returns true if the DiffMode is set to DiffModeEnabled or
+// DiffModeWarn.
+func (d DriftDetection) MustDetectChanges() bool {
+ return d.GetMode() == DriftDetectionEnabled || d.GetMode() == DriftDetectionWarn
+}
+
+// HelmChartTemplate defines the template from which the controller will
+// generate a v1.HelmChart object in the same namespace as the referenced
+// v1.Source.
+type HelmChartTemplate struct {
+ // ObjectMeta holds the template for metadata like labels and annotations.
+ // +optional
+ ObjectMeta *HelmChartTemplateObjectMeta `json:"metadata,omitempty"`
+
+ // Spec holds the template for the v1.HelmChartSpec for this HelmRelease.
+ // +required
+ Spec HelmChartTemplateSpec `json:"spec"`
+}
+
+// HelmChartTemplateObjectMeta defines the template for the ObjectMeta of a
+// v1.HelmChart.
+type HelmChartTemplateObjectMeta struct {
+ // Map of string keys and values that can be used to organize and categorize
+ // (scope and select) objects.
+ // More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/
+ // +optional
+ Labels map[string]string `json:"labels,omitempty"`
+
+ // Annotations is an unstructured key value map stored with a resource that may be
+ // set by external tools to store and retrieve arbitrary metadata. They are not
+ // queryable and should be preserved when modifying objects.
+ // More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/
+ // +optional
+ Annotations map[string]string `json:"annotations,omitempty"`
+}
+
+// HelmChartTemplateSpec defines the template from which the controller will
+// generate a v1.HelmChartSpec object.
+type HelmChartTemplateSpec struct {
+ // The name or path the Helm chart is available at in the SourceRef.
+ // +kubebuilder:validation:MinLength=1
+ // +kubebuilder:validation:MaxLength=2048
+ // +required
+ Chart string `json:"chart"`
+
+ // Version semver expression, ignored for charts from v1.GitRepository and
+ // v1beta2.Bucket sources. Defaults to latest when omitted.
+ // +kubebuilder:default:=*
+ // +optional
+ Version string `json:"version,omitempty"`
+
+ // The name and namespace of the v1.Source the chart is available at.
+ // +required
+ SourceRef CrossNamespaceObjectReference `json:"sourceRef"`
+
+ // Interval at which to check the v1.Source for updates. Defaults to
+ // 'HelmReleaseSpec.Interval'.
+ // +kubebuilder:validation:Type=string
+ // +kubebuilder:validation:Pattern="^([0-9]+(\\.[0-9]+)?(ms|s|m|h))+$"
+ // +optional
+ Interval *metav1.Duration `json:"interval,omitempty"`
+
+ // Determines what enables the creation of a new artifact. Valid values are
+ // ('ChartVersion', 'Revision').
+ // See the documentation of the values for an explanation on their behavior.
+ // Defaults to ChartVersion when omitted.
+ // +kubebuilder:validation:Enum=ChartVersion;Revision
+ // +kubebuilder:default:=ChartVersion
+ // +optional
+ ReconcileStrategy string `json:"reconcileStrategy,omitempty"`
+
+ // Alternative list of values files to use as the chart values (values.yaml
+ // is not included by default), expected to be a relative path in the SourceRef.
+ // Values files are merged in the order of this list with the last file overriding
+ // the first. Ignored when omitted.
+ // +optional
+ ValuesFiles []string `json:"valuesFiles,omitempty"`
+
+ // IgnoreMissingValuesFiles controls whether to silently ignore missing values files rather than failing.
+ // +optional
+ IgnoreMissingValuesFiles bool `json:"ignoreMissingValuesFiles,omitempty"`
+
+ // Verify contains the secret name containing the trusted public keys
+ // used to verify the signature and specifies which provider to use to check
+ // whether OCI image is authentic.
+ // This field is only supported for OCI sources.
+ // Chart dependencies, which are not bundled in the umbrella chart artifact,
+ // are not verified.
+ // +optional
+ Verify *HelmChartTemplateVerification `json:"verify,omitempty"`
+}
+
+// GetInterval returns the configured interval for the v1.HelmChart,
+// or the given default.
+func (in HelmChartTemplate) GetInterval(defaultInterval metav1.Duration) metav1.Duration {
+ if in.Spec.Interval == nil {
+ return defaultInterval
+ }
+ return *in.Spec.Interval
+}
+
+// GetNamespace returns the namespace targeted namespace for the
+// v1.HelmChart, or the given default.
+func (in HelmChartTemplate) GetNamespace(defaultNamespace string) string {
+ if in.Spec.SourceRef.Namespace == "" {
+ return defaultNamespace
+ }
+ return in.Spec.SourceRef.Namespace
+}
+
+// HelmChartTemplateVerification verifies the authenticity of an OCI Helm chart.
+type HelmChartTemplateVerification struct {
+ // Provider specifies the technology used to sign the OCI Helm chart.
+ // +kubebuilder:validation:Enum=cosign;notation
+ // +kubebuilder:default:=cosign
+ Provider string `json:"provider"`
+
+ // SecretRef specifies the Kubernetes Secret containing the
+ // trusted public keys.
+ // +optional
+ SecretRef *meta.LocalObjectReference `json:"secretRef,omitempty"`
+}
+
+// WaitStrategyName is a strategy for waiting for resources to be ready.
+type WaitStrategyName string
+
+const (
+ // WaitStrategyPoller is the strategy for polling resource statuses via kstatus.
+ WaitStrategyPoller WaitStrategyName = "poller"
+
+ // WaitStrategyLegacy is the legacy strategy for waiting for resources to be ready
+ // used in Helm v3.
+ WaitStrategyLegacy WaitStrategyName = "legacy"
+)
+
+// WaitStrategy defines Helm's wait strategy for waiting for applied
+// resources to become ready.
+type WaitStrategy struct {
+ // Name is Helm's wait strategy for waiting for applied resources to
+ // become ready. One of 'poller' or 'legacy'. The 'poller' strategy uses
+ // kstatus to poll resource statuses, while the 'legacy' strategy uses
+ // Helm v3's waiting logic.
+ // Defaults to 'poller', or to 'legacy' when UseHelm3Defaults feature
+ // gate is enabled.
+ // +kubebuilder:validation:Enum=poller;legacy
+ // +required
+ Name WaitStrategyName `json:"name"`
+}
+
+// GetWaitStrategy returns the wait strategy for the Helm actions.
+func (in *HelmRelease) GetWaitStrategy() WaitStrategyName {
+ if in.Spec.WaitStrategy != nil {
+ return in.Spec.WaitStrategy.Name
+ }
+ return ""
+}
+
+// Remediation defines a consistent interface for InstallRemediation and
+// UpgradeRemediation.
+// +kubebuilder:object:generate=false
+type Remediation interface {
+ GetRetries() int
+ MustIgnoreTestFailures(bool) bool
+ MustRemediateLastFailure() bool
+ GetStrategy() RemediationStrategy
+ GetFailureCount(hr *HelmRelease) int64
+ IncrementFailureCount(hr *HelmRelease)
+ RetriesExhausted(hr *HelmRelease) bool
+ IsUninstallAfterUpgrade() bool
+}
+
+// Strategy defines a consistent interface for InstallStrategy and
+// UpgradeStrategy.
+// +kubebuilder:object:generate=false
+type Strategy interface {
+ GetRetry(defaultToRetryOnFailure bool) Retry
+}
+
+// Retry defines a consistent interface for retry strategies from
+// InstallStrategy and UpgradeStrategy.
+// +kubebuilder:object:generate=false
+type Retry interface {
+ GetRetryInterval() time.Duration
+}
+
+// Install holds the configuration for Helm install actions performed for this
+// HelmRelease.
+type Install struct {
+ // Timeout is the time to wait for any individual Kubernetes operation (like
+ // Jobs for hooks) during the performance of a Helm install action. Defaults to
+ // 'HelmReleaseSpec.Timeout'.
+ // +kubebuilder:validation:Type=string
+ // +kubebuilder:validation:Pattern="^([0-9]+(\\.[0-9]+)?(ms|s|m|h))+$"
+ // +optional
+ Timeout *metav1.Duration `json:"timeout,omitempty"`
+
+ // Strategy defines the install strategy to use for this HelmRelease.
+ // Defaults to 'RemediateOnFailure', or 'RetryOnFailure' when the
+ // DefaultToRetryOnFailure feature gate is enabled.
+ // +optional
+ Strategy *InstallStrategy `json:"strategy,omitempty"`
+
+ // Remediation holds the remediation configuration for when the Helm install
+ // action for the HelmRelease fails. The default is to not perform any action.
+ // +optional
+ Remediation *InstallRemediation `json:"remediation,omitempty"`
+
+ // DisableTakeOwnership disables taking ownership of existing resources
+ // during the Helm install action. Defaults to false.
+ // +optional
+ DisableTakeOwnership bool `json:"disableTakeOwnership,omitempty"`
+
+ // DisableWait disables the waiting for resources to be ready after a Helm
+ // install has been performed.
+ // +optional
+ DisableWait bool `json:"disableWait,omitempty"`
+
+ // DisableWaitForJobs disables waiting for jobs to complete after a Helm
+ // install has been performed.
+ // +optional
+ DisableWaitForJobs bool `json:"disableWaitForJobs,omitempty"`
+
+ // DisableHooks prevents hooks from running during the Helm install action.
+ // +optional
+ DisableHooks bool `json:"disableHooks,omitempty"`
+
+ // DisableOpenAPIValidation prevents the Helm install action from validating
+ // rendered templates against the Kubernetes OpenAPI Schema.
+ // +optional
+ DisableOpenAPIValidation bool `json:"disableOpenAPIValidation,omitempty"`
+
+ // DisableSchemaValidation prevents the Helm install action from validating
+ // the values against the JSON Schema.
+ // +optional
+ DisableSchemaValidation bool `json:"disableSchemaValidation,omitempty"`
+
+ // Replace tells the Helm install action to re-use the 'ReleaseName', but only
+ // if that name is a deleted release which remains in the history.
+ // +optional
+ Replace bool `json:"replace,omitempty"`
+
+ // SkipCRDs tells the Helm install action to not install any CRDs. By default,
+ // CRDs are installed if not already present.
+ //
+ // Deprecated use CRD policy (`crds`) attribute with value `Skip` instead.
+ //
+ // +deprecated
+ // +optional
+ SkipCRDs bool `json:"skipCRDs,omitempty"`
+
+ // CRDs upgrade CRDs from the Helm Chart's crds directory according
+ // to the CRD upgrade policy provided here. Valid values are `Skip`,
+ // `Create` or `CreateReplace`. Default is `Create` and if omitted
+ // CRDs are installed but not updated.
+ //
+ // Skip: do neither install nor replace (update) any CRDs.
+ //
+ // Create: new CRDs are created, existing CRDs are neither updated nor deleted.
+ //
+ // CreateReplace: new CRDs are created, existing CRDs are updated (replaced)
+ // but not deleted.
+ //
+ // By default, CRDs are applied (installed) during Helm install action.
+ // With this option users can opt in to CRD replace existing CRDs on Helm
+ // install actions, which is not (yet) natively supported by Helm.
+ // https://helm.sh/docs/chart_best_practices/custom_resource_definitions.
+ //
+ // +kubebuilder:validation:Enum=Skip;Create;CreateReplace
+ // +optional
+ CRDs CRDsPolicy `json:"crds,omitempty"`
+
+ // CreateNamespace tells the Helm install action to create the
+ // HelmReleaseSpec.TargetNamespace if it does not exist yet.
+ // On uninstall, the namespace will not be garbage collected.
+ // +optional
+ CreateNamespace bool `json:"createNamespace,omitempty"`
+
+ // ServerSideApply enables server-side apply for resources during install.
+ // Defaults to true (or false when UseHelm3Defaults feature gate is enabled).
+ // +optional
+ ServerSideApply *bool `json:"serverSideApply,omitempty"`
+}
+
+// GetTimeout returns the configured timeout for the Helm install action,
+// or the given default.
+func (in Install) GetTimeout(defaultTimeout metav1.Duration) metav1.Duration {
+ if in.Timeout == nil {
+ return defaultTimeout
+ }
+ return *in.Timeout
+}
+
+// GetRemediation returns the configured Remediation for the Helm install action.
+func (in Install) GetRemediation() Remediation {
+ if in.Remediation == nil {
+ return InstallRemediation{}
+ }
+ return *in.Remediation
+}
+
+// GetRetry returns the configured retry strategy for the Helm install
+// action.
+func (in Install) GetRetry(defaultToRetryOnFailure bool) Retry {
+ if in.Strategy == nil {
+ if defaultToRetryOnFailure {
+ return &InstallStrategy{Name: string(ActionStrategyRetryOnFailure)}
+ }
+ return nil
+ }
+ if in.Strategy.Name != string(ActionStrategyRetryOnFailure) {
+ return nil
+ }
+ return in.Strategy
+}
+
+// GetDisableWait returns whether waiting is disabled for the Helm install action.
+func (in Install) GetDisableWait() bool {
+ return in.DisableWait
+}
+
+// InstallStrategy holds the configuration for Helm install strategy.
+// +kubebuilder:validation:XValidation:rule="!has(self.retryInterval) || self.name != 'RemediateOnFailure'", message=".retryInterval cannot be set when .name is 'RemediateOnFailure'"
+type InstallStrategy struct {
+ // Name of the install strategy.
+ // +kubebuilder:validation:Enum=RemediateOnFailure;RetryOnFailure
+ // +required
+ Name string `json:"name"`
+
+ // RetryInterval is the interval at which to retry a failed install.
+ // Can be used only when Name is set to RetryOnFailure.
+ // Defaults to '5m'.
+ // +kubebuilder:validation:Type=string
+ // +kubebuilder:validation:Pattern="^([0-9]+(\\.[0-9]+)?(ms|s|m|h))+$"
+ // +optional
+ RetryInterval *metav1.Duration `json:"retryInterval,omitempty"`
+}
+
+// GetRetryInterval returns the configured retry interval for the Helm install
+// action, or the default.
+func (in InstallStrategy) GetRetryInterval() time.Duration {
+ if in.RetryInterval == nil {
+ return 5 * time.Minute
+ }
+ return in.RetryInterval.Duration
+}
+
+// InstallRemediation holds the configuration for Helm install remediation.
+type InstallRemediation struct {
+ // Retries is the number of retries that should be attempted on failures before
+ // bailing. Remediation, using an uninstall, is performed between each attempt.
+ // Defaults to '0', a negative integer equals to unlimited retries.
+ // +optional
+ Retries int `json:"retries,omitempty"`
+
+ // IgnoreTestFailures tells the controller to skip remediation when the Helm
+ // tests are run after an install action but fail. Defaults to
+ // 'Test.IgnoreFailures'.
+ // +optional
+ IgnoreTestFailures *bool `json:"ignoreTestFailures,omitempty"`
+
+ // RemediateLastFailure tells the controller to remediate the last failure, when
+ // no retries remain. Defaults to 'false'.
+ // +optional
+ RemediateLastFailure *bool `json:"remediateLastFailure,omitempty"`
+}
+
+// GetRetries returns the number of retries that should be attempted on
+// failures.
+func (in InstallRemediation) GetRetries() int {
+ return in.Retries
+}
+
+// MustIgnoreTestFailures returns the configured IgnoreTestFailures or the given
+// default.
+func (in InstallRemediation) MustIgnoreTestFailures(def bool) bool {
+ if in.IgnoreTestFailures == nil {
+ return def
+ }
+ return *in.IgnoreTestFailures
+}
+
+// MustRemediateLastFailure returns whether to remediate the last failure when
+// no retries remain.
+func (in InstallRemediation) MustRemediateLastFailure() bool {
+ if in.RemediateLastFailure == nil {
+ return false
+ }
+ return *in.RemediateLastFailure
+}
+
+// GetStrategy returns the strategy to use for failure remediation.
+func (in InstallRemediation) GetStrategy() RemediationStrategy {
+ return UninstallRemediationStrategy
+}
+
+// GetFailureCount gets the failure count.
+func (in InstallRemediation) GetFailureCount(hr *HelmRelease) int64 {
+ return hr.Status.InstallFailures
+}
+
+// IncrementFailureCount increments the failure count.
+func (in InstallRemediation) IncrementFailureCount(hr *HelmRelease) {
+ hr.Status.InstallFailures++
+}
+
+// RetriesExhausted returns true if there are no remaining retries.
+func (in InstallRemediation) RetriesExhausted(hr *HelmRelease) bool {
+ return in.Retries >= 0 && in.GetFailureCount(hr) > int64(in.Retries)
+}
+
+// IsUninstallAfterUpgrade returns false.
+func (in InstallRemediation) IsUninstallAfterUpgrade() bool {
+ return false
+}
+
+// CRDsPolicy defines the install/upgrade approach to use for CRDs when
+// installing or upgrading a HelmRelease.
+type CRDsPolicy string
+
+const (
+ // Skip CRDs do neither install nor replace (update) any CRDs.
+ Skip CRDsPolicy = "Skip"
+ // Create CRDs which do not already exist, do not replace (update) already existing
+ // CRDs and keep (do not delete) CRDs which no longer exist in the current release.
+ Create CRDsPolicy = "Create"
+ // Create CRDs which do not already exist, Replace (update) already existing CRDs
+ // and keep (do not delete) CRDs which no longer exist in the current release.
+ CreateReplace CRDsPolicy = "CreateReplace"
+)
+
+// ServerSideApplyMode defines the server-side apply mode for Helm upgrade and
+// rollback actions.
+type ServerSideApplyMode string
+
+const (
+ // ServerSideApplyEnabled enables server-side apply for resources.
+ ServerSideApplyEnabled ServerSideApplyMode = "enabled"
+
+ // ServerSideApplyDisabled disables server-side apply for resources.
+ ServerSideApplyDisabled ServerSideApplyMode = "disabled"
+
+ // ServerSideApplyAuto uses the release's previous apply method.
+ ServerSideApplyAuto ServerSideApplyMode = "auto"
+)
+
+// Upgrade holds the configuration for Helm upgrade actions for this
+// HelmRelease.
+type Upgrade struct {
+ // Timeout is the time to wait for any individual Kubernetes operation (like
+ // Jobs for hooks) during the performance of a Helm upgrade action. Defaults to
+ // 'HelmReleaseSpec.Timeout'.
+ // +kubebuilder:validation:Type=string
+ // +kubebuilder:validation:Pattern="^([0-9]+(\\.[0-9]+)?(ms|s|m|h))+$"
+ // +optional
+ Timeout *metav1.Duration `json:"timeout,omitempty"`
+
+ // Strategy defines the upgrade strategy to use for this HelmRelease.
+ // Defaults to 'RemediateOnFailure', or 'RetryOnFailure' when the
+ // DefaultToRetryOnFailure feature gate is enabled.
+ // +optional
+ Strategy *UpgradeStrategy `json:"strategy,omitempty"`
+
+ // Remediation holds the remediation configuration for when the Helm upgrade
+ // action for the HelmRelease fails. The default is to not perform any action.
+ // +optional
+ Remediation *UpgradeRemediation `json:"remediation,omitempty"`
+
+ // DisableTakeOwnership disables taking ownership of existing resources
+ // during the Helm upgrade action. Defaults to false.
+ // +optional
+ DisableTakeOwnership bool `json:"disableTakeOwnership,omitempty"`
+
+ // DisableWait disables the waiting for resources to be ready after a Helm
+ // upgrade has been performed.
+ // +optional
+ DisableWait bool `json:"disableWait,omitempty"`
+
+ // DisableWaitForJobs disables waiting for jobs to complete after a Helm
+ // upgrade has been performed.
+ // +optional
+ DisableWaitForJobs bool `json:"disableWaitForJobs,omitempty"`
+
+ // DisableHooks prevents hooks from running during the Helm upgrade action.
+ // +optional
+ DisableHooks bool `json:"disableHooks,omitempty"`
+
+ // DisableOpenAPIValidation prevents the Helm upgrade action from validating
+ // rendered templates against the Kubernetes OpenAPI Schema.
+ // +optional
+ DisableOpenAPIValidation bool `json:"disableOpenAPIValidation,omitempty"`
+
+ // DisableSchemaValidation prevents the Helm upgrade action from validating
+ // the values against the JSON Schema.
+ // +optional
+ DisableSchemaValidation bool `json:"disableSchemaValidation,omitempty"`
+
+ // Force forces resource updates through a replacement strategy.
+ // +optional
+ Force bool `json:"force,omitempty"`
+
+ // PreserveValues will make Helm reuse the last release's values and merge in
+ // overrides from 'Values'. Setting this flag makes the HelmRelease
+ // non-declarative.
+ // +optional
+ PreserveValues bool `json:"preserveValues,omitempty"`
+
+ // CleanupOnFail allows deletion of new resources created during the Helm
+ // upgrade action when it fails.
+ // +optional
+ CleanupOnFail bool `json:"cleanupOnFail,omitempty"`
+
+ // CRDs upgrade CRDs from the Helm Chart's crds directory according
+ // to the CRD upgrade policy provided here. Valid values are `Skip`,
+ // `Create` or `CreateReplace`. Default is `Skip` and if omitted
+ // CRDs are neither installed nor upgraded.
+ //
+ // Skip: do neither install nor replace (update) any CRDs.
+ //
+ // Create: new CRDs are created, existing CRDs are neither updated nor deleted.
+ //
+ // CreateReplace: new CRDs are created, existing CRDs are updated (replaced)
+ // but not deleted.
+ //
+ // By default, CRDs are not applied during Helm upgrade action. With this
+ // option users can opt-in to CRD upgrade, which is not (yet) natively supported by Helm.
+ // https://helm.sh/docs/chart_best_practices/custom_resource_definitions.
+ //
+ // +kubebuilder:validation:Enum=Skip;Create;CreateReplace
+ // +optional
+ CRDs CRDsPolicy `json:"crds,omitempty"`
+
+ // ServerSideApply enables server-side apply for resources during upgrade.
+ // Can be "enabled", "disabled", or "auto".
+ // When "auto", server-side apply usage will be based on the release's previous usage.
+ // Defaults to "auto".
+ // +kubebuilder:validation:Enum=enabled;disabled;auto
+ // +optional
+ ServerSideApply ServerSideApplyMode `json:"serverSideApply,omitempty"`
+}
+
+// GetTimeout returns the configured timeout for the Helm upgrade action, or the
+// given default.
+func (in Upgrade) GetTimeout(defaultTimeout metav1.Duration) metav1.Duration {
+ if in.Timeout == nil {
+ return defaultTimeout
+ }
+ return *in.Timeout
+}
+
+// GetRemediation returns the configured Remediation for the Helm upgrade
+// action.
+func (in Upgrade) GetRemediation() Remediation {
+ if in.Remediation == nil {
+ return UpgradeRemediation{}
+ }
+ return *in.Remediation
+}
+
+// GetRetry returns the configured retry strategy for the Helm upgrade
+// action.
+func (in Upgrade) GetRetry(defaultToRetryOnFailure bool) Retry {
+ if in.Strategy == nil {
+ if defaultToRetryOnFailure {
+ return &UpgradeStrategy{Name: string(ActionStrategyRetryOnFailure)}
+ }
+ return nil
+ }
+ if in.Strategy.Name != string(ActionStrategyRetryOnFailure) {
+ return nil
+ }
+ return in.Strategy
+}
+
+// GetDisableWait returns whether waiting is disabled for the Helm upgrade action.
+func (in Upgrade) GetDisableWait() bool {
+ return in.DisableWait
+}
+
+// UpgradeStrategy holds the configuration for Helm upgrade strategy.
+// +kubebuilder:validation:XValidation:rule="!has(self.retryInterval) || self.name == 'RetryOnFailure'", message=".retryInterval can only be set when .name is 'RetryOnFailure'"
+type UpgradeStrategy struct {
+ // Name of the upgrade strategy.
+ // +kubebuilder:validation:Enum=RemediateOnFailure;RetryOnFailure
+ // +required
+ Name string `json:"name"`
+
+ // RetryInterval is the interval at which to retry a failed upgrade.
+ // Can be used only when Name is set to RetryOnFailure.
+ // Defaults to '5m'.
+ // +kubebuilder:validation:Type=string
+ // +kubebuilder:validation:Pattern="^([0-9]+(\\.[0-9]+)?(ms|s|m|h))+$"
+ // +optional
+ RetryInterval *metav1.Duration `json:"retryInterval,omitempty"`
+}
+
+// GetRetryInterval returns the configured retry interval for the Helm upgrade
+// action, or the default.
+func (in UpgradeStrategy) GetRetryInterval() time.Duration {
+ if in.RetryInterval == nil {
+ return 5 * time.Minute
+ }
+ return in.RetryInterval.Duration
+}
+
+// UpgradeRemediation holds the configuration for Helm upgrade remediation.
+type UpgradeRemediation struct {
+ // Retries is the number of retries that should be attempted on failures before
+ // bailing. Remediation, using 'Strategy', is performed between each attempt.
+ // Defaults to '0', a negative integer equals to unlimited retries.
+ // +optional
+ Retries int `json:"retries,omitempty"`
+
+ // IgnoreTestFailures tells the controller to skip remediation when the Helm
+ // tests are run after an upgrade action but fail.
+ // Defaults to 'Test.IgnoreFailures'.
+ // +optional
+ IgnoreTestFailures *bool `json:"ignoreTestFailures,omitempty"`
+
+ // RemediateLastFailure tells the controller to remediate the last failure, when
+ // no retries remain. Defaults to 'false' unless 'Retries' is greater than 0.
+ // +optional
+ RemediateLastFailure *bool `json:"remediateLastFailure,omitempty"`
+
+ // Strategy to use for failure remediation. Defaults to 'rollback'.
+ // +kubebuilder:validation:Enum=rollback;uninstall
+ // +optional
+ Strategy *RemediationStrategy `json:"strategy,omitempty"`
+}
+
+// GetRetries returns the number of retries that should be attempted on
+// failures.
+func (in UpgradeRemediation) GetRetries() int {
+ return in.Retries
+}
+
+// MustIgnoreTestFailures returns the configured IgnoreTestFailures or the given
+// default.
+func (in UpgradeRemediation) MustIgnoreTestFailures(def bool) bool {
+ if in.IgnoreTestFailures == nil {
+ return def
+ }
+ return *in.IgnoreTestFailures
+}
+
+// MustRemediateLastFailure returns whether to remediate the last failure when
+// no retries remain.
+func (in UpgradeRemediation) MustRemediateLastFailure() bool {
+ if in.RemediateLastFailure == nil {
+ return in.Retries > 0
+ }
+ return *in.RemediateLastFailure
+}
+
+// GetStrategy returns the strategy to use for failure remediation.
+func (in UpgradeRemediation) GetStrategy() RemediationStrategy {
+ if in.Strategy == nil {
+ return RollbackRemediationStrategy
+ }
+ return *in.Strategy
+}
+
+// GetFailureCount gets the failure count.
+func (in UpgradeRemediation) GetFailureCount(hr *HelmRelease) int64 {
+ return hr.Status.UpgradeFailures
+}
+
+// IncrementFailureCount increments the failure count.
+func (in UpgradeRemediation) IncrementFailureCount(hr *HelmRelease) {
+ hr.Status.UpgradeFailures++
+}
+
+// RetriesExhausted returns true if there are no remaining retries.
+func (in UpgradeRemediation) RetriesExhausted(hr *HelmRelease) bool {
+ return in.Retries >= 0 && in.GetFailureCount(hr) > int64(in.Retries)
+}
+
+// IsUninstallAfterUpgrade returns true if the remediation strategy is uninstall.
+func (in UpgradeRemediation) IsUninstallAfterUpgrade() bool {
+ return in.GetStrategy() == UninstallRemediationStrategy
+}
+
+// ActionStrategyName is a valid name for an action strategy.
+type ActionStrategyName string
+
+const (
+ // ActionStrategyRemediateOnFailure is the action strategy name for
+ // remediate on failure.
+ ActionStrategyRemediateOnFailure ActionStrategyName = "RemediateOnFailure"
+
+ // ActionStrategyRetryOnFailure is the action strategy name for retry on
+ // failure.
+ ActionStrategyRetryOnFailure ActionStrategyName = "RetryOnFailure"
+)
+
+// RemediationStrategy returns the strategy to use to remediate a failed install
+// or upgrade.
+type RemediationStrategy string
+
+const (
+ // RollbackRemediationStrategy represents a Helm remediation strategy of Helm
+ // rollback.
+ RollbackRemediationStrategy RemediationStrategy = "rollback"
+
+ // UninstallRemediationStrategy represents a Helm remediation strategy of Helm
+ // uninstall.
+ UninstallRemediationStrategy RemediationStrategy = "uninstall"
+)
+
+// Test holds the configuration for Helm test actions for this HelmRelease.
+type Test struct {
+ // Enable enables Helm test actions for this HelmRelease after an Helm install
+ // or upgrade action has been performed.
+ // +optional
+ Enable bool `json:"enable,omitempty"`
+
+ // Timeout is the time to wait for any individual Kubernetes operation during
+ // the performance of a Helm test action. Defaults to 'HelmReleaseSpec.Timeout'.
+ // +kubebuilder:validation:Type=string
+ // +kubebuilder:validation:Pattern="^([0-9]+(\\.[0-9]+)?(ms|s|m|h))+$"
+ // +optional
+ Timeout *metav1.Duration `json:"timeout,omitempty"`
+
+ // IgnoreFailures tells the controller to skip remediation when the Helm tests
+ // are run but fail. Can be overwritten for tests run after install or upgrade
+ // actions in 'Install.IgnoreTestFailures' and 'Upgrade.IgnoreTestFailures'.
+ // +optional
+ IgnoreFailures bool `json:"ignoreFailures,omitempty"`
+
+ // Filters is a list of tests to run or exclude from running.
+ Filters *[]Filter `json:"filters,omitempty"`
+}
+
+// GetTimeout returns the configured timeout for the Helm test action,
+// or the given default.
+func (in Test) GetTimeout(defaultTimeout metav1.Duration) metav1.Duration {
+ if in.Timeout == nil {
+ return defaultTimeout
+ }
+ return *in.Timeout
+}
+
+// Filter holds the configuration for individual Helm test filters.
+type Filter struct {
+ // Name is the name of the test.
+ // +kubebuilder:validation:MinLength=1
+ // +kubebuilder:validation:MaxLength=253
+ // +required
+ Name string `json:"name"`
+ // Exclude specifies whether the named test should be excluded.
+ // +optional
+ Exclude bool `json:"exclude,omitempty"`
+}
+
+// GetFilters returns the configured filters for the Helm test action/
+func (in Test) GetFilters() []Filter {
+ if in.Filters == nil {
+ var filters []Filter
+ return filters
+ }
+ return *in.Filters
+}
+
+// Rollback holds the configuration for Helm rollback actions for this
+// HelmRelease.
+type Rollback struct {
+ // Timeout is the time to wait for any individual Kubernetes operation (like
+ // Jobs for hooks) during the performance of a Helm rollback action. Defaults to
+ // 'HelmReleaseSpec.Timeout'.
+ // +kubebuilder:validation:Type=string
+ // +kubebuilder:validation:Pattern="^([0-9]+(\\.[0-9]+)?(ms|s|m|h))+$"
+ // +optional
+ Timeout *metav1.Duration `json:"timeout,omitempty"`
+
+ // DisableWait disables the waiting for resources to be ready after a Helm
+ // rollback has been performed.
+ // +optional
+ DisableWait bool `json:"disableWait,omitempty"`
+
+ // DisableWaitForJobs disables waiting for jobs to complete after a Helm
+ // rollback has been performed.
+ // +optional
+ DisableWaitForJobs bool `json:"disableWaitForJobs,omitempty"`
+
+ // DisableHooks prevents hooks from running during the Helm rollback action.
+ // +optional
+ DisableHooks bool `json:"disableHooks,omitempty"`
+
+ // Recreate performs pod restarts for any managed workloads.
+ //
+ // Deprecated: This behavior was deprecated in Helm 3:
+ // - Deprecation: https://github.com/helm/helm/pull/6463
+ // - Removal: https://github.com/helm/helm/pull/31023
+ // After helm-controller was upgraded to the Helm 4 SDK,
+ // this field is no longer functional and will print a
+ // warning if set to true. It will also be removed in a
+ // future release.
+ // +optional
+ Recreate bool `json:"recreate,omitempty"`
+
+ // Force forces resource updates through a replacement strategy.
+ // +optional
+ Force bool `json:"force,omitempty"`
+
+ // CleanupOnFail allows deletion of new resources created during the Helm
+ // rollback action when it fails.
+ // +optional
+ CleanupOnFail bool `json:"cleanupOnFail,omitempty"`
+
+ // ServerSideApply enables server-side apply for resources during rollback.
+ // Can be "enabled", "disabled", or "auto".
+ // When "auto", server-side apply usage will be based on the release's previous usage.
+ // Defaults to "auto".
+ // +kubebuilder:validation:Enum=enabled;disabled;auto
+ // +optional
+ ServerSideApply ServerSideApplyMode `json:"serverSideApply,omitempty"`
+}
+
+// GetTimeout returns the configured timeout for the Helm rollback action, or
+// the given default.
+func (in Rollback) GetTimeout(defaultTimeout metav1.Duration) metav1.Duration {
+ if in.Timeout == nil {
+ return defaultTimeout
+ }
+ return *in.Timeout
+}
+
+// GetDisableWait returns whether waiting is disabled for the Helm rollback action.
+func (in Rollback) GetDisableWait() bool {
+ return in.DisableWait
+}
+
+// Uninstall holds the configuration for Helm uninstall actions for this
+// HelmRelease.
+type Uninstall struct {
+ // Timeout is the time to wait for any individual Kubernetes operation (like
+ // Jobs for hooks) during the performance of a Helm uninstall action. Defaults
+ // to 'HelmReleaseSpec.Timeout'.
+ // +kubebuilder:validation:Type=string
+ // +kubebuilder:validation:Pattern="^([0-9]+(\\.[0-9]+)?(ms|s|m|h))+$"
+ // +optional
+ Timeout *metav1.Duration `json:"timeout,omitempty"`
+
+ // DisableHooks prevents hooks from running during the Helm rollback action.
+ // +optional
+ DisableHooks bool `json:"disableHooks,omitempty"`
+
+ // KeepHistory tells Helm to remove all associated resources and mark the
+ // release as deleted, but retain the release history.
+ // +optional
+ KeepHistory bool `json:"keepHistory,omitempty"`
+
+ // DisableWait disables waiting for all the resources to be deleted after
+ // a Helm uninstall is performed.
+ // +optional
+ DisableWait bool `json:"disableWait,omitempty"`
+
+ // DeletionPropagation specifies the deletion propagation policy when
+ // a Helm uninstall is performed.
+ // +kubebuilder:default=background
+ // +kubebuilder:validation:Enum=background;foreground;orphan
+ // +optional
+ DeletionPropagation *string `json:"deletionPropagation,omitempty"`
+}
+
+// GetTimeout returns the configured timeout for the Helm uninstall action, or
+// the given default.
+func (in Uninstall) GetTimeout(defaultTimeout metav1.Duration) metav1.Duration {
+ if in.Timeout == nil {
+ return defaultTimeout
+ }
+ return *in.Timeout
+}
+
+// GetDeletionPropagation returns the configured deletion propagation policy
+// for the Helm uninstall action, or 'background'.
+func (in Uninstall) GetDeletionPropagation() string {
+ if in.DeletionPropagation == nil {
+ return "background"
+ }
+ return *in.DeletionPropagation
+}
+
+// GetDisableWait returns whether waiting is disabled for the Helm uninstall action.
+func (in Uninstall) GetDisableWait() bool {
+ return in.DisableWait
+}
+
+// ReleaseAction is the action to perform a Helm release.
+type ReleaseAction string
+
+const (
+ // ReleaseActionInstall represents a Helm install action.
+ ReleaseActionInstall ReleaseAction = "install"
+ // ReleaseActionUpgrade represents a Helm upgrade action.
+ ReleaseActionUpgrade ReleaseAction = "upgrade"
+ // ReleaseActionRollback represents a Helm rollback action.
+ ReleaseActionRollback ReleaseAction = "rollback"
+ // ReleaseActionUninstall represents a Helm uninstall action.
+ ReleaseActionUninstall ReleaseAction = "uninstall"
+ // ReleaseActionUninstallRemediation represents a Helm uninstall action for remediation.
+ ReleaseActionUninstallRemediation ReleaseAction = "uninstall-remediation"
+)
+
+// HelmReleaseStatus defines the observed state of a HelmRelease.
+type HelmReleaseStatus struct {
+ // ObservedGeneration is the last observed generation.
+ // +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"`
+
+ // ObservedCommonMetadataDigest is the digest for the common metadata of
+ // the last successful reconciliation attempt.
+ // +optional
+ ObservedCommonMetadataDigest string `json:"observedCommonMetadataDigest,omitempty"`
+
+ // LastAttemptedGeneration is the last generation the controller attempted
+ // to reconcile.
+ // +optional
+ LastAttemptedGeneration int64 `json:"lastAttemptedGeneration,omitempty"`
+
+ // Conditions holds the conditions for the HelmRelease.
+ // +optional
+ Conditions []metav1.Condition `json:"conditions,omitempty"`
+
+ // HelmChart is the namespaced name of the HelmChart resource created by
+ // the controller for the HelmRelease.
+ // +optional
+ HelmChart string `json:"helmChart,omitempty"`
+
+ // StorageNamespace is the namespace of the Helm release storage for the
+ // current release.
+ // +kubebuilder:validation:MaxLength=63
+ // +kubebuilder:validation:MinLength=1
+ // +kubebuilder:validation:Optional
+ // +optional
+ StorageNamespace string `json:"storageNamespace,omitempty"`
+
+ // History holds the history of Helm releases performed for this HelmRelease
+ // up to the last successfully completed release.
+ // +optional
+ History Snapshots `json:"history,omitempty"`
+
+ // Inventory contains the list of Kubernetes resource object references
+ // that have been applied for this release.
+ // +optional
+ Inventory *ResourceInventory `json:"inventory,omitempty"`
+
+ // LastAttemptedReleaseAction is the last release action performed for this
+ // HelmRelease. It is used to determine the active retry or remediation
+ // strategy.
+ // +kubebuilder:validation:Enum=install;upgrade
+ // +optional
+ LastAttemptedReleaseAction ReleaseAction `json:"lastAttemptedReleaseAction,omitempty"`
+
+ // LastAttemptedReleaseActionDuration is the duration of the last
+ // release action performed for this HelmRelease.
+ // +kubebuilder:validation:Type=string
+ // +optional
+ LastAttemptedReleaseActionDuration *metav1.Duration `json:"lastAttemptedReleaseActionDuration,omitempty"`
+
+ // Failures is the reconciliation failure count against the latest desired
+ // state. It is reset after a successful reconciliation.
+ // +optional
+ Failures int64 `json:"failures,omitempty"`
+
+ // InstallFailures is the install failure count against the latest desired
+ // state. It is reset after a successful reconciliation.
+ // +optional
+ InstallFailures int64 `json:"installFailures,omitempty"`
+
+ // UpgradeFailures is the upgrade failure count against the latest desired
+ // state. It is reset after a successful reconciliation.
+ // +optional
+ UpgradeFailures int64 `json:"upgradeFailures,omitempty"`
+
+ // LastAttemptedRevision is the Source revision of the last reconciliation
+ // attempt. For OCIRepository sources, the 12 first characters of the digest are
+ // appended to the chart version e.g. "1.2.3+1234567890ab".
+ // +optional
+ LastAttemptedRevision string `json:"lastAttemptedRevision,omitempty"`
+
+ // LastAttemptedRevisionDigest is the digest of the last reconciliation attempt.
+ // This is only set for OCIRepository sources.
+ // +optional
+ LastAttemptedRevisionDigest string `json:"lastAttemptedRevisionDigest,omitempty"`
+
+ // LastAttemptedValuesChecksum is the SHA1 checksum for the values of the last
+ // reconciliation attempt.
+ //
+ // Deprecated: Use LastAttemptedConfigDigest instead.
+ // +optional
+ LastAttemptedValuesChecksum string `json:"lastAttemptedValuesChecksum,omitempty"`
+
+ // LastReleaseRevision is the revision of the last successful Helm release.
+ //
+ // Deprecated: Use History instead.
+ // +optional
+ LastReleaseRevision int `json:"lastReleaseRevision,omitempty"`
+
+ // LastAttemptedConfigDigest is the digest for the config (better known as
+ // "values") of the last reconciliation attempt.
+ // +optional
+ LastAttemptedConfigDigest string `json:"lastAttemptedConfigDigest,omitempty"`
+
+ // LastHandledResetAt holds the value of the most recent reset request
+ // value, so a change of the annotation value can be detected.
+ // +optional
+ LastHandledResetAt string `json:"lastHandledResetAt,omitempty"`
+
+ meta.ReconcileRequestStatus `json:",inline"`
+ meta.ForceRequestStatus `json:",inline"`
+}
+
+// ClearHistory clears the History.
+func (in *HelmReleaseStatus) ClearHistory() {
+ in.History = nil
+}
+
+// ClearFailures clears the failure counters.
+func (in *HelmReleaseStatus) ClearFailures() {
+ in.Failures = 0
+ in.InstallFailures = 0
+ in.UpgradeFailures = 0
+}
+
+// GetHelmChart returns the namespace and name of the HelmChart.
+func (in HelmReleaseStatus) GetHelmChart() (string, string) {
+ if in.HelmChart == "" {
+ return "", ""
+ }
+ if split := strings.Split(in.HelmChart, string(types.Separator)); len(split) > 1 {
+ return split[0], split[1]
+ }
+ return "", ""
+}
+
+func (in *HelmReleaseStatus) GetLastAttemptedRevision() string {
+ return in.LastAttemptedRevision
+}
+
+const (
+ // SourceIndexKey is the key used for indexing HelmReleases based on
+ // their sources.
+ SourceIndexKey string = ".metadata.source"
+)
+
+// +genclient
+// +kubebuilder:object:root=true
+// +kubebuilder:resource:shortName=hr
+// +kubebuilder:storageversion
+// +kubebuilder:subresource:status
+// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description=""
+// +kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].status",description=""
+// +kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].message",description=""
+
+// HelmRelease is the Schema for the helmreleases API
+type HelmRelease struct {
+ metav1.TypeMeta `json:",inline"`
+ metav1.ObjectMeta `json:"metadata,omitempty"`
+
+ Spec HelmReleaseSpec `json:"spec,omitempty"`
+ // +kubebuilder:default:={"observedGeneration":-1}
+ Status HelmReleaseStatus `json:"status,omitempty"`
+}
+
+// GetDriftDetection returns the configuration for detecting and handling
+// differences between the manifest in the Helm storage and the resources
+// currently existing in the cluster.
+func (in *HelmRelease) GetDriftDetection() DriftDetection {
+ if in.Spec.DriftDetection == nil {
+ return DriftDetection{}
+ }
+ return *in.Spec.DriftDetection
+}
+
+// GetInstall returns the configuration for Helm install actions for the
+// HelmRelease.
+func (in *HelmRelease) GetInstall() Install {
+ if in.Spec.Install == nil {
+ return Install{}
+ }
+ return *in.Spec.Install
+}
+
+// GetUpgrade returns the configuration for Helm upgrade actions for this
+// HelmRelease.
+func (in *HelmRelease) GetUpgrade() Upgrade {
+ if in.Spec.Upgrade == nil {
+ return Upgrade{}
+ }
+ return *in.Spec.Upgrade
+}
+
+// GetTest returns the configuration for Helm test actions for this HelmRelease.
+func (in *HelmRelease) GetTest() Test {
+ if in.Spec.Test == nil {
+ return Test{}
+ }
+ return *in.Spec.Test
+}
+
+// GetRollback returns the configuration for Helm rollback actions for this
+// HelmRelease.
+func (in *HelmRelease) GetRollback() Rollback {
+ if in.Spec.Rollback == nil {
+ return Rollback{}
+ }
+ return *in.Spec.Rollback
+}
+
+// GetUninstall returns the configuration for Helm uninstall actions for this
+// HelmRelease.
+func (in *HelmRelease) GetUninstall() Uninstall {
+ if in.Spec.Uninstall == nil {
+ return Uninstall{}
+ }
+ return *in.Spec.Uninstall
+}
+
+// GetActiveRemediation returns the active Remediation configuration for the
+// HelmRelease.
+func (in HelmRelease) GetActiveRemediation() Remediation {
+ switch in.Status.LastAttemptedReleaseAction {
+ case ReleaseActionInstall:
+ return in.GetInstall().GetRemediation()
+ case ReleaseActionUpgrade:
+ return in.GetUpgrade().GetRemediation()
+ default:
+ return nil
+ }
+}
+
+// GetActiveRetry returns the active retry configuration for the
+// HelmRelease. When defaultToRetryOnFailure is true and no strategy
+// is explicitly configured, it defaults to RetryOnFailure.
+func (in HelmRelease) GetActiveRetry(defaultToRetryOnFailure bool) Retry {
+ switch in.Status.LastAttemptedReleaseAction {
+ case ReleaseActionInstall:
+ return in.GetInstall().GetRetry(defaultToRetryOnFailure)
+ case ReleaseActionUpgrade:
+ return in.GetUpgrade().GetRetry(defaultToRetryOnFailure)
+ default:
+ return nil
+ }
+}
+
+// GetRequeueAfter returns the duration after which the HelmRelease
+// must be reconciled again.
+func (in HelmRelease) GetRequeueAfter() time.Duration {
+ return in.Spec.Interval.Duration
+}
+
+// GetValues unmarshals the raw values to a map[string]any and returns
+// the result.
+func (in HelmRelease) GetValues() map[string]any {
+ var values map[string]any
+ if in.Spec.Values != nil {
+ _ = yaml.Unmarshal(in.Spec.Values.Raw, &values)
+ }
+ return values
+}
+
+// GetReleaseName returns the configured release name, or a composition of
+// '[TargetNamespace-]Name'.
+func (in HelmRelease) GetReleaseName() string {
+ if in.Spec.ReleaseName != "" {
+ return in.Spec.ReleaseName
+ }
+ if in.Spec.TargetNamespace != "" {
+ return strings.Join([]string{in.Spec.TargetNamespace, in.Name}, "-")
+ }
+ return in.Name
+}
+
+// GetReleaseNamespace returns the configured TargetNamespace, or the namespace
+// of the HelmRelease.
+func (in HelmRelease) GetReleaseNamespace() string {
+ if in.Spec.TargetNamespace != "" {
+ return in.Spec.TargetNamespace
+ }
+ return in.Namespace
+}
+
+// GetStorageNamespace returns the configured StorageNamespace for helm, or the namespace
+// of the HelmRelease.
+func (in HelmRelease) GetStorageNamespace() string {
+ if in.Spec.StorageNamespace != "" {
+ return in.Spec.StorageNamespace
+ }
+ return in.Namespace
+}
+
+// GetHelmChartName returns the name used by the controller for the HelmChart creation.
+func (in HelmRelease) GetHelmChartName() string {
+ return strings.Join([]string{in.Namespace, in.Name}, "-")
+}
+
+// GetTimeout returns the configured Timeout, or the default of 300s.
+func (in HelmRelease) GetTimeout() metav1.Duration {
+ if in.Spec.Timeout == nil {
+ return metav1.Duration{Duration: 300 * time.Second}
+ }
+ return *in.Spec.Timeout
+}
+
+// GetMaxHistory returns the configured MaxHistory, or the default of 5.
+func (in HelmRelease) GetMaxHistory() int {
+ if in.Spec.MaxHistory == nil {
+ return defaultMaxHistory
+ }
+ return *in.Spec.MaxHistory
+}
+
+// UsePersistentClient returns the configured PersistentClient, or the default
+// of true.
+func (in HelmRelease) UsePersistentClient() bool {
+ if in.Spec.PersistentClient == nil {
+ return true
+ }
+ return *in.Spec.PersistentClient
+}
+
+// GetDependsOn returns the dependencies as a list of meta.NamespacedObjectReference.
+//
+// This function makes the HelmRelease type conformant with the meta.ObjectWithDependencies interface
+// and allows the controller-runtime to index HelmReleases by their dependencies.
+func (in HelmRelease) GetDependsOn() []meta.NamespacedObjectReference {
+ deps := make([]meta.NamespacedObjectReference, len(in.Spec.DependsOn))
+ for i := range in.Spec.DependsOn {
+ deps[i] = meta.NamespacedObjectReference{
+ Name: in.Spec.DependsOn[i].Name,
+ Namespace: in.Spec.DependsOn[i].Namespace,
+ }
+ }
+ return deps
+}
+
+// GetConditions returns the status conditions of the object.
+func (in HelmRelease) GetConditions() []metav1.Condition {
+ return in.Status.Conditions
+}
+
+// SetConditions sets the status conditions on the object.
+func (in *HelmRelease) SetConditions(conditions []metav1.Condition) {
+ in.Status.Conditions = conditions
+}
+
+// HasChartRef returns true if the HelmRelease has a ChartRef.
+func (in *HelmRelease) HasChartRef() bool {
+ return in.Spec.ChartRef != nil
+}
+
+// HasChartTemplate returns true if the HelmRelease has a ChartTemplate.
+func (in *HelmRelease) HasChartTemplate() bool {
+ return in.Spec.Chart != nil
+}
+
+// GetLastHandledReconcileRequest returns the last handled reconcile request.
+func (in HelmRelease) GetLastHandledReconcileRequest() string {
+ return in.Status.GetLastHandledReconcileRequest()
+}
+
+// GetLastHandledForceRequestStatus returns the last handled force request status.
+func (in *HelmRelease) GetLastHandledForceRequestStatus() *string {
+ return &in.Status.LastHandledForceAt
+}
+
+// +kubebuilder:object:root=true
+
+// HelmReleaseList contains a list of HelmRelease objects.
+type HelmReleaseList struct {
+ metav1.TypeMeta `json:",inline"`
+ metav1.ListMeta `json:"metadata,omitempty"`
+ Items []HelmRelease `json:"items"`
+}
+
+func init() {
+ SchemeBuilder.Register(&HelmRelease{}, &HelmReleaseList{})
+}
diff --git a/api/v2/inventory_types.go b/api/v2/inventory_types.go
new file mode 100644
index 000000000..e6efdcb9a
--- /dev/null
+++ b/api/v2/inventory_types.go
@@ -0,0 +1,44 @@
+/*
+Copyright 2026 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package v2
+
+const (
+ // InventoryBuildFailedReason represents the fact that the inventory
+ // build failed.
+ InventoryBuildFailedReason string = "InventoryBuildFailed"
+
+ // NamespaceCheckSkippedReason represents the fact that namespace
+ // scope check was skipped due to RESTMapper error.
+ NamespaceCheckSkippedReason string = "NamespaceCheckSkipped"
+)
+
+// ResourceInventory contains a list of Kubernetes resource object references
+// that have been applied by a HelmRelease.
+type ResourceInventory struct {
+ // Entries of Kubernetes resource object references.
+ Entries []ResourceRef `json:"entries"`
+}
+
+// ResourceRef contains the information necessary to locate a resource within a cluster.
+type ResourceRef struct {
+ // ID is the string representation of the Kubernetes resource object's metadata,
+ // in the format '___'.
+ ID string `json:"id"`
+
+ // Version is the API version of the Kubernetes resource object's kind.
+ Version string `json:"v"`
+}
diff --git a/api/v2/reference_types.go b/api/v2/reference_types.go
new file mode 100644
index 000000000..9f3ee4a1c
--- /dev/null
+++ b/api/v2/reference_types.go
@@ -0,0 +1,90 @@
+/*
+Copyright 2024 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package v2
+
+// CrossNamespaceObjectReference contains enough information to let you locate
+// the typed referenced object at cluster level.
+type CrossNamespaceObjectReference struct {
+ // APIVersion of the referent.
+ // +optional
+ APIVersion string `json:"apiVersion,omitempty"`
+
+ // Kind of the referent.
+ // +kubebuilder:validation:Enum=HelmRepository;GitRepository;Bucket
+ // +required
+ Kind string `json:"kind,omitempty"`
+
+ // Name of the referent.
+ // +kubebuilder:validation:MinLength=1
+ // +kubebuilder:validation:MaxLength=253
+ // +required
+ Name string `json:"name"`
+
+ // Namespace of the referent.
+ // +kubebuilder:validation:MinLength=1
+ // +kubebuilder:validation:MaxLength=63
+ // +kubebuilder:validation:Optional
+ // +optional
+ Namespace string `json:"namespace,omitempty"`
+}
+
+// CrossNamespaceSourceReference contains enough information to let you locate
+// the typed referenced object at cluster level.
+type CrossNamespaceSourceReference struct {
+ // APIVersion of the referent.
+ // +optional
+ APIVersion string `json:"apiVersion,omitempty"`
+
+ // Kind of the referent.
+ // +kubebuilder:validation:Enum=OCIRepository;HelmChart;ExternalArtifact
+ // +required
+ Kind string `json:"kind"`
+
+ // Name of the referent.
+ // +kubebuilder:validation:MinLength=1
+ // +kubebuilder:validation:MaxLength=253
+ // +required
+ Name string `json:"name"`
+
+ // Namespace of the referent, defaults to the namespace of the Kubernetes
+ // resource object that contains the reference.
+ // +kubebuilder:validation:MinLength=1
+ // +kubebuilder:validation:MaxLength=63
+ // +kubebuilder:validation:Optional
+ // +optional
+ Namespace string `json:"namespace,omitempty"`
+}
+
+// DependencyReference defines a HelmRelease dependency on another HelmRelease resource.
+type DependencyReference struct {
+ // Name of the referent.
+ // +required
+ Name string `json:"name"`
+
+ // Namespace of the referent, defaults to the namespace of the HelmRelease
+ // resource object that contains the reference.
+ // +optional
+ Namespace string `json:"namespace,omitempty"`
+
+ // ReadyExpr is a CEL expression that can be used to assess the readiness
+ // of a dependency. When specified, the built-in readiness check
+ // is replaced by the logic defined in the CEL expression.
+ // To make the CEL expression additive to the built-in readiness check,
+ // the feature gate `AdditiveCELDependencyCheck` must be set to `true`.
+ // +optional
+ ReadyExpr string `json:"readyExpr,omitempty"`
+}
diff --git a/api/v2/snapshot_types.go b/api/v2/snapshot_types.go
new file mode 100644
index 000000000..4951c7ec3
--- /dev/null
+++ b/api/v2/snapshot_types.go
@@ -0,0 +1,274 @@
+/*
+Copyright 2024 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package v2
+
+import (
+ "fmt"
+ "sort"
+
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+const (
+ // CurrentSnapshotAPIVersion is the current API version for snapshots.
+ // This is used to distinguish between snapshots created with different
+ // helm-controller versions, allowing for graceful migration when the
+ // digest calculation method changes. This will typically happen when
+ // there is a major Helm version upgrade that introduces breaking
+ // changes to the Chart or Release APIs. This version should be bumped
+ // accordingly when such changes occur.
+ CurrentSnapshotAPIVersion = SnapshotAPIVersion2
+
+ // SnapshotAPIVersion2 is the API version for snapshots created with
+ // Helm v4 (Chart API v2), introduced in helm-controller v1.5.0.
+ SnapshotAPIVersion2 = "v2"
+)
+
+const (
+ // snapshotStatusDeployed indicates that the release the snapshot was taken
+ // from is currently deployed.
+ snapshotStatusDeployed = "deployed"
+ // snapshotStatusSuperseded indicates that the release the snapshot was taken
+ // from has been superseded by a newer release.
+ snapshotStatusSuperseded = "superseded"
+
+ // snapshotTestPhaseFailed indicates that the test of the release the snapshot
+ // was taken from has failed.
+ snapshotTestPhaseFailed = "Failed"
+)
+
+// Snapshots is a list of Snapshot objects.
+type Snapshots []*Snapshot
+
+// Len returns the number of Snapshots.
+func (in Snapshots) Len() int {
+ return len(in)
+}
+
+// SortByVersion sorts the Snapshots by version, in descending order.
+func (in Snapshots) SortByVersion() {
+ sort.Slice(in, func(i, j int) bool {
+ return in[i].Version > in[j].Version
+ })
+}
+
+// Latest returns the most recent Snapshot.
+func (in Snapshots) Latest() *Snapshot {
+ if len(in) == 0 {
+ return nil
+ }
+ in.SortByVersion()
+ return in[0]
+}
+
+// Previous returns the most recent Snapshot before the Latest that has a
+// status of "deployed" or "superseded", or nil if there is no such Snapshot.
+// Unless ignoreTests is true, Snapshots with a test in the "Failed" phase are
+// ignored.
+func (in Snapshots) Previous(ignoreTests bool) *Snapshot {
+ if len(in) < 2 {
+ return nil
+ }
+ in.SortByVersion()
+ for i := range in[1:] {
+ s := in[i+1]
+ if s.Status == snapshotStatusDeployed || s.Status == snapshotStatusSuperseded {
+ if ignoreTests || !s.HasTestInPhase(snapshotTestPhaseFailed) {
+ return s
+ }
+ }
+ }
+ return nil
+}
+
+// Truncate removes all Snapshots up to the Previous deployed Snapshot.
+// If there is no previous-deployed Snapshot, the most recent 5 Snapshots are
+// retained.
+func (in *Snapshots) Truncate(ignoreTests bool) {
+ if in.Len() < 2 {
+ return
+ }
+
+ in.SortByVersion()
+ for i := range (*in)[1:] {
+ s := (*in)[i+1]
+ if s.Status == snapshotStatusDeployed || s.Status == snapshotStatusSuperseded {
+ if ignoreTests || !s.HasTestInPhase(snapshotTestPhaseFailed) {
+ *in = (*in)[:i+2]
+ return
+ }
+ }
+ }
+
+ if in.Len() > defaultMaxHistory {
+ // If none of the Snapshots are deployed or superseded, and there
+ // are more than the defaultMaxHistory, truncate to the most recent
+ // Snapshots.
+ *in = (*in)[:defaultMaxHistory]
+ }
+}
+
+// TruncateIgnoringPreviousSnapshots sorts the Snapshots by version
+// in descending order and retains only the most recent 5 Snapshots.
+func (in *Snapshots) TruncateIgnoringPreviousSnapshots() {
+ in.SortByVersion()
+ if in.Len() > defaultMaxHistory {
+ *in = (*in)[:defaultMaxHistory]
+ }
+}
+
+// Snapshot captures a point-in-time copy of the status information for a Helm release,
+// as managed by the controller.
+type Snapshot struct {
+ // APIVersion is the API version of the Snapshot.
+ // When the calculation method of the Digest field is changed, this
+ // field will be used to distinguish between the old and new methods.
+ // +optional
+ APIVersion string `json:"apiVersion,omitempty"`
+ // Digest is the checksum of the release object in storage.
+ // It has the format of `:`.
+ // +required
+ Digest string `json:"digest"`
+ // Name is the name of the release.
+ // +required
+ Name string `json:"name"`
+ // Namespace is the namespace the release is deployed to.
+ // +required
+ Namespace string `json:"namespace"`
+ // Version is the version of the release object in storage.
+ // +required
+ Version int `json:"version"`
+ // Status is the current state of the release.
+ // +required
+ Status string `json:"status"`
+ // Action is the action that resulted in this snapshot being created.
+ // +optional
+ Action ReleaseAction `json:"action,omitempty"`
+ // ChartName is the chart name of the release object in storage.
+ // +required
+ ChartName string `json:"chartName"`
+ // ChartVersion is the chart version of the release object in
+ // storage.
+ // +required
+ ChartVersion string `json:"chartVersion"`
+ // AppVersion is the chart app version of the release object in storage.
+ // +optional
+ AppVersion string `json:"appVersion,omitempty"`
+ // ConfigDigest is the checksum of the config (better known as
+ // "values") of the release object in storage.
+ // It has the format of `:`.
+ // +required
+ ConfigDigest string `json:"configDigest"`
+ // FirstDeployed is when the release was first deployed.
+ // +required
+ FirstDeployed metav1.Time `json:"firstDeployed"`
+ // LastDeployed is when the release was last deployed.
+ // +required
+ LastDeployed metav1.Time `json:"lastDeployed"`
+ // Deleted is when the release was deleted.
+ // +optional
+ Deleted metav1.Time `json:"deleted,omitempty"`
+ // TestHooks is the list of test hooks for the release as observed to be
+ // run by the controller.
+ // +optional
+ TestHooks *map[string]*TestHookStatus `json:"testHooks,omitempty"`
+ // OCIDigest is the digest of the OCI artifact associated with the release.
+ // +optional
+ OCIDigest string `json:"ociDigest,omitempty"`
+}
+
+// FullReleaseName returns the full name of the release in the format
+// of '/.
+func (in *Snapshot) FullReleaseName() string {
+ if in == nil {
+ return ""
+ }
+ return fmt.Sprintf("%s/%s.v%d", in.Namespace, in.Name, in.Version)
+}
+
+// VersionedChartName returns the full name of the chart in the format of
+// '@'.
+func (in *Snapshot) VersionedChartName() string {
+ if in == nil {
+ return ""
+ }
+ return fmt.Sprintf("%s@%s", in.ChartName, in.ChartVersion)
+}
+
+// HasBeenTested returns true if TestHooks is not nil. This includes an empty
+// map, which indicates the chart has no tests.
+func (in *Snapshot) HasBeenTested() bool {
+ return in != nil && in.TestHooks != nil
+}
+
+// GetTestHooks returns the TestHooks for the release if not nil.
+func (in *Snapshot) GetTestHooks() map[string]*TestHookStatus {
+ if in == nil || in.TestHooks == nil {
+ return nil
+ }
+ return *in.TestHooks
+}
+
+// HasTestInPhase returns true if any of the TestHooks is in the given phase.
+func (in *Snapshot) HasTestInPhase(phase string) bool {
+ if in != nil {
+ for _, h := range in.GetTestHooks() {
+ if h.Phase == phase {
+ return true
+ }
+ }
+ }
+ return false
+}
+
+// SetTestHooks sets the TestHooks for the release.
+func (in *Snapshot) SetTestHooks(hooks map[string]*TestHookStatus) {
+ if in == nil || hooks == nil {
+ return
+ }
+ in.TestHooks = &hooks
+}
+
+// Targets returns true if the Snapshot targets the given release data.
+func (in *Snapshot) Targets(name, namespace string, version int) bool {
+ if in != nil {
+ return in.Name == name && in.Namespace == namespace && in.Version == version
+ }
+ return false
+}
+
+// GetAction returns the ReleaseAction for the Snapshot.
+func (in *Snapshot) GetAction() ReleaseAction {
+ if in == nil {
+ return ""
+ }
+ return in.Action
+}
+
+// TestHookStatus holds the status information for a test hook as observed
+// to be run by the controller.
+type TestHookStatus struct {
+ // LastStarted is the time the test hook was last started.
+ // +optional
+ LastStarted metav1.Time `json:"lastStarted,omitempty"`
+ // LastCompleted is the time the test hook last completed.
+ // +optional
+ LastCompleted metav1.Time `json:"lastCompleted,omitempty"`
+ // Phase the test hook was observed to be in.
+ // +optional
+ Phase string `json:"phase,omitempty"`
+}
diff --git a/api/v2/snapshot_types_test.go b/api/v2/snapshot_types_test.go
new file mode 100644
index 000000000..4b7d6c7d5
--- /dev/null
+++ b/api/v2/snapshot_types_test.go
@@ -0,0 +1,409 @@
+/*
+Copyright 2024 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package v2
+
+import (
+ "reflect"
+ "testing"
+)
+
+func TestSnapshots_Sort(t *testing.T) {
+ tests := []struct {
+ name string
+ in Snapshots
+ want Snapshots
+ }{
+ {
+ name: "sorts by descending version",
+ in: Snapshots{
+ {Version: 1},
+ {Version: 3},
+ {Version: 2},
+ },
+ want: Snapshots{
+ {Version: 3},
+ {Version: 2},
+ {Version: 1},
+ },
+ },
+ {
+ name: "already sorted",
+ in: Snapshots{
+ {Version: 3},
+ {Version: 2},
+ {Version: 1},
+ },
+ want: Snapshots{
+ {Version: 3},
+ {Version: 2},
+ {Version: 1},
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ tt.in.SortByVersion()
+
+ if !reflect.DeepEqual(tt.in, tt.want) {
+ t.Errorf("SortByVersion() got %v, want %v", tt.in, tt.want)
+ }
+ })
+ }
+}
+
+func TestSnapshots_Latest(t *testing.T) {
+ tests := []struct {
+ name string
+ in Snapshots
+ want *Snapshot
+ }{
+ {
+ name: "returns most recent snapshot",
+ in: Snapshots{
+ {Version: 1},
+ {Version: 3},
+ {Version: 2},
+ },
+ want: &Snapshot{Version: 3},
+ },
+ {
+ name: "returns nil if empty",
+ in: Snapshots{},
+ want: nil,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if got := tt.in.Latest(); !reflect.DeepEqual(got, tt.want) {
+ t.Errorf("Latest() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
+
+func TestSnapshots_Previous(t *testing.T) {
+ tests := []struct {
+ name string
+ in Snapshots
+ ignoreTests bool
+ want *Snapshot
+ }{
+ {
+ name: "returns previous snapshot",
+ in: Snapshots{
+ {Version: 2, Status: "deployed"},
+ {Version: 3, Status: "failed"},
+ {Version: 1, Status: "superseded"},
+ },
+ want: &Snapshot{Version: 2, Status: "deployed"},
+ },
+ {
+ name: "includes snapshots with failed tests",
+ in: Snapshots{
+ {Version: 4, Status: "deployed"},
+ {Version: 1, Status: "superseded"},
+ {Version: 2, Status: "superseded"},
+ {Version: 3, Status: "superseded", TestHooks: &map[string]*TestHookStatus{
+ "test": {Phase: "Failed"},
+ }},
+ },
+ ignoreTests: true,
+ want: &Snapshot{Version: 3, Status: "superseded", TestHooks: &map[string]*TestHookStatus{
+ "test": {Phase: "Failed"},
+ }},
+ },
+ {
+ name: "ignores snapshots with failed tests",
+ in: Snapshots{
+ {Version: 4, Status: "deployed"},
+ {Version: 1, Status: "superseded"},
+ {Version: 2, Status: "superseded"},
+ {Version: 3, Status: "superseded", TestHooks: &map[string]*TestHookStatus{
+ "test": {Phase: "Failed"},
+ }},
+ },
+ ignoreTests: false,
+ want: &Snapshot{Version: 2, Status: "superseded"},
+ },
+ {
+ name: "returns nil without previous snapshot",
+ in: Snapshots{
+ {Version: 1, Status: "deployed"},
+ },
+ want: nil,
+ },
+ {
+ name: "returns nil without snapshot matching criteria",
+ in: Snapshots{
+ {Version: 4, Status: "deployed"},
+ {Version: 3, Status: "superseded", TestHooks: &map[string]*TestHookStatus{
+ "test": {Phase: "Failed"},
+ }},
+ },
+ ignoreTests: false,
+ want: nil,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if got := tt.in.Previous(tt.ignoreTests); !reflect.DeepEqual(got, tt.want) {
+ t.Errorf("Previous() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
+
+func TestSnapshots_Truncate(t *testing.T) {
+ tests := []struct {
+ name string
+ in Snapshots
+ ignoreTests bool
+ want Snapshots
+ }{
+ {
+ name: "keeps previous snapshot",
+ in: Snapshots{
+ {Version: 1, Status: "superseded"},
+ {Version: 3, Status: "failed"},
+ {Version: 2, Status: "superseded"},
+ {Version: 4, Status: "deployed"},
+ },
+ want: Snapshots{
+ {Version: 4, Status: "deployed"},
+ {Version: 3, Status: "failed"},
+ {Version: 2, Status: "superseded"},
+ },
+ },
+ {
+ name: "ignores snapshots with failed tests",
+ in: Snapshots{
+ {Version: 4, Status: "deployed"},
+ {Version: 3, Status: "superseded", TestHooks: &map[string]*TestHookStatus{
+ "upgrade-test-fail-podinfo-fault-test-tiz9x": {Phase: "Failed"},
+ "upgrade-test-fail-podinfo-grpc-test-gddcw": {},
+ }},
+ {Version: 2, Status: "superseded", TestHooks: &map[string]*TestHookStatus{
+ "upgrade-test-fail-podinfo-grpc-test-h0tc2": {
+ Phase: "Succeeded",
+ },
+ "upgrade-test-fail-podinfo-jwt-test-vzusa": {
+ Phase: "Succeeded",
+ },
+ "upgrade-test-fail-podinfo-service-test-b647e": {
+ Phase: "Succeeded",
+ },
+ }},
+ },
+ ignoreTests: false,
+ want: Snapshots{
+ {Version: 4, Status: "deployed"},
+ {Version: 3, Status: "superseded", TestHooks: &map[string]*TestHookStatus{
+ "upgrade-test-fail-podinfo-fault-test-tiz9x": {Phase: "Failed"},
+ "upgrade-test-fail-podinfo-grpc-test-gddcw": {},
+ }},
+ {Version: 2, Status: "superseded", TestHooks: &map[string]*TestHookStatus{
+ "upgrade-test-fail-podinfo-grpc-test-h0tc2": {
+ Phase: "Succeeded",
+ },
+ "upgrade-test-fail-podinfo-jwt-test-vzusa": {
+ Phase: "Succeeded",
+ },
+ "upgrade-test-fail-podinfo-service-test-b647e": {
+ Phase: "Succeeded",
+ },
+ }},
+ },
+ },
+ {
+ name: "keeps previous snapshot with failed tests",
+ in: Snapshots{
+ {Version: 4, Status: "deployed"},
+ {Version: 3, Status: "superseded", TestHooks: &map[string]*TestHookStatus{
+ "upgrade-test-fail-podinfo-fault-test-tiz9x": {Phase: "Failed"},
+ "upgrade-test-fail-podinfo-grpc-test-gddcw": {},
+ }},
+ {Version: 2, Status: "superseded", TestHooks: &map[string]*TestHookStatus{
+ "upgrade-test-fail-podinfo-grpc-test-h0tc2": {
+ Phase: "Succeeded",
+ },
+ "upgrade-test-fail-podinfo-jwt-test-vzusa": {
+ Phase: "Succeeded",
+ },
+ "upgrade-test-fail-podinfo-service-test-b647e": {
+ Phase: "Succeeded",
+ },
+ }},
+ {Version: 1, Status: "superseded"},
+ },
+ ignoreTests: true,
+ want: Snapshots{
+ {Version: 4, Status: "deployed"},
+ {Version: 3, Status: "superseded", TestHooks: &map[string]*TestHookStatus{
+ "upgrade-test-fail-podinfo-fault-test-tiz9x": {Phase: "Failed"},
+ "upgrade-test-fail-podinfo-grpc-test-gddcw": {},
+ }},
+ },
+ },
+ {
+ name: "retains most recent snapshots when all have failed",
+ in: Snapshots{
+ {Version: 6, Status: "deployed"},
+ {Version: 5, Status: "failed"},
+ {Version: 4, Status: "failed"},
+ {Version: 3, Status: "failed"},
+ {Version: 2, Status: "failed"},
+ {Version: 1, Status: "failed"},
+ },
+ want: Snapshots{
+ {Version: 6, Status: "deployed"},
+ {Version: 5, Status: "failed"},
+ {Version: 4, Status: "failed"},
+ {Version: 3, Status: "failed"},
+ {Version: 2, Status: "failed"},
+ },
+ },
+ {
+ name: "without previous snapshot",
+ in: Snapshots{
+ {Version: 1, Status: "deployed"},
+ },
+ want: Snapshots{
+ {Version: 1, Status: "deployed"},
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ tt.in.Truncate(tt.ignoreTests)
+
+ if !reflect.DeepEqual(tt.in, tt.want) {
+ t.Errorf("Truncate() got %v, want %v", tt.in, tt.want)
+ }
+ })
+ }
+}
+
+func TestSnapshot_GetAction(t *testing.T) {
+ tests := []struct {
+ name string
+ snapshot *Snapshot
+ want ReleaseAction
+ }{
+ {
+ name: "nil snapshot",
+ snapshot: nil,
+ want: "",
+ },
+ {
+ name: "empty action",
+ snapshot: &Snapshot{},
+ want: "",
+ },
+ {
+ name: "install action",
+ snapshot: &Snapshot{Action: ReleaseActionInstall},
+ want: ReleaseActionInstall,
+ },
+ {
+ name: "upgrade action",
+ snapshot: &Snapshot{Action: ReleaseActionUpgrade},
+ want: ReleaseActionUpgrade,
+ },
+ {
+ name: "rollback action",
+ snapshot: &Snapshot{Action: ReleaseActionRollback},
+ want: ReleaseActionRollback,
+ },
+ {
+ name: "uninstall action",
+ snapshot: &Snapshot{Action: ReleaseActionUninstall},
+ want: ReleaseActionUninstall,
+ },
+ {
+ name: "uninstall-remediation action",
+ snapshot: &Snapshot{Action: ReleaseActionUninstallRemediation},
+ want: ReleaseActionUninstallRemediation,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if got := tt.snapshot.GetAction(); got != tt.want {
+ t.Errorf("GetAction() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
+
+func TestSnapshots_TruncateIgnoringPreviousSnapshots(t *testing.T) {
+ tests := []struct {
+ name string
+ in Snapshots
+ want Snapshots
+ }{
+ {
+ name: "keeps previous snapshot",
+ in: Snapshots{
+ {Version: 1, Status: "superseded"},
+ {Version: 3, Status: "failed"},
+ {Version: 2, Status: "superseded"},
+ {Version: 4, Status: "deployed"},
+ },
+ want: Snapshots{
+ {Version: 4, Status: "deployed"},
+ {Version: 3, Status: "failed"},
+ {Version: 2, Status: "superseded"},
+ {Version: 1, Status: "superseded"},
+ },
+ },
+ {
+ name: "retains most recent snapshots when all have failed",
+ in: Snapshots{
+ {Version: 6, Status: "deployed"},
+ {Version: 5, Status: "failed"},
+ {Version: 4, Status: "failed"},
+ {Version: 3, Status: "failed"},
+ {Version: 2, Status: "failed"},
+ {Version: 1, Status: "failed"},
+ },
+ want: Snapshots{
+ {Version: 6, Status: "deployed"},
+ {Version: 5, Status: "failed"},
+ {Version: 4, Status: "failed"},
+ {Version: 3, Status: "failed"},
+ {Version: 2, Status: "failed"},
+ },
+ },
+ {
+ name: "without previous snapshot",
+ in: Snapshots{
+ {Version: 1, Status: "deployed"},
+ },
+ want: Snapshots{
+ {Version: 1, Status: "deployed"},
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ tt.in.TruncateIgnoringPreviousSnapshots()
+
+ if !reflect.DeepEqual(tt.in, tt.want) {
+ t.Errorf("TruncateIgnoringPreviousSnapshots() got %v, want %v", tt.in, tt.want)
+ }
+ })
+ }
+}
diff --git a/api/v2/zz_generated.deepcopy.go b/api/v2/zz_generated.deepcopy.go
new file mode 100644
index 000000000..f371a2181
--- /dev/null
+++ b/api/v2/zz_generated.deepcopy.go
@@ -0,0 +1,894 @@
+//go:build !ignore_autogenerated
+
+/*
+Copyright 2025 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by controller-gen. DO NOT EDIT.
+
+package v2
+
+import (
+ "github.com/fluxcd/pkg/apis/kustomize"
+ "github.com/fluxcd/pkg/apis/meta"
+ apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
+ "k8s.io/apimachinery/pkg/apis/meta/v1"
+ runtime "k8s.io/apimachinery/pkg/runtime"
+)
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *CommonMetadata) DeepCopyInto(out *CommonMetadata) {
+ *out = *in
+ if in.Annotations != nil {
+ in, out := &in.Annotations, &out.Annotations
+ *out = make(map[string]string, len(*in))
+ for key, val := range *in {
+ (*out)[key] = val
+ }
+ }
+ if in.Labels != nil {
+ in, out := &in.Labels, &out.Labels
+ *out = make(map[string]string, len(*in))
+ for key, val := range *in {
+ (*out)[key] = val
+ }
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CommonMetadata.
+func (in *CommonMetadata) DeepCopy() *CommonMetadata {
+ if in == nil {
+ return nil
+ }
+ out := new(CommonMetadata)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *CrossNamespaceObjectReference) DeepCopyInto(out *CrossNamespaceObjectReference) {
+ *out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CrossNamespaceObjectReference.
+func (in *CrossNamespaceObjectReference) DeepCopy() *CrossNamespaceObjectReference {
+ if in == nil {
+ return nil
+ }
+ out := new(CrossNamespaceObjectReference)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *CrossNamespaceSourceReference) DeepCopyInto(out *CrossNamespaceSourceReference) {
+ *out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CrossNamespaceSourceReference.
+func (in *CrossNamespaceSourceReference) DeepCopy() *CrossNamespaceSourceReference {
+ if in == nil {
+ return nil
+ }
+ out := new(CrossNamespaceSourceReference)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *DependencyReference) DeepCopyInto(out *DependencyReference) {
+ *out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DependencyReference.
+func (in *DependencyReference) DeepCopy() *DependencyReference {
+ if in == nil {
+ return nil
+ }
+ out := new(DependencyReference)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *DriftDetection) DeepCopyInto(out *DriftDetection) {
+ *out = *in
+ if in.Ignore != nil {
+ in, out := &in.Ignore, &out.Ignore
+ *out = make([]IgnoreRule, len(*in))
+ for i := range *in {
+ (*in)[i].DeepCopyInto(&(*out)[i])
+ }
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DriftDetection.
+func (in *DriftDetection) DeepCopy() *DriftDetection {
+ if in == nil {
+ return nil
+ }
+ out := new(DriftDetection)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *Filter) DeepCopyInto(out *Filter) {
+ *out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Filter.
+func (in *Filter) DeepCopy() *Filter {
+ if in == nil {
+ return nil
+ }
+ out := new(Filter)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *HelmChartTemplate) DeepCopyInto(out *HelmChartTemplate) {
+ *out = *in
+ if in.ObjectMeta != nil {
+ in, out := &in.ObjectMeta, &out.ObjectMeta
+ *out = new(HelmChartTemplateObjectMeta)
+ (*in).DeepCopyInto(*out)
+ }
+ in.Spec.DeepCopyInto(&out.Spec)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HelmChartTemplate.
+func (in *HelmChartTemplate) DeepCopy() *HelmChartTemplate {
+ if in == nil {
+ return nil
+ }
+ out := new(HelmChartTemplate)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *HelmChartTemplateObjectMeta) DeepCopyInto(out *HelmChartTemplateObjectMeta) {
+ *out = *in
+ if in.Labels != nil {
+ in, out := &in.Labels, &out.Labels
+ *out = make(map[string]string, len(*in))
+ for key, val := range *in {
+ (*out)[key] = val
+ }
+ }
+ if in.Annotations != nil {
+ in, out := &in.Annotations, &out.Annotations
+ *out = make(map[string]string, len(*in))
+ for key, val := range *in {
+ (*out)[key] = val
+ }
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HelmChartTemplateObjectMeta.
+func (in *HelmChartTemplateObjectMeta) DeepCopy() *HelmChartTemplateObjectMeta {
+ if in == nil {
+ return nil
+ }
+ out := new(HelmChartTemplateObjectMeta)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *HelmChartTemplateSpec) DeepCopyInto(out *HelmChartTemplateSpec) {
+ *out = *in
+ out.SourceRef = in.SourceRef
+ if in.Interval != nil {
+ in, out := &in.Interval, &out.Interval
+ *out = new(v1.Duration)
+ **out = **in
+ }
+ if in.ValuesFiles != nil {
+ in, out := &in.ValuesFiles, &out.ValuesFiles
+ *out = make([]string, len(*in))
+ copy(*out, *in)
+ }
+ if in.Verify != nil {
+ in, out := &in.Verify, &out.Verify
+ *out = new(HelmChartTemplateVerification)
+ (*in).DeepCopyInto(*out)
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HelmChartTemplateSpec.
+func (in *HelmChartTemplateSpec) DeepCopy() *HelmChartTemplateSpec {
+ if in == nil {
+ return nil
+ }
+ out := new(HelmChartTemplateSpec)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *HelmChartTemplateVerification) DeepCopyInto(out *HelmChartTemplateVerification) {
+ *out = *in
+ if in.SecretRef != nil {
+ in, out := &in.SecretRef, &out.SecretRef
+ *out = new(meta.LocalObjectReference)
+ **out = **in
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HelmChartTemplateVerification.
+func (in *HelmChartTemplateVerification) DeepCopy() *HelmChartTemplateVerification {
+ if in == nil {
+ return nil
+ }
+ out := new(HelmChartTemplateVerification)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *HelmRelease) DeepCopyInto(out *HelmRelease) {
+ *out = *in
+ out.TypeMeta = in.TypeMeta
+ in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
+ in.Spec.DeepCopyInto(&out.Spec)
+ in.Status.DeepCopyInto(&out.Status)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HelmRelease.
+func (in *HelmRelease) DeepCopy() *HelmRelease {
+ if in == nil {
+ return nil
+ }
+ out := new(HelmRelease)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *HelmRelease) DeepCopyObject() runtime.Object {
+ if c := in.DeepCopy(); c != nil {
+ return c
+ }
+ return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *HelmReleaseList) DeepCopyInto(out *HelmReleaseList) {
+ *out = *in
+ out.TypeMeta = in.TypeMeta
+ in.ListMeta.DeepCopyInto(&out.ListMeta)
+ if in.Items != nil {
+ in, out := &in.Items, &out.Items
+ *out = make([]HelmRelease, len(*in))
+ for i := range *in {
+ (*in)[i].DeepCopyInto(&(*out)[i])
+ }
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HelmReleaseList.
+func (in *HelmReleaseList) DeepCopy() *HelmReleaseList {
+ if in == nil {
+ return nil
+ }
+ out := new(HelmReleaseList)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *HelmReleaseList) DeepCopyObject() runtime.Object {
+ if c := in.DeepCopy(); c != nil {
+ return c
+ }
+ return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *HelmReleaseSpec) DeepCopyInto(out *HelmReleaseSpec) {
+ *out = *in
+ if in.Chart != nil {
+ in, out := &in.Chart, &out.Chart
+ *out = new(HelmChartTemplate)
+ (*in).DeepCopyInto(*out)
+ }
+ if in.ChartRef != nil {
+ in, out := &in.ChartRef, &out.ChartRef
+ *out = new(CrossNamespaceSourceReference)
+ **out = **in
+ }
+ out.Interval = in.Interval
+ if in.KubeConfig != nil {
+ in, out := &in.KubeConfig, &out.KubeConfig
+ *out = new(meta.KubeConfigReference)
+ (*in).DeepCopyInto(*out)
+ }
+ if in.DependsOn != nil {
+ in, out := &in.DependsOn, &out.DependsOn
+ *out = make([]DependencyReference, len(*in))
+ copy(*out, *in)
+ }
+ if in.Timeout != nil {
+ in, out := &in.Timeout, &out.Timeout
+ *out = new(v1.Duration)
+ **out = **in
+ }
+ if in.MaxHistory != nil {
+ in, out := &in.MaxHistory, &out.MaxHistory
+ *out = new(int)
+ **out = **in
+ }
+ if in.PersistentClient != nil {
+ in, out := &in.PersistentClient, &out.PersistentClient
+ *out = new(bool)
+ **out = **in
+ }
+ if in.DriftDetection != nil {
+ in, out := &in.DriftDetection, &out.DriftDetection
+ *out = new(DriftDetection)
+ (*in).DeepCopyInto(*out)
+ }
+ if in.Install != nil {
+ in, out := &in.Install, &out.Install
+ *out = new(Install)
+ (*in).DeepCopyInto(*out)
+ }
+ if in.Upgrade != nil {
+ in, out := &in.Upgrade, &out.Upgrade
+ *out = new(Upgrade)
+ (*in).DeepCopyInto(*out)
+ }
+ if in.Test != nil {
+ in, out := &in.Test, &out.Test
+ *out = new(Test)
+ (*in).DeepCopyInto(*out)
+ }
+ if in.Rollback != nil {
+ in, out := &in.Rollback, &out.Rollback
+ *out = new(Rollback)
+ (*in).DeepCopyInto(*out)
+ }
+ if in.Uninstall != nil {
+ in, out := &in.Uninstall, &out.Uninstall
+ *out = new(Uninstall)
+ (*in).DeepCopyInto(*out)
+ }
+ if in.ValuesFrom != nil {
+ in, out := &in.ValuesFrom, &out.ValuesFrom
+ *out = make([]ValuesReference, len(*in))
+ copy(*out, *in)
+ }
+ if in.Values != nil {
+ in, out := &in.Values, &out.Values
+ *out = new(apiextensionsv1.JSON)
+ (*in).DeepCopyInto(*out)
+ }
+ if in.CommonMetadata != nil {
+ in, out := &in.CommonMetadata, &out.CommonMetadata
+ *out = new(CommonMetadata)
+ (*in).DeepCopyInto(*out)
+ }
+ if in.PostRenderers != nil {
+ in, out := &in.PostRenderers, &out.PostRenderers
+ *out = make([]PostRenderer, len(*in))
+ for i := range *in {
+ (*in)[i].DeepCopyInto(&(*out)[i])
+ }
+ }
+ if in.WaitStrategy != nil {
+ in, out := &in.WaitStrategy, &out.WaitStrategy
+ *out = new(WaitStrategy)
+ **out = **in
+ }
+ if in.HealthCheckExprs != nil {
+ in, out := &in.HealthCheckExprs, &out.HealthCheckExprs
+ *out = make([]kustomize.CustomHealthCheck, len(*in))
+ copy(*out, *in)
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HelmReleaseSpec.
+func (in *HelmReleaseSpec) DeepCopy() *HelmReleaseSpec {
+ if in == nil {
+ return nil
+ }
+ out := new(HelmReleaseSpec)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *HelmReleaseStatus) DeepCopyInto(out *HelmReleaseStatus) {
+ *out = *in
+ if in.Conditions != nil {
+ in, out := &in.Conditions, &out.Conditions
+ *out = make([]v1.Condition, len(*in))
+ for i := range *in {
+ (*in)[i].DeepCopyInto(&(*out)[i])
+ }
+ }
+ if in.History != nil {
+ in, out := &in.History, &out.History
+ *out = make(Snapshots, len(*in))
+ for i := range *in {
+ if (*in)[i] != nil {
+ in, out := &(*in)[i], &(*out)[i]
+ *out = new(Snapshot)
+ (*in).DeepCopyInto(*out)
+ }
+ }
+ }
+ if in.Inventory != nil {
+ in, out := &in.Inventory, &out.Inventory
+ *out = new(ResourceInventory)
+ (*in).DeepCopyInto(*out)
+ }
+ if in.LastAttemptedReleaseActionDuration != nil {
+ in, out := &in.LastAttemptedReleaseActionDuration, &out.LastAttemptedReleaseActionDuration
+ *out = new(v1.Duration)
+ **out = **in
+ }
+ out.ReconcileRequestStatus = in.ReconcileRequestStatus
+ out.ForceRequestStatus = in.ForceRequestStatus
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HelmReleaseStatus.
+func (in *HelmReleaseStatus) DeepCopy() *HelmReleaseStatus {
+ if in == nil {
+ return nil
+ }
+ out := new(HelmReleaseStatus)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *IgnoreRule) DeepCopyInto(out *IgnoreRule) {
+ *out = *in
+ if in.Paths != nil {
+ in, out := &in.Paths, &out.Paths
+ *out = make([]string, len(*in))
+ copy(*out, *in)
+ }
+ if in.Target != nil {
+ in, out := &in.Target, &out.Target
+ *out = new(kustomize.Selector)
+ **out = **in
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IgnoreRule.
+func (in *IgnoreRule) DeepCopy() *IgnoreRule {
+ if in == nil {
+ return nil
+ }
+ out := new(IgnoreRule)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *Install) DeepCopyInto(out *Install) {
+ *out = *in
+ if in.Timeout != nil {
+ in, out := &in.Timeout, &out.Timeout
+ *out = new(v1.Duration)
+ **out = **in
+ }
+ if in.Strategy != nil {
+ in, out := &in.Strategy, &out.Strategy
+ *out = new(InstallStrategy)
+ (*in).DeepCopyInto(*out)
+ }
+ if in.Remediation != nil {
+ in, out := &in.Remediation, &out.Remediation
+ *out = new(InstallRemediation)
+ (*in).DeepCopyInto(*out)
+ }
+ if in.ServerSideApply != nil {
+ in, out := &in.ServerSideApply, &out.ServerSideApply
+ *out = new(bool)
+ **out = **in
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Install.
+func (in *Install) DeepCopy() *Install {
+ if in == nil {
+ return nil
+ }
+ out := new(Install)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *InstallRemediation) DeepCopyInto(out *InstallRemediation) {
+ *out = *in
+ if in.IgnoreTestFailures != nil {
+ in, out := &in.IgnoreTestFailures, &out.IgnoreTestFailures
+ *out = new(bool)
+ **out = **in
+ }
+ if in.RemediateLastFailure != nil {
+ in, out := &in.RemediateLastFailure, &out.RemediateLastFailure
+ *out = new(bool)
+ **out = **in
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InstallRemediation.
+func (in *InstallRemediation) DeepCopy() *InstallRemediation {
+ if in == nil {
+ return nil
+ }
+ out := new(InstallRemediation)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *InstallStrategy) DeepCopyInto(out *InstallStrategy) {
+ *out = *in
+ if in.RetryInterval != nil {
+ in, out := &in.RetryInterval, &out.RetryInterval
+ *out = new(v1.Duration)
+ **out = **in
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InstallStrategy.
+func (in *InstallStrategy) DeepCopy() *InstallStrategy {
+ if in == nil {
+ return nil
+ }
+ out := new(InstallStrategy)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *Kustomize) DeepCopyInto(out *Kustomize) {
+ *out = *in
+ if in.Patches != nil {
+ in, out := &in.Patches, &out.Patches
+ *out = make([]kustomize.Patch, len(*in))
+ for i := range *in {
+ (*in)[i].DeepCopyInto(&(*out)[i])
+ }
+ }
+ if in.Images != nil {
+ in, out := &in.Images, &out.Images
+ *out = make([]kustomize.Image, len(*in))
+ copy(*out, *in)
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Kustomize.
+func (in *Kustomize) DeepCopy() *Kustomize {
+ if in == nil {
+ return nil
+ }
+ out := new(Kustomize)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *PostRenderer) DeepCopyInto(out *PostRenderer) {
+ *out = *in
+ if in.Kustomize != nil {
+ in, out := &in.Kustomize, &out.Kustomize
+ *out = new(Kustomize)
+ (*in).DeepCopyInto(*out)
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PostRenderer.
+func (in *PostRenderer) DeepCopy() *PostRenderer {
+ if in == nil {
+ return nil
+ }
+ out := new(PostRenderer)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ResourceInventory) DeepCopyInto(out *ResourceInventory) {
+ *out = *in
+ if in.Entries != nil {
+ in, out := &in.Entries, &out.Entries
+ *out = make([]ResourceRef, len(*in))
+ copy(*out, *in)
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResourceInventory.
+func (in *ResourceInventory) DeepCopy() *ResourceInventory {
+ if in == nil {
+ return nil
+ }
+ out := new(ResourceInventory)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ResourceRef) DeepCopyInto(out *ResourceRef) {
+ *out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResourceRef.
+func (in *ResourceRef) DeepCopy() *ResourceRef {
+ if in == nil {
+ return nil
+ }
+ out := new(ResourceRef)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *Rollback) DeepCopyInto(out *Rollback) {
+ *out = *in
+ if in.Timeout != nil {
+ in, out := &in.Timeout, &out.Timeout
+ *out = new(v1.Duration)
+ **out = **in
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Rollback.
+func (in *Rollback) DeepCopy() *Rollback {
+ if in == nil {
+ return nil
+ }
+ out := new(Rollback)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *Snapshot) DeepCopyInto(out *Snapshot) {
+ *out = *in
+ in.FirstDeployed.DeepCopyInto(&out.FirstDeployed)
+ in.LastDeployed.DeepCopyInto(&out.LastDeployed)
+ in.Deleted.DeepCopyInto(&out.Deleted)
+ if in.TestHooks != nil {
+ in, out := &in.TestHooks, &out.TestHooks
+ *out = new(map[string]*TestHookStatus)
+ if **in != nil {
+ in, out := *in, *out
+ *out = make(map[string]*TestHookStatus, len(*in))
+ for key, val := range *in {
+ var outVal *TestHookStatus
+ if val == nil {
+ (*out)[key] = nil
+ } else {
+ inVal := (*in)[key]
+ in, out := &inVal, &outVal
+ *out = new(TestHookStatus)
+ (*in).DeepCopyInto(*out)
+ }
+ (*out)[key] = outVal
+ }
+ }
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Snapshot.
+func (in *Snapshot) DeepCopy() *Snapshot {
+ if in == nil {
+ return nil
+ }
+ out := new(Snapshot)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in Snapshots) DeepCopyInto(out *Snapshots) {
+ {
+ in := &in
+ *out = make(Snapshots, len(*in))
+ for i := range *in {
+ if (*in)[i] != nil {
+ in, out := &(*in)[i], &(*out)[i]
+ *out = new(Snapshot)
+ (*in).DeepCopyInto(*out)
+ }
+ }
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Snapshots.
+func (in Snapshots) DeepCopy() Snapshots {
+ if in == nil {
+ return nil
+ }
+ out := new(Snapshots)
+ in.DeepCopyInto(out)
+ return *out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *Test) DeepCopyInto(out *Test) {
+ *out = *in
+ if in.Timeout != nil {
+ in, out := &in.Timeout, &out.Timeout
+ *out = new(v1.Duration)
+ **out = **in
+ }
+ if in.Filters != nil {
+ in, out := &in.Filters, &out.Filters
+ *out = new([]Filter)
+ if **in != nil {
+ in, out := *in, *out
+ *out = make([]Filter, len(*in))
+ copy(*out, *in)
+ }
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Test.
+func (in *Test) DeepCopy() *Test {
+ if in == nil {
+ return nil
+ }
+ out := new(Test)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *TestHookStatus) DeepCopyInto(out *TestHookStatus) {
+ *out = *in
+ in.LastStarted.DeepCopyInto(&out.LastStarted)
+ in.LastCompleted.DeepCopyInto(&out.LastCompleted)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TestHookStatus.
+func (in *TestHookStatus) DeepCopy() *TestHookStatus {
+ if in == nil {
+ return nil
+ }
+ out := new(TestHookStatus)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *Uninstall) DeepCopyInto(out *Uninstall) {
+ *out = *in
+ if in.Timeout != nil {
+ in, out := &in.Timeout, &out.Timeout
+ *out = new(v1.Duration)
+ **out = **in
+ }
+ if in.DeletionPropagation != nil {
+ in, out := &in.DeletionPropagation, &out.DeletionPropagation
+ *out = new(string)
+ **out = **in
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Uninstall.
+func (in *Uninstall) DeepCopy() *Uninstall {
+ if in == nil {
+ return nil
+ }
+ out := new(Uninstall)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *Upgrade) DeepCopyInto(out *Upgrade) {
+ *out = *in
+ if in.Timeout != nil {
+ in, out := &in.Timeout, &out.Timeout
+ *out = new(v1.Duration)
+ **out = **in
+ }
+ if in.Strategy != nil {
+ in, out := &in.Strategy, &out.Strategy
+ *out = new(UpgradeStrategy)
+ (*in).DeepCopyInto(*out)
+ }
+ if in.Remediation != nil {
+ in, out := &in.Remediation, &out.Remediation
+ *out = new(UpgradeRemediation)
+ (*in).DeepCopyInto(*out)
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Upgrade.
+func (in *Upgrade) DeepCopy() *Upgrade {
+ if in == nil {
+ return nil
+ }
+ out := new(Upgrade)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *UpgradeRemediation) DeepCopyInto(out *UpgradeRemediation) {
+ *out = *in
+ if in.IgnoreTestFailures != nil {
+ in, out := &in.IgnoreTestFailures, &out.IgnoreTestFailures
+ *out = new(bool)
+ **out = **in
+ }
+ if in.RemediateLastFailure != nil {
+ in, out := &in.RemediateLastFailure, &out.RemediateLastFailure
+ *out = new(bool)
+ **out = **in
+ }
+ if in.Strategy != nil {
+ in, out := &in.Strategy, &out.Strategy
+ *out = new(RemediationStrategy)
+ **out = **in
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UpgradeRemediation.
+func (in *UpgradeRemediation) DeepCopy() *UpgradeRemediation {
+ if in == nil {
+ return nil
+ }
+ out := new(UpgradeRemediation)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *UpgradeStrategy) DeepCopyInto(out *UpgradeStrategy) {
+ *out = *in
+ if in.RetryInterval != nil {
+ in, out := &in.RetryInterval, &out.RetryInterval
+ *out = new(v1.Duration)
+ **out = **in
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UpgradeStrategy.
+func (in *UpgradeStrategy) DeepCopy() *UpgradeStrategy {
+ if in == nil {
+ return nil
+ }
+ out := new(UpgradeStrategy)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *WaitStrategy) DeepCopyInto(out *WaitStrategy) {
+ *out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WaitStrategy.
+func (in *WaitStrategy) DeepCopy() *WaitStrategy {
+ if in == nil {
+ return nil
+ }
+ out := new(WaitStrategy)
+ in.DeepCopyInto(out)
+ return out
+}
diff --git a/api/v2beta1/doc.go b/api/v2beta1/doc.go
index 4cdc98c12..da87c13ba 100644
--- a/api/v2beta1/doc.go
+++ b/api/v2beta1/doc.go
@@ -15,6 +15,9 @@ limitations under the License.
*/
// Package v2beta1 contains API Schema definitions for the helm v2beta1 API group
+//
+// Deprecated: v2beta1 is no longer supported, use v2 instead.
+//
// +kubebuilder:object:generate=true
// +groupName=helm.toolkit.fluxcd.io
package v2beta1
diff --git a/api/v2beta1/helmrelease_types.go b/api/v2beta1/helmrelease_types.go
index 4678a35cc..785512ba6 100644
--- a/api/v2beta1/helmrelease_types.go
+++ b/api/v2beta1/helmrelease_types.go
@@ -28,6 +28,9 @@ import (
"github.com/fluxcd/pkg/apis/kustomize"
"github.com/fluxcd/pkg/apis/meta"
+
+ v2 "github.com/fluxcd/helm-controller/api/v2"
+ "github.com/fluxcd/helm-controller/api/v2beta2"
)
const HelmReleaseKind = "HelmRelease"
@@ -67,7 +70,15 @@ type HelmReleaseSpec struct {
// Chart defines the template of the v1beta2.HelmChart that should be created
// for this HelmRelease.
// +required
- Chart HelmChartTemplate `json:"chart"`
+ Chart *HelmChartTemplate `json:"chart,omitempty"`
+
+ // ChartRef holds a reference to a source controller resource containing the
+ // Helm chart artifact.
+ //
+ // Note: this field is provisional to the v2 API, and not actively used
+ // by v2beta1 HelmReleases.
+ // +optional
+ ChartRef *v2.CrossNamespaceSourceReference `json:"chartRef,omitempty"`
// Interval at which to reconcile the Helm release.
// This interval is approximate and may be subject to jitter to ensure
@@ -154,6 +165,15 @@ type HelmReleaseSpec struct {
// +optional
PersistentClient *bool `json:"persistentClient,omitempty"`
+ // DriftDetection holds the configuration for detecting and handling
+ // differences between the manifest in the Helm storage and the resources
+ // currently existing in the cluster.
+ //
+ // Note: this field is provisional to the v2beta2 API, and not actively used
+ // by v2beta1 HelmReleases.
+ // +optional
+ DriftDetection *v2beta2.DriftDetection `json:"driftDetection,omitempty"`
+
// Install holds the configuration for Helm install actions for this HelmRelease.
// +optional
Install *Install `json:"install,omitempty"`
@@ -863,6 +883,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.
@@ -905,6 +930,62 @@ type HelmReleaseStatus struct {
// state. It is reset after a successful reconciliation.
// +optional
UpgradeFailures int64 `json:"upgradeFailures,omitempty"`
+
+ // StorageNamespace is the namespace of the Helm release storage for the
+ // current release.
+ //
+ // Note: this field is provisional to the v2beta2 API, and not actively used
+ // by v2beta1 HelmReleases.
+ // +optional
+ StorageNamespace string `json:"storageNamespace,omitempty"`
+
+ // History holds the history of Helm releases performed for this HelmRelease
+ // up to the last successfully completed release.
+ //
+ // Note: this field is provisional to the v2beta2 API, and not actively used
+ // by v2beta1 HelmReleases.
+ // +optional
+ History v2.Snapshots `json:"history,omitempty"`
+
+ // LastAttemptedGeneration is the last generation the controller attempted
+ // to reconcile.
+ //
+ // Note: this field is provisional to the v2beta2 API, and not actively used
+ // by v2beta1 HelmReleases.
+ // +optional
+ LastAttemptedGeneration int64 `json:"lastAttemptedGeneration,omitempty"`
+
+ // LastAttemptedConfigDigest is the digest for the config (better known as
+ // "values") of the last reconciliation attempt.
+ //
+ // Note: this field is provisional to the v2beta2 API, and not actively used
+ // by v2beta1 HelmReleases.
+ // +optional
+ LastAttemptedConfigDigest string `json:"lastAttemptedConfigDigest,omitempty"`
+
+ // LastAttemptedReleaseAction is the last release action performed for this
+ // HelmRelease. It is used to determine the active remediation strategy.
+ //
+ // Note: this field is provisional to the v2beta2 API, and not actively used
+ // by v2beta1 HelmReleases.
+ // +optional
+ LastAttemptedReleaseAction string `json:"lastAttemptedReleaseAction,omitempty"`
+
+ // LastHandledForceAt holds the value of the most recent force request
+ // value, so a change of the annotation value can be detected.
+ //
+ // Note: this field is provisional to the v2beta2 API, and not actively used
+ // by v2beta1 HelmReleases.
+ // +optional
+ LastHandledForceAt string `json:"lastHandledForceAt,omitempty"`
+
+ // LastHandledResetAt holds the value of the most recent reset request
+ // value, so a change of the annotation value can be detected.
+ //
+ // Note: this field is provisional to the v2beta2 API, and not actively used
+ // by v2beta1 HelmReleases.
+ // +optional
+ LastHandledResetAt string `json:"lastHandledResetAt,omitempty"`
}
// GetHelmChart returns the namespace and name of the HelmChart.
@@ -1014,13 +1095,9 @@ const (
)
// +genclient
-// +genclient:Namespaced
// +kubebuilder:object:root=true
// +kubebuilder:resource:shortName=hr
-// +kubebuilder:subresource:status
-// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description=""
-// +kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].status",description=""
-// +kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].message",description=""
+// +kubebuilder:skipversion
// HelmRelease is the Schema for the helmreleases API
type HelmRelease struct {
@@ -1038,10 +1115,10 @@ func (in HelmRelease) GetRequeueAfter() time.Duration {
return in.Spec.Interval.Duration
}
-// GetValues unmarshals the raw values to a map[string]interface{} and returns
+// GetValues unmarshals the raw values to a map[string]any and returns
// the result.
-func (in HelmRelease) GetValues() map[string]interface{} {
- var values map[string]interface{}
+func (in HelmRelease) GetValues() map[string]any {
+ var values map[string]any
if in.Spec.Values != nil {
_ = json.Unmarshal(in.Spec.Values.Raw, &values)
}
@@ -1124,6 +1201,7 @@ func (in *HelmRelease) SetConditions(conditions []metav1.Condition) {
}
// GetStatusConditions returns a pointer to the Status.Conditions slice.
+//
// Deprecated: use GetConditions instead.
func (in *HelmRelease) GetStatusConditions() *[]metav1.Condition {
return &in.Status.Conditions
diff --git a/api/v2beta1/zz_generated.deepcopy.go b/api/v2beta1/zz_generated.deepcopy.go
index a224748e3..c8f0c0d03 100644
--- a/api/v2beta1/zz_generated.deepcopy.go
+++ b/api/v2beta1/zz_generated.deepcopy.go
@@ -1,8 +1,7 @@
//go:build !ignore_autogenerated
-// +build !ignore_autogenerated
/*
-Copyright 2021 The Flux authors
+Copyright 2025 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -22,6 +21,8 @@ limitations under the License.
package v2beta1
import (
+ "github.com/fluxcd/helm-controller/api/v2"
+ "github.com/fluxcd/helm-controller/api/v2beta2"
"github.com/fluxcd/pkg/apis/kustomize"
"github.com/fluxcd/pkg/apis/meta"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
@@ -207,12 +208,21 @@ func (in *HelmReleaseList) DeepCopyObject() runtime.Object {
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *HelmReleaseSpec) DeepCopyInto(out *HelmReleaseSpec) {
*out = *in
- in.Chart.DeepCopyInto(&out.Chart)
+ if in.Chart != nil {
+ in, out := &in.Chart, &out.Chart
+ *out = new(HelmChartTemplate)
+ (*in).DeepCopyInto(*out)
+ }
+ if in.ChartRef != nil {
+ in, out := &in.ChartRef, &out.ChartRef
+ *out = new(v2.CrossNamespaceSourceReference)
+ **out = **in
+ }
out.Interval = in.Interval
if in.KubeConfig != nil {
in, out := &in.KubeConfig, &out.KubeConfig
*out = new(meta.KubeConfigReference)
- **out = **in
+ (*in).DeepCopyInto(*out)
}
if in.DependsOn != nil {
in, out := &in.DependsOn, &out.DependsOn
@@ -234,6 +244,11 @@ func (in *HelmReleaseSpec) DeepCopyInto(out *HelmReleaseSpec) {
*out = new(bool)
**out = **in
}
+ if in.DriftDetection != nil {
+ in, out := &in.DriftDetection, &out.DriftDetection
+ *out = new(v2beta2.DriftDetection)
+ (*in).DeepCopyInto(*out)
+ }
if in.Install != nil {
in, out := &in.Install, &out.Install
*out = new(Install)
@@ -299,6 +314,17 @@ func (in *HelmReleaseStatus) DeepCopyInto(out *HelmReleaseStatus) {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
+ if in.History != nil {
+ in, out := &in.History, &out.History
+ *out = make(v2.Snapshots, len(*in))
+ for i := range *in {
+ if (*in)[i] != nil {
+ in, out := &(*in)[i], &(*out)[i]
+ *out = new(v2.Snapshot)
+ (*in).DeepCopyInto(*out)
+ }
+ }
+ }
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HelmReleaseStatus.
diff --git a/api/v2beta2/annotations.go b/api/v2beta2/annotations.go
new file mode 100644
index 000000000..bcf4664be
--- /dev/null
+++ b/api/v2beta2/annotations.go
@@ -0,0 +1,84 @@
+/*
+Copyright 2023 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package v2beta2
+
+import "github.com/fluxcd/pkg/apis/meta"
+
+const (
+ // ForceRequestAnnotation is the annotation used for triggering a one-off forced
+ // Helm release, even when there are no new changes in the HelmRelease.
+ // The value is interpreted as a token, and must equal the value of
+ // meta.ReconcileRequestAnnotation in order to trigger a release.
+ ForceRequestAnnotation string = "reconcile.fluxcd.io/forceAt"
+
+ // ResetRequestAnnotation is the annotation used for resetting the failure counts
+ // of a HelmRelease, so that it can be retried again.
+ // The value is interpreted as a token, and must equal the value of
+ // meta.ReconcileRequestAnnotation in order to reset the failure counts.
+ ResetRequestAnnotation string = "reconcile.fluxcd.io/resetAt"
+)
+
+// ShouldHandleResetRequest returns true if the HelmRelease has a reset request
+// annotation, and the value of the annotation matches the value of the
+// meta.ReconcileRequestAnnotation annotation.
+//
+// To ensure that the reset request is handled only once, the value of
+// HelmReleaseStatus.LastHandledResetAt is updated to match the value of the
+// reset request annotation (even if the reset request is not handled because
+// the value of the meta.ReconcileRequestAnnotation annotation does not match).
+func ShouldHandleResetRequest(obj *HelmRelease) bool {
+ return handleRequest(obj, ResetRequestAnnotation, &obj.Status.LastHandledResetAt)
+}
+
+// ShouldHandleForceRequest returns true if the HelmRelease has a force request
+// annotation, and the value of the annotation matches the value of the
+// meta.ReconcileRequestAnnotation annotation.
+//
+// To ensure that the force request is handled only once, the value of
+// HelmReleaseStatus.LastHandledForceAt is updated to match the value of the
+// force request annotation (even if the force request is not handled because
+// the value of the meta.ReconcileRequestAnnotation annotation does not match).
+func ShouldHandleForceRequest(obj *HelmRelease) bool {
+ return handleRequest(obj, ForceRequestAnnotation, &obj.Status.LastHandledForceAt)
+}
+
+// handleRequest returns true if the HelmRelease has a request annotation, and
+// the value of the annotation matches the value of the meta.ReconcileRequestAnnotation
+// annotation.
+//
+// The lastHandled argument is used to ensure that the request is handled only
+// once, and is updated to match the value of the request annotation (even if
+// the request is not handled because the value of the meta.ReconcileRequestAnnotation
+// annotation does not match).
+func handleRequest(obj *HelmRelease, annotation string, lastHandled *string) bool {
+ requestAt, requestOk := obj.GetAnnotations()[annotation]
+ reconcileAt, reconcileOk := meta.ReconcileAnnotationValue(obj.GetAnnotations())
+
+ var lastHandledRequest string
+ if requestOk {
+ lastHandledRequest = *lastHandled
+ *lastHandled = requestAt
+ }
+
+ if requestOk && reconcileOk && requestAt == reconcileAt {
+ lastHandledReconcile := obj.Status.GetLastHandledReconcileRequest()
+ if lastHandledReconcile != reconcileAt && lastHandledRequest != requestAt {
+ return true
+ }
+ }
+ return false
+}
diff --git a/api/v2beta2/annotations_test.go b/api/v2beta2/annotations_test.go
new file mode 100644
index 000000000..44b593005
--- /dev/null
+++ b/api/v2beta2/annotations_test.go
@@ -0,0 +1,165 @@
+/*
+Copyright 2023 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package v2beta2
+
+import (
+ "testing"
+
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+ "github.com/fluxcd/pkg/apis/meta"
+)
+
+func TestShouldHandleResetRequest(t *testing.T) {
+ t.Run("should handle reset request", func(t *testing.T) {
+ obj := &HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Annotations: map[string]string{
+ meta.ReconcileRequestAnnotation: "b",
+ ResetRequestAnnotation: "b",
+ },
+ },
+ Status: HelmReleaseStatus{
+ LastHandledResetAt: "a",
+ ReconcileRequestStatus: meta.ReconcileRequestStatus{
+ LastHandledReconcileAt: "a",
+ },
+ },
+ }
+
+ if !ShouldHandleResetRequest(obj) {
+ t.Error("ShouldHandleResetRequest() = false")
+ }
+
+ if obj.Status.LastHandledResetAt != "b" {
+ t.Error("ShouldHandleResetRequest did not update LastHandledResetAt")
+ }
+ })
+}
+
+func TestShouldHandleForceRequest(t *testing.T) {
+ t.Run("should handle force request", func(t *testing.T) {
+ obj := &HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Annotations: map[string]string{
+ meta.ReconcileRequestAnnotation: "b",
+ ForceRequestAnnotation: "b",
+ },
+ },
+ Status: HelmReleaseStatus{
+ LastHandledForceAt: "a",
+ ReconcileRequestStatus: meta.ReconcileRequestStatus{
+ LastHandledReconcileAt: "a",
+ },
+ },
+ }
+
+ if !ShouldHandleForceRequest(obj) {
+ t.Error("ShouldHandleForceRequest() = false")
+ }
+
+ if obj.Status.LastHandledForceAt != "b" {
+ t.Error("ShouldHandleForceRequest did not update LastHandledForceAt")
+ }
+ })
+}
+
+func Test_handleRequest(t *testing.T) {
+ const requestAnnotation = "requestAnnotation"
+
+ tests := []struct {
+ name string
+ annotations map[string]string
+ lastHandledReconcile string
+ lastHandledRequest string
+ want bool
+ expectLastHandledRequest string
+ }{
+ {
+ name: "valid request and reconcile annotations",
+ annotations: map[string]string{
+ meta.ReconcileRequestAnnotation: "b",
+ requestAnnotation: "b",
+ },
+ want: true,
+ expectLastHandledRequest: "b",
+ },
+ {
+ name: "mismatched annotations",
+ annotations: map[string]string{
+ meta.ReconcileRequestAnnotation: "b",
+ requestAnnotation: "c",
+ },
+ want: false,
+ expectLastHandledRequest: "c",
+ },
+ {
+ name: "reconcile matches previous request",
+ annotations: map[string]string{
+ meta.ReconcileRequestAnnotation: "b",
+ requestAnnotation: "b",
+ },
+ lastHandledReconcile: "a",
+ lastHandledRequest: "b",
+ want: false,
+ expectLastHandledRequest: "b",
+ },
+ {
+ name: "request matches previous reconcile",
+ annotations: map[string]string{
+ meta.ReconcileRequestAnnotation: "b",
+ requestAnnotation: "b",
+ },
+ lastHandledReconcile: "b",
+ lastHandledRequest: "a",
+ want: false,
+ expectLastHandledRequest: "b",
+ },
+ {
+ name: "missing annotations",
+ annotations: map[string]string{},
+ lastHandledRequest: "a",
+ want: false,
+ expectLastHandledRequest: "a",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ obj := &HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Annotations: tt.annotations,
+ },
+ Status: HelmReleaseStatus{
+ ReconcileRequestStatus: meta.ReconcileRequestStatus{
+ LastHandledReconcileAt: tt.lastHandledReconcile,
+ },
+ },
+ }
+
+ lastHandled := tt.lastHandledRequest
+ result := handleRequest(obj, requestAnnotation, &lastHandled)
+
+ if result != tt.want {
+ t.Errorf("handleRequest() = %v, want %v", result, tt.want)
+ }
+ if lastHandled != tt.expectLastHandledRequest {
+ t.Errorf("lastHandledRequest = %v, want %v", lastHandled, tt.expectLastHandledRequest)
+ }
+ })
+ }
+}
diff --git a/api/v2beta2/condition_types.go b/api/v2beta2/condition_types.go
new file mode 100644
index 000000000..10172dfb1
--- /dev/null
+++ b/api/v2beta2/condition_types.go
@@ -0,0 +1,98 @@
+/*
+Copyright 2022 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package v2beta2
+
+const (
+ // ReleasedCondition represents the status of the last release attempt
+ // (install/upgrade/test) against the latest desired state.
+ ReleasedCondition string = "Released"
+
+ // TestSuccessCondition represents the status of the last test attempt against
+ // the latest desired state.
+ TestSuccessCondition string = "TestSuccess"
+
+ // RemediatedCondition represents the status of the last remediation attempt
+ // (uninstall/rollback) due to a failure of the last release attempt against the
+ // latest desired state.
+ RemediatedCondition string = "Remediated"
+)
+
+const (
+ // InstallSucceededReason represents the fact that the Helm install for the
+ // HelmRelease succeeded.
+ InstallSucceededReason string = "InstallSucceeded"
+
+ // InstallFailedReason represents the fact that the Helm install for the
+ // HelmRelease failed.
+ InstallFailedReason string = "InstallFailed"
+
+ // UpgradeSucceededReason represents the fact that the Helm upgrade for the
+ // HelmRelease succeeded.
+ UpgradeSucceededReason string = "UpgradeSucceeded"
+
+ // UpgradeFailedReason represents the fact that the Helm upgrade for the
+ // HelmRelease failed.
+ UpgradeFailedReason string = "UpgradeFailed"
+
+ // TestSucceededReason represents the fact that the Helm tests for the
+ // HelmRelease succeeded.
+ TestSucceededReason string = "TestSucceeded"
+
+ // TestFailedReason represents the fact that the Helm tests for the HelmRelease
+ // failed.
+ TestFailedReason string = "TestFailed"
+
+ // RollbackSucceededReason represents the fact that the Helm rollback for the
+ // HelmRelease succeeded.
+ RollbackSucceededReason string = "RollbackSucceeded"
+
+ // RollbackFailedReason represents the fact that the Helm test for the
+ // HelmRelease failed.
+ RollbackFailedReason string = "RollbackFailed"
+
+ // UninstallSucceededReason represents the fact that the Helm uninstall for the
+ // HelmRelease succeeded.
+ UninstallSucceededReason string = "UninstallSucceeded"
+
+ // UninstallFailedReason represents the fact that the Helm uninstall for the
+ // HelmRelease failed.
+ UninstallFailedReason string = "UninstallFailed"
+
+ // ArtifactFailedReason represents the fact that the artifact download for the
+ // HelmRelease failed.
+ ArtifactFailedReason string = "ArtifactFailed"
+
+ // InitFailedReason represents the fact that the initialization of the Helm
+ // configuration failed.
+ InitFailedReason string = "InitFailed"
+
+ // GetLastReleaseFailedReason represents the fact that observing the last
+ // release failed.
+ GetLastReleaseFailedReason string = "GetLastReleaseFailed"
+
+ // DependencyNotReadyReason represents the fact that
+ // one of the dependencies is not ready.
+ DependencyNotReadyReason string = "DependencyNotReady"
+
+ // ReconciliationSucceededReason represents the fact that
+ // the reconciliation succeeded.
+ ReconciliationSucceededReason string = "ReconciliationSucceeded"
+
+ // ReconciliationFailedReason represents the fact that
+ // the reconciliation failed.
+ ReconciliationFailedReason string = "ReconciliationFailed"
+)
diff --git a/api/v2beta2/doc.go b/api/v2beta2/doc.go
new file mode 100644
index 000000000..282bff813
--- /dev/null
+++ b/api/v2beta2/doc.go
@@ -0,0 +1,20 @@
+/*
+Copyright 2022 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Package v2beta2 contains API Schema definitions for the helm v2beta2 API group
+// +kubebuilder:object:generate=true
+// +groupName=helm.toolkit.fluxcd.io
+package v2beta2
diff --git a/api/v2beta2/groupversion_info.go b/api/v2beta2/groupversion_info.go
new file mode 100644
index 000000000..ea03d5f67
--- /dev/null
+++ b/api/v2beta2/groupversion_info.go
@@ -0,0 +1,33 @@
+/*
+Copyright 2022 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package v2beta2
+
+import (
+ "k8s.io/apimachinery/pkg/runtime/schema"
+ "sigs.k8s.io/controller-runtime/pkg/scheme"
+)
+
+var (
+ // GroupVersion is group version used to register these objects
+ GroupVersion = schema.GroupVersion{Group: "helm.toolkit.fluxcd.io", Version: "v2beta2"}
+
+ // SchemeBuilder is used to add go types to the GroupVersionKind scheme
+ SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion}
+
+ // AddToScheme adds the types in this group-version to the given scheme.
+ AddToScheme = SchemeBuilder.AddToScheme
+)
diff --git a/api/v2beta2/helmrelease_types.go b/api/v2beta2/helmrelease_types.go
new file mode 100644
index 000000000..48990e0c7
--- /dev/null
+++ b/api/v2beta2/helmrelease_types.go
@@ -0,0 +1,1296 @@
+/*
+Copyright 2022 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package v2beta2
+
+import (
+ "strings"
+ "time"
+
+ apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/types"
+ "sigs.k8s.io/yaml"
+
+ "github.com/fluxcd/pkg/apis/kustomize"
+ "github.com/fluxcd/pkg/apis/meta"
+
+ v2 "github.com/fluxcd/helm-controller/api/v2"
+)
+
+const (
+ // HelmReleaseKind is the kind in string format.
+ HelmReleaseKind = "HelmRelease"
+ // HelmReleaseFinalizer is set on a HelmRelease when it is first handled by
+ // the controller, and removed when this object is deleted.
+ HelmReleaseFinalizer = "finalizers.fluxcd.io"
+)
+
+const (
+ // defaultMaxHistory is the default number of Helm release versions to keep.
+ defaultMaxHistory = 5
+)
+
+// Kustomize Helm PostRenderer specification.
+type Kustomize struct {
+ // Strategic merge and JSON patches, defined as inline YAML objects,
+ // capable of targeting objects based on kind, label and annotation selectors.
+ // +optional
+ Patches []kustomize.Patch `json:"patches,omitempty"`
+
+ // Strategic merge patches, defined as inline YAML objects.
+ //
+ // Deprecated: use Patches instead.
+ // +optional
+ PatchesStrategicMerge []apiextensionsv1.JSON `json:"patchesStrategicMerge,omitempty"`
+
+ // JSON 6902 patches, defined as inline YAML objects.
+ //
+ // Deprecated: use Patches instead.
+ // +optional
+ PatchesJSON6902 []kustomize.JSON6902Patch `json:"patchesJson6902,omitempty"`
+
+ // Images is a list of (image name, new name, new tag or digest)
+ // for changing image names, tags or digests. This can also be achieved with a
+ // patch, but this operator is simpler to specify.
+ // +optional
+ Images []kustomize.Image `json:"images,omitempty" json:"images,omitempty"`
+}
+
+// PostRenderer contains a Helm PostRenderer specification.
+type PostRenderer struct {
+ // Kustomization to apply as PostRenderer.
+ // +optional
+ Kustomize *Kustomize `json:"kustomize,omitempty"`
+}
+
+// HelmReleaseSpec defines the desired state of a Helm release.
+// +kubebuilder:validation:XValidation:rule="(has(self.chart) && !has(self.chartRef)) || (!has(self.chart) && has(self.chartRef))", message="either chart or chartRef must be set"
+type HelmReleaseSpec struct {
+ // Chart defines the template of the v1beta2.HelmChart that should be created
+ // for this HelmRelease.
+ // +optional
+ Chart *HelmChartTemplate `json:"chart,omitempty"`
+
+ // ChartRef holds a reference to a source controller resource containing the
+ // Helm chart artifact.
+ //
+ // Note: this field is provisional to the v2 API, and not actively used
+ // by v2beta2 HelmReleases.
+ // +optional
+ ChartRef *CrossNamespaceSourceReference `json:"chartRef,omitempty"`
+
+ // Interval at which to reconcile the Helm release.
+ // +kubebuilder:validation:Type=string
+ // +kubebuilder:validation:Pattern="^([0-9]+(\\.[0-9]+)?(ms|s|m|h))+$"
+ // +required
+ Interval metav1.Duration `json:"interval"`
+
+ // KubeConfig for reconciling the HelmRelease on a remote cluster.
+ // When used in combination with HelmReleaseSpec.ServiceAccountName,
+ // forces the controller to act on behalf of that Service Account at the
+ // target cluster.
+ // If the --default-service-account flag is set, its value will be used as
+ // a controller level fallback for when HelmReleaseSpec.ServiceAccountName
+ // is empty.
+ // +optional
+ KubeConfig *meta.KubeConfigReference `json:"kubeConfig,omitempty"`
+
+ // Suspend tells the controller to suspend reconciliation for this HelmRelease,
+ // it does not apply to already started reconciliations. Defaults to false.
+ // +optional
+ Suspend bool `json:"suspend,omitempty"`
+
+ // ReleaseName used for the Helm release. Defaults to a composition of
+ // '[TargetNamespace-]Name'.
+ // +kubebuilder:validation:MinLength=1
+ // +kubebuilder:validation:MaxLength=53
+ // +kubebuilder:validation:Optional
+ // +optional
+ ReleaseName string `json:"releaseName,omitempty"`
+
+ // TargetNamespace to target when performing operations for the HelmRelease.
+ // Defaults to the namespace of the HelmRelease.
+ // +kubebuilder:validation:MinLength=1
+ // +kubebuilder:validation:MaxLength=63
+ // +kubebuilder:validation:Optional
+ // +optional
+ TargetNamespace string `json:"targetNamespace,omitempty"`
+
+ // StorageNamespace used for the Helm storage.
+ // Defaults to the namespace of the HelmRelease.
+ // +kubebuilder:validation:MinLength=1
+ // +kubebuilder:validation:MaxLength=63
+ // +kubebuilder:validation:Optional
+ // +optional
+ StorageNamespace string `json:"storageNamespace,omitempty"`
+
+ // DependsOn may contain a meta.NamespacedObjectReference slice with
+ // references to HelmRelease resources that must be ready before this HelmRelease
+ // can be reconciled.
+ // +optional
+ DependsOn []meta.NamespacedObjectReference `json:"dependsOn,omitempty"`
+
+ // Timeout is the time to wait for any individual Kubernetes operation (like Jobs
+ // for hooks) during the performance of a Helm action. Defaults to '5m0s'.
+ // +kubebuilder:validation:Type=string
+ // +kubebuilder:validation:Pattern="^([0-9]+(\\.[0-9]+)?(ms|s|m|h))+$"
+ // +optional
+ Timeout *metav1.Duration `json:"timeout,omitempty"`
+
+ // MaxHistory is the number of revisions saved by Helm for this HelmRelease.
+ // Use '0' for an unlimited number of revisions; defaults to '5'.
+ // +optional
+ MaxHistory *int `json:"maxHistory,omitempty"`
+
+ // The name of the Kubernetes service account to impersonate
+ // when reconciling this HelmRelease.
+ // +kubebuilder:validation:MinLength=1
+ // +kubebuilder:validation:MaxLength=253
+ // +optional
+ ServiceAccountName string `json:"serviceAccountName,omitempty"`
+
+ // PersistentClient tells the controller to use a persistent Kubernetes
+ // client for this release. When enabled, the client will be reused for the
+ // duration of the reconciliation, instead of being created and destroyed
+ // for each (step of a) Helm action.
+ //
+ // This can improve performance, but may cause issues with some Helm charts
+ // that for example do create Custom Resource Definitions during installation
+ // outside Helm's CRD lifecycle hooks, which are then not observed to be
+ // available by e.g. post-install hooks.
+ //
+ // If not set, it defaults to true.
+ //
+ // +optional
+ PersistentClient *bool `json:"persistentClient,omitempty"`
+
+ // DriftDetection holds the configuration for detecting and handling
+ // differences between the manifest in the Helm storage and the resources
+ // currently existing in the cluster.
+ // +optional
+ DriftDetection *DriftDetection `json:"driftDetection,omitempty"`
+
+ // Install holds the configuration for Helm install actions for this HelmRelease.
+ // +optional
+ Install *Install `json:"install,omitempty"`
+
+ // Upgrade holds the configuration for Helm upgrade actions for this HelmRelease.
+ // +optional
+ Upgrade *Upgrade `json:"upgrade,omitempty"`
+
+ // Test holds the configuration for Helm test actions for this HelmRelease.
+ // +optional
+ Test *Test `json:"test,omitempty"`
+
+ // Rollback holds the configuration for Helm rollback actions for this HelmRelease.
+ // +optional
+ Rollback *Rollback `json:"rollback,omitempty"`
+
+ // Uninstall holds the configuration for Helm uninstall actions for this HelmRelease.
+ // +optional
+ Uninstall *Uninstall `json:"uninstall,omitempty"`
+
+ // ValuesFrom holds references to resources containing Helm values for this HelmRelease,
+ // and information about how they should be merged.
+ ValuesFrom []ValuesReference `json:"valuesFrom,omitempty"`
+
+ // Values holds the values for this Helm release.
+ // +optional
+ Values *apiextensionsv1.JSON `json:"values,omitempty"`
+
+ // PostRenderers holds an array of Helm PostRenderers, which will be applied in order
+ // of their definition.
+ // +optional
+ PostRenderers []PostRenderer `json:"postRenderers,omitempty"`
+}
+
+// DriftDetectionMode represents the modes in which a controller can detect and
+// handle differences between the manifest in the Helm storage and the resources
+// currently existing in the cluster.
+type DriftDetectionMode string
+
+var (
+ // DriftDetectionEnabled instructs the controller to actively detect any
+ // changes between the manifest in the Helm storage and the resources
+ // currently existing in the cluster.
+ // If any differences are detected, the controller will automatically
+ // correct the cluster state by performing a Helm upgrade.
+ DriftDetectionEnabled DriftDetectionMode = "enabled"
+
+ // DriftDetectionWarn instructs the controller to actively detect any
+ // changes between the manifest in the Helm storage and the resources
+ // currently existing in the cluster.
+ // If any differences are detected, the controller will emit a warning
+ // without automatically correcting the cluster state.
+ DriftDetectionWarn DriftDetectionMode = "warn"
+
+ // DriftDetectionDisabled instructs the controller to skip detection of
+ // differences entirely.
+ // This is the default behavior, and the controller will not actively
+ // detect or respond to differences between the manifest in the Helm
+ // storage and the resources currently existing in the cluster.
+ DriftDetectionDisabled DriftDetectionMode = "disabled"
+)
+
+var (
+ // DriftDetectionMetadataKey is the label or annotation key used to disable
+ // the diffing of an object.
+ DriftDetectionMetadataKey = GroupVersion.Group + "/driftDetection"
+ // DriftDetectionDisabledValue is the value used to disable the diffing of
+ // an object using DriftDetectionMetadataKey.
+ DriftDetectionDisabledValue = "disabled"
+)
+
+// IgnoreRule defines a rule to selectively disregard specific changes during
+// the drift detection process.
+type IgnoreRule struct {
+ // Paths is a list of JSON Pointer (RFC 6901) paths to be excluded from
+ // consideration in a Kubernetes object.
+ // +required
+ Paths []string `json:"paths"`
+
+ // Target is a selector for specifying Kubernetes objects to which this
+ // rule applies.
+ // If Target is not set, the Paths will be ignored for all Kubernetes
+ // objects within the manifest of the Helm release.
+ // +optional
+ Target *kustomize.Selector `json:"target,omitempty"`
+}
+
+// DriftDetection defines the strategy for performing differential analysis and
+// provides a way to define rules for ignoring specific changes during this
+// process.
+type DriftDetection struct {
+ // Mode defines how differences should be handled between the Helm manifest
+ // and the manifest currently applied to the cluster.
+ // If not explicitly set, it defaults to DiffModeDisabled.
+ // +kubebuilder:validation:Enum=enabled;warn;disabled
+ // +optional
+ Mode DriftDetectionMode `json:"mode,omitempty"`
+
+ // Ignore contains a list of rules for specifying which changes to ignore
+ // during diffing.
+ // +optional
+ Ignore []IgnoreRule `json:"ignore,omitempty"`
+}
+
+// GetMode returns the DiffMode set on the Diff, or DiffModeDisabled if not
+// set.
+func (d DriftDetection) GetMode() DriftDetectionMode {
+ if d.Mode == "" {
+ return DriftDetectionDisabled
+ }
+ return d.Mode
+}
+
+// MustDetectChanges returns true if the DiffMode is set to DiffModeEnabled or
+// DiffModeWarn.
+func (d DriftDetection) MustDetectChanges() bool {
+ return d.GetMode() == DriftDetectionEnabled || d.GetMode() == DriftDetectionWarn
+}
+
+// HelmChartTemplate defines the template from which the controller will
+// generate a v1beta2.HelmChart object in the same namespace as the referenced
+// v1.Source.
+type HelmChartTemplate struct {
+ // ObjectMeta holds the template for metadata like labels and annotations.
+ // +optional
+ ObjectMeta *HelmChartTemplateObjectMeta `json:"metadata,omitempty"`
+
+ // Spec holds the template for the v1beta2.HelmChartSpec for this HelmRelease.
+ // +required
+ Spec HelmChartTemplateSpec `json:"spec"`
+}
+
+// HelmChartTemplateObjectMeta defines the template for the ObjectMeta of a
+// v1beta2.HelmChart.
+type HelmChartTemplateObjectMeta struct {
+ // Map of string keys and values that can be used to organize and categorize
+ // (scope and select) objects.
+ // More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/
+ // +optional
+ Labels map[string]string `json:"labels,omitempty"`
+
+ // Annotations is an unstructured key value map stored with a resource that may be
+ // set by external tools to store and retrieve arbitrary metadata. They are not
+ // queryable and should be preserved when modifying objects.
+ // More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/
+ // +optional
+ Annotations map[string]string `json:"annotations,omitempty"`
+}
+
+// HelmChartTemplateSpec defines the template from which the controller will
+// generate a v1beta2.HelmChartSpec object.
+type HelmChartTemplateSpec struct {
+ // The name or path the Helm chart is available at in the SourceRef.
+ // +kubebuilder:validation:MinLength=1
+ // +kubebuilder:validation:MaxLength=2048
+ // +required
+ Chart string `json:"chart"`
+
+ // Version semver expression, ignored for charts from v1beta2.GitRepository and
+ // v1beta2.Bucket sources. Defaults to latest when omitted.
+ // +kubebuilder:default:=*
+ // +optional
+ Version string `json:"version,omitempty"`
+
+ // The name and namespace of the v1.Source the chart is available at.
+ // +required
+ SourceRef CrossNamespaceObjectReference `json:"sourceRef"`
+
+ // Interval at which to check the v1.Source for updates. Defaults to
+ // 'HelmReleaseSpec.Interval'.
+ // +kubebuilder:validation:Type=string
+ // +kubebuilder:validation:Pattern="^([0-9]+(\\.[0-9]+)?(ms|s|m|h))+$"
+ // +optional
+ Interval *metav1.Duration `json:"interval,omitempty"`
+
+ // Determines what enables the creation of a new artifact. Valid values are
+ // ('ChartVersion', 'Revision').
+ // See the documentation of the values for an explanation on their behavior.
+ // Defaults to ChartVersion when omitted.
+ // +kubebuilder:validation:Enum=ChartVersion;Revision
+ // +kubebuilder:default:=ChartVersion
+ // +optional
+ ReconcileStrategy string `json:"reconcileStrategy,omitempty"`
+
+ // Alternative list of values files to use as the chart values (values.yaml
+ // is not included by default), expected to be a relative path in the SourceRef.
+ // Values files are merged in the order of this list with the last file overriding
+ // the first. Ignored when omitted.
+ // +optional
+ ValuesFiles []string `json:"valuesFiles,omitempty"`
+
+ // Alternative values file to use as the default chart values, expected to
+ // be a relative path in the SourceRef. Deprecated in favor of ValuesFiles,
+ // for backwards compatibility the file defined here is merged before the
+ // ValuesFiles items. Ignored when omitted.
+ // +optional
+ // +deprecated
+ ValuesFile string `json:"valuesFile,omitempty"`
+
+ // IgnoreMissingValuesFiles controls whether to silently ignore missing values files rather than failing.
+ // +optional
+ IgnoreMissingValuesFiles bool `json:"ignoreMissingValuesFiles,omitempty"`
+
+ // Verify contains the secret name containing the trusted public keys
+ // used to verify the signature and specifies which provider to use to check
+ // whether OCI image is authentic.
+ // This field is only supported for OCI sources.
+ // Chart dependencies, which are not bundled in the umbrella chart artifact,
+ // are not verified.
+ // +optional
+ Verify *HelmChartTemplateVerification `json:"verify,omitempty"`
+}
+
+// GetInterval returns the configured interval for the v1beta2.HelmChart,
+// or the given default.
+func (in HelmChartTemplate) GetInterval(defaultInterval metav1.Duration) metav1.Duration {
+ if in.Spec.Interval == nil {
+ return defaultInterval
+ }
+ return *in.Spec.Interval
+}
+
+// GetNamespace returns the namespace targeted namespace for the
+// v1beta2.HelmChart, or the given default.
+func (in HelmChartTemplate) GetNamespace(defaultNamespace string) string {
+ if in.Spec.SourceRef.Namespace == "" {
+ return defaultNamespace
+ }
+ return in.Spec.SourceRef.Namespace
+}
+
+// HelmChartTemplateVerification verifies the authenticity of an OCI Helm chart.
+type HelmChartTemplateVerification struct {
+ // Provider specifies the technology used to sign the OCI Helm chart.
+ // +kubebuilder:validation:Enum=cosign;notation
+ // +kubebuilder:default:=cosign
+ Provider string `json:"provider"`
+
+ // SecretRef specifies the Kubernetes Secret containing the
+ // trusted public keys.
+ // +optional
+ SecretRef *meta.LocalObjectReference `json:"secretRef,omitempty"`
+}
+
+// Remediation defines a consistent interface for InstallRemediation and
+// UpgradeRemediation.
+// +kubebuilder:object:generate=false
+type Remediation interface {
+ GetRetries() int
+ MustIgnoreTestFailures(bool) bool
+ MustRemediateLastFailure() bool
+ GetStrategy() RemediationStrategy
+ GetFailureCount(hr *HelmRelease) int64
+ IncrementFailureCount(hr *HelmRelease)
+ RetriesExhausted(hr *HelmRelease) bool
+}
+
+// Install holds the configuration for Helm install actions performed for this
+// HelmRelease.
+type Install struct {
+ // Timeout is the time to wait for any individual Kubernetes operation (like
+ // Jobs for hooks) during the performance of a Helm install action. Defaults to
+ // 'HelmReleaseSpec.Timeout'.
+ // +kubebuilder:validation:Type=string
+ // +kubebuilder:validation:Pattern="^([0-9]+(\\.[0-9]+)?(ms|s|m|h))+$"
+ // +optional
+ Timeout *metav1.Duration `json:"timeout,omitempty"`
+
+ // Remediation holds the remediation configuration for when the Helm install
+ // action for the HelmRelease fails. The default is to not perform any action.
+ // +optional
+ Remediation *InstallRemediation `json:"remediation,omitempty"`
+
+ // DisableWait disables the waiting for resources to be ready after a Helm
+ // install has been performed.
+ // +optional
+ DisableWait bool `json:"disableWait,omitempty"`
+
+ // DisableWaitForJobs disables waiting for jobs to complete after a Helm
+ // install has been performed.
+ // +optional
+ DisableWaitForJobs bool `json:"disableWaitForJobs,omitempty"`
+
+ // DisableHooks prevents hooks from running during the Helm install action.
+ // +optional
+ DisableHooks bool `json:"disableHooks,omitempty"`
+
+ // DisableOpenAPIValidation prevents the Helm install action from validating
+ // rendered templates against the Kubernetes OpenAPI Schema.
+ // +optional
+ DisableOpenAPIValidation bool `json:"disableOpenAPIValidation,omitempty"`
+
+ // Replace tells the Helm install action to re-use the 'ReleaseName', but only
+ // if that name is a deleted release which remains in the history.
+ // +optional
+ Replace bool `json:"replace,omitempty"`
+
+ // SkipCRDs tells the Helm install action to not install any CRDs. By default,
+ // CRDs are installed if not already present.
+ //
+ // Deprecated use CRD policy (`crds`) attribute with value `Skip` instead.
+ //
+ // +deprecated
+ // +optional
+ SkipCRDs bool `json:"skipCRDs,omitempty"`
+
+ // CRDs upgrade CRDs from the Helm Chart's crds directory according
+ // to the CRD upgrade policy provided here. Valid values are `Skip`,
+ // `Create` or `CreateReplace`. Default is `Create` and if omitted
+ // CRDs are installed but not updated.
+ //
+ // Skip: do neither install nor replace (update) any CRDs.
+ //
+ // Create: new CRDs are created, existing CRDs are neither updated nor deleted.
+ //
+ // CreateReplace: new CRDs are created, existing CRDs are updated (replaced)
+ // but not deleted.
+ //
+ // By default, CRDs are applied (installed) during Helm install action.
+ // With this option users can opt in to CRD replace existing CRDs on Helm
+ // install actions, which is not (yet) natively supported by Helm.
+ // https://helm.sh/docs/chart_best_practices/custom_resource_definitions.
+ //
+ // +kubebuilder:validation:Enum=Skip;Create;CreateReplace
+ // +optional
+ CRDs CRDsPolicy `json:"crds,omitempty"`
+
+ // CreateNamespace tells the Helm install action to create the
+ // HelmReleaseSpec.TargetNamespace if it does not exist yet.
+ // On uninstall, the namespace will not be garbage collected.
+ // +optional
+ CreateNamespace bool `json:"createNamespace,omitempty"`
+}
+
+// GetTimeout returns the configured timeout for the Helm install action,
+// or the given default.
+func (in Install) GetTimeout(defaultTimeout metav1.Duration) metav1.Duration {
+ if in.Timeout == nil {
+ return defaultTimeout
+ }
+ return *in.Timeout
+}
+
+// GetRemediation returns the configured Remediation for the Helm install action.
+func (in Install) GetRemediation() Remediation {
+ if in.Remediation == nil {
+ return InstallRemediation{}
+ }
+ return *in.Remediation
+}
+
+// InstallRemediation holds the configuration for Helm install remediation.
+type InstallRemediation struct {
+ // Retries is the number of retries that should be attempted on failures before
+ // bailing. Remediation, using an uninstall, is performed between each attempt.
+ // Defaults to '0', a negative integer equals to unlimited retries.
+ // +optional
+ Retries int `json:"retries,omitempty"`
+
+ // IgnoreTestFailures tells the controller to skip remediation when the Helm
+ // tests are run after an install action but fail. Defaults to
+ // 'Test.IgnoreFailures'.
+ // +optional
+ IgnoreTestFailures *bool `json:"ignoreTestFailures,omitempty"`
+
+ // RemediateLastFailure tells the controller to remediate the last failure, when
+ // no retries remain. Defaults to 'false'.
+ // +optional
+ RemediateLastFailure *bool `json:"remediateLastFailure,omitempty"`
+}
+
+// GetRetries returns the number of retries that should be attempted on
+// failures.
+func (in InstallRemediation) GetRetries() int {
+ return in.Retries
+}
+
+// MustIgnoreTestFailures returns the configured IgnoreTestFailures or the given
+// default.
+func (in InstallRemediation) MustIgnoreTestFailures(def bool) bool {
+ if in.IgnoreTestFailures == nil {
+ return def
+ }
+ return *in.IgnoreTestFailures
+}
+
+// MustRemediateLastFailure returns whether to remediate the last failure when
+// no retries remain.
+func (in InstallRemediation) MustRemediateLastFailure() bool {
+ if in.RemediateLastFailure == nil {
+ return false
+ }
+ return *in.RemediateLastFailure
+}
+
+// GetStrategy returns the strategy to use for failure remediation.
+func (in InstallRemediation) GetStrategy() RemediationStrategy {
+ return UninstallRemediationStrategy
+}
+
+// GetFailureCount gets the failure count.
+func (in InstallRemediation) GetFailureCount(hr *HelmRelease) int64 {
+ return hr.Status.InstallFailures
+}
+
+// IncrementFailureCount increments the failure count.
+func (in InstallRemediation) IncrementFailureCount(hr *HelmRelease) {
+ hr.Status.InstallFailures++
+}
+
+// RetriesExhausted returns true if there are no remaining retries.
+func (in InstallRemediation) RetriesExhausted(hr *HelmRelease) bool {
+ return in.Retries >= 0 && in.GetFailureCount(hr) > int64(in.Retries)
+}
+
+// CRDsPolicy defines the install/upgrade approach to use for CRDs when
+// installing or upgrading a HelmRelease.
+type CRDsPolicy string
+
+const (
+ // Skip CRDs do neither install nor replace (update) any CRDs.
+ Skip CRDsPolicy = "Skip"
+ // Create CRDs which do not already exist, do not replace (update) already existing
+ // CRDs and keep (do not delete) CRDs which no longer exist in the current release.
+ Create CRDsPolicy = "Create"
+ // Create CRDs which do not already exist, Replace (update) already existing CRDs
+ // and keep (do not delete) CRDs which no longer exist in the current release.
+ CreateReplace CRDsPolicy = "CreateReplace"
+)
+
+// Upgrade holds the configuration for Helm upgrade actions for this
+// HelmRelease.
+type Upgrade struct {
+ // Timeout is the time to wait for any individual Kubernetes operation (like
+ // Jobs for hooks) during the performance of a Helm upgrade action. Defaults to
+ // 'HelmReleaseSpec.Timeout'.
+ // +kubebuilder:validation:Type=string
+ // +kubebuilder:validation:Pattern="^([0-9]+(\\.[0-9]+)?(ms|s|m|h))+$"
+ // +optional
+ Timeout *metav1.Duration `json:"timeout,omitempty"`
+
+ // Remediation holds the remediation configuration for when the Helm upgrade
+ // action for the HelmRelease fails. The default is to not perform any action.
+ // +optional
+ Remediation *UpgradeRemediation `json:"remediation,omitempty"`
+
+ // DisableWait disables the waiting for resources to be ready after a Helm
+ // upgrade has been performed.
+ // +optional
+ DisableWait bool `json:"disableWait,omitempty"`
+
+ // DisableWaitForJobs disables waiting for jobs to complete after a Helm
+ // upgrade has been performed.
+ // +optional
+ DisableWaitForJobs bool `json:"disableWaitForJobs,omitempty"`
+
+ // DisableHooks prevents hooks from running during the Helm upgrade action.
+ // +optional
+ DisableHooks bool `json:"disableHooks,omitempty"`
+
+ // DisableOpenAPIValidation prevents the Helm upgrade action from validating
+ // rendered templates against the Kubernetes OpenAPI Schema.
+ // +optional
+ DisableOpenAPIValidation bool `json:"disableOpenAPIValidation,omitempty"`
+
+ // Force forces resource updates through a replacement strategy.
+ // +optional
+ Force bool `json:"force,omitempty"`
+
+ // PreserveValues will make Helm reuse the last release's values and merge in
+ // overrides from 'Values'. Setting this flag makes the HelmRelease
+ // non-declarative.
+ // +optional
+ PreserveValues bool `json:"preserveValues,omitempty"`
+
+ // CleanupOnFail allows deletion of new resources created during the Helm
+ // upgrade action when it fails.
+ // +optional
+ CleanupOnFail bool `json:"cleanupOnFail,omitempty"`
+
+ // CRDs upgrade CRDs from the Helm Chart's crds directory according
+ // to the CRD upgrade policy provided here. Valid values are `Skip`,
+ // `Create` or `CreateReplace`. Default is `Skip` and if omitted
+ // CRDs are neither installed nor upgraded.
+ //
+ // Skip: do neither install nor replace (update) any CRDs.
+ //
+ // Create: new CRDs are created, existing CRDs are neither updated nor deleted.
+ //
+ // CreateReplace: new CRDs are created, existing CRDs are updated (replaced)
+ // but not deleted.
+ //
+ // By default, CRDs are not applied during Helm upgrade action. With this
+ // option users can opt-in to CRD upgrade, which is not (yet) natively supported by Helm.
+ // https://helm.sh/docs/chart_best_practices/custom_resource_definitions.
+ //
+ // +kubebuilder:validation:Enum=Skip;Create;CreateReplace
+ // +optional
+ CRDs CRDsPolicy `json:"crds,omitempty"`
+}
+
+// GetTimeout returns the configured timeout for the Helm upgrade action, or the
+// given default.
+func (in Upgrade) GetTimeout(defaultTimeout metav1.Duration) metav1.Duration {
+ if in.Timeout == nil {
+ return defaultTimeout
+ }
+ return *in.Timeout
+}
+
+// GetRemediation returns the configured Remediation for the Helm upgrade
+// action.
+func (in Upgrade) GetRemediation() Remediation {
+ if in.Remediation == nil {
+ return UpgradeRemediation{}
+ }
+ return *in.Remediation
+}
+
+// UpgradeRemediation holds the configuration for Helm upgrade remediation.
+type UpgradeRemediation struct {
+ // Retries is the number of retries that should be attempted on failures before
+ // bailing. Remediation, using 'Strategy', is performed between each attempt.
+ // Defaults to '0', a negative integer equals to unlimited retries.
+ // +optional
+ Retries int `json:"retries,omitempty"`
+
+ // IgnoreTestFailures tells the controller to skip remediation when the Helm
+ // tests are run after an upgrade action but fail.
+ // Defaults to 'Test.IgnoreFailures'.
+ // +optional
+ IgnoreTestFailures *bool `json:"ignoreTestFailures,omitempty"`
+
+ // RemediateLastFailure tells the controller to remediate the last failure, when
+ // no retries remain. Defaults to 'false' unless 'Retries' is greater than 0.
+ // +optional
+ RemediateLastFailure *bool `json:"remediateLastFailure,omitempty"`
+
+ // Strategy to use for failure remediation. Defaults to 'rollback'.
+ // +kubebuilder:validation:Enum=rollback;uninstall
+ // +optional
+ Strategy *RemediationStrategy `json:"strategy,omitempty"`
+}
+
+// GetRetries returns the number of retries that should be attempted on
+// failures.
+func (in UpgradeRemediation) GetRetries() int {
+ return in.Retries
+}
+
+// MustIgnoreTestFailures returns the configured IgnoreTestFailures or the given
+// default.
+func (in UpgradeRemediation) MustIgnoreTestFailures(def bool) bool {
+ if in.IgnoreTestFailures == nil {
+ return def
+ }
+ return *in.IgnoreTestFailures
+}
+
+// MustRemediateLastFailure returns whether to remediate the last failure when
+// no retries remain.
+func (in UpgradeRemediation) MustRemediateLastFailure() bool {
+ if in.RemediateLastFailure == nil {
+ return in.Retries > 0
+ }
+ return *in.RemediateLastFailure
+}
+
+// GetStrategy returns the strategy to use for failure remediation.
+func (in UpgradeRemediation) GetStrategy() RemediationStrategy {
+ if in.Strategy == nil {
+ return RollbackRemediationStrategy
+ }
+ return *in.Strategy
+}
+
+// GetFailureCount gets the failure count.
+func (in UpgradeRemediation) GetFailureCount(hr *HelmRelease) int64 {
+ return hr.Status.UpgradeFailures
+}
+
+// IncrementFailureCount increments the failure count.
+func (in UpgradeRemediation) IncrementFailureCount(hr *HelmRelease) {
+ hr.Status.UpgradeFailures++
+}
+
+// RetriesExhausted returns true if there are no remaining retries.
+func (in UpgradeRemediation) RetriesExhausted(hr *HelmRelease) bool {
+ return in.Retries >= 0 && in.GetFailureCount(hr) > int64(in.Retries)
+}
+
+// RemediationStrategy returns the strategy to use to remediate a failed install
+// or upgrade.
+type RemediationStrategy string
+
+const (
+ // RollbackRemediationStrategy represents a Helm remediation strategy of Helm
+ // rollback.
+ RollbackRemediationStrategy RemediationStrategy = "rollback"
+
+ // UninstallRemediationStrategy represents a Helm remediation strategy of Helm
+ // uninstall.
+ UninstallRemediationStrategy RemediationStrategy = "uninstall"
+)
+
+// Test holds the configuration for Helm test actions for this HelmRelease.
+type Test struct {
+ // Enable enables Helm test actions for this HelmRelease after an Helm install
+ // or upgrade action has been performed.
+ // +optional
+ Enable bool `json:"enable,omitempty"`
+
+ // Timeout is the time to wait for any individual Kubernetes operation during
+ // the performance of a Helm test action. Defaults to 'HelmReleaseSpec.Timeout'.
+ // +kubebuilder:validation:Type=string
+ // +kubebuilder:validation:Pattern="^([0-9]+(\\.[0-9]+)?(ms|s|m|h))+$"
+ // +optional
+ Timeout *metav1.Duration `json:"timeout,omitempty"`
+
+ // IgnoreFailures tells the controller to skip remediation when the Helm tests
+ // are run but fail. Can be overwritten for tests run after install or upgrade
+ // actions in 'Install.IgnoreTestFailures' and 'Upgrade.IgnoreTestFailures'.
+ // +optional
+ IgnoreFailures bool `json:"ignoreFailures,omitempty"`
+
+ // Filters is a list of tests to run or exclude from running.
+ Filters *[]Filter `json:"filters,omitempty"`
+}
+
+// GetTimeout returns the configured timeout for the Helm test action,
+// or the given default.
+func (in Test) GetTimeout(defaultTimeout metav1.Duration) metav1.Duration {
+ if in.Timeout == nil {
+ return defaultTimeout
+ }
+ return *in.Timeout
+}
+
+// Filter holds the configuration for individual Helm test filters.
+type Filter struct {
+ // Name is the name of the test.
+ // +kubebuilder:validation:MinLength=1
+ // +kubebuilder:validation:MaxLength=253
+ // +required
+ Name string `json:"name"`
+ // Exclude specifies whether the named test should be excluded.
+ // +optional
+ Exclude bool `json:"exclude,omitempty"`
+}
+
+// GetFilters returns the configured filters for the Helm test action/
+func (in Test) GetFilters() []Filter {
+ if in.Filters == nil {
+ var filters []Filter
+ return filters
+ }
+ return *in.Filters
+}
+
+// Rollback holds the configuration for Helm rollback actions for this
+// HelmRelease.
+type Rollback struct {
+ // Timeout is the time to wait for any individual Kubernetes operation (like
+ // Jobs for hooks) during the performance of a Helm rollback action. Defaults to
+ // 'HelmReleaseSpec.Timeout'.
+ // +kubebuilder:validation:Type=string
+ // +kubebuilder:validation:Pattern="^([0-9]+(\\.[0-9]+)?(ms|s|m|h))+$"
+ // +optional
+ Timeout *metav1.Duration `json:"timeout,omitempty"`
+
+ // DisableWait disables the waiting for resources to be ready after a Helm
+ // rollback has been performed.
+ // +optional
+ DisableWait bool `json:"disableWait,omitempty"`
+
+ // DisableWaitForJobs disables waiting for jobs to complete after a Helm
+ // rollback has been performed.
+ // +optional
+ DisableWaitForJobs bool `json:"disableWaitForJobs,omitempty"`
+
+ // DisableHooks prevents hooks from running during the Helm rollback action.
+ // +optional
+ DisableHooks bool `json:"disableHooks,omitempty"`
+
+ // Recreate performs pod restarts for the resource if applicable.
+ // +optional
+ Recreate bool `json:"recreate,omitempty"`
+
+ // Force forces resource updates through a replacement strategy.
+ // +optional
+ Force bool `json:"force,omitempty"`
+
+ // CleanupOnFail allows deletion of new resources created during the Helm
+ // rollback action when it fails.
+ // +optional
+ CleanupOnFail bool `json:"cleanupOnFail,omitempty"`
+}
+
+// GetTimeout returns the configured timeout for the Helm rollback action, or
+// the given default.
+func (in Rollback) GetTimeout(defaultTimeout metav1.Duration) metav1.Duration {
+ if in.Timeout == nil {
+ return defaultTimeout
+ }
+ return *in.Timeout
+}
+
+// Uninstall holds the configuration for Helm uninstall actions for this
+// HelmRelease.
+type Uninstall struct {
+ // Timeout is the time to wait for any individual Kubernetes operation (like
+ // Jobs for hooks) during the performance of a Helm uninstall action. Defaults
+ // to 'HelmReleaseSpec.Timeout'.
+ // +kubebuilder:validation:Type=string
+ // +kubebuilder:validation:Pattern="^([0-9]+(\\.[0-9]+)?(ms|s|m|h))+$"
+ // +optional
+ Timeout *metav1.Duration `json:"timeout,omitempty"`
+
+ // DisableHooks prevents hooks from running during the Helm rollback action.
+ // +optional
+ DisableHooks bool `json:"disableHooks,omitempty"`
+
+ // KeepHistory tells Helm to remove all associated resources and mark the
+ // release as deleted, but retain the release history.
+ // +optional
+ KeepHistory bool `json:"keepHistory,omitempty"`
+
+ // DisableWait disables waiting for all the resources to be deleted after
+ // a Helm uninstall is performed.
+ // +optional
+ DisableWait bool `json:"disableWait,omitempty"`
+
+ // DeletionPropagation specifies the deletion propagation policy when
+ // a Helm uninstall is performed.
+ // +kubebuilder:default=background
+ // +kubebuilder:validation:Enum=background;foreground;orphan
+ // +optional
+ DeletionPropagation *string `json:"deletionPropagation,omitempty"`
+}
+
+// GetTimeout returns the configured timeout for the Helm uninstall action, or
+// the given default.
+func (in Uninstall) GetTimeout(defaultTimeout metav1.Duration) metav1.Duration {
+ if in.Timeout == nil {
+ return defaultTimeout
+ }
+ return *in.Timeout
+}
+
+// GetDeletionPropagation returns the configured deletion propagation policy
+// for the Helm uninstall action, or 'background'.
+func (in Uninstall) GetDeletionPropagation() string {
+ if in.DeletionPropagation == nil {
+ return "background"
+ }
+ return *in.DeletionPropagation
+}
+
+// ReleaseAction is the action to perform a Helm release.
+type ReleaseAction string
+
+const (
+ // ReleaseActionInstall represents a Helm install action.
+ ReleaseActionInstall ReleaseAction = "install"
+ // ReleaseActionUpgrade represents a Helm upgrade action.
+ ReleaseActionUpgrade ReleaseAction = "upgrade"
+)
+
+// HelmReleaseStatus defines the observed state of a HelmRelease.
+type HelmReleaseStatus struct {
+ // ObservedGeneration is the last observed generation.
+ // +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
+ LastAttemptedGeneration int64 `json:"lastAttemptedGeneration,omitempty"`
+
+ // Conditions holds the conditions for the HelmRelease.
+ // +optional
+ Conditions []metav1.Condition `json:"conditions,omitempty"`
+
+ // HelmChart is the namespaced name of the HelmChart resource created by
+ // the controller for the HelmRelease.
+ // +optional
+ HelmChart string `json:"helmChart,omitempty"`
+
+ // StorageNamespace is the namespace of the Helm release storage for the
+ // current release.
+ // +kubebuilder:validation:MaxLength=63
+ // +kubebuilder:validation:MinLength=1
+ // +kubebuilder:validation:Optional
+ // +optional
+ StorageNamespace string `json:"storageNamespace,omitempty"`
+
+ // History holds the history of Helm releases performed for this HelmRelease
+ // up to the last successfully completed release.
+ // +optional
+ History v2.Snapshots `json:"history,omitempty"`
+
+ // LastAttemptedReleaseAction is the last release action performed for this
+ // HelmRelease. It is used to determine the active remediation strategy.
+ // +kubebuilder:validation:Enum=install;upgrade
+ // +optional
+ LastAttemptedReleaseAction ReleaseAction `json:"lastAttemptedReleaseAction,omitempty"`
+
+ // Failures is the reconciliation failure count against the latest desired
+ // state. It is reset after a successful reconciliation.
+ // +optional
+ Failures int64 `json:"failures,omitempty"`
+
+ // InstallFailures is the install failure count against the latest desired
+ // state. It is reset after a successful reconciliation.
+ // +optional
+ InstallFailures int64 `json:"installFailures,omitempty"`
+
+ // UpgradeFailures is the upgrade failure count against the latest desired
+ // state. It is reset after a successful reconciliation.
+ // +optional
+ UpgradeFailures int64 `json:"upgradeFailures,omitempty"`
+
+ // LastAppliedRevision is the revision of the last successfully applied
+ // source.
+ //
+ // Deprecated: the revision can now be found in the History.
+ // +optional
+ LastAppliedRevision string `json:"lastAppliedRevision,omitempty"`
+
+ // LastAttemptedRevision is the Source revision of the last reconciliation
+ // attempt. For OCIRepository sources, the 12 first characters of the digest are
+ // appended to the chart version e.g. "1.2.3+1234567890ab".
+ // +optional
+ LastAttemptedRevision string `json:"lastAttemptedRevision,omitempty"`
+
+ // LastAttemptedRevisionDigest is the digest of the last reconciliation attempt.
+ // This is only set for OCIRepository sources.
+ // +optional
+ LastAttemptedRevisionDigest string `json:"lastAttemptedRevisionDigest,omitempty"`
+
+ // LastAttemptedValuesChecksum is the SHA1 checksum for the values of the last
+ // reconciliation attempt.
+ //
+ // Deprecated: Use LastAttemptedConfigDigest instead.
+ // +optional
+ LastAttemptedValuesChecksum string `json:"lastAttemptedValuesChecksum,omitempty"`
+
+ // LastReleaseRevision is the revision of the last successful Helm release.
+ //
+ // Deprecated: Use History instead.
+ // +optional
+ LastReleaseRevision int `json:"lastReleaseRevision,omitempty"`
+
+ // LastAttemptedConfigDigest is the digest for the config (better known as
+ // "values") of the last reconciliation attempt.
+ // +optional
+ LastAttemptedConfigDigest string `json:"lastAttemptedConfigDigest,omitempty"`
+
+ // LastHandledForceAt holds the value of the most recent force request
+ // value, so a change of the annotation value can be detected.
+ // +optional
+ LastHandledForceAt string `json:"lastHandledForceAt,omitempty"`
+
+ // LastHandledResetAt holds the value of the most recent reset request
+ // value, so a change of the annotation value can be detected.
+ // +optional
+ LastHandledResetAt string `json:"lastHandledResetAt,omitempty"`
+
+ meta.ReconcileRequestStatus `json:",inline"`
+}
+
+// ClearHistory clears the History.
+func (in *HelmReleaseStatus) ClearHistory() {
+ in.History = nil
+}
+
+// ClearFailures clears the failure counters.
+func (in *HelmReleaseStatus) ClearFailures() {
+ in.Failures = 0
+ in.InstallFailures = 0
+ in.UpgradeFailures = 0
+}
+
+// GetHelmChart returns the namespace and name of the HelmChart.
+func (in HelmReleaseStatus) GetHelmChart() (string, string) {
+ if in.HelmChart == "" {
+ return "", ""
+ }
+ if split := strings.Split(in.HelmChart, string(types.Separator)); len(split) > 1 {
+ return split[0], split[1]
+ }
+ return "", ""
+}
+
+func (in *HelmReleaseStatus) GetLastAttemptedRevision() string {
+ return in.LastAttemptedRevision
+}
+
+const (
+ // SourceIndexKey is the key used for indexing HelmReleases based on
+ // their sources.
+ SourceIndexKey string = ".metadata.source"
+)
+
+// +genclient
+// +kubebuilder:object:root=true
+// +kubebuilder:skipversion
+
+// HelmRelease is the Schema for the helmreleases API
+type HelmRelease struct {
+ metav1.TypeMeta `json:",inline"`
+ metav1.ObjectMeta `json:"metadata,omitempty"`
+
+ Spec HelmReleaseSpec `json:"spec,omitempty"`
+ // +kubebuilder:default:={"observedGeneration":-1}
+ Status HelmReleaseStatus `json:"status,omitempty"`
+}
+
+// GetDriftDetection returns the configuration for detecting and handling
+// differences between the manifest in the Helm storage and the resources
+// currently existing in the cluster.
+func (in *HelmRelease) GetDriftDetection() DriftDetection {
+ if in.Spec.DriftDetection == nil {
+ return DriftDetection{}
+ }
+ return *in.Spec.DriftDetection
+}
+
+// GetInstall returns the configuration for Helm install actions for the
+// HelmRelease.
+func (in *HelmRelease) GetInstall() Install {
+ if in.Spec.Install == nil {
+ return Install{}
+ }
+ return *in.Spec.Install
+}
+
+// GetUpgrade returns the configuration for Helm upgrade actions for this
+// HelmRelease.
+func (in *HelmRelease) GetUpgrade() Upgrade {
+ if in.Spec.Upgrade == nil {
+ return Upgrade{}
+ }
+ return *in.Spec.Upgrade
+}
+
+// GetTest returns the configuration for Helm test actions for this HelmRelease.
+func (in *HelmRelease) GetTest() Test {
+ if in.Spec.Test == nil {
+ return Test{}
+ }
+ return *in.Spec.Test
+}
+
+// GetRollback returns the configuration for Helm rollback actions for this
+// HelmRelease.
+func (in *HelmRelease) GetRollback() Rollback {
+ if in.Spec.Rollback == nil {
+ return Rollback{}
+ }
+ return *in.Spec.Rollback
+}
+
+// GetUninstall returns the configuration for Helm uninstall actions for this
+// HelmRelease.
+func (in *HelmRelease) GetUninstall() Uninstall {
+ if in.Spec.Uninstall == nil {
+ return Uninstall{}
+ }
+ return *in.Spec.Uninstall
+}
+
+// GetActiveRemediation returns the active Remediation configuration for the
+// HelmRelease.
+func (in HelmRelease) GetActiveRemediation() Remediation {
+ switch in.Status.LastAttemptedReleaseAction {
+ case ReleaseActionInstall:
+ return in.GetInstall().GetRemediation()
+ case ReleaseActionUpgrade:
+ return in.GetUpgrade().GetRemediation()
+ default:
+ return nil
+ }
+}
+
+// GetRequeueAfter returns the duration after which the HelmRelease
+// must be reconciled again.
+func (in HelmRelease) GetRequeueAfter() time.Duration {
+ return in.Spec.Interval.Duration
+}
+
+// GetValues unmarshals the raw values to a map[string]any and returns
+// the result.
+func (in HelmRelease) GetValues() map[string]any {
+ var values map[string]any
+ if in.Spec.Values != nil {
+ _ = yaml.Unmarshal(in.Spec.Values.Raw, &values)
+ }
+ return values
+}
+
+// GetReleaseName returns the configured release name, or a composition of
+// '[TargetNamespace-]Name'.
+func (in HelmRelease) GetReleaseName() string {
+ if in.Spec.ReleaseName != "" {
+ return in.Spec.ReleaseName
+ }
+ if in.Spec.TargetNamespace != "" {
+ return strings.Join([]string{in.Spec.TargetNamespace, in.Name}, "-")
+ }
+ return in.Name
+}
+
+// GetReleaseNamespace returns the configured TargetNamespace, or the namespace
+// of the HelmRelease.
+func (in HelmRelease) GetReleaseNamespace() string {
+ if in.Spec.TargetNamespace != "" {
+ return in.Spec.TargetNamespace
+ }
+ return in.Namespace
+}
+
+// GetStorageNamespace returns the configured StorageNamespace for helm, or the namespace
+// of the HelmRelease.
+func (in HelmRelease) GetStorageNamespace() string {
+ if in.Spec.StorageNamespace != "" {
+ return in.Spec.StorageNamespace
+ }
+ return in.Namespace
+}
+
+// GetHelmChartName returns the name used by the controller for the HelmChart creation.
+func (in HelmRelease) GetHelmChartName() string {
+ return strings.Join([]string{in.Namespace, in.Name}, "-")
+}
+
+// GetTimeout returns the configured Timeout, or the default of 300s.
+func (in HelmRelease) GetTimeout() metav1.Duration {
+ if in.Spec.Timeout == nil {
+ return metav1.Duration{Duration: 300 * time.Second}
+ }
+ return *in.Spec.Timeout
+}
+
+// GetMaxHistory returns the configured MaxHistory, or the default of 5.
+func (in HelmRelease) GetMaxHistory() int {
+ if in.Spec.MaxHistory == nil {
+ return defaultMaxHistory
+ }
+ return *in.Spec.MaxHistory
+}
+
+// UsePersistentClient returns the configured PersistentClient, or the default
+// of true.
+func (in HelmRelease) UsePersistentClient() bool {
+ if in.Spec.PersistentClient == nil {
+ return true
+ }
+ return *in.Spec.PersistentClient
+}
+
+// GetDependsOn returns the list of dependencies across-namespaces.
+func (in HelmRelease) GetDependsOn() []meta.NamespacedObjectReference {
+ return in.Spec.DependsOn
+}
+
+// GetConditions returns the status conditions of the object.
+func (in HelmRelease) GetConditions() []metav1.Condition {
+ return in.Status.Conditions
+}
+
+// SetConditions sets the status conditions on the object.
+func (in *HelmRelease) SetConditions(conditions []metav1.Condition) {
+ in.Status.Conditions = conditions
+}
+
+// GetStatusConditions returns a pointer to the Status.Conditions slice.
+//
+// Deprecated: use GetConditions instead.
+func (in *HelmRelease) GetStatusConditions() *[]metav1.Condition {
+ return &in.Status.Conditions
+}
+
+// IsChartRefPresent returns true if the HelmRelease has a ChartRef.
+func (in *HelmRelease) HasChartRef() bool {
+ return in.Spec.ChartRef != nil
+}
+
+// IsChartTemplatePresent returns true if the HelmRelease has a ChartTemplate.
+func (in *HelmRelease) HasChartTemplate() bool {
+ return in.Spec.Chart != nil
+}
+
+// +kubebuilder:object:root=true
+
+// HelmReleaseList contains a list of HelmRelease objects.
+type HelmReleaseList struct {
+ metav1.TypeMeta `json:",inline"`
+ metav1.ListMeta `json:"metadata,omitempty"`
+ Items []HelmRelease `json:"items"`
+}
+
+func init() {
+ SchemeBuilder.Register(&HelmRelease{}, &HelmReleaseList{})
+}
diff --git a/api/v2beta2/reference_types.go b/api/v2beta2/reference_types.go
new file mode 100644
index 000000000..385118673
--- /dev/null
+++ b/api/v2beta2/reference_types.go
@@ -0,0 +1,115 @@
+/*
+Copyright 2022 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package v2beta2
+
+// CrossNamespaceObjectReference contains enough information to let you locate
+// the typed referenced object at cluster level.
+type CrossNamespaceObjectReference struct {
+ // APIVersion of the referent.
+ // +optional
+ APIVersion string `json:"apiVersion,omitempty"`
+
+ // Kind of the referent.
+ // +kubebuilder:validation:Enum=HelmRepository;GitRepository;Bucket
+ // +required
+ Kind string `json:"kind,omitempty"`
+
+ // Name of the referent.
+ // +kubebuilder:validation:MinLength=1
+ // +kubebuilder:validation:MaxLength=253
+ // +required
+ Name string `json:"name"`
+
+ // Namespace of the referent.
+ // +kubebuilder:validation:MinLength=1
+ // +kubebuilder:validation:MaxLength=63
+ // +kubebuilder:validation:Optional
+ // +optional
+ Namespace string `json:"namespace,omitempty"`
+}
+
+// CrossNamespaceSourceReference contains enough information to let you locate
+// the typed referenced object at cluster level.
+type CrossNamespaceSourceReference struct {
+ // APIVersion of the referent.
+ // +optional
+ APIVersion string `json:"apiVersion,omitempty"`
+
+ // Kind of the referent.
+ // +kubebuilder:validation:Enum=OCIRepository;HelmChart
+ // +required
+ Kind string `json:"kind"`
+
+ // Name of the referent.
+ // +kubebuilder:validation:MinLength=1
+ // +kubebuilder:validation:MaxLength=253
+ // +required
+ Name string `json:"name"`
+
+ // Namespace of the referent, defaults to the namespace of the Kubernetes
+ // resource object that contains the reference.
+ // +kubebuilder:validation:MinLength=1
+ // +kubebuilder:validation:MaxLength=63
+ // +kubebuilder:validation:Optional
+ // +optional
+ Namespace string `json:"namespace,omitempty"`
+}
+
+// ValuesReference contains a reference to a resource containing Helm values,
+// and optionally the key they can be found at.
+type ValuesReference struct {
+ // Kind of the values referent, valid values are ('Secret', 'ConfigMap').
+ // +kubebuilder:validation:Enum=Secret;ConfigMap
+ // +required
+ Kind string `json:"kind"`
+
+ // Name of the values referent. Should reside in the same namespace as the
+ // referring resource.
+ // +kubebuilder:validation:MinLength=1
+ // +kubebuilder:validation:MaxLength=253
+ // +required
+ Name string `json:"name"`
+
+ // ValuesKey is the data key where the values.yaml or a specific value can be
+ // found at. Defaults to 'values.yaml'.
+ // +kubebuilder:validation:MaxLength=253
+ // +kubebuilder:validation:Pattern=`^[\-._a-zA-Z0-9]+$`
+ // +optional
+ ValuesKey string `json:"valuesKey,omitempty"`
+
+ // TargetPath is the YAML dot notation path the value should be merged at. When
+ // set, the ValuesKey is expected to be a single flat value. Defaults to 'None',
+ // which results in the values getting merged at the root.
+ // +kubebuilder:validation:MaxLength=250
+ // +kubebuilder:validation:Pattern=`^([a-zA-Z0-9_\-.\\\/]|\[[0-9]{1,5}\])+$`
+ // +optional
+ TargetPath string `json:"targetPath,omitempty"`
+
+ // Optional marks this ValuesReference as optional. When set, a not found error
+ // for the values reference is ignored, but any ValuesKey, TargetPath or
+ // transient error will still result in a reconciliation failure.
+ // +optional
+ Optional bool `json:"optional,omitempty"`
+}
+
+// GetValuesKey returns the defined ValuesKey, or the default ('values.yaml').
+func (in ValuesReference) GetValuesKey() string {
+ if in.ValuesKey == "" {
+ return "values.yaml"
+ }
+ return in.ValuesKey
+}
diff --git a/api/v2beta2/snapshot_types.go b/api/v2beta2/snapshot_types.go
new file mode 100644
index 000000000..ca7589a32
--- /dev/null
+++ b/api/v2beta2/snapshot_types.go
@@ -0,0 +1,236 @@
+/*
+Copyright 2023 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package v2beta2
+
+import (
+ "fmt"
+ "sort"
+
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+const (
+ // snapshotStatusDeployed indicates that the release the snapshot was taken
+ // from is currently deployed.
+ snapshotStatusDeployed = "deployed"
+ // snapshotStatusSuperseded indicates that the release the snapshot was taken
+ // from has been superseded by a newer release.
+ snapshotStatusSuperseded = "superseded"
+
+ // snapshotTestPhaseFailed indicates that the test of the release the snapshot
+ // was taken from has failed.
+ snapshotTestPhaseFailed = "Failed"
+)
+
+// Snapshots is a list of Snapshot objects.
+type Snapshots []*Snapshot
+
+// Len returns the number of Snapshots.
+func (in Snapshots) Len() int {
+ return len(in)
+}
+
+// SortByVersion sorts the Snapshots by version, in descending order.
+func (in Snapshots) SortByVersion() {
+ sort.Slice(in, func(i, j int) bool {
+ return in[i].Version > in[j].Version
+ })
+}
+
+// Latest returns the most recent Snapshot.
+func (in Snapshots) Latest() *Snapshot {
+ if len(in) == 0 {
+ return nil
+ }
+ in.SortByVersion()
+ return in[0]
+}
+
+// Previous returns the most recent Snapshot before the Latest that has a
+// status of "deployed" or "superseded", or nil if there is no such Snapshot.
+// Unless ignoreTests is true, Snapshots with a test in the "Failed" phase are
+// ignored.
+func (in Snapshots) Previous(ignoreTests bool) *Snapshot {
+ if len(in) < 2 {
+ return nil
+ }
+ in.SortByVersion()
+ for i := range in[1:] {
+ s := in[i+1]
+ if s.Status == snapshotStatusDeployed || s.Status == snapshotStatusSuperseded {
+ if ignoreTests || !s.HasTestInPhase(snapshotTestPhaseFailed) {
+ return s
+ }
+ }
+ }
+ return nil
+}
+
+// Truncate removes all Snapshots up to the Previous deployed Snapshot.
+// If there is no previous-deployed Snapshot, the most recent 5 Snapshots are
+// retained.
+func (in *Snapshots) Truncate(ignoreTests bool) {
+ if in.Len() < 2 {
+ return
+ }
+
+ in.SortByVersion()
+ for i := range (*in)[1:] {
+ s := (*in)[i+1]
+ if s.Status == snapshotStatusDeployed || s.Status == snapshotStatusSuperseded {
+ if ignoreTests || !s.HasTestInPhase(snapshotTestPhaseFailed) {
+ *in = (*in)[:i+2]
+ return
+ }
+ }
+ }
+
+ if in.Len() > defaultMaxHistory {
+ // If none of the Snapshots are deployed or superseded, and there
+ // are more than the defaultMaxHistory, truncate to the most recent
+ // Snapshots.
+ *in = (*in)[:defaultMaxHistory]
+ }
+}
+
+// Snapshot captures a point-in-time copy of the status information for a Helm release,
+// as managed by the controller.
+type Snapshot struct {
+ // APIVersion is the API version of the Snapshot.
+ // Provisional: when the calculation method of the Digest field is changed,
+ // this field will be used to distinguish between the old and new methods.
+ // +optional
+ APIVersion string `json:"apiVersion,omitempty"`
+ // Digest is the checksum of the release object in storage.
+ // It has the format of `:`.
+ // +required
+ Digest string `json:"digest"`
+ // Name is the name of the release.
+ // +required
+ Name string `json:"name"`
+ // Namespace is the namespace the release is deployed to.
+ // +required
+ Namespace string `json:"namespace"`
+ // Version is the version of the release object in storage.
+ // +required
+ Version int `json:"version"`
+ // Status is the current state of the release.
+ // +required
+ Status string `json:"status"`
+ // ChartName is the chart name of the release object in storage.
+ // +required
+ ChartName string `json:"chartName"`
+ // ChartVersion is the chart version of the release object in
+ // storage.
+ // +required
+ ChartVersion string `json:"chartVersion"`
+ // ConfigDigest is the checksum of the config (better known as
+ // "values") of the release object in storage.
+ // It has the format of `:`.
+ // +required
+ ConfigDigest string `json:"configDigest"`
+ // FirstDeployed is when the release was first deployed.
+ // +required
+ FirstDeployed metav1.Time `json:"firstDeployed"`
+ // LastDeployed is when the release was last deployed.
+ // +required
+ LastDeployed metav1.Time `json:"lastDeployed"`
+ // Deleted is when the release was deleted.
+ // +optional
+ Deleted metav1.Time `json:"deleted,omitempty"`
+ // TestHooks is the list of test hooks for the release as observed to be
+ // run by the controller.
+ // +optional
+ TestHooks *map[string]*TestHookStatus `json:"testHooks,omitempty"`
+ // OCIDigest is the digest of the OCI artifact associated with the release.
+ // +optional
+ OCIDigest string `json:"ociDigest,omitempty"`
+}
+
+// FullReleaseName returns the full name of the release in the format
+// of '/.
+func (in *Snapshot) FullReleaseName() string {
+ if in == nil {
+ return ""
+ }
+ return fmt.Sprintf("%s/%s.v%d", in.Namespace, in.Name, in.Version)
+}
+
+// VersionedChartName returns the full name of the chart in the format of
+// '@'.
+func (in *Snapshot) VersionedChartName() string {
+ if in == nil {
+ return ""
+ }
+ return fmt.Sprintf("%s@%s", in.ChartName, in.ChartVersion)
+}
+
+// HasBeenTested returns true if TestHooks is not nil. This includes an empty
+// map, which indicates the chart has no tests.
+func (in *Snapshot) HasBeenTested() bool {
+ return in != nil && in.TestHooks != nil
+}
+
+// GetTestHooks returns the TestHooks for the release if not nil.
+func (in *Snapshot) GetTestHooks() map[string]*TestHookStatus {
+ if in == nil || in.TestHooks == nil {
+ return nil
+ }
+ return *in.TestHooks
+}
+
+// HasTestInPhase returns true if any of the TestHooks is in the given phase.
+func (in *Snapshot) HasTestInPhase(phase string) bool {
+ if in != nil {
+ for _, h := range in.GetTestHooks() {
+ if h.Phase == phase {
+ return true
+ }
+ }
+ }
+ return false
+}
+
+// SetTestHooks sets the TestHooks for the release.
+func (in *Snapshot) SetTestHooks(hooks map[string]*TestHookStatus) {
+ if in == nil || hooks == nil {
+ return
+ }
+ in.TestHooks = &hooks
+}
+
+// Targets returns true if the Snapshot targets the given release data.
+func (in *Snapshot) Targets(name, namespace string, version int) bool {
+ if in != nil {
+ return in.Name == name && in.Namespace == namespace && in.Version == version
+ }
+ return false
+}
+
+// TestHookStatus holds the status information for a test hook as observed
+// to be run by the controller.
+type TestHookStatus struct {
+ // LastStarted is the time the test hook was last started.
+ // +optional
+ LastStarted metav1.Time `json:"lastStarted,omitempty"`
+ // LastCompleted is the time the test hook last completed.
+ // +optional
+ LastCompleted metav1.Time `json:"lastCompleted,omitempty"`
+ // Phase the test hook was observed to be in.
+ // +optional
+ Phase string `json:"phase,omitempty"`
+}
diff --git a/api/v2beta2/snapshot_types_test.go b/api/v2beta2/snapshot_types_test.go
new file mode 100644
index 000000000..32911877f
--- /dev/null
+++ b/api/v2beta2/snapshot_types_test.go
@@ -0,0 +1,298 @@
+/*
+Copyright 2023 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package v2beta2
+
+import (
+ "reflect"
+ "testing"
+)
+
+func TestSnapshots_Sort(t *testing.T) {
+ tests := []struct {
+ name string
+ in Snapshots
+ want Snapshots
+ }{
+ {
+ name: "sorts by descending version",
+ in: Snapshots{
+ {Version: 1},
+ {Version: 3},
+ {Version: 2},
+ },
+ want: Snapshots{
+ {Version: 3},
+ {Version: 2},
+ {Version: 1},
+ },
+ },
+ {
+ name: "already sorted",
+ in: Snapshots{
+ {Version: 3},
+ {Version: 2},
+ {Version: 1},
+ },
+ want: Snapshots{
+ {Version: 3},
+ {Version: 2},
+ {Version: 1},
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ tt.in.SortByVersion()
+
+ if !reflect.DeepEqual(tt.in, tt.want) {
+ t.Errorf("SortByVersion() got %v, want %v", tt.in, tt.want)
+ }
+ })
+ }
+}
+
+func TestSnapshots_Latest(t *testing.T) {
+ tests := []struct {
+ name string
+ in Snapshots
+ want *Snapshot
+ }{
+ {
+ name: "returns most recent snapshot",
+ in: Snapshots{
+ {Version: 1},
+ {Version: 3},
+ {Version: 2},
+ },
+ want: &Snapshot{Version: 3},
+ },
+ {
+ name: "returns nil if empty",
+ in: Snapshots{},
+ want: nil,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if got := tt.in.Latest(); !reflect.DeepEqual(got, tt.want) {
+ t.Errorf("Latest() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
+
+func TestSnapshots_Previous(t *testing.T) {
+ tests := []struct {
+ name string
+ in Snapshots
+ ignoreTests bool
+ want *Snapshot
+ }{
+ {
+ name: "returns previous snapshot",
+ in: Snapshots{
+ {Version: 2, Status: "deployed"},
+ {Version: 3, Status: "failed"},
+ {Version: 1, Status: "superseded"},
+ },
+ want: &Snapshot{Version: 2, Status: "deployed"},
+ },
+ {
+ name: "includes snapshots with failed tests",
+ in: Snapshots{
+ {Version: 4, Status: "deployed"},
+ {Version: 1, Status: "superseded"},
+ {Version: 2, Status: "superseded"},
+ {Version: 3, Status: "superseded", TestHooks: &map[string]*TestHookStatus{
+ "test": {Phase: "Failed"},
+ }},
+ },
+ ignoreTests: true,
+ want: &Snapshot{Version: 3, Status: "superseded", TestHooks: &map[string]*TestHookStatus{
+ "test": {Phase: "Failed"},
+ }},
+ },
+ {
+ name: "ignores snapshots with failed tests",
+ in: Snapshots{
+ {Version: 4, Status: "deployed"},
+ {Version: 1, Status: "superseded"},
+ {Version: 2, Status: "superseded"},
+ {Version: 3, Status: "superseded", TestHooks: &map[string]*TestHookStatus{
+ "test": {Phase: "Failed"},
+ }},
+ },
+ ignoreTests: false,
+ want: &Snapshot{Version: 2, Status: "superseded"},
+ },
+ {
+ name: "returns nil without previous snapshot",
+ in: Snapshots{
+ {Version: 1, Status: "deployed"},
+ },
+ want: nil,
+ },
+ {
+ name: "returns nil without snapshot matching criteria",
+ in: Snapshots{
+ {Version: 4, Status: "deployed"},
+ {Version: 3, Status: "superseded", TestHooks: &map[string]*TestHookStatus{
+ "test": {Phase: "Failed"},
+ }},
+ },
+ ignoreTests: false,
+ want: nil,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if got := tt.in.Previous(tt.ignoreTests); !reflect.DeepEqual(got, tt.want) {
+ t.Errorf("Previous() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
+
+func TestSnapshots_Truncate(t *testing.T) {
+ tests := []struct {
+ name string
+ in Snapshots
+ ignoreTests bool
+ want Snapshots
+ }{
+ {
+ name: "keeps previous snapshot",
+ in: Snapshots{
+ {Version: 1, Status: "superseded"},
+ {Version: 3, Status: "failed"},
+ {Version: 2, Status: "superseded"},
+ {Version: 4, Status: "deployed"},
+ },
+ want: Snapshots{
+ {Version: 4, Status: "deployed"},
+ {Version: 3, Status: "failed"},
+ {Version: 2, Status: "superseded"},
+ },
+ },
+ {
+ name: "ignores snapshots with failed tests",
+ in: Snapshots{
+ {Version: 4, Status: "deployed"},
+ {Version: 3, Status: "superseded", TestHooks: &map[string]*TestHookStatus{
+ "upgrade-test-fail-podinfo-fault-test-tiz9x": {Phase: "Failed"},
+ "upgrade-test-fail-podinfo-grpc-test-gddcw": {},
+ }},
+ {Version: 2, Status: "superseded", TestHooks: &map[string]*TestHookStatus{
+ "upgrade-test-fail-podinfo-grpc-test-h0tc2": {
+ Phase: "Succeeded",
+ },
+ "upgrade-test-fail-podinfo-jwt-test-vzusa": {
+ Phase: "Succeeded",
+ },
+ "upgrade-test-fail-podinfo-service-test-b647e": {
+ Phase: "Succeeded",
+ },
+ }},
+ },
+ ignoreTests: false,
+ want: Snapshots{
+ {Version: 4, Status: "deployed"},
+ {Version: 3, Status: "superseded", TestHooks: &map[string]*TestHookStatus{
+ "upgrade-test-fail-podinfo-fault-test-tiz9x": {Phase: "Failed"},
+ "upgrade-test-fail-podinfo-grpc-test-gddcw": {},
+ }},
+ {Version: 2, Status: "superseded", TestHooks: &map[string]*TestHookStatus{
+ "upgrade-test-fail-podinfo-grpc-test-h0tc2": {
+ Phase: "Succeeded",
+ },
+ "upgrade-test-fail-podinfo-jwt-test-vzusa": {
+ Phase: "Succeeded",
+ },
+ "upgrade-test-fail-podinfo-service-test-b647e": {
+ Phase: "Succeeded",
+ },
+ }},
+ },
+ },
+ {
+ name: "keeps previous snapshot with failed tests",
+ in: Snapshots{
+ {Version: 4, Status: "deployed"},
+ {Version: 3, Status: "superseded", TestHooks: &map[string]*TestHookStatus{
+ "upgrade-test-fail-podinfo-fault-test-tiz9x": {Phase: "Failed"},
+ "upgrade-test-fail-podinfo-grpc-test-gddcw": {},
+ }},
+ {Version: 2, Status: "superseded", TestHooks: &map[string]*TestHookStatus{
+ "upgrade-test-fail-podinfo-grpc-test-h0tc2": {
+ Phase: "Succeeded",
+ },
+ "upgrade-test-fail-podinfo-jwt-test-vzusa": {
+ Phase: "Succeeded",
+ },
+ "upgrade-test-fail-podinfo-service-test-b647e": {
+ Phase: "Succeeded",
+ },
+ }},
+ {Version: 1, Status: "superseded"},
+ },
+ ignoreTests: true,
+ want: Snapshots{
+ {Version: 4, Status: "deployed"},
+ {Version: 3, Status: "superseded", TestHooks: &map[string]*TestHookStatus{
+ "upgrade-test-fail-podinfo-fault-test-tiz9x": {Phase: "Failed"},
+ "upgrade-test-fail-podinfo-grpc-test-gddcw": {},
+ }},
+ },
+ },
+ {
+ name: "retains most recent snapshots when all have failed",
+ in: Snapshots{
+ {Version: 6, Status: "deployed"},
+ {Version: 5, Status: "failed"},
+ {Version: 4, Status: "failed"},
+ {Version: 3, Status: "failed"},
+ {Version: 2, Status: "failed"},
+ {Version: 1, Status: "failed"},
+ },
+ want: Snapshots{
+ {Version: 6, Status: "deployed"},
+ {Version: 5, Status: "failed"},
+ {Version: 4, Status: "failed"},
+ {Version: 3, Status: "failed"},
+ {Version: 2, Status: "failed"},
+ },
+ },
+ {
+ name: "without previous snapshot",
+ in: Snapshots{
+ {Version: 1, Status: "deployed"},
+ },
+ want: Snapshots{
+ {Version: 1, Status: "deployed"},
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ tt.in.Truncate(tt.ignoreTests)
+
+ if !reflect.DeepEqual(tt.in, tt.want) {
+ t.Errorf("Truncate() got %v, want %v", tt.in, tt.want)
+ }
+ })
+ }
+}
diff --git a/api/v2beta2/zz_generated.deepcopy.go b/api/v2beta2/zz_generated.deepcopy.go
new file mode 100644
index 000000000..3e6a7daa5
--- /dev/null
+++ b/api/v2beta2/zz_generated.deepcopy.go
@@ -0,0 +1,749 @@
+//go:build !ignore_autogenerated
+
+/*
+Copyright 2025 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by controller-gen. DO NOT EDIT.
+
+package v2beta2
+
+import (
+ "github.com/fluxcd/helm-controller/api/v2"
+ "github.com/fluxcd/pkg/apis/kustomize"
+ "github.com/fluxcd/pkg/apis/meta"
+ "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ runtime "k8s.io/apimachinery/pkg/runtime"
+)
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *CrossNamespaceObjectReference) DeepCopyInto(out *CrossNamespaceObjectReference) {
+ *out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CrossNamespaceObjectReference.
+func (in *CrossNamespaceObjectReference) DeepCopy() *CrossNamespaceObjectReference {
+ if in == nil {
+ return nil
+ }
+ out := new(CrossNamespaceObjectReference)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *CrossNamespaceSourceReference) DeepCopyInto(out *CrossNamespaceSourceReference) {
+ *out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CrossNamespaceSourceReference.
+func (in *CrossNamespaceSourceReference) DeepCopy() *CrossNamespaceSourceReference {
+ if in == nil {
+ return nil
+ }
+ out := new(CrossNamespaceSourceReference)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *DriftDetection) DeepCopyInto(out *DriftDetection) {
+ *out = *in
+ if in.Ignore != nil {
+ in, out := &in.Ignore, &out.Ignore
+ *out = make([]IgnoreRule, len(*in))
+ for i := range *in {
+ (*in)[i].DeepCopyInto(&(*out)[i])
+ }
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DriftDetection.
+func (in *DriftDetection) DeepCopy() *DriftDetection {
+ if in == nil {
+ return nil
+ }
+ out := new(DriftDetection)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *Filter) DeepCopyInto(out *Filter) {
+ *out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Filter.
+func (in *Filter) DeepCopy() *Filter {
+ if in == nil {
+ return nil
+ }
+ out := new(Filter)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *HelmChartTemplate) DeepCopyInto(out *HelmChartTemplate) {
+ *out = *in
+ if in.ObjectMeta != nil {
+ in, out := &in.ObjectMeta, &out.ObjectMeta
+ *out = new(HelmChartTemplateObjectMeta)
+ (*in).DeepCopyInto(*out)
+ }
+ in.Spec.DeepCopyInto(&out.Spec)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HelmChartTemplate.
+func (in *HelmChartTemplate) DeepCopy() *HelmChartTemplate {
+ if in == nil {
+ return nil
+ }
+ out := new(HelmChartTemplate)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *HelmChartTemplateObjectMeta) DeepCopyInto(out *HelmChartTemplateObjectMeta) {
+ *out = *in
+ if in.Labels != nil {
+ in, out := &in.Labels, &out.Labels
+ *out = make(map[string]string, len(*in))
+ for key, val := range *in {
+ (*out)[key] = val
+ }
+ }
+ if in.Annotations != nil {
+ in, out := &in.Annotations, &out.Annotations
+ *out = make(map[string]string, len(*in))
+ for key, val := range *in {
+ (*out)[key] = val
+ }
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HelmChartTemplateObjectMeta.
+func (in *HelmChartTemplateObjectMeta) DeepCopy() *HelmChartTemplateObjectMeta {
+ if in == nil {
+ return nil
+ }
+ out := new(HelmChartTemplateObjectMeta)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *HelmChartTemplateSpec) DeepCopyInto(out *HelmChartTemplateSpec) {
+ *out = *in
+ out.SourceRef = in.SourceRef
+ if in.Interval != nil {
+ in, out := &in.Interval, &out.Interval
+ *out = new(metav1.Duration)
+ **out = **in
+ }
+ if in.ValuesFiles != nil {
+ in, out := &in.ValuesFiles, &out.ValuesFiles
+ *out = make([]string, len(*in))
+ copy(*out, *in)
+ }
+ if in.Verify != nil {
+ in, out := &in.Verify, &out.Verify
+ *out = new(HelmChartTemplateVerification)
+ (*in).DeepCopyInto(*out)
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HelmChartTemplateSpec.
+func (in *HelmChartTemplateSpec) DeepCopy() *HelmChartTemplateSpec {
+ if in == nil {
+ return nil
+ }
+ out := new(HelmChartTemplateSpec)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *HelmChartTemplateVerification) DeepCopyInto(out *HelmChartTemplateVerification) {
+ *out = *in
+ if in.SecretRef != nil {
+ in, out := &in.SecretRef, &out.SecretRef
+ *out = new(meta.LocalObjectReference)
+ **out = **in
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HelmChartTemplateVerification.
+func (in *HelmChartTemplateVerification) DeepCopy() *HelmChartTemplateVerification {
+ if in == nil {
+ return nil
+ }
+ out := new(HelmChartTemplateVerification)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *HelmRelease) DeepCopyInto(out *HelmRelease) {
+ *out = *in
+ out.TypeMeta = in.TypeMeta
+ in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
+ in.Spec.DeepCopyInto(&out.Spec)
+ in.Status.DeepCopyInto(&out.Status)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HelmRelease.
+func (in *HelmRelease) DeepCopy() *HelmRelease {
+ if in == nil {
+ return nil
+ }
+ out := new(HelmRelease)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *HelmRelease) DeepCopyObject() runtime.Object {
+ if c := in.DeepCopy(); c != nil {
+ return c
+ }
+ return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *HelmReleaseList) DeepCopyInto(out *HelmReleaseList) {
+ *out = *in
+ out.TypeMeta = in.TypeMeta
+ in.ListMeta.DeepCopyInto(&out.ListMeta)
+ if in.Items != nil {
+ in, out := &in.Items, &out.Items
+ *out = make([]HelmRelease, len(*in))
+ for i := range *in {
+ (*in)[i].DeepCopyInto(&(*out)[i])
+ }
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HelmReleaseList.
+func (in *HelmReleaseList) DeepCopy() *HelmReleaseList {
+ if in == nil {
+ return nil
+ }
+ out := new(HelmReleaseList)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *HelmReleaseList) DeepCopyObject() runtime.Object {
+ if c := in.DeepCopy(); c != nil {
+ return c
+ }
+ return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *HelmReleaseSpec) DeepCopyInto(out *HelmReleaseSpec) {
+ *out = *in
+ if in.Chart != nil {
+ in, out := &in.Chart, &out.Chart
+ *out = new(HelmChartTemplate)
+ (*in).DeepCopyInto(*out)
+ }
+ if in.ChartRef != nil {
+ in, out := &in.ChartRef, &out.ChartRef
+ *out = new(CrossNamespaceSourceReference)
+ **out = **in
+ }
+ out.Interval = in.Interval
+ if in.KubeConfig != nil {
+ in, out := &in.KubeConfig, &out.KubeConfig
+ *out = new(meta.KubeConfigReference)
+ (*in).DeepCopyInto(*out)
+ }
+ if in.DependsOn != nil {
+ in, out := &in.DependsOn, &out.DependsOn
+ *out = make([]meta.NamespacedObjectReference, len(*in))
+ copy(*out, *in)
+ }
+ if in.Timeout != nil {
+ in, out := &in.Timeout, &out.Timeout
+ *out = new(metav1.Duration)
+ **out = **in
+ }
+ if in.MaxHistory != nil {
+ in, out := &in.MaxHistory, &out.MaxHistory
+ *out = new(int)
+ **out = **in
+ }
+ if in.PersistentClient != nil {
+ in, out := &in.PersistentClient, &out.PersistentClient
+ *out = new(bool)
+ **out = **in
+ }
+ if in.DriftDetection != nil {
+ in, out := &in.DriftDetection, &out.DriftDetection
+ *out = new(DriftDetection)
+ (*in).DeepCopyInto(*out)
+ }
+ if in.Install != nil {
+ in, out := &in.Install, &out.Install
+ *out = new(Install)
+ (*in).DeepCopyInto(*out)
+ }
+ if in.Upgrade != nil {
+ in, out := &in.Upgrade, &out.Upgrade
+ *out = new(Upgrade)
+ (*in).DeepCopyInto(*out)
+ }
+ if in.Test != nil {
+ in, out := &in.Test, &out.Test
+ *out = new(Test)
+ (*in).DeepCopyInto(*out)
+ }
+ if in.Rollback != nil {
+ in, out := &in.Rollback, &out.Rollback
+ *out = new(Rollback)
+ (*in).DeepCopyInto(*out)
+ }
+ if in.Uninstall != nil {
+ in, out := &in.Uninstall, &out.Uninstall
+ *out = new(Uninstall)
+ (*in).DeepCopyInto(*out)
+ }
+ if in.ValuesFrom != nil {
+ in, out := &in.ValuesFrom, &out.ValuesFrom
+ *out = make([]ValuesReference, len(*in))
+ copy(*out, *in)
+ }
+ if in.Values != nil {
+ in, out := &in.Values, &out.Values
+ *out = new(v1.JSON)
+ (*in).DeepCopyInto(*out)
+ }
+ if in.PostRenderers != nil {
+ in, out := &in.PostRenderers, &out.PostRenderers
+ *out = make([]PostRenderer, len(*in))
+ for i := range *in {
+ (*in)[i].DeepCopyInto(&(*out)[i])
+ }
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HelmReleaseSpec.
+func (in *HelmReleaseSpec) DeepCopy() *HelmReleaseSpec {
+ if in == nil {
+ return nil
+ }
+ out := new(HelmReleaseSpec)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *HelmReleaseStatus) DeepCopyInto(out *HelmReleaseStatus) {
+ *out = *in
+ if in.Conditions != nil {
+ in, out := &in.Conditions, &out.Conditions
+ *out = make([]metav1.Condition, len(*in))
+ for i := range *in {
+ (*in)[i].DeepCopyInto(&(*out)[i])
+ }
+ }
+ if in.History != nil {
+ in, out := &in.History, &out.History
+ *out = make(v2.Snapshots, len(*in))
+ for i := range *in {
+ if (*in)[i] != nil {
+ in, out := &(*in)[i], &(*out)[i]
+ *out = new(v2.Snapshot)
+ (*in).DeepCopyInto(*out)
+ }
+ }
+ }
+ out.ReconcileRequestStatus = in.ReconcileRequestStatus
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HelmReleaseStatus.
+func (in *HelmReleaseStatus) DeepCopy() *HelmReleaseStatus {
+ if in == nil {
+ return nil
+ }
+ out := new(HelmReleaseStatus)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *IgnoreRule) DeepCopyInto(out *IgnoreRule) {
+ *out = *in
+ if in.Paths != nil {
+ in, out := &in.Paths, &out.Paths
+ *out = make([]string, len(*in))
+ copy(*out, *in)
+ }
+ if in.Target != nil {
+ in, out := &in.Target, &out.Target
+ *out = new(kustomize.Selector)
+ **out = **in
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IgnoreRule.
+func (in *IgnoreRule) DeepCopy() *IgnoreRule {
+ if in == nil {
+ return nil
+ }
+ out := new(IgnoreRule)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *Install) DeepCopyInto(out *Install) {
+ *out = *in
+ if in.Timeout != nil {
+ in, out := &in.Timeout, &out.Timeout
+ *out = new(metav1.Duration)
+ **out = **in
+ }
+ if in.Remediation != nil {
+ in, out := &in.Remediation, &out.Remediation
+ *out = new(InstallRemediation)
+ (*in).DeepCopyInto(*out)
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Install.
+func (in *Install) DeepCopy() *Install {
+ if in == nil {
+ return nil
+ }
+ out := new(Install)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *InstallRemediation) DeepCopyInto(out *InstallRemediation) {
+ *out = *in
+ if in.IgnoreTestFailures != nil {
+ in, out := &in.IgnoreTestFailures, &out.IgnoreTestFailures
+ *out = new(bool)
+ **out = **in
+ }
+ if in.RemediateLastFailure != nil {
+ in, out := &in.RemediateLastFailure, &out.RemediateLastFailure
+ *out = new(bool)
+ **out = **in
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InstallRemediation.
+func (in *InstallRemediation) DeepCopy() *InstallRemediation {
+ if in == nil {
+ return nil
+ }
+ out := new(InstallRemediation)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *Kustomize) DeepCopyInto(out *Kustomize) {
+ *out = *in
+ if in.Patches != nil {
+ in, out := &in.Patches, &out.Patches
+ *out = make([]kustomize.Patch, len(*in))
+ for i := range *in {
+ (*in)[i].DeepCopyInto(&(*out)[i])
+ }
+ }
+ if in.PatchesStrategicMerge != nil {
+ in, out := &in.PatchesStrategicMerge, &out.PatchesStrategicMerge
+ *out = make([]v1.JSON, len(*in))
+ for i := range *in {
+ (*in)[i].DeepCopyInto(&(*out)[i])
+ }
+ }
+ if in.PatchesJSON6902 != nil {
+ in, out := &in.PatchesJSON6902, &out.PatchesJSON6902
+ *out = make([]kustomize.JSON6902Patch, len(*in))
+ for i := range *in {
+ (*in)[i].DeepCopyInto(&(*out)[i])
+ }
+ }
+ if in.Images != nil {
+ in, out := &in.Images, &out.Images
+ *out = make([]kustomize.Image, len(*in))
+ copy(*out, *in)
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Kustomize.
+func (in *Kustomize) DeepCopy() *Kustomize {
+ if in == nil {
+ return nil
+ }
+ out := new(Kustomize)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *PostRenderer) DeepCopyInto(out *PostRenderer) {
+ *out = *in
+ if in.Kustomize != nil {
+ in, out := &in.Kustomize, &out.Kustomize
+ *out = new(Kustomize)
+ (*in).DeepCopyInto(*out)
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PostRenderer.
+func (in *PostRenderer) DeepCopy() *PostRenderer {
+ if in == nil {
+ return nil
+ }
+ out := new(PostRenderer)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *Rollback) DeepCopyInto(out *Rollback) {
+ *out = *in
+ if in.Timeout != nil {
+ in, out := &in.Timeout, &out.Timeout
+ *out = new(metav1.Duration)
+ **out = **in
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Rollback.
+func (in *Rollback) DeepCopy() *Rollback {
+ if in == nil {
+ return nil
+ }
+ out := new(Rollback)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *Snapshot) DeepCopyInto(out *Snapshot) {
+ *out = *in
+ in.FirstDeployed.DeepCopyInto(&out.FirstDeployed)
+ in.LastDeployed.DeepCopyInto(&out.LastDeployed)
+ in.Deleted.DeepCopyInto(&out.Deleted)
+ if in.TestHooks != nil {
+ in, out := &in.TestHooks, &out.TestHooks
+ *out = new(map[string]*TestHookStatus)
+ if **in != nil {
+ in, out := *in, *out
+ *out = make(map[string]*TestHookStatus, len(*in))
+ for key, val := range *in {
+ var outVal *TestHookStatus
+ if val == nil {
+ (*out)[key] = nil
+ } else {
+ inVal := (*in)[key]
+ in, out := &inVal, &outVal
+ *out = new(TestHookStatus)
+ (*in).DeepCopyInto(*out)
+ }
+ (*out)[key] = outVal
+ }
+ }
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Snapshot.
+func (in *Snapshot) DeepCopy() *Snapshot {
+ if in == nil {
+ return nil
+ }
+ out := new(Snapshot)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in Snapshots) DeepCopyInto(out *Snapshots) {
+ {
+ in := &in
+ *out = make(Snapshots, len(*in))
+ for i := range *in {
+ if (*in)[i] != nil {
+ in, out := &(*in)[i], &(*out)[i]
+ *out = new(Snapshot)
+ (*in).DeepCopyInto(*out)
+ }
+ }
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Snapshots.
+func (in Snapshots) DeepCopy() Snapshots {
+ if in == nil {
+ return nil
+ }
+ out := new(Snapshots)
+ in.DeepCopyInto(out)
+ return *out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *Test) DeepCopyInto(out *Test) {
+ *out = *in
+ if in.Timeout != nil {
+ in, out := &in.Timeout, &out.Timeout
+ *out = new(metav1.Duration)
+ **out = **in
+ }
+ if in.Filters != nil {
+ in, out := &in.Filters, &out.Filters
+ *out = new([]Filter)
+ if **in != nil {
+ in, out := *in, *out
+ *out = make([]Filter, len(*in))
+ copy(*out, *in)
+ }
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Test.
+func (in *Test) DeepCopy() *Test {
+ if in == nil {
+ return nil
+ }
+ out := new(Test)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *TestHookStatus) DeepCopyInto(out *TestHookStatus) {
+ *out = *in
+ in.LastStarted.DeepCopyInto(&out.LastStarted)
+ in.LastCompleted.DeepCopyInto(&out.LastCompleted)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TestHookStatus.
+func (in *TestHookStatus) DeepCopy() *TestHookStatus {
+ if in == nil {
+ return nil
+ }
+ out := new(TestHookStatus)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *Uninstall) DeepCopyInto(out *Uninstall) {
+ *out = *in
+ if in.Timeout != nil {
+ in, out := &in.Timeout, &out.Timeout
+ *out = new(metav1.Duration)
+ **out = **in
+ }
+ if in.DeletionPropagation != nil {
+ in, out := &in.DeletionPropagation, &out.DeletionPropagation
+ *out = new(string)
+ **out = **in
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Uninstall.
+func (in *Uninstall) DeepCopy() *Uninstall {
+ if in == nil {
+ return nil
+ }
+ out := new(Uninstall)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *Upgrade) DeepCopyInto(out *Upgrade) {
+ *out = *in
+ if in.Timeout != nil {
+ in, out := &in.Timeout, &out.Timeout
+ *out = new(metav1.Duration)
+ **out = **in
+ }
+ if in.Remediation != nil {
+ in, out := &in.Remediation, &out.Remediation
+ *out = new(UpgradeRemediation)
+ (*in).DeepCopyInto(*out)
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Upgrade.
+func (in *Upgrade) DeepCopy() *Upgrade {
+ if in == nil {
+ return nil
+ }
+ out := new(Upgrade)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *UpgradeRemediation) DeepCopyInto(out *UpgradeRemediation) {
+ *out = *in
+ if in.IgnoreTestFailures != nil {
+ in, out := &in.IgnoreTestFailures, &out.IgnoreTestFailures
+ *out = new(bool)
+ **out = **in
+ }
+ if in.RemediateLastFailure != nil {
+ in, out := &in.RemediateLastFailure, &out.RemediateLastFailure
+ *out = new(bool)
+ **out = **in
+ }
+ if in.Strategy != nil {
+ in, out := &in.Strategy, &out.Strategy
+ *out = new(RemediationStrategy)
+ **out = **in
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UpgradeRemediation.
+func (in *UpgradeRemediation) DeepCopy() *UpgradeRemediation {
+ if in == nil {
+ return nil
+ }
+ out := new(UpgradeRemediation)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ValuesReference) DeepCopyInto(out *ValuesReference) {
+ *out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ValuesReference.
+func (in *ValuesReference) DeepCopy() *ValuesReference {
+ if in == nil {
+ return nil
+ }
+ out := new(ValuesReference)
+ in.DeepCopyInto(out)
+ return out
+}
diff --git a/config/crd/bases/helm.toolkit.fluxcd.io_helmreleases.yaml b/config/crd/bases/helm.toolkit.fluxcd.io_helmreleases.yaml
index d60c61267..b1f6cd9c1 100644
--- a/config/crd/bases/helm.toolkit.fluxcd.io_helmreleases.yaml
+++ b/config/crd/bases/helm.toolkit.fluxcd.io_helmreleases.yaml
@@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
- controller-gen.kubebuilder.io/version: v0.12.0
+ controller-gen.kubebuilder.io/version: v0.19.0
name: helmreleases.helm.toolkit.fluxcd.io
spec:
group: helm.toolkit.fluxcd.io
@@ -26,20 +26,25 @@ spec:
- jsonPath: .status.conditions[?(@.type=="Ready")].message
name: Status
type: string
- name: v2beta1
+ name: v2
schema:
openAPIV3Schema:
description: HelmRelease is the Schema for the helmreleases API
properties:
apiVersion:
- description: 'APIVersion defines the versioned schema of this representation
- of an object. Servers should convert recognized schemas to the latest
- internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+ description: |-
+ APIVersion defines the versioned schema of this representation of an object.
+ Servers should convert recognized schemas to the latest internal value, and
+ may reject unrecognized values.
+ More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
type: string
kind:
- description: 'Kind is a string value representing the REST resource this
- object represents. Servers may infer this from the endpoint the client
- submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+ description: |-
+ Kind is a string value representing the REST resource this object represents.
+ Servers may infer this from the endpoint the client submits requests to.
+ Cannot be updated.
+ In CamelCase.
+ More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
type: string
metadata:
type: object
@@ -47,8 +52,9 @@ spec:
description: HelmReleaseSpec defines the desired state of a Helm release.
properties:
chart:
- description: Chart defines the template of the v1beta2.HelmChart that
- should be created for this HelmRelease.
+ description: |-
+ Chart defines the template of the v1.HelmChart that should be created
+ for this HelmRelease.
properties:
metadata:
description: ObjectMeta holds the template for metadata like labels
@@ -57,46 +63,55 @@ spec:
annotations:
additionalProperties:
type: string
- description: 'Annotations is an unstructured key value map
- stored with a resource that may be set by external tools
- to store and retrieve arbitrary metadata. They are not queryable
- and should be preserved when modifying objects. More info:
- https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/'
+ description: |-
+ Annotations is an unstructured key value map stored with a resource that may be
+ set by external tools to store and retrieve arbitrary metadata. They are not
+ queryable and should be preserved when modifying objects.
+ More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/
type: object
labels:
additionalProperties:
type: string
- description: 'Map of string keys and values that can be used
- to organize and categorize (scope and select) objects. More
- info: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/'
+ description: |-
+ Map of string keys and values that can be used to organize and categorize
+ (scope and select) objects.
+ More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/
type: object
type: object
spec:
- description: Spec holds the template for the v1beta2.HelmChartSpec
+ description: Spec holds the template for the v1.HelmChartSpec
for this HelmRelease.
properties:
chart:
description: The name or path the Helm chart is available
at in the SourceRef.
+ maxLength: 2048
+ minLength: 1
type: string
+ ignoreMissingValuesFiles:
+ description: IgnoreMissingValuesFiles controls whether to
+ silently ignore missing values files rather than failing.
+ type: boolean
interval:
- description: Interval at which to check the v1beta2.Source
- for updates. Defaults to 'HelmReleaseSpec.Interval'.
+ description: |-
+ Interval at which to check the v1.Source for updates. Defaults to
+ 'HelmReleaseSpec.Interval'.
pattern: ^([0-9]+(\.[0-9]+)?(ms|s|m|h))+$
type: string
reconcileStrategy:
default: ChartVersion
- description: Determines what enables the creation of a new
- artifact. Valid values are ('ChartVersion', 'Revision').
- See the documentation of the values for an explanation on
- their behavior. Defaults to ChartVersion when omitted.
+ description: |-
+ Determines what enables the creation of a new artifact. Valid values are
+ ('ChartVersion', 'Revision').
+ See the documentation of the values for an explanation on their behavior.
+ Defaults to ChartVersion when omitted.
enum:
- ChartVersion
- Revision
type: string
sourceRef:
- description: The name and namespace of the v1beta2.Source
- the chart is available at.
+ description: The name and namespace of the v1.Source the chart
+ is available at.
properties:
apiVersion:
description: APIVersion of the referent.
@@ -119,31 +134,26 @@ spec:
minLength: 1
type: string
required:
+ - kind
- name
type: object
- valuesFile:
- description: Alternative values file to use as the default
- chart values, expected to be a relative path in the SourceRef.
- Deprecated in favor of ValuesFiles, for backwards compatibility
- the file defined here is merged before the ValuesFiles items.
- Ignored when omitted.
- type: string
valuesFiles:
- description: Alternative list of values files to use as the
- chart values (values.yaml is not included by default), expected
- to be a relative path in the SourceRef. Values files are
- merged in the order of this list with the last file overriding
+ description: |-
+ Alternative list of values files to use as the chart values (values.yaml
+ is not included by default), expected to be a relative path in the SourceRef.
+ Values files are merged in the order of this list with the last file overriding
the first. Ignored when omitted.
items:
type: string
type: array
verify:
- description: Verify contains the secret name containing the
- trusted public keys used to verify the signature and specifies
- which provider to use to check whether OCI image is authentic.
- This field is only supported for OCI sources. Chart dependencies,
- which are not bundled in the umbrella chart artifact, are
- not verified.
+ description: |-
+ Verify contains the secret name containing the trusted public keys
+ used to verify the signature and specifies which provider to use to check
+ whether OCI image is authentic.
+ This field is only supported for OCI sources.
+ Chart dependencies, which are not bundled in the umbrella chart artifact,
+ are not verified.
properties:
provider:
default: cosign
@@ -151,10 +161,12 @@ spec:
sign the OCI Helm chart.
enum:
- cosign
+ - notation
type: string
secretRef:
- description: SecretRef specifies the Kubernetes Secret
- containing the trusted public keys.
+ description: |-
+ SecretRef specifies the Kubernetes Secret containing the
+ trusted public keys.
properties:
name:
description: Name of the referent.
@@ -167,9 +179,9 @@ spec:
type: object
version:
default: '*'
- description: Version semver expression, ignored for charts
- from v1beta2.GitRepository and v1beta2.Bucket sources. Defaults
- to latest when omitted.
+ description: |-
+ Version semver expression, ignored for charts from v1.GitRepository and
+ v1beta2.Bucket sources. Defaults to latest when omitted.
type: string
required:
- chart
@@ -178,132 +190,400 @@ spec:
required:
- spec
type: object
+ chartRef:
+ description: |-
+ ChartRef holds a reference to a source controller resource containing the
+ Helm chart artifact.
+ properties:
+ apiVersion:
+ description: APIVersion of the referent.
+ type: string
+ kind:
+ description: Kind of the referent.
+ enum:
+ - OCIRepository
+ - HelmChart
+ - ExternalArtifact
+ type: string
+ name:
+ description: Name of the referent.
+ maxLength: 253
+ minLength: 1
+ type: string
+ namespace:
+ description: |-
+ Namespace of the referent, defaults to the namespace of the Kubernetes
+ resource object that contains the reference.
+ maxLength: 63
+ minLength: 1
+ type: string
+ required:
+ - kind
+ - name
+ type: object
+ commonMetadata:
+ description: |-
+ CommonMetadata specifies the common labels and annotations that are
+ applied to all resources. Any existing label or annotation will be
+ overridden if its key matches a common one.
+ properties:
+ annotations:
+ additionalProperties:
+ type: string
+ description: Annotations to be added to the object's metadata.
+ type: object
+ labels:
+ additionalProperties:
+ type: string
+ description: Labels to be added to the object's metadata.
+ type: object
+ type: object
dependsOn:
- description: DependsOn may contain a meta.NamespacedObjectReference
- slice with references to HelmRelease resources that must be ready
- before this HelmRelease can be reconciled.
+ description: |-
+ DependsOn may contain a DependencyReference slice with
+ references to HelmRelease resources that must be ready before this HelmRelease
+ can be reconciled.
items:
- description: NamespacedObjectReference contains enough information
- to locate the referenced Kubernetes resource object in any namespace.
+ description: DependencyReference defines a HelmRelease dependency
+ on another HelmRelease resource.
properties:
name:
description: Name of the referent.
type: string
namespace:
- description: Namespace of the referent, when not specified it
- acts as LocalObjectReference.
+ description: |-
+ Namespace of the referent, defaults to the namespace of the HelmRelease
+ resource object that contains the reference.
+ type: string
+ readyExpr:
+ description: |-
+ ReadyExpr is a CEL expression that can be used to assess the readiness
+ of a dependency. When specified, the built-in readiness check
+ is replaced by the logic defined in the CEL expression.
+ To make the CEL expression additive to the built-in readiness check,
+ the feature gate `AdditiveCELDependencyCheck` must be set to `true`.
type: string
required:
- name
type: object
type: array
+ driftDetection:
+ description: |-
+ DriftDetection holds the configuration for detecting and handling
+ differences between the manifest in the Helm storage and the resources
+ currently existing in the cluster.
+ properties:
+ ignore:
+ description: |-
+ Ignore contains a list of rules for specifying which changes to ignore
+ during diffing.
+ items:
+ description: |-
+ IgnoreRule defines a rule to selectively disregard specific changes during
+ the drift detection process.
+ properties:
+ paths:
+ description: |-
+ Paths is a list of JSON Pointer (RFC 6901) paths to be excluded from
+ consideration in a Kubernetes object.
+ items:
+ type: string
+ type: array
+ target:
+ description: |-
+ Target is a selector for specifying Kubernetes objects to which this
+ rule applies.
+ If Target is not set, the Paths will be ignored for all Kubernetes
+ objects within the manifest of the Helm release.
+ properties:
+ annotationSelector:
+ description: |-
+ AnnotationSelector is a string that follows the label selection expression
+ https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#api
+ It matches with the resource annotations.
+ type: string
+ group:
+ description: |-
+ Group is the API group to select resources from.
+ Together with Version and Kind it is capable of unambiguously identifying and/or selecting resources.
+ https://github.com/kubernetes/community/blob/master/contributors/design-proposals/api-machinery/api-group.md
+ type: string
+ kind:
+ description: |-
+ Kind of the API Group to select resources from.
+ Together with Group and Version it is capable of unambiguously
+ identifying and/or selecting resources.
+ https://github.com/kubernetes/community/blob/master/contributors/design-proposals/api-machinery/api-group.md
+ type: string
+ labelSelector:
+ description: |-
+ LabelSelector is a string that follows the label selection expression
+ https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#api
+ It matches with the resource labels.
+ type: string
+ name:
+ description: Name to match resources with.
+ type: string
+ namespace:
+ description: Namespace to select resources from.
+ type: string
+ version:
+ description: |-
+ Version of the API Group to select resources from.
+ Together with Group and Kind it is capable of unambiguously identifying and/or selecting resources.
+ https://github.com/kubernetes/community/blob/master/contributors/design-proposals/api-machinery/api-group.md
+ type: string
+ type: object
+ required:
+ - paths
+ type: object
+ type: array
+ mode:
+ description: |-
+ Mode defines how differences should be handled between the Helm manifest
+ and the manifest currently applied to the cluster.
+ If not explicitly set, it defaults to DiffModeDisabled.
+ enum:
+ - enabled
+ - warn
+ - disabled
+ type: string
+ type: object
+ healthCheckExprs:
+ description: |-
+ HealthCheckExprs is a list of healthcheck expressions for evaluating the
+ health of custom resources using Common Expression Language (CEL).
+ The expressions are evaluated only when the specific Helm action
+ taking place has wait enabled, i.e. DisableWait is false, and the
+ 'poller' WaitStrategy is used.
+ items:
+ description: CustomHealthCheck defines the health check for custom
+ resources.
+ properties:
+ apiVersion:
+ description: APIVersion of the custom resource under evaluation.
+ type: string
+ current:
+ description: |-
+ Current is the CEL expression that determines if the status
+ of the custom resource has reached the desired state.
+ type: string
+ failed:
+ description: |-
+ Failed is the CEL expression that determines if the status
+ of the custom resource has failed to reach the desired state.
+ type: string
+ inProgress:
+ description: |-
+ InProgress is the CEL expression that determines if the status
+ of the custom resource has not yet reached the desired state.
+ type: string
+ kind:
+ description: Kind of the custom resource under evaluation.
+ type: string
+ required:
+ - apiVersion
+ - current
+ - kind
+ type: object
+ type: array
install:
description: Install holds the configuration for Helm install actions
for this HelmRelease.
properties:
crds:
- description: "CRDs upgrade CRDs from the Helm Chart's crds directory
- according to the CRD upgrade policy provided here. Valid values
- are `Skip`, `Create` or `CreateReplace`. Default is `Create`
- and if omitted CRDs are installed but not updated. \n Skip:
- do neither install nor replace (update) any CRDs. \n Create:
- new CRDs are created, existing CRDs are neither updated nor
- deleted. \n CreateReplace: new CRDs are created, existing CRDs
- are updated (replaced) but not deleted. \n By default, CRDs
- are applied (installed) during Helm install action. With this
- option users can opt-in to CRD replace existing CRDs on Helm
+ description: |-
+ CRDs upgrade CRDs from the Helm Chart's crds directory according
+ to the CRD upgrade policy provided here. Valid values are `Skip`,
+ `Create` or `CreateReplace`. Default is `Create` and if omitted
+ CRDs are installed but not updated.
+
+ Skip: do neither install nor replace (update) any CRDs.
+
+ Create: new CRDs are created, existing CRDs are neither updated nor deleted.
+
+ CreateReplace: new CRDs are created, existing CRDs are updated (replaced)
+ but not deleted.
+
+ By default, CRDs are applied (installed) during Helm install action.
+ With this option users can opt in to CRD replace existing CRDs on Helm
install actions, which is not (yet) natively supported by Helm.
- https://helm.sh/docs/chart_best_practices/custom_resource_definitions."
+ https://helm.sh/docs/chart_best_practices/custom_resource_definitions.
enum:
- Skip
- Create
- CreateReplace
type: string
createNamespace:
- description: CreateNamespace tells the Helm install action to
- create the HelmReleaseSpec.TargetNamespace if it does not exist
- yet. On uninstall, the namespace will not be garbage collected.
+ description: |-
+ CreateNamespace tells the Helm install action to create the
+ HelmReleaseSpec.TargetNamespace if it does not exist yet.
+ On uninstall, the namespace will not be garbage collected.
type: boolean
disableHooks:
description: DisableHooks prevents hooks from running during the
Helm install action.
type: boolean
disableOpenAPIValidation:
- description: DisableOpenAPIValidation prevents the Helm install
- action from validating rendered templates against the Kubernetes
- OpenAPI Schema.
+ description: |-
+ DisableOpenAPIValidation prevents the Helm install action from validating
+ rendered templates against the Kubernetes OpenAPI Schema.
+ type: boolean
+ disableSchemaValidation:
+ description: |-
+ DisableSchemaValidation prevents the Helm install action from validating
+ the values against the JSON Schema.
+ type: boolean
+ disableTakeOwnership:
+ description: |-
+ DisableTakeOwnership disables taking ownership of existing resources
+ during the Helm install action. Defaults to false.
type: boolean
disableWait:
- description: DisableWait disables the waiting for resources to
- be ready after a Helm install has been performed.
+ description: |-
+ DisableWait disables the waiting for resources to be ready after a Helm
+ install has been performed.
type: boolean
disableWaitForJobs:
- description: DisableWaitForJobs disables waiting for jobs to complete
- after a Helm install has been performed.
+ description: |-
+ DisableWaitForJobs disables waiting for jobs to complete after a Helm
+ install has been performed.
type: boolean
remediation:
- description: Remediation holds the remediation configuration for
- when the Helm install action for the HelmRelease fails. The
- default is to not perform any action.
+ description: |-
+ Remediation holds the remediation configuration for when the Helm install
+ action for the HelmRelease fails. The default is to not perform any action.
properties:
ignoreTestFailures:
- description: IgnoreTestFailures tells the controller to skip
- remediation when the Helm tests are run after an install
- action but fail. Defaults to 'Test.IgnoreFailures'.
+ description: |-
+ IgnoreTestFailures tells the controller to skip remediation when the Helm
+ tests are run after an install action but fail. Defaults to
+ 'Test.IgnoreFailures'.
type: boolean
remediateLastFailure:
- description: RemediateLastFailure tells the controller to
- remediate the last failure, when no retries remain. Defaults
- to 'false'.
+ description: |-
+ RemediateLastFailure tells the controller to remediate the last failure, when
+ no retries remain. Defaults to 'false'.
type: boolean
retries:
- description: Retries is the number of retries that should
- be attempted on failures before bailing. Remediation, using
- an uninstall, is performed between each attempt. Defaults
- to '0', a negative integer equals to unlimited retries.
+ description: |-
+ Retries is the number of retries that should be attempted on failures before
+ bailing. Remediation, using an uninstall, is performed between each attempt.
+ Defaults to '0', a negative integer equals to unlimited retries.
type: integer
type: object
replace:
- description: Replace tells the Helm install action to re-use the
- 'ReleaseName', but only if that name is a deleted release which
- remains in the history.
+ description: |-
+ Replace tells the Helm install action to re-use the 'ReleaseName', but only
+ if that name is a deleted release which remains in the history.
+ type: boolean
+ serverSideApply:
+ description: |-
+ ServerSideApply enables server-side apply for resources during install.
+ Defaults to true (or false when UseHelm3Defaults feature gate is enabled).
type: boolean
skipCRDs:
- description: "SkipCRDs tells the Helm install action to not install
- any CRDs. By default, CRDs are installed if not already present.
- \n Deprecated use CRD policy (`crds`) attribute with value `Skip`
- instead."
+ description: |-
+ SkipCRDs tells the Helm install action to not install any CRDs. By default,
+ CRDs are installed if not already present.
+
+ Deprecated use CRD policy (`crds`) attribute with value `Skip` instead.
type: boolean
+ strategy:
+ description: |-
+ Strategy defines the install strategy to use for this HelmRelease.
+ Defaults to 'RemediateOnFailure', or 'RetryOnFailure' when the
+ DefaultToRetryOnFailure feature gate is enabled.
+ properties:
+ name:
+ description: Name of the install strategy.
+ enum:
+ - RemediateOnFailure
+ - RetryOnFailure
+ type: string
+ retryInterval:
+ description: |-
+ RetryInterval is the interval at which to retry a failed install.
+ Can be used only when Name is set to RetryOnFailure.
+ Defaults to '5m'.
+ pattern: ^([0-9]+(\.[0-9]+)?(ms|s|m|h))+$
+ type: string
+ required:
+ - name
+ type: object
+ x-kubernetes-validations:
+ - message: .retryInterval cannot be set when .name is 'RemediateOnFailure'
+ rule: '!has(self.retryInterval) || self.name != ''RemediateOnFailure'''
timeout:
- description: Timeout is the time to wait for any individual Kubernetes
- operation (like Jobs for hooks) during the performance of a
- Helm install action. Defaults to 'HelmReleaseSpec.Timeout'.
+ description: |-
+ Timeout is the time to wait for any individual Kubernetes operation (like
+ Jobs for hooks) during the performance of a Helm install action. Defaults to
+ 'HelmReleaseSpec.Timeout'.
pattern: ^([0-9]+(\.[0-9]+)?(ms|s|m|h))+$
type: string
type: object
interval:
- description: Interval at which to reconcile the Helm release. This
- interval is approximate and may be subject to jitter to ensure efficient
- use of resources.
+ description: Interval at which to reconcile the Helm release.
pattern: ^([0-9]+(\.[0-9]+)?(ms|s|m|h))+$
type: string
kubeConfig:
- description: KubeConfig for reconciling the HelmRelease on a remote
- cluster. When used in combination with HelmReleaseSpec.ServiceAccountName,
- forces the controller to act on behalf of that Service Account at
- the target cluster. If the --default-service-account flag is set,
- its value will be used as a controller level fallback for when HelmReleaseSpec.ServiceAccountName
+ description: |-
+ KubeConfig for reconciling the HelmRelease on a remote cluster.
+ When used in combination with HelmReleaseSpec.ServiceAccountName,
+ forces the controller to act on behalf of that Service Account at the
+ target cluster.
+ If the --default-service-account flag is set, its value will be used as
+ a controller level fallback for when HelmReleaseSpec.ServiceAccountName
is empty.
properties:
+ configMapRef:
+ description: |-
+ ConfigMapRef holds an optional name of a ConfigMap that contains
+ the following keys:
+
+ - `provider`: the provider to use. One of `aws`, `azure`, `gcp`, or
+ `generic`. Required.
+ - `cluster`: the fully qualified resource name of the Kubernetes
+ cluster in the cloud provider API. Not used by the `generic`
+ provider. Required when one of `address` or `ca.crt` is not set.
+ - `address`: the address of the Kubernetes API server. Required
+ for `generic`. For the other providers, if not specified, the
+ first address in the cluster resource will be used, and if
+ specified, it must match one of the addresses in the cluster
+ resource.
+ If audiences is not set, will be used as the audience for the
+ `generic` provider.
+ - `ca.crt`: the optional PEM-encoded CA certificate for the
+ Kubernetes API server. If not set, the controller will use the
+ CA certificate from the cluster resource.
+ - `audiences`: the optional audiences as a list of
+ line-break-separated strings for the Kubernetes ServiceAccount
+ token. Defaults to the `address` for the `generic` provider, or
+ to specific values for the other providers depending on the
+ provider.
+ - `serviceAccountName`: the optional name of the Kubernetes
+ ServiceAccount in the same namespace that should be used
+ for authentication. If not specified, the controller
+ ServiceAccount will be used.
+
+ Mutually exclusive with SecretRef.
+ properties:
+ name:
+ description: Name of the referent.
+ type: string
+ required:
+ - name
+ type: object
secretRef:
- description: SecretRef holds the name of a secret that contains
- a key with the kubeconfig file as the value. If no key is set,
- the key will default to 'value'. It is recommended that the
- kubeconfig is self-contained, and the secret is regularly updated
- if credentials such as a cloud-access-token expire. Cloud specific
- `cmd-path` auth helpers will not function without adding binaries
- and credentials to the Pod that is responsible for reconciling
- Kubernetes resources.
+ description: |-
+ SecretRef holds an optional name of a secret that contains a key with
+ the kubeconfig file as the value. If no key is set, the key will default
+ to 'value'. Mutually exclusive with ConfigMapRef.
+ It is recommended that the kubeconfig is self-contained, and the secret
+ is regularly updated if credentials such as a cloud-access-token expire.
+ Cloud specific `cmd-path` auth helpers will not function without adding
+ binaries and credentials to the Pod that is responsible for reconciling
+ Kubernetes resources. Supported only for the generic provider.
properties:
key:
description: Key in the Secret, when not specified an implementation-specific
@@ -315,28 +595,37 @@ spec:
required:
- name
type: object
- required:
- - secretRef
type: object
+ x-kubernetes-validations:
+ - message: exactly one of spec.kubeConfig.configMapRef or spec.kubeConfig.secretRef
+ must be specified
+ rule: has(self.configMapRef) || has(self.secretRef)
+ - message: exactly one of spec.kubeConfig.configMapRef or spec.kubeConfig.secretRef
+ must be specified
+ rule: '!has(self.configMapRef) || !has(self.secretRef)'
maxHistory:
- description: MaxHistory is the number of revisions saved by Helm for
- this HelmRelease. Use '0' for an unlimited number of revisions;
- defaults to '10'.
+ description: |-
+ MaxHistory is the number of revisions saved by Helm for this HelmRelease.
+ Use '0' for an unlimited number of revisions; defaults to '5'.
type: integer
persistentClient:
- description: "PersistentClient tells the controller to use a persistent
- Kubernetes client for this release. When enabled, the client will
- be reused for the duration of the reconciliation, instead of being
- created and destroyed for each (step of a) Helm action. \n This
- can improve performance, but may cause issues with some Helm charts
+ description: |-
+ PersistentClient tells the controller to use a persistent Kubernetes
+ client for this release. When enabled, the client will be reused for the
+ duration of the reconciliation, instead of being created and destroyed
+ for each (step of a) Helm action.
+
+ This can improve performance, but may cause issues with some Helm charts
that for example do create Custom Resource Definitions during installation
- outside Helm's CRD lifecycle hooks, which are then not observed
- to be available by e.g. post-install hooks. \n If not set, it defaults
- to true."
+ outside Helm's CRD lifecycle hooks, which are then not observed to be
+ available by e.g. post-install hooks.
+
+ If not set, it defaults to true.
type: boolean
postRenderers:
- description: PostRenderers holds an array of Helm PostRenderers, which
- will be applied in order of their definition.
+ description: |-
+ PostRenderers holds an array of Helm PostRenderers, which will be applied in order
+ of their definition.
items:
description: PostRenderer contains a Helm PostRenderer specification.
properties:
@@ -344,19 +633,19 @@ spec:
description: Kustomization to apply as PostRenderer.
properties:
images:
- description: Images is a list of (image name, new name,
- new tag or digest) for changing image names, tags or digests.
- This can also be achieved with a patch, but this operator
- is simpler to specify.
+ description: |-
+ Images is a list of (image name, new name, new tag or digest)
+ for changing image names, tags or digests. This can also be achieved with a
+ patch, but this operator is simpler to specify.
items:
description: Image contains an image name, a new name,
a new tag or digest, which will replace the original
name and tag.
properties:
digest:
- description: Digest is the value used to replace the
- original image tag. If digest is present NewTag
- value is ignored.
+ description: |-
+ Digest is the value used to replace the original image tag.
+ If digest is present NewTag value is ignored.
type: string
name:
description: Name is a tag-less image name.
@@ -374,137 +663,46 @@ spec:
type: object
type: array
patches:
- description: Strategic merge and JSON patches, defined as
- inline YAML objects, capable of targeting objects based
- on kind, label and annotation selectors.
+ description: |-
+ Strategic merge and JSON patches, defined as inline YAML objects,
+ capable of targeting objects based on kind, label and annotation selectors.
items:
- description: Patch contains an inline StrategicMerge or
- JSON6902 patch, and the target the patch should be applied
- to.
+ description: |-
+ Patch contains an inline StrategicMerge or JSON6902 patch, and the target the patch should
+ be applied to.
properties:
patch:
- description: Patch contains an inline StrategicMerge
- patch or an inline JSON6902 patch with an array
- of operation objects.
+ description: |-
+ Patch contains an inline StrategicMerge patch or an inline JSON6902 patch with
+ an array of operation objects.
type: string
target:
description: Target points to the resources that the
patch document should be applied to.
properties:
annotationSelector:
- description: AnnotationSelector is a string that
- follows the label selection expression https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#api
- It matches with the resource annotations.
- type: string
- group:
- description: Group is the API group to select
- resources from. Together with Version and Kind
- it is capable of unambiguously identifying and/or
- selecting resources. https://github.com/kubernetes/community/blob/master/contributors/design-proposals/api-machinery/api-group.md
- type: string
- kind:
- description: Kind of the API Group to select resources
- from. Together with Group and Version it is
- capable of unambiguously identifying and/or
- selecting resources. https://github.com/kubernetes/community/blob/master/contributors/design-proposals/api-machinery/api-group.md
- type: string
- labelSelector:
- description: LabelSelector is a string that follows
- the label selection expression https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#api
- It matches with the resource labels.
- type: string
- name:
- description: Name to match resources with.
- type: string
- namespace:
- description: Namespace to select resources from.
- type: string
- version:
- description: Version of the API Group to select
- resources from. Together with Group and Kind
- it is capable of unambiguously identifying and/or
- selecting resources. https://github.com/kubernetes/community/blob/master/contributors/design-proposals/api-machinery/api-group.md
- type: string
- type: object
- required:
- - patch
- type: object
- type: array
- patchesJson6902:
- description: JSON 6902 patches, defined as inline YAML objects.
- items:
- description: JSON6902Patch contains a JSON6902 patch and
- the target the patch should be applied to.
- properties:
- patch:
- description: Patch contains the JSON6902 patch document
- with an array of operation objects.
- items:
- description: JSON6902 is a JSON6902 operation object.
- https://datatracker.ietf.org/doc/html/rfc6902#section-4
- properties:
- from:
- description: From contains a JSON-pointer value
- that references a location within the target
- document where the operation is performed.
- The meaning of the value depends on the value
- of Op, and is NOT taken into account by all
- operations.
- type: string
- op:
- description: Op indicates the operation to perform.
- Its value MUST be one of "add", "remove",
- "replace", "move", "copy", or "test". https://datatracker.ietf.org/doc/html/rfc6902#section-4
- enum:
- - test
- - remove
- - add
- - replace
- - move
- - copy
- type: string
- path:
- description: Path contains the JSON-pointer
- value that references a location within the
- target document where the operation is performed.
- The meaning of the value depends on the value
- of Op.
- type: string
- value:
- description: Value contains a valid JSON structure.
- The meaning of the value depends on the value
- of Op, and is NOT taken into account by all
- operations.
- x-kubernetes-preserve-unknown-fields: true
- required:
- - op
- - path
- type: object
- type: array
- target:
- description: Target points to the resources that the
- patch document should be applied to.
- properties:
- annotationSelector:
- description: AnnotationSelector is a string that
- follows the label selection expression https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#api
+ description: |-
+ AnnotationSelector is a string that follows the label selection expression
+ https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#api
It matches with the resource annotations.
type: string
group:
- description: Group is the API group to select
- resources from. Together with Version and Kind
- it is capable of unambiguously identifying and/or
- selecting resources. https://github.com/kubernetes/community/blob/master/contributors/design-proposals/api-machinery/api-group.md
+ description: |-
+ Group is the API group to select resources from.
+ Together with Version and Kind it is capable of unambiguously identifying and/or selecting resources.
+ https://github.com/kubernetes/community/blob/master/contributors/design-proposals/api-machinery/api-group.md
type: string
kind:
- description: Kind of the API Group to select resources
- from. Together with Group and Version it is
- capable of unambiguously identifying and/or
- selecting resources. https://github.com/kubernetes/community/blob/master/contributors/design-proposals/api-machinery/api-group.md
+ description: |-
+ Kind of the API Group to select resources from.
+ Together with Group and Version it is capable of unambiguously
+ identifying and/or selecting resources.
+ https://github.com/kubernetes/community/blob/master/contributors/design-proposals/api-machinery/api-group.md
type: string
labelSelector:
- description: LabelSelector is a string that follows
- the label selection expression https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#api
+ description: |-
+ LabelSelector is a string that follows the label selection expression
+ https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#api
It matches with the resource labels.
type: string
name:
@@ -514,29 +712,23 @@ spec:
description: Namespace to select resources from.
type: string
version:
- description: Version of the API Group to select
- resources from. Together with Group and Kind
- it is capable of unambiguously identifying and/or
- selecting resources. https://github.com/kubernetes/community/blob/master/contributors/design-proposals/api-machinery/api-group.md
+ description: |-
+ Version of the API Group to select resources from.
+ Together with Group and Kind it is capable of unambiguously identifying and/or selecting resources.
+ https://github.com/kubernetes/community/blob/master/contributors/design-proposals/api-machinery/api-group.md
type: string
type: object
required:
- patch
- - target
type: object
type: array
- patchesStrategicMerge:
- description: Strategic merge patches, defined as inline
- YAML objects.
- items:
- x-kubernetes-preserve-unknown-fields: true
- type: array
type: object
type: object
type: array
releaseName:
- description: ReleaseName used for the Helm release. Defaults to a
- composition of '[TargetNamespace-]Name'.
+ description: |-
+ ReleaseName used for the Helm release. Defaults to a composition of
+ '[TargetNamespace-]Name'.
maxLength: 53
minLength: 1
type: string
@@ -545,54 +737,82 @@ spec:
for this HelmRelease.
properties:
cleanupOnFail:
- description: CleanupOnFail allows deletion of new resources created
- during the Helm rollback action when it fails.
+ description: |-
+ CleanupOnFail allows deletion of new resources created during the Helm
+ rollback action when it fails.
type: boolean
disableHooks:
description: DisableHooks prevents hooks from running during the
Helm rollback action.
type: boolean
disableWait:
- description: DisableWait disables the waiting for resources to
- be ready after a Helm rollback has been performed.
+ description: |-
+ DisableWait disables the waiting for resources to be ready after a Helm
+ rollback has been performed.
type: boolean
disableWaitForJobs:
- description: DisableWaitForJobs disables waiting for jobs to complete
- after a Helm rollback has been performed.
+ description: |-
+ DisableWaitForJobs disables waiting for jobs to complete after a Helm
+ rollback has been performed.
type: boolean
force:
description: Force forces resource updates through a replacement
strategy.
type: boolean
recreate:
- description: Recreate performs pod restarts for the resource if
- applicable.
+ description: |-
+ Recreate performs pod restarts for any managed workloads.
+
+ Deprecated: This behavior was deprecated in Helm 3:
+ - Deprecation: https://github.com/helm/helm/pull/6463
+ - Removal: https://github.com/helm/helm/pull/31023
+ After helm-controller was upgraded to the Helm 4 SDK,
+ this field is no longer functional and will print a
+ warning if set to true. It will also be removed in a
+ future release.
type: boolean
+ serverSideApply:
+ description: |-
+ ServerSideApply enables server-side apply for resources during rollback.
+ Can be "enabled", "disabled", or "auto".
+ When "auto", server-side apply usage will be based on the release's previous usage.
+ Defaults to "auto".
+ enum:
+ - enabled
+ - disabled
+ - auto
+ type: string
timeout:
- description: Timeout is the time to wait for any individual Kubernetes
- operation (like Jobs for hooks) during the performance of a
- Helm rollback action. Defaults to 'HelmReleaseSpec.Timeout'.
+ description: |-
+ Timeout is the time to wait for any individual Kubernetes operation (like
+ Jobs for hooks) during the performance of a Helm rollback action. Defaults to
+ 'HelmReleaseSpec.Timeout'.
pattern: ^([0-9]+(\.[0-9]+)?(ms|s|m|h))+$
type: string
type: object
serviceAccountName:
- description: The name of the Kubernetes service account to impersonate
+ description: |-
+ The name of the Kubernetes service account to impersonate
when reconciling this HelmRelease.
+ maxLength: 253
+ minLength: 1
type: string
storageNamespace:
- description: StorageNamespace used for the Helm storage. Defaults
- to the namespace of the HelmRelease.
+ description: |-
+ StorageNamespace used for the Helm storage.
+ Defaults to the namespace of the HelmRelease.
maxLength: 63
minLength: 1
type: string
suspend:
- description: Suspend tells the controller to suspend reconciliation
- for this HelmRelease, it does not apply to already started reconciliations.
- Defaults to false.
+ description: |-
+ Suspend tells the controller to suspend reconciliation for this HelmRelease,
+ it does not apply to already started reconciliations. Defaults to false.
type: boolean
targetNamespace:
- description: TargetNamespace to target when performing operations
- for the HelmRelease. Defaults to the namespace of the HelmRelease.
+ description: |-
+ TargetNamespace to target when performing operations for the HelmRelease.
+ Defaults to the namespace of the HelmRelease.
maxLength: 63
minLength: 1
type: string
@@ -601,26 +821,47 @@ spec:
this HelmRelease.
properties:
enable:
- description: Enable enables Helm test actions for this HelmRelease
- after an Helm install or upgrade action has been performed.
+ description: |-
+ Enable enables Helm test actions for this HelmRelease after an Helm install
+ or upgrade action has been performed.
type: boolean
+ filters:
+ description: Filters is a list of tests to run or exclude from
+ running.
+ items:
+ description: Filter holds the configuration for individual Helm
+ test filters.
+ properties:
+ exclude:
+ description: Exclude specifies whether the named test should
+ be excluded.
+ type: boolean
+ name:
+ description: Name is the name of the test.
+ maxLength: 253
+ minLength: 1
+ type: string
+ required:
+ - name
+ type: object
+ type: array
ignoreFailures:
- description: IgnoreFailures tells the controller to skip remediation
- when the Helm tests are run but fail. Can be overwritten for
- tests run after install or upgrade actions in 'Install.IgnoreTestFailures'
- and 'Upgrade.IgnoreTestFailures'.
+ description: |-
+ IgnoreFailures tells the controller to skip remediation when the Helm tests
+ are run but fail. Can be overwritten for tests run after install or upgrade
+ actions in 'Install.IgnoreTestFailures' and 'Upgrade.IgnoreTestFailures'.
type: boolean
timeout:
- description: Timeout is the time to wait for any individual Kubernetes
- operation during the performance of a Helm test action. Defaults
- to 'HelmReleaseSpec.Timeout'.
+ description: |-
+ Timeout is the time to wait for any individual Kubernetes operation during
+ the performance of a Helm test action. Defaults to 'HelmReleaseSpec.Timeout'.
pattern: ^([0-9]+(\.[0-9]+)?(ms|s|m|h))+$
type: string
type: object
timeout:
- description: Timeout is the time to wait for any individual Kubernetes
- operation (like Jobs for hooks) during the performance of a Helm
- action. Defaults to '5m0s'.
+ description: |-
+ Timeout is the time to wait for any individual Kubernetes operation (like Jobs
+ for hooks) during the performance of a Helm action. Defaults to '5m0s'.
pattern: ^([0-9]+(\.[0-9]+)?(ms|s|m|h))+$
type: string
uninstall:
@@ -629,8 +870,9 @@ spec:
properties:
deletionPropagation:
default: background
- description: DeletionPropagation specifies the deletion propagation
- policy when a Helm uninstall is performed.
+ description: |-
+ DeletionPropagation specifies the deletion propagation policy when
+ a Helm uninstall is performed.
enum:
- background
- foreground
@@ -641,17 +883,20 @@ spec:
Helm rollback action.
type: boolean
disableWait:
- description: DisableWait disables waiting for all the resources
- to be deleted after a Helm uninstall is performed.
+ description: |-
+ DisableWait disables waiting for all the resources to be deleted after
+ a Helm uninstall is performed.
type: boolean
keepHistory:
- description: KeepHistory tells Helm to remove all associated resources
- and mark the release as deleted, but retain the release history.
+ description: |-
+ KeepHistory tells Helm to remove all associated resources and mark the
+ release as deleted, but retain the release history.
type: boolean
timeout:
- description: Timeout is the time to wait for any individual Kubernetes
- operation (like Jobs for hooks) during the performance of a
- Helm uninstall action. Defaults to 'HelmReleaseSpec.Timeout'.
+ description: |-
+ Timeout is the time to wait for any individual Kubernetes operation (like
+ Jobs for hooks) during the performance of a Helm uninstall action. Defaults
+ to 'HelmReleaseSpec.Timeout'.
pattern: ^([0-9]+(\.[0-9]+)?(ms|s|m|h))+$
type: string
type: object
@@ -660,21 +905,27 @@ spec:
for this HelmRelease.
properties:
cleanupOnFail:
- description: CleanupOnFail allows deletion of new resources created
- during the Helm upgrade action when it fails.
+ description: |-
+ CleanupOnFail allows deletion of new resources created during the Helm
+ upgrade action when it fails.
type: boolean
crds:
- description: "CRDs upgrade CRDs from the Helm Chart's crds directory
- according to the CRD upgrade policy provided here. Valid values
- are `Skip`, `Create` or `CreateReplace`. Default is `Skip` and
- if omitted CRDs are neither installed nor upgraded. \n Skip:
- do neither install nor replace (update) any CRDs. \n Create:
- new CRDs are created, existing CRDs are neither updated nor
- deleted. \n CreateReplace: new CRDs are created, existing CRDs
- are updated (replaced) but not deleted. \n By default, CRDs
- are not applied during Helm upgrade action. With this option
- users can opt-in to CRD upgrade, which is not (yet) natively
- supported by Helm. https://helm.sh/docs/chart_best_practices/custom_resource_definitions."
+ description: |-
+ CRDs upgrade CRDs from the Helm Chart's crds directory according
+ to the CRD upgrade policy provided here. Valid values are `Skip`,
+ `Create` or `CreateReplace`. Default is `Skip` and if omitted
+ CRDs are neither installed nor upgraded.
+
+ Skip: do neither install nor replace (update) any CRDs.
+
+ Create: new CRDs are created, existing CRDs are neither updated nor deleted.
+
+ CreateReplace: new CRDs are created, existing CRDs are updated (replaced)
+ but not deleted.
+
+ By default, CRDs are not applied during Helm upgrade action. With this
+ option users can opt-in to CRD upgrade, which is not (yet) natively supported by Helm.
+ https://helm.sh/docs/chart_best_practices/custom_resource_definitions.
enum:
- Skip
- Create
@@ -685,47 +936,61 @@ spec:
Helm upgrade action.
type: boolean
disableOpenAPIValidation:
- description: DisableOpenAPIValidation prevents the Helm upgrade
- action from validating rendered templates against the Kubernetes
- OpenAPI Schema.
+ description: |-
+ DisableOpenAPIValidation prevents the Helm upgrade action from validating
+ rendered templates against the Kubernetes OpenAPI Schema.
+ type: boolean
+ disableSchemaValidation:
+ description: |-
+ DisableSchemaValidation prevents the Helm upgrade action from validating
+ the values against the JSON Schema.
+ type: boolean
+ disableTakeOwnership:
+ description: |-
+ DisableTakeOwnership disables taking ownership of existing resources
+ during the Helm upgrade action. Defaults to false.
type: boolean
disableWait:
- description: DisableWait disables the waiting for resources to
- be ready after a Helm upgrade has been performed.
+ description: |-
+ DisableWait disables the waiting for resources to be ready after a Helm
+ upgrade has been performed.
type: boolean
disableWaitForJobs:
- description: DisableWaitForJobs disables waiting for jobs to complete
- after a Helm upgrade has been performed.
+ description: |-
+ DisableWaitForJobs disables waiting for jobs to complete after a Helm
+ upgrade has been performed.
type: boolean
force:
description: Force forces resource updates through a replacement
strategy.
type: boolean
preserveValues:
- description: PreserveValues will make Helm reuse the last release's
- values and merge in overrides from 'Values'. Setting this flag
- makes the HelmRelease non-declarative.
+ description: |-
+ PreserveValues will make Helm reuse the last release's values and merge in
+ overrides from 'Values'. Setting this flag makes the HelmRelease
+ non-declarative.
type: boolean
remediation:
- description: Remediation holds the remediation configuration for
- when the Helm upgrade action for the HelmRelease fails. The
- default is to not perform any action.
+ description: |-
+ Remediation holds the remediation configuration for when the Helm upgrade
+ action for the HelmRelease fails. The default is to not perform any action.
properties:
ignoreTestFailures:
- description: IgnoreTestFailures tells the controller to skip
- remediation when the Helm tests are run after an upgrade
- action but fail. Defaults to 'Test.IgnoreFailures'.
+ description: |-
+ IgnoreTestFailures tells the controller to skip remediation when the Helm
+ tests are run after an upgrade action but fail.
+ Defaults to 'Test.IgnoreFailures'.
type: boolean
remediateLastFailure:
- description: RemediateLastFailure tells the controller to
- remediate the last failure, when no retries remain. Defaults
- to 'false' unless 'Retries' is greater than 0.
+ description: |-
+ RemediateLastFailure tells the controller to remediate the last failure, when
+ no retries remain. Defaults to 'false' unless 'Retries' is greater than 0.
type: boolean
retries:
- description: Retries is the number of retries that should
- be attempted on failures before bailing. Remediation, using
- 'Strategy', is performed between each attempt. Defaults
- to '0', a negative integer equals to unlimited retries.
+ description: |-
+ Retries is the number of retries that should be attempted on failures before
+ bailing. Remediation, using 'Strategy', is performed between each attempt.
+ Defaults to '0', a negative integer equals to unlimited retries.
type: integer
strategy:
description: Strategy to use for failure remediation. Defaults
@@ -735,10 +1000,47 @@ spec:
- uninstall
type: string
type: object
+ serverSideApply:
+ description: |-
+ ServerSideApply enables server-side apply for resources during upgrade.
+ Can be "enabled", "disabled", or "auto".
+ When "auto", server-side apply usage will be based on the release's previous usage.
+ Defaults to "auto".
+ enum:
+ - enabled
+ - disabled
+ - auto
+ type: string
+ strategy:
+ description: |-
+ Strategy defines the upgrade strategy to use for this HelmRelease.
+ Defaults to 'RemediateOnFailure', or 'RetryOnFailure' when the
+ DefaultToRetryOnFailure feature gate is enabled.
+ properties:
+ name:
+ description: Name of the upgrade strategy.
+ enum:
+ - RemediateOnFailure
+ - RetryOnFailure
+ type: string
+ retryInterval:
+ description: |-
+ RetryInterval is the interval at which to retry a failed upgrade.
+ Can be used only when Name is set to RetryOnFailure.
+ Defaults to '5m'.
+ pattern: ^([0-9]+(\.[0-9]+)?(ms|s|m|h))+$
+ type: string
+ required:
+ - name
+ type: object
+ x-kubernetes-validations:
+ - message: .retryInterval can only be set when .name is 'RetryOnFailure'
+ rule: '!has(self.retryInterval) || self.name == ''RetryOnFailure'''
timeout:
- description: Timeout is the time to wait for any individual Kubernetes
- operation (like Jobs for hooks) during the performance of a
- Helm upgrade action. Defaults to 'HelmReleaseSpec.Timeout'.
+ description: |-
+ Timeout is the time to wait for any individual Kubernetes operation (like
+ Jobs for hooks) during the performance of a Helm upgrade action. Defaults to
+ 'HelmReleaseSpec.Timeout'.
pattern: ^([0-9]+(\.[0-9]+)?(ms|s|m|h))+$
type: string
type: object
@@ -746,13 +1048,13 @@ spec:
description: Values holds the values for this Helm release.
x-kubernetes-preserve-unknown-fields: true
valuesFrom:
- description: ValuesFrom holds references to resources containing Helm
- values for this HelmRelease, and information about how they should
- be merged.
+ description: |-
+ ValuesFrom holds references to resources containing Helm values for this HelmRelease,
+ and information about how they should be merged.
items:
- description: ValuesReference contains a reference to a resource
- containing Helm values, and optionally the key they can be found
- at.
+ description: |-
+ ValuesReference contains a reference to a resource containing Helm values,
+ and optionally the key they can be found at.
properties:
kind:
description: Kind of the values referent, valid values are ('Secret',
@@ -762,30 +1064,30 @@ spec:
- ConfigMap
type: string
name:
- description: Name of the values referent. Should reside in the
- same namespace as the referring resource.
+ description: |-
+ Name of the values referent. Should reside in the same namespace as the
+ referring resource.
maxLength: 253
minLength: 1
type: string
optional:
- description: Optional marks this ValuesReference as optional.
- When set, a not found error for the values reference is ignored,
- but any ValuesKey, TargetPath or transient error will still
- result in a reconciliation failure.
+ description: |-
+ Optional marks this ValuesReference as optional. When set, a not found error
+ for the values reference is ignored, but any ValuesKey, TargetPath or
+ transient error will still result in a reconciliation failure.
type: boolean
targetPath:
- description: TargetPath is the YAML dot notation path the value
- should be merged at. When set, the ValuesKey is expected to
- be a single flat value. Defaults to 'None', which results
- in the values getting merged at the root.
+ description: |-
+ TargetPath is the YAML dot notation path the value should be merged at. When
+ set, the ValuesKey is expected to be a single flat value. Defaults to 'None',
+ which results in the values getting merged at the root.
maxLength: 250
pattern: ^([a-zA-Z0-9_\-.\\\/]|\[[0-9]{1,5}\])+$
type: string
valuesKey:
- description: ValuesKey is the data key where the values.yaml
- or a specific value can be found at. Defaults to 'values.yaml'.
- When set, must be a valid Data Key, consisting of alphanumeric
- characters, '-', '_' or '.'.
+ description: |-
+ ValuesKey is the data key where the values.yaml or a specific value can be
+ found at. Defaults to 'values.yaml'.
maxLength: 253
pattern: ^[\-._a-zA-Z0-9]+$
type: string
@@ -794,10 +1096,33 @@ spec:
- name
type: object
type: array
+ waitStrategy:
+ description: |-
+ WaitStrategy defines Helm's wait strategy for waiting for applied
+ resources to become ready.
+ properties:
+ name:
+ description: |-
+ Name is Helm's wait strategy for waiting for applied resources to
+ become ready. One of 'poller' or 'legacy'. The 'poller' strategy uses
+ kstatus to poll resource statuses, while the 'legacy' strategy uses
+ Helm v3's waiting logic.
+ Defaults to 'poller', or to 'legacy' when UseHelm3Defaults feature
+ gate is enabled.
+ enum:
+ - poller
+ - legacy
+ type: string
+ required:
+ - name
+ type: object
required:
- - chart
- interval
type: object
+ x-kubernetes-validations:
+ - message: either chart or chartRef must be set
+ rule: (has(self.chart) && !has(self.chartRef)) || (!has(self.chart)
+ && has(self.chartRef))
status:
default:
observedGeneration: -1
@@ -806,43 +1131,35 @@ spec:
conditions:
description: Conditions holds the conditions for the HelmRelease.
items:
- description: "Condition contains details for one aspect of the current
- state of this API Resource. --- This struct is intended for direct
- use as an array at the field path .status.conditions. For example,
- \n type FooStatus struct{ // Represents the observations of a
- foo's current state. // Known .status.conditions.type are: \"Available\",
- \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge
- // +listType=map // +listMapKey=type Conditions []metav1.Condition
- `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\"
- protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }"
+ description: Condition contains details for one aspect of the current
+ state of this API Resource.
properties:
lastTransitionTime:
- description: lastTransitionTime is the last time the condition
- transitioned from one status to another. This should be when
- the underlying condition changed. If that is not known, then
- using the time when the API field changed is acceptable.
+ description: |-
+ lastTransitionTime is the last time the condition transitioned from one status to another.
+ This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
format: date-time
type: string
message:
- description: message is a human readable message indicating
- details about the transition. This may be an empty string.
+ description: |-
+ message is a human readable message indicating details about the transition.
+ This may be an empty string.
maxLength: 32768
type: string
observedGeneration:
- description: observedGeneration represents the .metadata.generation
- that the condition was set based upon. For instance, if .metadata.generation
- is currently 12, but the .status.conditions[x].observedGeneration
- is 9, the condition is out of date with respect to the current
- state of the instance.
+ description: |-
+ observedGeneration represents the .metadata.generation that the condition was set based upon.
+ For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
+ with respect to the current state of the instance.
format: int64
minimum: 0
type: integer
reason:
- description: reason contains a programmatic identifier indicating
- the reason for the condition's last transition. Producers
- of specific condition types may define expected values and
- meanings for this field, and whether the values are considered
- a guaranteed API. The value should be a CamelCase string.
+ description: |-
+ reason contains a programmatic identifier indicating the reason for the condition's last transition.
+ Producers of specific condition types may define expected values and meanings for this field,
+ and whether the values are considered a guaranteed API.
+ The value should be a CamelCase string.
This field may not be empty.
maxLength: 1024
minLength: 1
@@ -857,10 +1174,6 @@ spec:
type: string
type:
description: type of condition in CamelCase or in foo.example.com/CamelCase.
- --- Many .condition.type values are consistent across resources
- like Available, but because arbitrary conditions can be useful
- (see .node.status.conditions), the ability to deconflict is
- important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt)
maxLength: 316
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
type: string
@@ -873,47 +1186,251 @@ spec:
type: object
type: array
failures:
- description: Failures is the reconciliation failure count against
- the latest desired state. It is reset after a successful reconciliation.
+ description: |-
+ Failures is the reconciliation failure count against the latest desired
+ state. It is reset after a successful reconciliation.
format: int64
type: integer
helmChart:
- description: HelmChart is the namespaced name of the HelmChart resource
- created by the controller for the HelmRelease.
+ description: |-
+ HelmChart is the namespaced name of the HelmChart resource created by
+ the controller for the HelmRelease.
type: string
+ history:
+ description: |-
+ History holds the history of Helm releases performed for this HelmRelease
+ up to the last successfully completed release.
+ items:
+ description: |-
+ Snapshot captures a point-in-time copy of the status information for a Helm release,
+ as managed by the controller.
+ properties:
+ action:
+ description: Action is the action that resulted in this snapshot
+ being created.
+ type: string
+ apiVersion:
+ description: |-
+ APIVersion is the API version of the Snapshot.
+ When the calculation method of the Digest field is changed, this
+ field will be used to distinguish between the old and new methods.
+ type: string
+ appVersion:
+ description: AppVersion is the chart app version of the release
+ object in storage.
+ type: string
+ chartName:
+ description: ChartName is the chart name of the release object
+ in storage.
+ type: string
+ chartVersion:
+ description: |-
+ ChartVersion is the chart version of the release object in
+ storage.
+ type: string
+ configDigest:
+ description: |-
+ ConfigDigest is the checksum of the config (better known as
+ "values") of the release object in storage.
+ It has the format of `:`.
+ type: string
+ deleted:
+ description: Deleted is when the release was deleted.
+ format: date-time
+ type: string
+ digest:
+ description: |-
+ Digest is the checksum of the release object in storage.
+ It has the format of `:`.
+ type: string
+ firstDeployed:
+ description: FirstDeployed is when the release was first deployed.
+ format: date-time
+ type: string
+ lastDeployed:
+ description: LastDeployed is when the release was last deployed.
+ format: date-time
+ type: string
+ name:
+ description: Name is the name of the release.
+ type: string
+ namespace:
+ description: Namespace is the namespace the release is deployed
+ to.
+ type: string
+ ociDigest:
+ description: OCIDigest is the digest of the OCI artifact associated
+ with the release.
+ type: string
+ status:
+ description: Status is the current state of the release.
+ type: string
+ testHooks:
+ additionalProperties:
+ description: |-
+ TestHookStatus holds the status information for a test hook as observed
+ to be run by the controller.
+ properties:
+ lastCompleted:
+ description: LastCompleted is the time the test hook last
+ completed.
+ format: date-time
+ type: string
+ lastStarted:
+ description: LastStarted is the time the test hook was
+ last started.
+ format: date-time
+ type: string
+ phase:
+ description: Phase the test hook was observed to be in.
+ type: string
+ type: object
+ description: |-
+ TestHooks is the list of test hooks for the release as observed to be
+ run by the controller.
+ type: object
+ version:
+ description: Version is the version of the release object in
+ storage.
+ type: integer
+ required:
+ - chartName
+ - chartVersion
+ - configDigest
+ - digest
+ - firstDeployed
+ - lastDeployed
+ - name
+ - namespace
+ - status
+ - version
+ type: object
+ type: array
installFailures:
- description: InstallFailures is the install failure count against
- the latest desired state. It is reset after a successful reconciliation.
+ description: |-
+ InstallFailures is the install failure count against the latest desired
+ state. It is reset after a successful reconciliation.
+ format: int64
+ type: integer
+ inventory:
+ description: |-
+ Inventory contains the list of Kubernetes resource object references
+ that have been applied for this release.
+ properties:
+ entries:
+ description: Entries of Kubernetes resource object references.
+ items:
+ description: ResourceRef contains the information necessary
+ to locate a resource within a cluster.
+ properties:
+ id:
+ description: |-
+ ID is the string representation of the Kubernetes resource object's metadata,
+ in the format '___'.
+ type: string
+ v:
+ description: Version is the API version of the Kubernetes
+ resource object's kind.
+ type: string
+ required:
+ - id
+ - v
+ type: object
+ type: array
+ required:
+ - entries
+ type: object
+ lastAttemptedConfigDigest:
+ description: |-
+ LastAttemptedConfigDigest is the digest for the config (better known as
+ "values") of the last reconciliation attempt.
+ type: string
+ lastAttemptedGeneration:
+ description: |-
+ LastAttemptedGeneration is the last generation the controller attempted
+ to reconcile.
format: int64
type: integer
- lastAppliedRevision:
- description: LastAppliedRevision is the revision of the last successfully
- applied source.
+ lastAttemptedReleaseAction:
+ description: |-
+ LastAttemptedReleaseAction is the last release action performed for this
+ HelmRelease. It is used to determine the active retry or remediation
+ strategy.
+ enum:
+ - install
+ - upgrade
+ type: string
+ lastAttemptedReleaseActionDuration:
+ description: |-
+ LastAttemptedReleaseActionDuration is the duration of the last
+ release action performed for this HelmRelease.
type: string
lastAttemptedRevision:
- description: LastAttemptedRevision is the revision of the last reconciliation
- attempt.
+ description: |-
+ LastAttemptedRevision is the Source revision of the last reconciliation
+ attempt. For OCIRepository sources, the 12 first characters of the digest are
+ appended to the chart version e.g. "1.2.3+1234567890ab".
+ type: string
+ lastAttemptedRevisionDigest:
+ description: |-
+ LastAttemptedRevisionDigest is the digest of the last reconciliation attempt.
+ This is only set for OCIRepository sources.
type: string
lastAttemptedValuesChecksum:
- description: LastAttemptedValuesChecksum is the SHA1 checksum of the
- values of the last reconciliation attempt.
+ description: |-
+ LastAttemptedValuesChecksum is the SHA1 checksum for the values of the last
+ reconciliation attempt.
+
+ Deprecated: Use LastAttemptedConfigDigest instead.
+ type: string
+ lastHandledForceAt:
+ description: |-
+ LastHandledForceAt holds the value of the most recent
+ force request value, so a change of the annotation value
+ can be detected.
type: string
lastHandledReconcileAt:
- description: LastHandledReconcileAt holds the value of the most recent
- reconcile request value, so a change of the annotation value can
- be detected.
+ description: |-
+ LastHandledReconcileAt holds the value of the most recent
+ reconcile request value, so a change of the annotation value
+ can be detected.
+ type: string
+ lastHandledResetAt:
+ description: |-
+ LastHandledResetAt holds the value of the most recent reset request
+ value, so a change of the annotation value can be detected.
type: string
lastReleaseRevision:
- description: LastReleaseRevision is the revision of the last successful
- Helm release.
+ description: |-
+ LastReleaseRevision is the revision of the last successful Helm release.
+
+ Deprecated: Use History instead.
type: integer
+ observedCommonMetadataDigest:
+ description: |-
+ ObservedCommonMetadataDigest is the digest for the common metadata of
+ the last successful reconciliation attempt.
+ type: string
observedGeneration:
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
+ current release.
+ maxLength: 63
+ minLength: 1
+ type: string
upgradeFailures:
- description: UpgradeFailures is the upgrade failure count against
- the latest desired state. It is reset after a successful reconciliation.
+ description: |-
+ UpgradeFailures is the upgrade failure count against the latest desired
+ state. It is reset after a successful reconciliation.
format: int64
type: integer
type: object
diff --git a/config/default/kustomization.yaml b/config/default/kustomization.yaml
index 13da997cc..108f6445b 100644
--- a/config/default/kustomization.yaml
+++ b/config/default/kustomization.yaml
@@ -2,8 +2,8 @@ apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: helm-system
resources:
-- https://github.com/fluxcd/source-controller/releases/download/v1.1.0/source-controller.crds.yaml
-- https://github.com/fluxcd/source-controller/releases/download/v1.1.0/source-controller.deployment.yaml
+- https://github.com/fluxcd/source-controller/releases/download/v1.8.0/source-controller.crds.yaml
+- https://github.com/fluxcd/source-controller/releases/download/v1.8.0/source-controller.deployment.yaml
- ../crd
- ../rbac
- ../manager
diff --git a/config/manager/kustomization.yaml b/config/manager/kustomization.yaml
index 3af176ea2..fcf967b3b 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.36.0
+ newTag: v1.5.0
diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml
index 954df8e75..f2ef2c043 100644
--- a/config/rbac/role.yaml
+++ b/config/rbac/role.yaml
@@ -45,6 +45,7 @@ rules:
- source.toolkit.fluxcd.io
resources:
- helmcharts
+ - ocirepositories
verbs:
- get
- list
@@ -53,5 +54,6 @@ rules:
- source.toolkit.fluxcd.io
resources:
- helmcharts/status
+ - ocirepositories/status
verbs:
- get
diff --git a/config/samples/helm_v2beta1_helmrelease_gitrepository.yaml b/config/samples/helm_v2_helmrelease_gitrepository.yaml
similarity index 56%
rename from config/samples/helm_v2beta1_helmrelease_gitrepository.yaml
rename to config/samples/helm_v2_helmrelease_gitrepository.yaml
index 256b8ca98..a46a742b2 100644
--- a/config/samples/helm_v2beta1_helmrelease_gitrepository.yaml
+++ b/config/samples/helm_v2_helmrelease_gitrepository.yaml
@@ -1,4 +1,4 @@
-apiVersion: helm.toolkit.fluxcd.io/v2beta1
+apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: podinfo-gitrepository
@@ -10,9 +10,3 @@ spec:
sourceRef:
kind: GitRepository
name: podinfo
- interval: 1m
- upgrade:
- remediation:
- remediateLastFailure: true
- test:
- enable: true
diff --git a/config/samples/helm_v2beta1_helmrelease_helmrepository.yaml b/config/samples/helm_v2_helmrelease_helmrepository.yaml
similarity index 59%
rename from config/samples/helm_v2beta1_helmrelease_helmrepository.yaml
rename to config/samples/helm_v2_helmrelease_helmrepository.yaml
index 7a52c3a36..c4c4d1a5d 100644
--- a/config/samples/helm_v2beta1_helmrelease_helmrepository.yaml
+++ b/config/samples/helm_v2_helmrelease_helmrepository.yaml
@@ -1,4 +1,4 @@
-apiVersion: helm.toolkit.fluxcd.io/v2beta1
+apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: podinfo-helmrepository
@@ -11,9 +11,4 @@ spec:
sourceRef:
kind: HelmRepository
name: podinfo
- interval: 1m
- upgrade:
- remediation:
- remediateLastFailure: true
- test:
- enable: true
+ interval: 10m
diff --git a/config/samples/helm_v2_helmrelease_ocirepository.yaml b/config/samples/helm_v2_helmrelease_ocirepository.yaml
new file mode 100644
index 000000000..aafef9c0b
--- /dev/null
+++ b/config/samples/helm_v2_helmrelease_ocirepository.yaml
@@ -0,0 +1,13 @@
+apiVersion: helm.toolkit.fluxcd.io/v2
+kind: HelmRelease
+metadata:
+ name: podinfo-ocirepository
+spec:
+ interval: 5m
+ chartRef:
+ kind: OCIRepository
+ name: podinfo
+ test:
+ enable: true
+ values:
+ replicaCount: 2
diff --git a/config/samples/source_v1beta2_helmrepository.yaml b/config/samples/source_v1_helmrepository.yaml
similarity index 71%
rename from config/samples/source_v1beta2_helmrepository.yaml
rename to config/samples/source_v1_helmrepository.yaml
index c83ef482b..f4f6e3216 100644
--- a/config/samples/source_v1beta2_helmrepository.yaml
+++ b/config/samples/source_v1_helmrepository.yaml
@@ -1,4 +1,4 @@
-apiVersion: source.toolkit.fluxcd.io/v1beta2
+apiVersion: source.toolkit.fluxcd.io/v1
kind: HelmRepository
metadata:
name: podinfo
diff --git a/config/samples/source_v1_ocirepository.yaml b/config/samples/source_v1_ocirepository.yaml
new file mode 100644
index 000000000..075d8b931
--- /dev/null
+++ b/config/samples/source_v1_ocirepository.yaml
@@ -0,0 +1,9 @@
+apiVersion: source.toolkit.fluxcd.io/v1
+kind: OCIRepository
+metadata:
+ name: podinfo
+spec:
+ interval: 1m
+ url: oci://ghcr.io/stefanprodan/charts/podinfo
+ ref:
+ semver: 6.x
diff --git a/config/testdata/charts/crds/bootstrap/templates/git-repository.yaml b/config/testdata/charts/crds/bootstrap/templates/git-repository.yaml
index 8a82764fd..500b105d3 100644
--- a/config/testdata/charts/crds/bootstrap/templates/git-repository.yaml
+++ b/config/testdata/charts/crds/bootstrap/templates/git-repository.yaml
@@ -11,6 +11,6 @@ spec:
{{- if .Values.branch }}
branch: "{{ .Values.branch }}"
{{- end}}
- {{- if .Values.branch }}
+ {{- if .Values.tag }}
tag: "{{ .Values.tag }}"
{{- end}}
diff --git a/config/testdata/crds-upgrade/create-replace/helmrelease.yaml b/config/testdata/crds-upgrade/create-replace/helmrelease.yaml
index 5f21e5110..542a9ce8f 100644
--- a/config/testdata/crds-upgrade/create-replace/helmrelease.yaml
+++ b/config/testdata/crds-upgrade/create-replace/helmrelease.yaml
@@ -1,5 +1,5 @@
---
-apiVersion: helm.toolkit.fluxcd.io/v2beta1
+apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: crds-upgrade-test
diff --git a/config/testdata/crds-upgrade/create/helmrelease.yaml b/config/testdata/crds-upgrade/create/helmrelease.yaml
index de3b993e1..aef9e8026 100644
--- a/config/testdata/crds-upgrade/create/helmrelease.yaml
+++ b/config/testdata/crds-upgrade/create/helmrelease.yaml
@@ -1,5 +1,5 @@
---
-apiVersion: helm.toolkit.fluxcd.io/v2beta1
+apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: crds-upgrade-test
diff --git a/config/testdata/crds-upgrade/init/helmrelease.yaml b/config/testdata/crds-upgrade/init/helmrelease.yaml
index bfc595332..1080646cd 100644
--- a/config/testdata/crds-upgrade/init/helmrelease.yaml
+++ b/config/testdata/crds-upgrade/init/helmrelease.yaml
@@ -1,5 +1,5 @@
---
-apiVersion: helm.toolkit.fluxcd.io/v2beta1
+apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: crds-upgrade-test
diff --git a/config/testdata/delete-ns/test.yaml b/config/testdata/delete-ns/test.yaml
index f2bc4a082..cf95c70a8 100644
--- a/config/testdata/delete-ns/test.yaml
+++ b/config/testdata/delete-ns/test.yaml
@@ -42,7 +42,7 @@ subjects:
name: gotk-reconciler
namespace: delete-ns
---
-apiVersion: source.toolkit.fluxcd.io/v1beta2
+apiVersion: source.toolkit.fluxcd.io/v1
kind: HelmRepository
metadata:
name: podinfo
@@ -51,7 +51,7 @@ spec:
interval: 1m
url: https://stefanprodan.github.io/podinfo
---
-apiVersion: helm.toolkit.fluxcd.io/v2beta1
+apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: podinfo
diff --git a/config/testdata/dependencies/helmrelease-backend.yaml b/config/testdata/dependencies/helmrelease-backend.yaml
index abbad7c6c..f00b9226c 100644
--- a/config/testdata/dependencies/helmrelease-backend.yaml
+++ b/config/testdata/dependencies/helmrelease-backend.yaml
@@ -1,4 +1,4 @@
-apiVersion: helm.toolkit.fluxcd.io/v2beta1
+apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: backend
diff --git a/config/testdata/dependencies/helmrelease-frontend.yaml b/config/testdata/dependencies/helmrelease-frontend.yaml
index 5756725b4..32c9e30df 100644
--- a/config/testdata/dependencies/helmrelease-frontend.yaml
+++ b/config/testdata/dependencies/helmrelease-frontend.yaml
@@ -1,4 +1,4 @@
-apiVersion: helm.toolkit.fluxcd.io/v2beta1
+apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: frontend
diff --git a/config/testdata/impersonation/test.yaml b/config/testdata/impersonation/test.yaml
index e60c74a81..ba314f4d6 100644
--- a/config/testdata/impersonation/test.yaml
+++ b/config/testdata/impersonation/test.yaml
@@ -42,7 +42,7 @@ subjects:
name: gotk-reconciler
namespace: impersonation
---
-apiVersion: source.toolkit.fluxcd.io/v1beta2
+apiVersion: source.toolkit.fluxcd.io/v1
kind: HelmRepository
metadata:
name: podinfo
@@ -51,7 +51,7 @@ spec:
interval: 1m
url: https://stefanprodan.github.io/podinfo
---
-apiVersion: helm.toolkit.fluxcd.io/v2beta1
+apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: podinfo
@@ -67,7 +67,7 @@ spec:
kind: HelmRepository
name: podinfo
---
-apiVersion: helm.toolkit.fluxcd.io/v2beta1
+apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: podinfo-fail
diff --git a/config/testdata/install-create-target-ns/helmrelease.yaml b/config/testdata/install-create-target-ns/helmrelease.yaml
index 69b3b8c10..4f08c3c64 100644
--- a/config/testdata/install-create-target-ns/helmrelease.yaml
+++ b/config/testdata/install-create-target-ns/helmrelease.yaml
@@ -1,4 +1,4 @@
-apiVersion: helm.toolkit.fluxcd.io/v2beta1
+apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: install-create-target-ns
diff --git a/config/testdata/install-fail-remediate/helmrelease.yaml b/config/testdata/install-fail-remediate/helmrelease.yaml
index 94733cee9..0f65fb392 100644
--- a/config/testdata/install-fail-remediate/helmrelease.yaml
+++ b/config/testdata/install-fail-remediate/helmrelease.yaml
@@ -1,9 +1,9 @@
-apiVersion: helm.toolkit.fluxcd.io/v2beta1
+apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: install-fail-remediate
spec:
- interval: 5m
+ interval: 30s
chart:
spec:
chart: podinfo
@@ -11,10 +11,12 @@ spec:
sourceRef:
kind: HelmRepository
name: podinfo
- interval: 1m
+ interval: 10m
install:
remediation:
remediateLastFailure: true
+ uninstall:
+ keepHistory: true
values:
resources:
requests:
diff --git a/config/testdata/install-fail-retry/helmrelease.yaml b/config/testdata/install-fail-retry/helmrelease.yaml
index 72ad3adcb..e630385b6 100644
--- a/config/testdata/install-fail-retry/helmrelease.yaml
+++ b/config/testdata/install-fail-retry/helmrelease.yaml
@@ -1,9 +1,9 @@
-apiVersion: helm.toolkit.fluxcd.io/v2beta1
+apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: install-fail-retry
spec:
- interval: 5m
+ interval: 30s
chart:
spec:
chart: podinfo
@@ -11,7 +11,7 @@ spec:
sourceRef:
kind: HelmRepository
name: podinfo
- interval: 1m
+ interval: 10m
install:
remediation:
retries: 1
diff --git a/config/testdata/install-fail/helmrelease.yaml b/config/testdata/install-fail/helmrelease.yaml
index 7cd37fc71..6380ced42 100644
--- a/config/testdata/install-fail/helmrelease.yaml
+++ b/config/testdata/install-fail/helmrelease.yaml
@@ -1,9 +1,9 @@
-apiVersion: helm.toolkit.fluxcd.io/v2beta1
+apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: install-fail
spec:
- interval: 5m
+ interval: 30s
chart:
spec:
chart: podinfo
@@ -11,7 +11,7 @@ spec:
sourceRef:
kind: HelmRepository
name: podinfo
- interval: 1m
+ interval: 10m
values:
resources:
requests:
diff --git a/config/testdata/install-from-hc-source/test.yaml b/config/testdata/install-from-hc-source/test.yaml
new file mode 100644
index 000000000..b27fe3769
--- /dev/null
+++ b/config/testdata/install-from-hc-source/test.yaml
@@ -0,0 +1,29 @@
+---
+apiVersion: source.toolkit.fluxcd.io/v1
+kind: HelmChart
+metadata:
+ name: podinfo-hc
+spec:
+ chart: podinfo
+ version: '6.2.1'
+ sourceRef:
+ kind: HelmRepository
+ name: podinfo-oci
+ interval: 30s
+ verify:
+ provider: cosign
+---
+apiVersion: helm.toolkit.fluxcd.io/v2
+kind: HelmRelease
+metadata:
+ name: podinfo-from-hc
+spec:
+ chartRef:
+ kind: HelmChart
+ name: podinfo-hc
+ interval: 30s
+ values:
+ resources:
+ requests:
+ cpu: 100m
+ memory: 64Mi
diff --git a/config/testdata/install-from-ocirepo-source/test.yaml b/config/testdata/install-from-ocirepo-source/test.yaml
new file mode 100644
index 000000000..f87fd839b
--- /dev/null
+++ b/config/testdata/install-from-ocirepo-source/test.yaml
@@ -0,0 +1,25 @@
+---
+apiVersion: source.toolkit.fluxcd.io/v1
+kind: OCIRepository
+metadata:
+ name: podinfo-ocirepo
+spec:
+ interval: 30s
+ url: oci://ghcr.io/stefanprodan/charts/podinfo
+ ref:
+ tag: 6.6.0
+---
+apiVersion: helm.toolkit.fluxcd.io/v2
+kind: HelmRelease
+metadata:
+ name: podinfo-from-ocirepo
+spec:
+ chartRef:
+ kind: OCIRepository
+ name: podinfo-ocirepo
+ interval: 30s
+ values:
+ resources:
+ requests:
+ cpu: 100m
+ memory: 64Mi
diff --git a/config/testdata/install-test-fail-ignore/helmrelease.yaml b/config/testdata/install-test-fail-ignore/helmrelease.yaml
index d4d050f89..f62e18cf0 100644
--- a/config/testdata/install-test-fail-ignore/helmrelease.yaml
+++ b/config/testdata/install-test-fail-ignore/helmrelease.yaml
@@ -1,9 +1,9 @@
-apiVersion: helm.toolkit.fluxcd.io/v2beta1
+apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: install-test-fail-ignore
spec:
- interval: 5m
+ interval: 30s
chart:
spec:
chart: podinfo
@@ -11,7 +11,7 @@ spec:
sourceRef:
kind: HelmRepository
name: podinfo
- interval: 1m
+ interval: 10m
test:
enable: true
ignoreFailures: true
diff --git a/config/testdata/install-test-fail/helmrelease.yaml b/config/testdata/install-test-fail/helmrelease.yaml
index 39ea4d260..c7e3f435e 100644
--- a/config/testdata/install-test-fail/helmrelease.yaml
+++ b/config/testdata/install-test-fail/helmrelease.yaml
@@ -1,9 +1,9 @@
-apiVersion: helm.toolkit.fluxcd.io/v2beta1
+apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: install-test-fail
spec:
- interval: 5m
+ interval: 30s
chart:
spec:
chart: podinfo
@@ -11,7 +11,7 @@ spec:
sourceRef:
kind: HelmRepository
name: podinfo
- interval: 1m
+ interval: 10m
test:
enable: true
values:
diff --git a/config/testdata/job-ttl/helmrelease.yaml b/config/testdata/job-ttl/helmrelease.yaml
new file mode 100644
index 000000000..8f759b9ad
--- /dev/null
+++ b/config/testdata/job-ttl/helmrelease.yaml
@@ -0,0 +1,25 @@
+apiVersion: helm.toolkit.fluxcd.io/v2
+kind: HelmRelease
+metadata:
+ name: job-ttl
+spec:
+ interval: 5m
+ timeout: 2m
+ chart:
+ spec:
+ chart: podinfo
+ version: '>=6.10.1 <7.0.0'
+ sourceRef:
+ kind: HelmRepository
+ name: podinfo
+ interval: 1m
+ values:
+ # Enable a post-install hook Job with ttlSecondsAfterFinished.
+ # The Job completes immediately and gets garbage-collected by the TTL controller.
+ # The fix ensures the wait doesn't fail when the Job is NotFound after being
+ # garbage-collected.
+ hooks:
+ postInstall:
+ job:
+ enabled: true
+ ttlSecondsAfterFinished: 0
diff --git a/config/testdata/podinfo/helmrelease-git.yaml b/config/testdata/podinfo/helmrelease-git.yaml
index 2e8d46084..4bb04cc65 100644
--- a/config/testdata/podinfo/helmrelease-git.yaml
+++ b/config/testdata/podinfo/helmrelease-git.yaml
@@ -1,4 +1,4 @@
-apiVersion: helm.toolkit.fluxcd.io/v2beta1
+apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: podinfo-git
diff --git a/config/testdata/podinfo/helmrelease-oci.yaml b/config/testdata/podinfo/helmrelease-oci.yaml
index 10e078bee..9c69452ba 100644
--- a/config/testdata/podinfo/helmrelease-oci.yaml
+++ b/config/testdata/podinfo/helmrelease-oci.yaml
@@ -1,4 +1,4 @@
-apiVersion: helm.toolkit.fluxcd.io/v2beta1
+apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: podinfo-oci
diff --git a/config/testdata/podinfo/helmrelease.yaml b/config/testdata/podinfo/helmrelease.yaml
index 3c6c7b4b1..796160fb7 100644
--- a/config/testdata/podinfo/helmrelease.yaml
+++ b/config/testdata/podinfo/helmrelease.yaml
@@ -1,4 +1,4 @@
-apiVersion: helm.toolkit.fluxcd.io/v2beta1
+apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: podinfo
diff --git a/config/testdata/post-renderer-kustomize/helmrelease.yaml b/config/testdata/post-renderer-kustomize/helmrelease.yaml
index 6f33528ba..c7a59cd5a 100644
--- a/config/testdata/post-renderer-kustomize/helmrelease.yaml
+++ b/config/testdata/post-renderer-kustomize/helmrelease.yaml
@@ -1,4 +1,4 @@
-apiVersion: helm.toolkit.fluxcd.io/v2beta1
+apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: post-renderer-kustomize
@@ -7,7 +7,7 @@ spec:
chart:
spec:
chart: podinfo
- version: '>=6.0.0 <7.0.0'
+ version: '>=6.0.0 <6.9.0'
sourceRef:
kind: HelmRepository
name: podinfo
@@ -16,20 +16,20 @@ spec:
fullnameOverride: mypodinfo
postRenderers:
- kustomize:
- patchesStrategicMerge:
- - kind: Deployment
- apiVersion: apps/v1
- metadata:
- name: mypodinfo
- labels:
- xxxx: yyyy
- patchesJson6902:
+ patches:
+ - patch: |
+ kind: Deployment
+ apiVersion: apps/v1
+ metadata:
+ name: mypodinfo
+ labels:
+ xxxx: yyyy
- target:
group: apps
version: v1
kind: Deployment
name: mypodinfo
- patch:
- - op: add
- path: /metadata/labels/yyyy
- value: xxxx
+ patch: |
+ - op: add
+ path: /metadata/labels/yyyy
+ value: xxxx
diff --git a/config/testdata/server-side-apply/install.yaml b/config/testdata/server-side-apply/install.yaml
new file mode 100644
index 000000000..e2409af37
--- /dev/null
+++ b/config/testdata/server-side-apply/install.yaml
@@ -0,0 +1,21 @@
+apiVersion: helm.toolkit.fluxcd.io/v2
+kind: HelmRelease
+metadata:
+ name: server-side-apply
+spec:
+ interval: 5m
+ install:
+ serverSideApply: true
+ chart:
+ spec:
+ chart: podinfo
+ version: '>=6.0.0 <7.0.0'
+ sourceRef:
+ kind: HelmRepository
+ name: podinfo
+ interval: 1m
+ values:
+ resources:
+ requests:
+ cpu: 100m
+ memory: 64Mi
diff --git a/config/testdata/server-side-apply/no-ssa-install-ssa-upgrade.yaml b/config/testdata/server-side-apply/no-ssa-install-ssa-upgrade.yaml
new file mode 100644
index 000000000..756b68972
--- /dev/null
+++ b/config/testdata/server-side-apply/no-ssa-install-ssa-upgrade.yaml
@@ -0,0 +1,23 @@
+apiVersion: helm.toolkit.fluxcd.io/v2
+kind: HelmRelease
+metadata:
+ name: no-ssa-install-ssa-upgrade
+spec:
+ interval: 5m
+ install:
+ serverSideApply: false
+ upgrade:
+ serverSideApply: enabled
+ chart:
+ spec:
+ chart: podinfo
+ version: '>=6.0.0 <7.0.0'
+ sourceRef:
+ kind: HelmRepository
+ name: podinfo
+ interval: 1m
+ values:
+ resources:
+ requests:
+ cpu: 100m
+ memory: 64Mi
diff --git a/config/testdata/server-side-apply/no-ssa.yaml b/config/testdata/server-side-apply/no-ssa.yaml
new file mode 100644
index 000000000..7dd870749
--- /dev/null
+++ b/config/testdata/server-side-apply/no-ssa.yaml
@@ -0,0 +1,21 @@
+apiVersion: helm.toolkit.fluxcd.io/v2
+kind: HelmRelease
+metadata:
+ name: no-server-side-apply
+spec:
+ interval: 5m
+ install:
+ serverSideApply: false
+ chart:
+ spec:
+ chart: podinfo
+ version: '>=6.0.0 <7.0.0'
+ sourceRef:
+ kind: HelmRepository
+ name: podinfo
+ interval: 1m
+ values:
+ resources:
+ requests:
+ cpu: 100m
+ memory: 64Mi
diff --git a/config/testdata/server-side-apply/rollback-install.yaml b/config/testdata/server-side-apply/rollback-install.yaml
new file mode 100644
index 000000000..7194ab634
--- /dev/null
+++ b/config/testdata/server-side-apply/rollback-install.yaml
@@ -0,0 +1,21 @@
+apiVersion: helm.toolkit.fluxcd.io/v2
+kind: HelmRelease
+metadata:
+ name: server-side-apply-rollback
+spec:
+ interval: 30s
+ install:
+ serverSideApply: true
+ chart:
+ spec:
+ chart: podinfo
+ version: '>=6.0.0 <7.0.0'
+ sourceRef:
+ kind: HelmRepository
+ name: podinfo
+ interval: 10m
+ values:
+ resources:
+ requests:
+ cpu: 100m
+ memory: 64Mi
diff --git a/config/testdata/server-side-apply/rollback-upgrade.yaml b/config/testdata/server-side-apply/rollback-upgrade.yaml
new file mode 100644
index 000000000..1ab1b6cac
--- /dev/null
+++ b/config/testdata/server-side-apply/rollback-upgrade.yaml
@@ -0,0 +1,32 @@
+apiVersion: helm.toolkit.fluxcd.io/v2
+kind: HelmRelease
+metadata:
+ name: server-side-apply-rollback
+spec:
+ interval: 30s
+ install:
+ serverSideApply: true
+ upgrade:
+ serverSideApply: enabled
+ remediation:
+ remediateLastFailure: true
+ rollback:
+ serverSideApply: enabled
+ chart:
+ spec:
+ chart: podinfo
+ version: '>=6.0.0 <7.0.0'
+ sourceRef:
+ kind: HelmRepository
+ name: podinfo
+ interval: 10m
+ values:
+ resources:
+ requests:
+ cpu: 100m
+ memory: 64Mi
+ # Make wait fail to trigger rollback
+ replicaCount: 2
+ faults:
+ unready: true
+ timeout: 10s
diff --git a/config/testdata/server-side-apply/ssa-install-no-ssa-upgrade.yaml b/config/testdata/server-side-apply/ssa-install-no-ssa-upgrade.yaml
new file mode 100644
index 000000000..302552e59
--- /dev/null
+++ b/config/testdata/server-side-apply/ssa-install-no-ssa-upgrade.yaml
@@ -0,0 +1,23 @@
+apiVersion: helm.toolkit.fluxcd.io/v2
+kind: HelmRelease
+metadata:
+ name: ssa-install-no-ssa-upgrade
+spec:
+ interval: 5m
+ install:
+ serverSideApply: true
+ upgrade:
+ serverSideApply: disabled
+ chart:
+ spec:
+ chart: podinfo
+ version: '>=6.0.0 <7.0.0'
+ sourceRef:
+ kind: HelmRepository
+ name: podinfo
+ interval: 1m
+ values:
+ resources:
+ requests:
+ cpu: 100m
+ memory: 64Mi
diff --git a/config/testdata/server-side-apply/ssa-to-csa-field-removal.yaml b/config/testdata/server-side-apply/ssa-to-csa-field-removal.yaml
new file mode 100644
index 000000000..a3dd1dd52
--- /dev/null
+++ b/config/testdata/server-side-apply/ssa-to-csa-field-removal.yaml
@@ -0,0 +1,25 @@
+apiVersion: helm.toolkit.fluxcd.io/v2
+kind: HelmRelease
+metadata:
+ name: ssa-to-csa-field-removal
+spec:
+ interval: 5m
+ install:
+ serverSideApply: true
+ upgrade:
+ serverSideApply: disabled
+ chart:
+ spec:
+ chart: podinfo
+ version: '>=6.0.0 <7.0.0'
+ sourceRef:
+ kind: HelmRepository
+ name: podinfo
+ interval: 1m
+ values:
+ podAnnotations:
+ ssa-owned-field: "this-should-persist-after-csa-upgrade"
+ resources:
+ requests:
+ cpu: 100m
+ memory: 64Mi
diff --git a/config/testdata/server-side-apply/upgrade.yaml b/config/testdata/server-side-apply/upgrade.yaml
new file mode 100644
index 000000000..f25cb2a4a
--- /dev/null
+++ b/config/testdata/server-side-apply/upgrade.yaml
@@ -0,0 +1,24 @@
+apiVersion: helm.toolkit.fluxcd.io/v2
+kind: HelmRelease
+metadata:
+ name: server-side-apply
+spec:
+ interval: 5m
+ install:
+ serverSideApply: true
+ upgrade:
+ serverSideApply: enabled
+ chart:
+ spec:
+ chart: podinfo
+ version: '>=6.0.0 <7.0.0'
+ sourceRef:
+ kind: HelmRepository
+ name: podinfo
+ interval: 1m
+ values:
+ resources:
+ requests:
+ cpu: 100m
+ memory: 64Mi
+ replicaCount: 2
diff --git a/config/testdata/sources/helmrepository.yaml b/config/testdata/sources/helmrepository.yaml
index c83ef482b..f4f6e3216 100644
--- a/config/testdata/sources/helmrepository.yaml
+++ b/config/testdata/sources/helmrepository.yaml
@@ -1,4 +1,4 @@
-apiVersion: source.toolkit.fluxcd.io/v1beta2
+apiVersion: source.toolkit.fluxcd.io/v1
kind: HelmRepository
metadata:
name: podinfo
diff --git a/config/testdata/sources/helmrepository_oci.yaml b/config/testdata/sources/helmrepository_oci.yaml
index f0648c7a5..96e613f3e 100644
--- a/config/testdata/sources/helmrepository_oci.yaml
+++ b/config/testdata/sources/helmrepository_oci.yaml
@@ -1,4 +1,4 @@
-apiVersion: source.toolkit.fluxcd.io/v1beta2
+apiVersion: source.toolkit.fluxcd.io/v1
kind: HelmRepository
metadata:
name: podinfo-oci
diff --git a/config/testdata/status-defaults/helmrelease.yaml b/config/testdata/status-defaults/helmrelease.yaml
index 32d753ff7..43ce890fd 100644
--- a/config/testdata/status-defaults/helmrelease.yaml
+++ b/config/testdata/status-defaults/helmrelease.yaml
@@ -1,4 +1,4 @@
-apiVersion: helm.toolkit.fluxcd.io/v2beta1
+apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: status-defaults
diff --git a/config/testdata/targetnamespace/helmrelease.yaml b/config/testdata/targetnamespace/helmrelease.yaml
index abe5e5747..6af14b9d5 100644
--- a/config/testdata/targetnamespace/helmrelease.yaml
+++ b/config/testdata/targetnamespace/helmrelease.yaml
@@ -1,4 +1,4 @@
-apiVersion: helm.toolkit.fluxcd.io/v2beta1
+apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: targetnamespace
diff --git a/config/testdata/upgrade-fail-remediate-uninstall/install.yaml b/config/testdata/upgrade-fail-remediate-uninstall/install.yaml
index 7871e8ac9..fe549ad80 100644
--- a/config/testdata/upgrade-fail-remediate-uninstall/install.yaml
+++ b/config/testdata/upgrade-fail-remediate-uninstall/install.yaml
@@ -1,9 +1,9 @@
-apiVersion: helm.toolkit.fluxcd.io/v2beta1
+apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: upgrade-fail-remediate-uninstall
spec:
- interval: 5m
+ interval: 30s
chart:
spec:
chart: podinfo
@@ -11,7 +11,7 @@ spec:
sourceRef:
kind: HelmRepository
name: podinfo
- interval: 1m
+ interval: 10m
values:
resources:
requests:
diff --git a/config/testdata/upgrade-fail-remediate-uninstall/upgrade.yaml b/config/testdata/upgrade-fail-remediate-uninstall/upgrade.yaml
index 92e372f31..a7e2b62b8 100644
--- a/config/testdata/upgrade-fail-remediate-uninstall/upgrade.yaml
+++ b/config/testdata/upgrade-fail-remediate-uninstall/upgrade.yaml
@@ -1,9 +1,9 @@
-apiVersion: helm.toolkit.fluxcd.io/v2beta1
+apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: upgrade-fail-remediate-uninstall
spec:
- interval: 5m
+ interval: 30s
chart:
spec:
chart: podinfo
@@ -11,7 +11,7 @@ spec:
sourceRef:
kind: HelmRepository
name: podinfo
- interval: 1m
+ interval: 10m
upgrade:
remediation:
remediateLastFailure: true
diff --git a/config/testdata/upgrade-fail-remediate/install.yaml b/config/testdata/upgrade-fail-remediate/install.yaml
index a6b4e92a8..fe880f0d3 100644
--- a/config/testdata/upgrade-fail-remediate/install.yaml
+++ b/config/testdata/upgrade-fail-remediate/install.yaml
@@ -1,9 +1,9 @@
-apiVersion: helm.toolkit.fluxcd.io/v2beta1
+apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: upgrade-fail-remediate
spec:
- interval: 5m
+ interval: 30s
chart:
spec:
chart: podinfo
@@ -11,7 +11,7 @@ spec:
sourceRef:
kind: HelmRepository
name: podinfo
- interval: 1m
+ interval: 10m
values:
resources:
requests:
diff --git a/config/testdata/upgrade-fail-remediate/upgrade.yaml b/config/testdata/upgrade-fail-remediate/upgrade.yaml
index a2def1fac..e4d722d9f 100644
--- a/config/testdata/upgrade-fail-remediate/upgrade.yaml
+++ b/config/testdata/upgrade-fail-remediate/upgrade.yaml
@@ -1,9 +1,9 @@
-apiVersion: helm.toolkit.fluxcd.io/v2beta1
+apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: upgrade-fail-remediate
spec:
- interval: 5m
+ interval: 30s
chart:
spec:
chart: podinfo
@@ -11,7 +11,7 @@ spec:
sourceRef:
kind: HelmRepository
name: podinfo
- interval: 1m
+ interval: 10m
upgrade:
remediation:
remediateLastFailure: true
@@ -25,4 +25,4 @@ spec:
replicaCount: 2
faults:
unready: true
- timeout: 3s
+ timeout: 10s
diff --git a/config/testdata/upgrade-fail-retry/install.yaml b/config/testdata/upgrade-fail-retry/install.yaml
index 63cad76ee..efbae6806 100644
--- a/config/testdata/upgrade-fail-retry/install.yaml
+++ b/config/testdata/upgrade-fail-retry/install.yaml
@@ -1,9 +1,9 @@
-apiVersion: helm.toolkit.fluxcd.io/v2beta1
+apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: upgrade-fail-retry
spec:
- interval: 5m
+ interval: 30s
chart:
spec:
chart: podinfo
@@ -11,7 +11,7 @@ spec:
sourceRef:
kind: HelmRepository
name: podinfo
- interval: 1m
+ interval: 10m
values:
resources:
requests:
diff --git a/config/testdata/upgrade-fail-retry/upgrade.yaml b/config/testdata/upgrade-fail-retry/upgrade.yaml
index 32ced3592..301279a9c 100644
--- a/config/testdata/upgrade-fail-retry/upgrade.yaml
+++ b/config/testdata/upgrade-fail-retry/upgrade.yaml
@@ -1,9 +1,9 @@
-apiVersion: helm.toolkit.fluxcd.io/v2beta1
+apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: upgrade-fail-retry
spec:
- interval: 5m
+ interval: 30s
chart:
spec:
chart: podinfo
@@ -11,7 +11,7 @@ spec:
sourceRef:
kind: HelmRepository
name: podinfo
- interval: 1m
+ interval: 10m
upgrade:
remediation:
retries: 1
@@ -25,4 +25,4 @@ spec:
replicaCount: 2
faults:
unready: true
- timeout: 3s
+ timeout: 10s
diff --git a/config/testdata/upgrade-fail/install.yaml b/config/testdata/upgrade-fail/install.yaml
index 39a5414f2..6a32b4394 100644
--- a/config/testdata/upgrade-fail/install.yaml
+++ b/config/testdata/upgrade-fail/install.yaml
@@ -1,9 +1,9 @@
-apiVersion: helm.toolkit.fluxcd.io/v2beta1
+apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: upgrade-fail
spec:
- interval: 5m
+ interval: 30s
chart:
spec:
chart: podinfo
@@ -11,7 +11,7 @@ spec:
sourceRef:
kind: HelmRepository
name: podinfo
- interval: 1m
+ interval: 10m
values:
resources:
requests:
diff --git a/config/testdata/upgrade-fail/upgrade.yaml b/config/testdata/upgrade-fail/upgrade.yaml
index edcc45f0e..d1d6f18a2 100644
--- a/config/testdata/upgrade-fail/upgrade.yaml
+++ b/config/testdata/upgrade-fail/upgrade.yaml
@@ -1,9 +1,9 @@
-apiVersion: helm.toolkit.fluxcd.io/v2beta1
+apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: upgrade-fail
spec:
- interval: 5m
+ interval: 30s
chart:
spec:
chart: podinfo
@@ -11,7 +11,7 @@ spec:
sourceRef:
kind: HelmRepository
name: podinfo
- interval: 1m
+ interval: 10m
values:
resources:
requests:
diff --git a/config/testdata/upgrade-from-ocirepo-source/install.yaml b/config/testdata/upgrade-from-ocirepo-source/install.yaml
new file mode 100644
index 000000000..53a571102
--- /dev/null
+++ b/config/testdata/upgrade-from-ocirepo-source/install.yaml
@@ -0,0 +1,25 @@
+---
+apiVersion: source.toolkit.fluxcd.io/v1
+kind: OCIRepository
+metadata:
+ name: upgrade-from-ocirepo-source
+spec:
+ interval: 30s
+ url: oci://ghcr.io/stefanprodan/charts/podinfo
+ ref:
+ digest: "sha256:cdd538a0167e4b51152b71a477e51eb6737553510ce8797dbcc537e1342311bb"
+---
+apiVersion: helm.toolkit.fluxcd.io/v2
+kind: HelmRelease
+metadata:
+ name: upgrade-from-ocirepo-source
+spec:
+ chartRef:
+ kind: OCIRepository
+ name: upgrade-from-ocirepo-source
+ interval: 30s
+ values:
+ resources:
+ requests:
+ cpu: 100m
+ memory: 64Mi
diff --git a/config/testdata/upgrade-from-ocirepo-source/upgrade.yaml b/config/testdata/upgrade-from-ocirepo-source/upgrade.yaml
new file mode 100644
index 000000000..e402d034b
--- /dev/null
+++ b/config/testdata/upgrade-from-ocirepo-source/upgrade.yaml
@@ -0,0 +1,10 @@
+---
+apiVersion: source.toolkit.fluxcd.io/v1
+kind: OCIRepository
+metadata:
+ name: upgrade-from-ocirepo-source
+spec:
+ interval: 30s
+ url: oci://ghcr.io/stefanprodan/charts/podinfo
+ ref:
+ digest: "sha256:0cc9a8446c95009ef382f5eade883a67c257f77d50f84e78ecef2aac9428d1e5"
diff --git a/config/testdata/upgrade-test-fail/install.yaml b/config/testdata/upgrade-test-fail/install.yaml
index 78cbe3984..9aa493bd8 100644
--- a/config/testdata/upgrade-test-fail/install.yaml
+++ b/config/testdata/upgrade-test-fail/install.yaml
@@ -1,9 +1,9 @@
-apiVersion: helm.toolkit.fluxcd.io/v2beta1
+apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: upgrade-test-fail
spec:
- interval: 5m
+ interval: 30s
chart:
spec:
chart: podinfo
@@ -11,7 +11,7 @@ spec:
sourceRef:
kind: HelmRepository
name: podinfo
- interval: 1m
+ interval: 10m
values:
resources:
requests:
diff --git a/config/testdata/upgrade-test-fail/upgrade.yaml b/config/testdata/upgrade-test-fail/upgrade.yaml
index defdcde49..c6c7df1cc 100644
--- a/config/testdata/upgrade-test-fail/upgrade.yaml
+++ b/config/testdata/upgrade-test-fail/upgrade.yaml
@@ -1,9 +1,9 @@
-apiVersion: helm.toolkit.fluxcd.io/v2beta1
+apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: upgrade-test-fail
spec:
- interval: 5m
+ interval: 30s
chart:
spec:
chart: podinfo
@@ -11,7 +11,7 @@ spec:
sourceRef:
kind: HelmRepository
name: podinfo
- interval: 1m
+ interval: 10m
test:
enable: true
values:
diff --git a/config/testdata/valuesfrom/helmrelease.yaml b/config/testdata/valuesfrom/helmrelease.yaml
index 76937bfda..799e43d06 100644
--- a/config/testdata/valuesfrom/helmrelease.yaml
+++ b/config/testdata/valuesfrom/helmrelease.yaml
@@ -1,4 +1,4 @@
-apiVersion: helm.toolkit.fluxcd.io/v2beta1
+apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: valuesfrom
@@ -11,7 +11,7 @@ spec:
sourceRef:
kind: HelmRepository
name: podinfo
- interval: 1m
+ interval: 10m
valuesFrom:
- kind: ConfigMap
name: valuesfrom-config
diff --git a/docs/api/v2beta1/helm.md b/docs/api/v2/helm.md
similarity index 53%
rename from docs/api/v2beta1/helm.md
rename to docs/api/v2/helm.md
index 4076569d8..c17156d72 100644
--- a/docs/api/v2beta1/helm.md
+++ b/docs/api/v2/helm.md
@@ -1,17 +1,17 @@
-Helm API reference v2beta1
+Helm API reference v2
Packages:
-
-Package v2beta1 contains API Schema definitions for the helm v2beta1 API group
+
+Package v2 contains API Schema definitions for the helm v2 API group
Resource Types:
-HelmRelease is the Schema for the helmreleases API
@@ -29,7 +29,7 @@ Resource Types:
apiVersion
string
-helm.toolkit.fluxcd.io/v2beta1
+helm.toolkit.fluxcd.io/v2
|
@@ -59,7 +59,7 @@ Refer to the Kubernetes API documentation for the fields of the
spec
-
+
HelmReleaseSpec
@@ -72,18 +72,34 @@ HelmReleaseSpec
|
chart
-
+
HelmChartTemplate
|
- Chart defines the template of the v1beta2.HelmChart that should be created
+(Optional)
+ Chart defines the template of the v1.HelmChart that should be created
for this HelmRelease.
|
+chartRef
+
+
+CrossNamespaceSourceReference
+
+
+ |
+
+(Optional)
+ ChartRef holds a reference to a source controller resource containing the
+Helm chart artifact.
+ |
+
+
+
interval
@@ -92,9 +108,7 @@ Kubernetes meta/v1.Duration
|
- Interval at which to reconcile the Helm release.
-This interval is approximate and may be subject to jitter to ensure
-efficient use of resources.
+Interval at which to reconcile the Helm release.
|
@@ -173,14 +187,14 @@ Defaults to the namespace of the HelmRelease.
dependsOn
-
-[]github.com/fluxcd/pkg/apis/meta.NamespacedObjectReference
+
+[]DependencyReference
|
(Optional)
- DependsOn may contain a meta.NamespacedObjectReference slice with
+ DependsOn may contain a DependencyReference slice with
references to HelmRelease resources that must be ready before this HelmRelease
can be reconciled.
|
@@ -210,7 +224,7 @@ int
(Optional)
MaxHistory is the number of revisions saved by Helm for this HelmRelease.
-Use ‘0’ for an unlimited number of revisions; defaults to ‘10’.
+Use ‘0’ for an unlimited number of revisions; defaults to ‘5’.
|
@@ -248,9 +262,25 @@ available by e.g. post-install hooks.
+driftDetection
+
+
+DriftDetection
+
+
+ |
+
+(Optional)
+ DriftDetection holds the configuration for detecting and handling
+differences between the manifest in the Helm storage and the resources
+currently existing in the cluster.
+ |
+
+
+
install
-
+
Install
@@ -264,7 +294,7 @@ Install
|
upgrade
-
+
Upgrade
@@ -278,7 +308,7 @@ Upgrade
|
test
-
+
Test
@@ -292,7 +322,7 @@ Test
|
rollback
-
+
Rollback
@@ -306,7 +336,7 @@ Rollback
|
uninstall
-
+
Uninstall
@@ -320,8 +350,8 @@ Uninstall
|
valuesFrom
-
-[]ValuesReference
+
+[]github.com/fluxcd/pkg/apis/meta.ValuesReference
|
@@ -346,9 +376,25 @@ Kubernetes pkg/apis/apiextensions/v1.JSON
+commonMetadata
+
+
+CommonMetadata
+
+
+ |
+
+(Optional)
+ CommonMetadata specifies the common labels and annotations that are
+applied to all resources. Any existing label or annotation will be
+overridden if its key matches a common one.
+ |
+
+
+
postRenderers
-
+
[]PostRenderer
@@ -359,6 +405,39 @@ Kubernetes pkg/apis/apiextensions/v1.JSON
of their definition.
|
+
+
+waitStrategy
+
+
+WaitStrategy
+
+
+ |
+
+(Optional)
+ WaitStrategy defines Helm’s wait strategy for waiting for applied
+resources to become ready.
+ |
+
+
+
+healthCheckExprs
+
+
+[]github.com/fluxcd/pkg/apis/kustomize.CustomHealthCheck
+
+
+ |
+
+(Optional)
+ HealthCheckExprs is a list of healthcheck expressions for evaluating the
+health of custom resources using Common Expression Language (CEL).
+The expressions are evaluated only when the specific Helm action
+taking place has wait enabled, i.e. DisableWait is false, and the
+‘poller’ WaitStrategy is used.
+ |
+
@@ -366,7 +445,7 @@ of their definition.
status
-
+
HelmReleaseStatus
@@ -378,20 +457,68 @@ HelmReleaseStatus
-CRDsPolicy
+ActionStrategyName
+(string alias)
+ActionStrategyName is a valid name for an action strategy.
+CRDsPolicy
(string alias)
(Appears on:
-Install,
-Upgrade)
+Install,
+Upgrade)
CRDsPolicy defines the install/upgrade approach to use for CRDs when
installing or upgrading a HelmRelease.
-CrossNamespaceObjectReference
+CommonMetadata
+
+
+(Appears on:
+HelmReleaseSpec)
+
+CommonMetadata defines the common labels and annotations.
+
+CrossNamespaceObjectReference
(Appears on:
-HelmChartTemplateSpec)
+HelmChartTemplateSpec)
CrossNamespaceObjectReference contains enough information to let you locate
the typed referenced object at cluster level.
@@ -455,18 +582,14 @@ string
-DeploymentAction
-
-DeploymentAction defines a consistent interface for Install and Upgrade.
-HelmChartTemplate
+CrossNamespaceSourceReference
(Appears on:
-HelmReleaseSpec)
+HelmReleaseSpec)
-HelmChartTemplate defines the template from which the controller will
-generate a v1beta2.HelmChart object in the same namespace as the referenced
-v1beta2.Source.
+CrossNamespaceSourceReference contains enough information to let you locate
+the typed referenced object at cluster level.
-HelmChartTemplateObjectMeta
+DriftDetectionMode
+(string alias)
+
+(Appears on:
+DriftDetection)
+
+DriftDetectionMode represents the modes in which a controller can detect and
+handle differences between the manifest in the Helm storage and the resources
+currently existing in the cluster.
+Filter
(Appears on:
-HelmChartTemplate)
+Test)
-HelmChartTemplateObjectMeta defines the template for the ObjectMeta of a
-v1beta2.HelmChart.
+Filter holds the configuration for individual Helm test filters.
-HelmChartTemplateSpec
+HelmChartTemplate
(Appears on:
-HelmChartTemplate)
+HelmReleaseSpec)
-HelmChartTemplateSpec defines the template from which the controller will
-generate a v1beta2.HelmChartSpec object.
+HelmChartTemplate defines the template from which the controller will
+generate a v1.HelmChart object in the same namespace as the referenced
+v1.Source.
-HelmChartTemplateVerification
+HelmChartTemplateObjectMeta
(Appears on:
-HelmChartTemplateSpec)
+HelmChartTemplate)
-HelmChartTemplateVerification verifies the authenticity of an OCI Helm chart.
+HelmChartTemplateObjectMeta defines the template for the ObjectMeta of a
+v1.HelmChart.
+HelmChartTemplateSpec
+
+
+(Appears on:
+HelmChartTemplate)
+
+HelmChartTemplateSpec defines the template from which the controller will
+generate a v1.HelmChartSpec object.
+
+HelmChartTemplateVerification
+
+
+(Appears on:
+HelmChartTemplateSpec)
+
+HelmChartTemplateVerification verifies the authenticity of an OCI Helm chart.
+
-HelmReleaseSpec
+HelmReleaseSpec
(Appears on:
-HelmRelease)
+HelmRelease)
HelmReleaseSpec defines the desired state of a Helm release.
@@ -883,18 +1240,34 @@ trusted public keys.
chart
-
+
HelmChartTemplate
|
- Chart defines the template of the v1beta2.HelmChart that should be created
+(Optional)
+ Chart defines the template of the v1.HelmChart that should be created
for this HelmRelease.
|
+chartRef
+
+
+CrossNamespaceSourceReference
+
+
+ |
+
+(Optional)
+ ChartRef holds a reference to a source controller resource containing the
+Helm chart artifact.
+ |
+
+
+
interval
@@ -903,9 +1276,7 @@ Kubernetes meta/v1.Duration
|
- Interval at which to reconcile the Helm release.
-This interval is approximate and may be subject to jitter to ensure
-efficient use of resources.
+Interval at which to reconcile the Helm release.
|
@@ -984,14 +1355,14 @@ Defaults to the namespace of the HelmRelease.
dependsOn
-
-[]github.com/fluxcd/pkg/apis/meta.NamespacedObjectReference
+
+[]DependencyReference
|
(Optional)
- DependsOn may contain a meta.NamespacedObjectReference slice with
+ DependsOn may contain a DependencyReference slice with
references to HelmRelease resources that must be ready before this HelmRelease
can be reconciled.
|
@@ -1021,7 +1392,7 @@ int
(Optional)
MaxHistory is the number of revisions saved by Helm for this HelmRelease.
-Use ‘0’ for an unlimited number of revisions; defaults to ‘10’.
+Use ‘0’ for an unlimited number of revisions; defaults to ‘5’.
|
@@ -1059,9 +1430,25 @@ available by e.g. post-install hooks.
+driftDetection
+
+
+DriftDetection
+
+
+ |
+
+(Optional)
+ DriftDetection holds the configuration for detecting and handling
+differences between the manifest in the Helm storage and the resources
+currently existing in the cluster.
+ |
+
+
+
install
-
+
Install
@@ -1075,7 +1462,7 @@ Install
|
upgrade
-
+
Upgrade
@@ -1089,7 +1476,7 @@ Upgrade
|
test
-
+
Test
@@ -1103,7 +1490,7 @@ Test
|
rollback
-
+
Rollback
@@ -1117,7 +1504,7 @@ Rollback
|
uninstall
-
+
Uninstall
@@ -1131,8 +1518,8 @@ Uninstall
|
valuesFrom
-
-[]ValuesReference
+
+[]github.com/fluxcd/pkg/apis/meta.ValuesReference
|
@@ -1157,9 +1544,25 @@ Kubernetes pkg/apis/apiextensions/v1.JSON
+commonMetadata
+
+
+CommonMetadata
+
+
+ |
+
+(Optional)
+ CommonMetadata specifies the common labels and annotations that are
+applied to all resources. Any existing label or annotation will be
+overridden if its key matches a common one.
+ |
+
+
+
postRenderers
-
+
[]PostRenderer
@@ -1170,15 +1573,48 @@ Kubernetes pkg/apis/apiextensions/v1.JSON
of their definition.
|
+
+
+waitStrategy
+
+
+WaitStrategy
+
+
+ |
+
+(Optional)
+ WaitStrategy defines Helm’s wait strategy for waiting for applied
+resources to become ready.
+ |
+
+
+
+healthCheckExprs
+
+
+[]github.com/fluxcd/pkg/apis/kustomize.CustomHealthCheck
+
+
+ |
+
+(Optional)
+ HealthCheckExprs is a list of healthcheck expressions for evaluating the
+health of custom resources using Common Expression Language (CEL).
+The expressions are evaluated only when the specific Helm action
+taking place has wait enabled, i.e. DisableWait is false, and the
+‘poller’ WaitStrategy is used.
+ |
+
-HelmReleaseStatus
+HelmReleaseStatus
(Appears on:
-HelmRelease)
+HelmRelease)
HelmReleaseStatus defines the observed state of a HelmRelease.
@@ -1205,93 +1641,142 @@ int64
-ReconcileRequestStatus
+observedPostRenderersDigest
-
-github.com/fluxcd/pkg/apis/meta.ReconcileRequestStatus
-
+string
|
-
-(Members of ReconcileRequestStatus are embedded into this type.)
-
+(Optional)
+ObservedPostRenderersDigest is the digest for the post-renderers of
+the last successful reconciliation attempt.
|
-conditions
+observedCommonMetadataDigest
-
-[]Kubernetes meta/v1.Condition
-
+string
|
(Optional)
- Conditions holds the conditions for the HelmRelease.
+ObservedCommonMetadataDigest is the digest for the common metadata of
+the last successful reconciliation attempt.
|
-lastAppliedRevision
+lastAttemptedGeneration
-string
+int64
|
(Optional)
- LastAppliedRevision is the revision of the last successfully applied source.
+LastAttemptedGeneration is the last generation the controller attempted
+to reconcile.
|
-lastAttemptedRevision
+conditions
-string
+
+[]Kubernetes meta/v1.Condition
+
|
(Optional)
- LastAttemptedRevision is the revision of the last reconciliation attempt.
+Conditions holds the conditions for the HelmRelease.
|
-lastAttemptedValuesChecksum
+helmChart
string
|
(Optional)
- LastAttemptedValuesChecksum is the SHA1 checksum of the values of the last
-reconciliation attempt.
+HelmChart is the namespaced name of the HelmChart resource created by
+the controller for the HelmRelease.
|
-lastReleaseRevision
+storageNamespace
-int
+string
|
(Optional)
- LastReleaseRevision is the revision of the last successful Helm release.
+StorageNamespace is the namespace of the Helm release storage for the
+current release.
|
-helmChart
+history
-string
+
+Snapshots
+
|
(Optional)
- HelmChart is the namespaced name of the HelmChart resource created by
-the controller for the HelmRelease.
+History holds the history of Helm releases performed for this HelmRelease
+up to the last successfully completed release.
+ |
+
+
+
+inventory
+
+
+ResourceInventory
+
+
+ |
+
+(Optional)
+ Inventory contains the list of Kubernetes resource object references
+that have been applied for this release.
+ |
+
+
+
+lastAttemptedReleaseAction
+
+
+ReleaseAction
+
+
+ |
+
+(Optional)
+ LastAttemptedReleaseAction is the last release action performed for this
+HelmRelease. It is used to determine the active retry or remediation
+strategy.
+ |
+
+
+
+lastAttemptedReleaseActionDuration
+
+
+Kubernetes meta/v1.Duration
+
+
+ |
+
+(Optional)
+ LastAttemptedReleaseActionDuration is the duration of the last
+release action performed for this HelmRelease.
|
@@ -1333,15 +1818,176 @@ int64
state. It is reset after a successful reconciliation.
+
+
+lastAttemptedRevision
+
+string
+
+ |
+
+(Optional)
+ LastAttemptedRevision is the Source revision of the last reconciliation
+attempt. For OCIRepository sources, the 12 first characters of the digest are
+appended to the chart version e.g. “1.2.3+1234567890ab”.
+ |
+
+
+
+lastAttemptedRevisionDigest
+
+string
+
+ |
+
+(Optional)
+ LastAttemptedRevisionDigest is the digest of the last reconciliation attempt.
+This is only set for OCIRepository sources.
+ |
+
+
+
+lastAttemptedValuesChecksum
+
+string
+
+ |
+
+(Optional)
+ LastAttemptedValuesChecksum is the SHA1 checksum for the values of the last
+reconciliation attempt.
+Deprecated: Use LastAttemptedConfigDigest instead.
+ |
+
+
+
+lastReleaseRevision
+
+int
+
+ |
+
+(Optional)
+ LastReleaseRevision is the revision of the last successful Helm release.
+Deprecated: Use History instead.
+ |
+
+
+
+lastAttemptedConfigDigest
+
+string
+
+ |
+
+(Optional)
+ LastAttemptedConfigDigest is the digest for the config (better known as
+“values”) of the last reconciliation attempt.
+ |
+
+
+
+lastHandledResetAt
+
+string
+
+ |
+
+(Optional)
+ LastHandledResetAt holds the value of the most recent reset request
+value, so a change of the annotation value can be detected.
+ |
+
+
+
+ReconcileRequestStatus
+
+
+github.com/fluxcd/pkg/apis/meta.ReconcileRequestStatus
+
+
+ |
+
+
+(Members of ReconcileRequestStatus are embedded into this type.)
+
+ |
+
+
+
+ForceRequestStatus
+
+
+github.com/fluxcd/pkg/apis/meta.ForceRequestStatus
+
+
+ |
+
+
+(Members of ForceRequestStatus are embedded into this type.)
+
+ |
+
+
+
+
+
+IgnoreRule
+
+
+(Appears on:
+DriftDetection)
+
+IgnoreRule defines a rule to selectively disregard specific changes during
+the drift detection process.
+
-Install
+Install
(Appears on:
-HelmReleaseSpec)
+HelmReleaseSpec)
Install holds the configuration for Helm install actions performed for this
HelmRelease.
@@ -1373,9 +2019,25 @@ Jobs for hooks) during the performance of a Helm install action. Defaults to
|
+strategy
+
+
+InstallStrategy
+
+
+ |
+
+(Optional)
+ Strategy defines the install strategy to use for this HelmRelease.
+Defaults to ‘RemediateOnFailure’, or ‘RetryOnFailure’ when the
+DefaultToRetryOnFailure feature gate is enabled.
+ |
+
+
+
remediation
-
+
InstallRemediation
@@ -1388,6 +2050,19 @@ action for the HelmRelease fails. The default is to not perform any action.
|
+disableTakeOwnership
+
+bool
+
+ |
+
+(Optional)
+ DisableTakeOwnership disables taking ownership of existing resources
+during the Helm install action. Defaults to false.
+ |
+
+
+
disableWait
bool
@@ -1439,6 +2114,19 @@ rendered templates against the Kubernetes OpenAPI Schema.
|
+disableSchemaValidation
+
+bool
+
+ |
+
+(Optional)
+ DisableSchemaValidation prevents the Helm install action from validating
+the values against the JSON Schema.
+ |
+
+
+
replace
bool
@@ -1468,7 +2156,7 @@ CRDs are installed if not already present.
crds
-
+
CRDsPolicy
@@ -1484,7 +2172,7 @@ CRDs are installed but not updated.
CreateReplace: new CRDs are created, existing CRDs are updated (replaced)
but not deleted.
By default, CRDs are applied (installed) during Helm install action.
-With this option users can opt-in to CRD replace existing CRDs on Helm
+With this option users can opt in to CRD replace existing CRDs on Helm
install actions, which is not (yet) natively supported by Helm.
https://helm.sh/docs/chart_best_practices/custom_resource_definitions.
|
@@ -1503,15 +2191,28 @@ HelmReleaseSpec.TargetNamespace if it does not exist yet.
On uninstall, the namespace will not be garbage collected.
|
+
+
+serverSideApply
+
+bool
+
+ |
+
+(Optional)
+ ServerSideApply enables server-side apply for resources during install.
+Defaults to true (or false when UseHelm3Defaults feature gate is enabled).
+ |
+
-InstallRemediation
+InstallRemediation
(Appears on:
-Install)
+Install)
InstallRemediation holds the configuration for Helm install remediation.
+
+InstallStrategy
+
+
+(Appears on:
+Install)
+
+InstallStrategy holds the configuration for Helm install strategy.
+
+Kustomize
+
+
+(Appears on:
+PostRenderer)
+
+Kustomize Helm PostRenderer specification.
+
+PostRenderer
+
+
+(Appears on:
+HelmReleaseSpec)
+
+PostRenderer contains a Helm PostRenderer specification.
+
+ReleaseAction
+(string alias)
+
+(Appears on:
+HelmReleaseStatus,
+Snapshot)
+
+ReleaseAction is the action to perform a Helm release.
+Remediation
+
+Remediation defines a consistent interface for InstallRemediation and
+UpgradeRemediation.
+RemediationStrategy
+(string alias)
+
+(Appears on:
+UpgradeRemediation)
+
+RemediationStrategy returns the strategy to use to remediate a failed install
+or upgrade.
+ResourceInventory
+
+
+(Appears on:
+HelmReleaseStatus)
+
+ResourceInventory contains a list of Kubernetes resource object references
+that have been applied by a HelmRelease.
+
+ResourceRef
+
+
+(Appears on:
+ResourceInventory)
+
+ResourceRef contains the information necessary to locate a resource within a cluster.
+
+Retry
+
+Retry defines a consistent interface for retry strategies from
+InstallStrategy and UpgradeStrategy.
+Rollback
+
+
+(Appears on:
+HelmReleaseSpec)
+
+Rollback holds the configuration for Helm rollback actions for this
+HelmRelease.
+
+ServerSideApplyMode
+(string alias)
+
+(Appears on:
+Rollback,
+Upgrade)
+
+ServerSideApplyMode defines the server-side apply mode for Helm upgrade and
+rollback actions.
+Snapshot
+
+Snapshot captures a point-in-time copy of the status information for a Helm release,
+as managed by the controller.
+
-Kustomize
-
-
-(Appears on:
-PostRenderer)
-
-Kustomize Helm PostRenderer specification.
-
-PostRenderer
-
-
-(Appears on:
-HelmReleaseSpec)
-
-PostRenderer contains a Helm PostRenderer specification.
-
-Remediation
-
-Remediation defines a consistent interface for InstallRemediation and
-UpgradeRemediation.
-RemediationStrategy
-(string alias)
+Snapshots
+([]*./api/v2.Snapshot alias)
(Appears on:
-UpgradeRemediation)
+HelmReleaseStatus)
-RemediationStrategy returns the strategy to use to remediate a failed install
-or upgrade.
-Rollback
+
Snapshots is a list of Snapshot objects.
+Strategy
+
+Strategy defines a consistent interface for InstallStrategy and
+UpgradeStrategy.
+Test
(Appears on:
-HelmReleaseSpec)
+HelmReleaseSpec)
-Rollback holds the configuration for Helm rollback actions for this
-HelmRelease.
+Test holds the configuration for Helm test actions for this HelmRelease.
-Test
+TestHookStatus
(Appears on:
-HelmReleaseSpec)
+Snapshot)
-Test holds the configuration for Helm test actions for this HelmRelease.
+TestHookStatus holds the status information for a test hook as observed
+to be run by the controller.
-Uninstall
+Uninstall
(Appears on:
-HelmReleaseSpec)
+HelmReleaseSpec)
Uninstall holds the configuration for Helm uninstall actions for this
HelmRelease.
@@ -1961,11 +3109,11 @@ a Helm uninstall is performed.
-Upgrade holds the configuration for Helm upgrade actions for this
HelmRelease.
@@ -1997,9 +3145,25 @@ Jobs for hooks) during the performance of a Helm upgrade action. Defaults to
UpgradeRemediation holds the configuration for Helm upgrade remediation.
@@ -2190,7 +3397,7 @@ no retries remain. Defaults to ‘false’ unless ‘Retries’
strategy
-
+
RemediationStrategy
@@ -2204,14 +3411,13 @@ RemediationStrategy
-ValuesReference
+UpgradeStrategy
(Appears on:
-HelmReleaseSpec)
+Upgrade)
-ValuesReference contains a reference to a resource containing Helm values,
-and optionally the key they can be found at.
+UpgradeStrategy holds the configuration for Helm upgrade strategy.
+WaitStrategy
+
+
+(Appears on:
+HelmReleaseSpec)
+
+WaitStrategy defines Helm’s wait strategy for waiting for applied
+resources to become ready.
+
+WaitStrategyName
+(string alias)
+
+(Appears on:
+WaitStrategy)
+
+WaitStrategyName is a strategy for waiting for resources to be ready.
This page was automatically generated with gen-crd-api-reference-docs
diff --git a/docs/spec/README.md b/docs/spec/README.md
index e79f55ff9..3c481b43d 100644
--- a/docs/spec/README.md
+++ b/docs/spec/README.md
@@ -35,6 +35,7 @@ actions that should be (conditionally) executed. Based on this the reconciler:
- performs a Helm install or upgrade action if needed
- performs a Helm test action if enabled
- performs a reconciliation strategy (rollback, uninstall) and retries as configured if any Helm action failed
+- performs in cluster drift detection and correction if enabled
The controller that runs these Helm actions relies on [source-controller](https://github.com/fluxcd/source-controller)
for providing the Helm charts from Helm repositories or any other source that source-controller
@@ -50,7 +51,7 @@ trigger a Helm uninstall.
Alerting can be configured with a Kubernetes custom resource that specifies a webhook address, and a
group of `HelmRelease` resources to be monitored using the [notification-controller](https://github.com/fluxcd/notification-controller).
-The API design of the controller can be found at [helm.toolkit.fluxcd.io/v2beta1](./v2beta1/helmreleases.md).
+The API design of the controller can be found at [helm.toolkit.fluxcd.io/v2](./v2/helmreleases.md).
## Backward compatibility
diff --git a/docs/spec/v2/README.md b/docs/spec/v2/README.md
new file mode 100644
index 000000000..826a91650
--- /dev/null
+++ b/docs/spec/v2/README.md
@@ -0,0 +1,16 @@
+# helm.toolkit.fluxcd.io/v2
+
+This is the v2 API specification for declaratively managing Helm chart
+releases with Kubernetes manifests.
+
+## Specification
+
+- [HelmRelease CRD](helmreleases.md)
+ + [Example](helmreleases.md#example)
+ + [Writing a HelmRelease spec](helmreleases.md#writing-a-helmrelease-spec)
+ + [Working with HelmReleases](helmreleases.md#working-with-helmreleases)
+ + [HelmRelease Status](helmreleases.md#helmrelease-status)
+
+## Implementation
+
+* [helm-controller](https://github.com/fluxcd/helm-controller/)
diff --git a/docs/spec/v2/helmreleases.md b/docs/spec/v2/helmreleases.md
new file mode 100644
index 000000000..2d95676c4
--- /dev/null
+++ b/docs/spec/v2/helmreleases.md
@@ -0,0 +1,2291 @@
+# Helm Releases
+
+
+
+The `HelmRelease` API allows for controller-driven reconciliation of Helm
+releases via Helm actions such as install, upgrade, test, uninstall, and
+rollback. In addition to this, it detects and corrects cluster state drift
+from the desired release state.
+
+## Example
+
+The following is an example of a HelmRelease which installs the
+[podinfo Helm chart](https://github.com/stefanprodan/podinfo/tree/master/charts/podinfo).
+
+```yaml
+---
+apiVersion: source.toolkit.fluxcd.io/v1
+kind: HelmRepository
+metadata:
+ name: podinfo
+ namespace: default
+spec:
+ interval: 15m
+ url: https://stefanprodan.github.io/podinfo
+---
+apiVersion: helm.toolkit.fluxcd.io/v2
+kind: HelmRelease
+metadata:
+ name: podinfo
+ namespace: default
+spec:
+ interval: 15m
+ timeout: 5m
+ chart:
+ spec:
+ chart: podinfo
+ version: '6.5.*'
+ sourceRef:
+ kind: HelmRepository
+ name: podinfo
+ interval: 5m
+ releaseName: podinfo
+ install:
+ remediation:
+ retries: 3
+ upgrade:
+ remediation:
+ retries: 3
+ test:
+ enable: true
+ driftDetection:
+ mode: enabled
+ ignore:
+ - paths: ["/spec/replicas"]
+ target:
+ kind: Deployment
+ values:
+ replicaCount: 2
+```
+
+In the above example:
+
+- A [HelmRepository](https://fluxcd.io/flux/components/source/helmrepositories/)
+ named `podinfo` is created, pointing to the Helm repository from which the
+ podinfo chart can be installed.
+- A HelmRelease named `podinfo` is created, that will create a [HelmChart](https://fluxcd.io/flux/components/source/helmcharts/) object
+ from [the `.spec.chart`](#chart-template) and watch it for Artifact changes.
+- The controller will fetch the chart from the HelmChart's Artifact and use it
+ together with the `.spec.releaseName` and `.spec.values` to confirm if the
+ Helm release exists and is up-to-date.
+- If the Helm release does not exist, is not up-to-date, or has not observed to
+ be made by the controller based on [the HelmRelease's history](#history), then
+ the controller will install or upgrade the release. If this fails, it is
+ allowed to retry the operation a number of times while requeueing between
+ attempts, as defined by the respective [remediation configurations](#configuring-failure-handling).
+- If the [Helm tests](#test-configuration) for the release have not been run
+ before for this release, the HelmRelease will run them.
+- When the Helm release in storage is up-to-date, the controller will check if
+ the release in the cluster has drifted from the desired state, as defined by
+ the [drift detection configuration](#drift-detection). If it has, the
+ controller will [correct the drift](#drift-correction) by re-applying the
+ desired state.
+- The controller will repeat the above steps at the interval defined by
+ `.spec.interval`, or when the configuration changes in a way that affects the
+ desired state of the Helm release (e.g. a new chart version or values).
+
+You can run this example by saving the manifest into `podinfo.yaml`.
+
+1. Apply the resource on the cluster:
+
+ ```sh
+ kubectl apply -f podinfo.yaml
+ ```
+
+2. Run `kubectl get helmrelease` to see the HelmRelease:
+
+ ```console
+ NAME AGE READY STATUS
+ podinfo 15s True Helm test succeeded for release default/podinfo.v1 with chart podinfo@6.5.3: 3 test hooks completed successfully
+ ```
+
+3. Run `kubectl describe helmrelease podinfo` to see the [Conditions](#conditions)
+ and [History](#history) in the HelmRelease's Status:
+
+ ```console
+ ...
+ Status:
+ Conditions:
+ Last Transition Time: 2023-12-04T14:17:47Z
+ Message: Helm test succeeded for release default/podinfo.v1 with chart podinfo@6.5.3: 3 test hooks completed successfully
+ Observed Generation: 1
+ Reason: TestSucceeded
+ Status: True
+ Type: Ready
+ Last Transition Time: 2023-12-04T14:17:39Z
+ Message: Helm install succeeded for release default/podinfo.v1 with chart podinfo@6.5.3
+ Observed Generation: 1
+ Reason: InstallSucceeded
+ Status: True
+ Type: Released
+ Last Transition Time: 2023-12-04T14:17:47Z
+ Message: Helm test succeeded for release default/podinfo.v1 with chart podinfo@6.5.3: 3 test hooks completed successfully
+ Observed Generation: 1
+ Reason: TestSucceeded
+ Status: True
+ Type: TestSuccess
+ Helm Chart: default/default-podinfo
+ History:
+ Chart Name: podinfo
+ Chart Version: 6.5.3
+ Config Digest: sha256:e15c415d62760896bd8bec192a44c5716dc224db9e0fc609b9ac14718f8f9e56
+ Digest: sha256:e59aeb8b854f42e44756c2ef552a073051f1fc4f90e68aacbae7f824139580bc
+ First Deployed: 2023-12-04T14:17:35Z
+ Last Deployed: 2023-12-04T14:17:35Z
+ Name: podinfo
+ Namespace: default
+ Status: deployed
+ Test Hooks:
+ Podinfo - Grpc - Test - Scyhk:
+ Last Completed: 2023-12-04T14:17:42Z
+ Last Started: 2023-12-04T14:17:39Z
+ Phase: Succeeded
+ Podinfo - Jwt - Test - Scddu:
+ Last Completed: 2023-12-04T14:17:45Z
+ Last Started: 2023-12-04T14:17:42Z
+ Phase: Succeeded
+ Podinfo - Service - Test - Uibss:
+ Last Completed: 2023-12-04T14:17:47Z
+ Last Started: 2023-12-04T14:17:45Z
+ Phase: Succeeded
+ Version: 1
+ Last Applied Revision: 6.5.3
+ Last Attempted Config Digest: sha256:e15c415d62760896bd8bec192a44c5716dc224db9e0fc609b9ac14718f8f9e56
+ Last Attempted Generation: 1
+ Last Attempted Release Action: install
+ Last Attempted Revision: 6.5.3
+ Observed Generation: 1
+ Storage Namespace: default
+ Events:
+ Type Reason Age From Message
+ ---- ------ ---- ---- -------
+ Normal HelmChartCreated 23s helm-controller Created HelmChart/default/default-podinfo with SourceRef 'HelmRepository/default/podinfo'
+ Normal HelmChartInSync 22s helm-controller HelmChart/default/default-podinfo with SourceRef 'HelmRepository/default/podinfo' is in-sync
+ Normal InstallSucceeded 18s helm-controller Helm install succeeded for release default/podinfo.v1 with chart podinfo@6.5.3
+ Normal TestSucceeded 10s helm-controller Helm test succeeded for release default/podinfo.v1 with chart podinfo@6.5.3: 3 test hooks completed successfully
+ ```
+
+## Writing a HelmRelease spec
+
+As with all other Kubernetes config, a HelmRelease needs `apiVersion`,
+`kind`, and `metadata` fields. The name of a HelmRelease object must be a
+valid [DNS subdomain name](https://kubernetes.io/docs/concepts/overview/working-with-objects/names#dns-subdomain-names).
+
+A HelmRelease also needs a
+[`.spec` section](https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#spec-and-status).
+
+### Chart template
+
+`.spec.chart` is an optional field used by the helm-controller as a template to
+create a new [HelmChart resource](https://fluxcd.io/flux/components/source/helmcharts/).
+
+The spec for the HelmChart is provided via `.spec.chart.spec`, refer to
+[writing a HelmChart spec](https://fluxcd.io/flux/components/source/helmcharts/#writing-a-helmchart-spec)
+for in-depth information.
+
+Annotations and labels can be added by configuring the respective
+`.spec.chart.metadata` fields.
+
+The HelmChart is created in the same namespace as the `.sourceRef`, with a name
+matching the HelmRelease's `<.metadata.namespace>-<.metadata.name>`, and will
+be reported in `.status.helmChart`.
+
+The chart version of the last release attempt is reported in
+`.status.lastAttemptedRevision`. The controller will automatically perform a
+Helm release when the HelmChart produces a new chart (version).
+
+**Warning:** Changing the `.spec.chart` to a Helm chart with a different name
+(as specified in the chart's `Chart.yaml`) will cause the controller to
+uninstall any previous release before installing the new one.
+
+**Note:** On multi-tenant clusters, platform admins can disable cross-namespace
+references with the `--no-cross-namespace-refs=true` flag. When this flag is
+set, the HelmRelease can only refer to Sources in the same namespace as the
+HelmRelease object.
+
+### Chart reference
+
+`.spec.chartRef` is an optional field used to refer to the Source object which has an
+Artifact containing the Helm chart. It has two required fields:
+
+- `kind`: The Kind of the referred Source object. Supported Source types:
+ + [OCIRepository](https://fluxcd.io/flux/components/source/ocirepositories/)
+ + [HelmChart](https://fluxcd.io/flux/components/source/helmcharts/)
+ + [ExternalArtifact](https://fluxcd.io/flux/components/source/externalartifacts/) (requires `--feature-gates=ExternalArtifact=true` flag)
+- `name`: The Name of the referred Source object.
+
+For a referenced resource of kind `OCIRepository`, the chart version of the last
+release attempt is reported in `.status.lastAttemptedRevision`. The version is in
+the format `+`. The digest of the OCI artifact is appended
+to the version to ensure that a change in the artifact content triggers a new release.
+The controller will automatically perform a Helm upgrade when the `OCIRepository`
+detects a new digest in the OCI artifact stored in registry, even if the version
+inside `Chart.yaml` is unchanged.
+
+**Note:** Disabling the appending of the digest to the chart version can be done
+with the `--feature-gates=DisableChartDigestTracking=true` controller flag.
+
+**Warning:** One of `.spec.chart` or `.spec.chartRef` must be set, but not both.
+When switching from `.spec.chart` to `.spec.chartRef`, the controller will perform
+an Helm upgrade and will garbage collect the old HelmChart object.
+
+**Note:** On multi-tenant clusters, platform admins can disable cross-namespace
+references with the `--no-cross-namespace-refs=true` controller flag. When this flag is
+set, the HelmRelease can only refer to OCIRepositories in the same namespace as the
+HelmRelease object.
+
+#### OCIRepository reference example
+
+```yaml
+apiVersion: source.toolkit.fluxcd.io/v1
+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: ">= 6.0.0"
+---
+apiVersion: helm.toolkit.fluxcd.io/v2
+kind: HelmRelease
+metadata:
+ name: podinfo
+ namespace: default
+spec:
+ interval: 10m
+ chartRef:
+ kind: OCIRepository
+ name: podinfo
+ namespace: default
+ values:
+ replicaCount: 2
+```
+
+#### HelmChart reference example
+
+```yaml
+apiVersion: source.toolkit.fluxcd.io/v1
+kind: HelmChart
+metadata:
+ name: podinfo
+ namespace: default
+spec:
+ interval: 10m
+ chart: podinfo
+ sourceRef:
+ kind: HelmRepository
+ name: podinfo
+ version: "6.x"
+ valuesFiles:
+ - values-prod.yaml
+---
+apiVersion: helm.toolkit.fluxcd.io/v2
+kind: HelmRelease
+metadata:
+ name: podinfo
+ namespace: default
+spec:
+ interval: 10m
+ chartRef:
+ kind: HelmChart
+ name: podinfo
+ namespace: default
+ values:
+ replicaCount: 2
+```
+
+### Release name
+
+`.spec.releaseName` is an optional field used to specify the name of the Helm
+release. It defaults to a composition of `[-]`.
+
+**Warning:** Changing the release name of a HelmRelease which has already been
+installed will not rename the release. Instead, the existing release will be
+uninstalled before installing a new release with the new name.
+
+**Note:** When the composition exceeds the maximum length of 53 characters, the
+name is shortened by hashing the release name with SHA-256. The resulting name
+is then composed of the first 40 characters of the release name, followed by a
+dash (`-`), followed by the first 12 characters of the hash. For example,
+`a-very-lengthy-target-namespace-with-a-nice-object-name` becomes
+`a-very-lengthy-target-namespace-with-a-nic-97af5d7f41f3`.
+
+### Target namespace
+
+`.spec.targetNamespace` is an optional field used to specify the namespace to
+which the Helm release is made. It defaults to the namespace of the
+HelmRelease.
+
+**Warning:** Changing the target namespace of a HelmRelease which has already
+been installed will not move the release to the new namespace. Instead, the
+existing release will be uninstalled before installing a new release in the new
+target namespace.
+
+### Storage namespace
+
+`.spec.storageNamespace` is an optional field used to specify the namespace
+in which Helm stores release information. It defaults to the namespace of the
+HelmRelease.
+
+**Warning:** Changing the storage namespace of a HelmRelease which has already
+been installed will not move the release to the new namespace. Instead, the
+existing release will be uninstalled before installing a new release in the new
+storage namespace.
+
+**Note:** When making use of the Helm CLI and attempting to make use of
+`helm get` commands to inspect a release, the `-n` flag should target the
+storage namespace of the HelmRelease.
+
+### Service Account reference
+
+`.spec.serviceAccountName` is an optional field used to specify the
+Service Account to be impersonated while reconciling the HelmRelease.
+For more information, refer to [Role-based access control](#role-based-access-control).
+
+### Persistent client
+
+`.spec.persistentClient` is an optional field to instruct the controller to use
+a persistent Kubernetes client for this release. If specified, the client will
+be reused for the duration of the reconciliation, instead of being created and
+destroyed for each (step of a) Helm action. If not set, it defaults to `true.`
+
+**Note:** This method generally boosts performance but could potentially cause
+complications with specific Helm charts. For instance, charts creating Custom
+Resource Definitions outside Helm's CRD lifecycle hooks during installation
+might face issues where these resources are not recognized as available,
+especially by post-install hooks.
+
+### Max history
+
+`.spec.maxHistory` is an optional field to configure the number of release
+revisions saved by Helm. If not set, it defaults to `5`.
+
+**Note:** Although setting this to `0` for an unlimited number of revisions is
+permissible, it is advised against due to performance reasons.
+
+### Dependencies
+
+`.spec.dependsOn` is an optional list to refer to other HelmRelease objects
+which the HelmRelease depends on. If specified, the HelmRelease is only allowed
+to proceed after the referred HelmReleases are ready, i.e. have the `Ready`
+condition marked as `True`.
+
+This is helpful when there is a need to make sure other resources exist before
+the workloads defined in a HelmRelease are released. For example, before
+installing objects of a certain Custom Resource kind, the Custom Resource
+Defintions and the related controller must exist in the cluster.
+
+```yaml
+---
+apiVersion: helm.toolkit.fluxcd.io/v2
+kind: HelmRelease
+metadata:
+ name: backend
+ namespace: default
+spec:
+ # ...omitted for brevity
+---
+apiVersion: helm.toolkit.fluxcd.io/v2
+kind: HelmRelease
+metadata:
+ name: frontend
+ namespace: default
+spec:
+ # ...omitted for brevity
+ dependsOn:
+ - name: backend
+```
+
+**Note:** Circular dependencies between HelmRelease resources must be avoided,
+otherwise the interdependent HelmRelease resources will never be reconciled.
+
+#### Dependency Ready Expression
+
+`.spec.dependsOn[].readyExpr` is an optional field that can be used to define a CEL expression
+to determine the readiness of a HelmRelease dependency.
+
+This is helpful for when custom logic is needed to determine if a dependency is ready.
+For example, when performing a lockstep upgrade, the `readyExpr` can be used to
+verify that a dependency has a matching version in values before proceeding with the
+reconciliation of the dependent HelmRelease.
+
+```yaml
+apiVersion: helm.toolkit.fluxcd.io/v2
+kind: HelmRelease
+metadata:
+ name: backend
+ namespace: default
+spec:
+ # ...omitted for brevity
+ values:
+ app:
+ version: v1.2.3
+---
+apiVersion: helm.toolkit.fluxcd.io/v2
+kind: HelmRelease
+metadata:
+ name: frontend
+ namespace: default
+spec:
+ # ...omitted for brevity
+ values:
+ app:
+ version: v1.2.3
+ dependsOn:
+ - name: backend
+ readyExpr: >
+ dep.spec.values.app.version == self.spec.values.app.version &&
+ dep.status.conditions.filter(e, e.type == 'Ready').all(e, e.status == 'True') &&
+ dep.metadata.generation == dep.status.observedGeneration
+```
+
+The CEL expression contains the following variables:
+
+- `dep`: The dependency HelmRelease object being evaluated.
+- `self`: The HelmRelease object being reconciled.
+
+**Note:** When `readyExpr` is specified, the built-in readiness check is replaced by the logic
+defined in the CEL expression. You can configure the controller to run both the CEL expression
+evaluation and the built-in readiness check, with the `AdditiveCELDependencyCheck`
+[feature gate](https://fluxcd.io/flux/components/helm/options/#feature-gates).
+
+### Values
+
+The values for the Helm release can be specified in two ways:
+
+- [Values references](#values-references)
+- [Inline values](#inline-values)
+
+Changes to the combined values will trigger a new Helm release.
+
+#### Values references
+
+`.spec.valuesFrom` is an optional list to refer to ConfigMap and Secret
+resources from which to take values. The values are merged in the order given,
+with the later values overwriting earlier, and then [inline values](#inline-values)
+overwriting those. When `targetPath` is set, it will overwrite everything before,
+including inline values.
+
+An item on the list offers the following subkeys:
+
+- `kind`: Kind of the values referent, supported values are `ConfigMap` and
+ `Secret`.
+- `name`: The `.metadata.name` of the values referent, in the same namespace as
+ the HelmRelease.
+- `valuesKey` (Optional): The `.data` key where the values.yaml or a specific
+ value can be found. Defaults to `values.yaml` when omitted.
+- `targetPath` (Optional): The YAML dot notation path at which the value should
+ be merged. When set, the valuesKey is expected to be a single flat value.
+ Defaults to empty when omitted, which results in the values getting merged at
+ the root.
+- `optional` (Optional): Whether this values reference is optional. When
+ `true`, a not found error for the values reference is ignored, but any
+ `valuesKey`, `targetPath` or transient error will still result in a
+ reconciliation failure. Defaults to `false` when omitted.
+
+```yaml
+spec:
+ valuesFrom:
+ - kind: ConfigMap
+ name: prod-env-values
+ valuesKey: values-prod.yaml
+ - kind: Secret
+ name: prod-tls-values
+ valuesKey: crt
+ targetPath: tls.crt
+ optional: true
+```
+
+**Note:** The `targetPath` supports the same formatting as you would supply as
+an argument to the `helm` binary using `--set [path]=[value]`. In addition to
+this, the referred value can contain the same value formats (e.g. `{a,b,c}` for
+a list). You can read more about the available formats and limitations in the
+[Helm documentation](https://helm.sh/docs/intro/using_helm/#the-format-and-limitations-of---set).
+
+For JSON strings, the [limitations are the same as while using `helm`](https://github.com/helm/helm/issues/5618)
+and require you to escape the full JSON string (including `=`, `[`, `,`, `.`).
+
+To make a HelmRelease react immediately to changes in the referenced Secret
+or ConfigMap see [this](#reacting-immediately-to-configuration-dependencies)
+section.
+
+#### Inline values
+
+`.spec.values` is an optional field to inline values within a HelmRelease. When
+[values references](#values-references) are defined, inline values are merged
+with the values from these references, overwriting any existing ones.
+
+```yaml
+spec:
+ values:
+ replicaCount: 2
+```
+
+### Install configuration
+
+`.spec.install` is an optional field to specify the configuration for the
+controller to use when running a [Helm install action](https://helm.sh/docs/helm/helm_install/).
+
+The field offers the following subfields:
+
+- `.timeout` (Optional): The time to wait for any individual Kubernetes
+ operation (like Jobs for hooks) during the installation of the chart.
+ Defaults to the [global timeout value](#timeout).
+- `.crds` (Optional): The Custom Resource Definition install policy to use.
+ Valid values are `Skip`, `Create` and `CreateReplace`. Default is `Create`,
+ which will create Custom Resource Definitions when they do not exist. Refer
+ to [Custom Resource Definition lifecycle](#controlling-the-lifecycle-of-custom-resource-definitions)
+ for more information.
+- `.replace` (Optional): Instructs Helm to re-use the [release name](#release-name),
+ but only if that name is a deleted release which remains in the history.
+ Defaults to `false`.
+- `.createNamespace` (Optional): Instructs Helm to create the [target namespace](#target-namespace)
+ if it does not exist. On uninstall, the created namespace will not be garbage
+ collected. Defaults to `false`.
+- `.disableHooks` (Optional): Prevents [chart hooks](https://helm.sh/docs/topics/charts_hooks/)
+ from running during the installation of the chart. Defaults to `false`.
+- `.disableOpenAPIValidation` (Optional): Prevents Helm from validating the
+ rendered templates against the Kubernetes OpenAPI Schema. Defaults to `false`.
+- `.disableSchemaValidation` (Optional): Prevents Helm from validating the
+ values against the JSON Schema. Defaults to `false`.
+- `.disableTakeOwnership` (Optional): Disables taking ownership of existing resources
+ during the Helm install action. Defaults to `false`.
+- `.disableWait` (Optional): Disables waiting for resources to be ready after
+ the installation of the chart. Defaults to `false`.
+- `.disableWaitForJobs` (Optional): Disables waiting for any Jobs to complete
+ after the installation of the chart. Defaults to `false`.
+- `.serverSideApply` (Optional): Enables Server-Side Apply for resources during
+ the installation. When `true`, the controller uses Kubernetes Server-Side
+ Apply which provides better conflict detection and field ownership tracking.
+ Defaults to `true` (or `false` when the `UseHelm3Defaults` feature gate is
+ enabled).
+
+#### Install strategy
+
+`.spec.install.strategy` is an optional field to specify the strategy
+to use when running a Helm install action.
+
+The field offers the following subfields:
+
+- `.name` (Required): The name of the install strategy to use. One of
+ `RemediateOnFailure` or `RetryOnFailure`.
+ If the `.spec.install.strategy` field is not specified, the HelmRelease
+ reconciliation behaves as if `.spec.install.strategy.name` was set to
+ `RemediateOnFailure`, or `RetryOnFailure` when the
+ `DefaultToRetryOnFailure` feature gate is enabled.
+- `.retryInterval` (Optional): The time to wait between retries of failed
+ releases when the install strategy is set to `RetryOnFailure`. Defaults
+ to `5m`. Cannot be used with `RemediateOnFailure`.
+
+The default `RemediateOnFailure` strategy applies the rules defined by the
+`.spec.install.remediation` field to the install action, i.e. the same
+behavior of the controller prior to the introduction of the `RetryOnFailure`
+strategy.
+
+The `RetryOnFailure` strategy will retry a failed install with an upgrade
+after the interval defined by the `.spec.install.strategy.retryInterval`
+field.
+
+#### Install remediation
+
+`.spec.install.remediation` is an optional field to configure the remediation
+strategy to use when the installation of a Helm chart fails.
+
+The field offers the following subfields:
+
+- `.retries` (Optional): The number of retries that should be attempted on
+ failures before bailing. Remediation, using an [uninstall](#uninstall-configuration),
+ is performed between each attempt. Defaults to `0`, a negative integer equals
+ to an infinite number of retries.
+- `.ignoreTestFailures` (Optional): Instructs the controller to not remediate
+ when a [Helm test](#test-configuration) failure occurs. Defaults to
+ `.spec.test.ignoreFailures`.
+- `.remediateLastFailure` (Optional): Instructs the controller to remediate the
+ last failure when no retries remain. Defaults to `false`.
+
+### Upgrade configuration
+
+`.spec.upgrade` is an optional field to specify the configuration for the
+controller to use when running a [Helm upgrade action](https://helm.sh/docs/helm/helm_upgrade/).
+
+The field offers the following subfields:
+
+- `.timeout` (Optional): The time to wait for any individual Kubernetes
+ operation (like Jobs for hooks) during the upgrade of the release.
+ Defaults to the [global timeout value](#timeout).
+- `.crds` (Optional): The Custom Resource Definition upgrade policy to use.
+ Valid values are `Skip`, `Create` and `CreateReplace`. Default is `Skip`.
+ Refer to [Custom Resource Definition lifecycle](#controlling-the-lifecycle-of-custom-resource-definitions)
+ for more information.
+- `.cleanupOnFail` (Optional): Allows deletion of new resources created during
+ the upgrade of the release when it fails. Defaults to `false`.
+- `.disableHooks` (Optional): Prevents [chart hooks](https://helm.sh/docs/topics/charts_hooks/)
+ from running during the upgrade of the release. Defaults to `false`.
+- `.disableOpenAPIValidation` (Optional): Prevents Helm from validating the
+ rendered templates against the Kubernetes OpenAPI Schema. Defaults to `false`.
+- `.disableSchemaValidation` (Optional): Prevents Helm from validating the
+ values against the JSON Schema. Defaults to `false`.
+- `.disableTakeOwnership` (Optional): Disables taking ownership of existing resources
+ during the Helm upgrade action. Defaults to `false`.
+- `.disableWait` (Optional): Disables waiting for resources to be ready after
+ upgrading the release. Defaults to `false`.
+- `.disableWaitForJobs` (Optional): Disables waiting for any Jobs to complete
+ after upgrading the release. Defaults to `false`.
+- `.force` (Optional): Forces resource updates through a replacement strategy.
+ Defaults to `false`.
+- `.preserveValues` (Optional): Instructs Helm to re-use the values from the
+ last release while merging in overrides from [values](#values). Setting
+ this flag makes the HelmRelease non-declarative. Defaults to `false`.
+- `.serverSideApply` (Optional): Controls Server-Side Apply for resources during
+ the upgrade. Can be `enabled`, `disabled`, or `auto`. When `auto`, the apply
+ method will be based on the release's previous usage. Defaults to `auto`.
+
+#### Upgrade strategy
+
+`.spec.upgrade.strategy` is an optional field to specify the strategy
+to use when running a Helm upgrade action.
+
+The field offers the following subfields:
+
+- `.name` (Required): The name of the upgrade strategy to use. One of
+ `RemediateOnFailure` or `RetryOnFailure`. If the `.spec.upgrade.strategy`
+ field is not specified, the HelmRelease reconciliation behaves as if
+ `.spec.upgrade.strategy.name` was set to `RemediateOnFailure`, or
+ `RetryOnFailure` when the `DefaultToRetryOnFailure` feature gate is
+ enabled.
+- `.retryInterval` (Optional): The time to wait between retries of failed
+ releases when the upgrade strategy is set to `RetryOnFailure`. Defaults
+ to `5m`. Cannot be used with `RemediateOnFailure`.
+
+The default `RemediateOnFailure` strategy applies the rules defined by the
+`.spec.upgrade.remediation` field to the upgrade action, i.e. the same
+behavior of the controller prior to the introduction of the `RetryOnFailure`
+strategy.
+
+The `RetryOnFailure` strategy will retry failed upgrades in a regular
+interval defined by the `.spec.upgrade.strategy.retryInterval` field,
+without applying any remediation.
+
+#### Upgrade remediation
+
+`.spec.upgrade.remediation` is an optional field to configure the remediation
+strategy to use when the upgrade of a Helm release fails.
+
+The field offers the following subfields:
+
+- `.retries` (Optional): The number of retries that should be attempted on
+ failures before bailing. Remediation, using the `.strategy`, is performed
+ between each attempt. Defaults to `0`, a negative integer equals to an
+ infinite number of retries.
+- `.strategy` (Optional): The remediation strategy to use when a Helm upgrade
+ fails. Valid values are `rollback` and `uninstall`. Defaults to `rollback`.
+ After an `uninstall` remediation, the controller will attempt to reinstall
+ the release.
+- `.ignoreTestFailures` (Optional): Instructs the controller to not remediate
+ when a [Helm test](#test-configuration) failure occurs. Defaults to
+ `.spec.test.ignoreFailures`.
+- `.remediateLastFailure` (Optional): Instructs the controller to remediate the
+ last failure when no retries remain. Defaults to `false` unless `.retries` is
+ greater than `0`.
+
+### Test configuration
+
+`.spec.test` is an optional field to specify the configuration values for the
+[Helm test action](https://helm.sh/docs/helm/helm_test/).
+
+To make the controller run the [Helm tests available for the chart](https://helm.sh/docs/topics/chart_tests/)
+after a successful Helm install or upgrade, `.spec.test.enable` can be set to
+`true`. When enabled, the test results will be available in the
+[`.status.history`](#history) field and emitted as a Kubernetes Event.
+
+By default, when tests are enabled, failures in tests are considered release
+failures, and thus are subject to the triggering Helm action's remediation
+configuration. However, test failures can be ignored by setting
+`.spec.test.ignoreFailures` to `true`. In this case, no remediation action
+will be taken, and the test failure will not affect the `Ready` status
+condition. This can be overridden per Helm action by setting the respective
+[install](#install-configuration) or [upgrade](#upgrade-configuration)
+configuration option.
+
+```yaml
+spec:
+ test:
+ enable: true
+ ignoreFailures: true
+```
+
+#### Filtering tests
+
+`.spec.test.filters` is an optional list to include or exclude specific tests
+from being run.
+
+```yaml
+spec:
+ test:
+ enable: true
+ filters:
+ - name: my-release-test-connection
+ exclude: false
+ - name: my-release-test-migration
+ exclude: true
+```
+
+### Rollback configuration
+
+`.spec.rollback` is an optional field to specify the configuration values for
+a [Helm rollback action](https://helm.sh/docs/helm/helm_rollback/). This
+configuration applies when the [upgrade remediation strategy](#upgrade-remediation)
+is set to `rollback`.
+
+The field offers the following subfields:
+
+- `.timeout` (Optional): The time to wait for any individual Kubernetes
+ operation (like Jobs for hooks) during the rollback of the release.
+ Defaults to the [global timeout value](#timeout).
+- `.cleanupOnFail` (Optional): Allows deletion of new resources created during
+ the rollback of the release when it fails. Defaults to `false`.
+- `.disableHooks` (Optional): Prevents [chart hooks](https://helm.sh/docs/topics/charts_hooks/)
+ from running during the rollback of the release. Defaults to `false`.
+- `.disableWait` (Optional): Disables waiting for resources to be ready after
+ rolling back the release. Defaults to `false`.
+- `.disableWaitForJobs` (Optional): Disables waiting for any Jobs to complete
+ after rolling back the release. Defaults to `false`.
+- `.force` (Optional): Forces resource updates through a replacement strategy.
+ Defaults to `false`.
+- `.recreate` (Optional): Performs Pod restarts if applicable. Defaults to
+ `false`. **Warning**: As of Flux v2.8, this option is deprecated and no
+ longer has any effect. It will be removed in a future release. The
+ helm-controller will print a warning if this option is used. Please
+ see the [Helm 4 issue](https://github.com/fluxcd/helm-controller/issues/1300#issuecomment-3740272924)
+ for more details.
+- `.serverSideApply` (Optional): Controls Server-Side Apply for resources during
+ the rollback. Can be `enabled`, `disabled`, or `auto`. When `auto`, the apply
+ method will be based on the release's previous usage. Defaults to `auto`.
+
+### Uninstall configuration
+
+`.spec.uninstall` is an optional field to specify the configuration values for
+a [Helm uninstall action](https://helm.sh/docs/helm/helm_uninstall/). This
+configuration applies to the [install remediation](#install-remediation), and
+when the [upgrade remediation strategy](#upgrade-remediation) is set to
+`uninstall`.
+
+The field offers the following subfields:
+
+- `.timeout` (Optional): The time to wait for any individual Kubernetes
+ operation (like Jobs for hooks) during the uninstalltion of the release.
+ Defaults to the [global timeout value](#timeout).
+- `.deletionPropagation` (Optional): The [deletion propagation policy](https://kubernetes.io/docs/tasks/administer-cluster/use-cascading-deletion/)
+ when a Helm uninstall is performed. Valid values are `background`,
+ `foreground` and `orphan`. Defaults to `background`.
+- `.disableHooks` (Optional): Prevents [chart hooks](https://helm.sh/docs/topics/charts_hooks/)
+ from running during the uninstallation of the release. Defaults to `false`.
+- `.disableWait` (Optional): Disables waiting for resources to be deleted after
+ uninstalling the release. Defaults to `false`.
+- `.keepHistory` (Optional): Instructs Helm to remove all associated resources
+ and mark the release as deleted, but to retain the release history. Defaults
+ to `false`.
+
+### Drift detection
+
+`.spec.driftDetection` is an optional field to enable the detection (and
+correction) of cluster-state drift compared to the manifest from the Helm
+storage.
+
+When `.spec.driftDetection.mode` is set to `warn` or `enabled`, and the
+desired state of the HelmRelease is in-sync with the Helm release object in
+the storage, the controller will compare the manifest from the Helm storage
+with the current state of the cluster using a
+[server-side dry-run apply](https://kubernetes.io/docs/reference/using-api/server-side-apply/).
+
+If this comparison detects a drift (either due to a resource being created
+or modified during the dry-run), the controller will emit a Kubernetes Event
+with a short summary of the detected changes. In addition, a more extensive
+[JSON Patch](https://datatracker.ietf.org/doc/html/rfc6902) summary is logged
+to the controller logs (with `--log-level=debug`).
+
+#### Drift correction
+
+Furthermore, when `.spec.driftDetection.mode` is set to `enabled`, the
+controller will attempt to correct the drift by creating and patching the
+resources based on the server-side dry-run apply result.
+
+At the end of the correction attempt, it will emit a Kubernetes Event with a
+summary of the changes it made and any failures it encountered. In case of a
+failure, it will continue to detect and correct drift until the desired state
+has been reached, or a new Helm action is triggered (due to e.g. a change to
+the spec).
+
+#### Ignore rules
+
+`.spec.driftDetection.ignore` is an optional field to provide
+[JSON Pointers](https://datatracker.ietf.org/doc/html/rfc6901) to ignore while
+detecting and correcting drift. This can for example be useful when Horizontal
+Pod Autoscaling is enabled for a Deployment, or when a Helm chart has hooks
+which mutate a resource.
+
+```yaml
+spec:
+ driftDetection:
+ mode: enabled
+ ignore:
+ - paths: ["/spec/replicas"]
+```
+
+**Note:** It is possible to achieve a likewise behavior as using
+[ignore annotations](#ignore-annotation) by configuring a JSON Pointer
+targeting a whole document (`""`).
+
+To ignore `.paths` in a specific target resource, a `.target` selector can be
+applied to the ignored paths.
+
+```yaml
+spec:
+ driftDetection:
+ ignore:
+ - paths: ["/spec/replicas"]
+ target:
+ kind: Deployment
+```
+
+The following `.target` selectors are available, defining multiple fields
+causes the selector to be more specific:
+
+- `group` (Optional): Matches the `.apiVersion` group of resources while
+ offering support for regular expressions. For example, `apps`,
+ `helm.toolkit.fluxcd.io` or `.*.toolkit.fluxcd.io`.
+- `version` (Optional): Matches the `.apiVersion` version of resources while
+ offering support for regular expressions. For example, `v1`, `v2beta2` or
+ `v2beta[\d]`.
+- `kind` (Optional): Matches the `.kind` of resources while offering support
+ for regular expressions. For example, `Deployment`, `HelmRelelease` or
+ `(HelmRelease|HelmChart)`.
+- `name` (Optional): Matches the `.metadata.name` of resources while offering
+ support for regular expressions. For example, `podinfo` or `podinfo.*`.
+- `namespace` (Optional): Matches the `.metadata.namespace` of resources while
+ offering support for regular expressions. For example, `my-release-ns` or
+ `.*-system`.
+- `annotationSelector` (Optional): Matches the `.metadata.annotations` of
+ resources using a [label selector expression](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#label-selectors).
+ For example, `environment = production` or `environment notin (staging)`.
+- `labelSelector` (Optional): Matches the `.metadata.labels` of resources
+ using a [label selector expression](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#label-selectors).
+ For example, `environment = production` or `environment notin (staging)`.
+
+#### Ignore annotation
+
+To exclude certain resources from the comparison, they can be labeled or
+annotated with `helm.toolkit.fluxcd.io/driftDetection: disabled`. Using
+[post-renderers](#post-renderers), this can be applied to any resource
+rendered by Helm.
+
+```yaml
+spec:
+ postRenderers:
+ - kustomize:
+ patches:
+ - target:
+ version: v1
+ kind: Deployment
+ name: my-app
+ patch: |
+ - op: add
+ path: /metadata/annotations/helm.toolkit.fluxcd.io~1driftDetection
+ value: disabled
+```
+
+**Note:** In many cases, it may be better (and easier) to configure an [ignore
+rule](#ignore-rules) to ignore (a portion of) a resource.
+
+### Common metadata
+
+`.spec.commonMetadata` is an optional field used to specify any metadata that
+should be applied to all the Helm Chart's resources via kustomize post renderer. It has two optional fields:
+
+- `labels`: A map used for setting [labels](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/)
+ on an object. Any existing label will be overridden if it matches with a key in
+ this map.
+- `annotations`: A map used for setting [annotations](https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/)
+ on an object. Any existing annotation will be overridden if it matches with a key
+ in this map.
+
+### Post renderers
+
+`.spec.postRenderers` is an optional list to provide [post rendering](https://helm.sh/docs/topics/advanced/#post-rendering)
+capabilities using the following built-in Kustomize directives:
+
+- [patches](https://kubectl.docs.kubernetes.io/references/kustomize/kustomization/patches/) (`kustomize.patches`)
+- [images](https://kubectl.docs.kubernetes.io/references/kustomize/kustomization/images/) (`kustomize.images`)
+
+Post renderers are applied in the order given, and persisted by Helm to the
+manifest for the release in the storage.
+
+**Note:** [Helm has a limitation at present](https://github.com/helm/helm/issues/7891),
+which prevents post renderers from being applied to chart hooks.
+
+```yaml
+spec:
+ postRenderers:
+ - kustomize:
+ patches:
+ - target:
+ version: v1
+ kind: Deployment
+ name: metrics-server
+ patch: |
+ - op: add
+ path: /metadata/labels/environment
+ value: production
+ images:
+ - name: docker.io/bitnami/metrics-server
+ newName: docker.io/bitnami/metrics-server
+ newTag: 0.4.1-debian-10-r54
+```
+
+### Wait strategy
+
+`.spec.waitStrategy` is an optional field to configure how the controller waits
+for resources to become ready after Helm actions.
+
+The field offers the following subfields:
+
+- `.name` (Required): The strategy for waiting for resources to be ready.
+ One of `poller` or `legacy`. The `poller` strategy uses kstatus to poll resource
+ statuses, while the `legacy` strategy uses Helm v3's waiting logic. Defaults to
+ `poller`, or to `legacy` when the `UseHelm3Defaults` feature gate is enabled.
+
+```yaml
+spec:
+ waitStrategy:
+ name: poller
+```
+
+### Health check expressions
+
+`.spec.healthCheckExprs` can be used to define custom logic for performing health
+checks on custom resources using [Common Expression Language (CEL)](https://cel.dev/).
+
+The expressions are evaluated only when the Helm action taking place has wait
+enabled (i.e. `.spec..disableWait` is `false`) and the `poller`
+wait strategy is used (i.e. `.spec.waitStrategy.name` is `poller`).
+
+The `.spec.healthCheckExprs` field accepts a list of objects with the following fields:
+
+- `apiVersion`: The API version of the custom resource. Required.
+- `kind`: The kind of the custom resource. Required.
+- `current`: A required CEL expression that returns `true` if the resource is ready.
+- `inProgress`: An optional CEL expression that returns `true` if the resource
+ is still being reconciled.
+- `failed`: An optional CEL expression that returns `true` if the resource
+ failed to reconcile.
+
+The controller will evaluate the expressions in the following order:
+
+1. `inProgress` if specified
+2. `failed` if specified
+3. `current`
+
+The first expression that evaluates to `true` will determine the health
+status of the custom resource.
+
+For example, to define a set of health check expressions for the `SealedSecret`
+custom resource:
+
+```yaml
+apiVersion: helm.toolkit.fluxcd.io/v2
+kind: HelmRelease
+metadata:
+ name: sealed-secrets
+spec:
+ interval: 10m
+ chartRef:
+ kind: OCIRepository
+ name: sealed-secrets-chart
+ values:
+ replicaCount: 2
+ healthCheckExprs:
+ - apiVersion: bitnami.com/v1alpha1
+ kind: SealedSecret
+ failed: status.conditions.filter(e, e.type == 'Synced').all(e, e.status == 'False')
+ current: status.conditions.filter(e, e.type == 'Synced').all(e, e.status == 'True')
+```
+
+A common error is writing expressions that reference fields that do not
+exist in the custom resource. This will cause the controller to wait
+for the resource to be ready until the timeout is reached. To avoid this,
+make sure your CEL expressions are correct. The
+[CEL Playground](https://playcel.undistro.io/) is a useful resource for
+this task. The input passed to each expression is the custom resource
+object itself. You can check for field existence with the
+[`has(...)` CEL macro](https://github.com/google/cel-spec/blob/master/doc/langdef.md#macros),
+just be aware that `has(status)` errors if `status` does not (yet) exist
+on the top level of the resource you are using.
+
+It's worth checking if [the library](/flux/cheatsheets/cel-healthchecks/)
+has expressions for the custom resources you are using.
+
+### KubeConfig (Remote clusters)
+
+With the `.spec.kubeConfig` field a HelmRelease
+can apply and manage resources on a remote cluster.
+
+Two authentication alternatives are available:
+
+- `.spec.kubeConfig.secretRef`: Secret-based authentication using a
+ static kubeconfig stored in a Kubernetes Secret in the same namespace
+ as the HelmRelease.
+- `.spec.kubeConfig.configMapRef` (Recommended): Secret-less authentication
+ building a kubeconfig dynamically with parameters stored in a Kubernetes
+ ConfigMap in the same namespace as the HelmRelease via workload identity.
+
+To make a HelmRelease react immediately to changes in the referenced Secret
+or ConfigMap see [this](#reacting-immediately-to-configuration-dependencies)
+section.
+
+When both `.spec.kubeConfig` and
+[`.spec.serviceAccountName`](#service-account-reference) are specified,
+the controller will impersonate the ServiceAccount on the target cluster,
+i.e. a ServiceAccount with name `.spec.serviceAccountName` must exist in
+the target cluster inside a namespace with the same name as the namespace
+of the HelmRelease. For example, if the HelmRelease is in the namespace
+`apps` of the cluster where Flux is running, then the ServiceAccount
+must be in the `apps` namespace of the target remote cluster, and have the
+name `.spec.serviceAccountName`. In other words, the namespace of the
+HelmRelease must exist both in the cluster where Flux is running
+and in the target remote cluster where Flux will apply resources.
+
+The Helm storage is stored on the remote cluster in a namespace that equals to
+the namespace of the HelmRelease, or the [configured storage namespace](#storage-namespace).
+The release itself is made in a namespace that equals to the namespace of the
+HelmRelease, or the [configured target namespace](#target-namespace). The
+namespaces are expected to exist, with the exception that the target namespace
+can be created on demand by Helm when namespace creation is [configured during
+install](#install-configuration).
+
+Other references to Kubernetes resources in the HelmRelease, like
+[values references](#values-references), are expected to exist on
+the cluster where Flux is running.
+
+#### Secret-based authentication
+
+`.spec.kubeConfig.secretRef.name` is an optional field to specify the name of
+a Secret containing a KubeConfig. If specified, the Helm operations will be
+targeted at the default cluster specified in this KubeConfig instead of using
+the in-cluster Service Account.
+
+The Secret defined in the `.secretRef` must exist in the same namespace as the
+HelmRelease. On every reconciliation, the KubeConfig bytes will be loaded from
+the `.secretRef.key` (default: `value` or `value.yaml`) of the Secret's data,
+and the Secret can thus be regularly updated if cluster access tokens have to
+rotate due to expiration.
+
+```yaml
+---
+apiVersion: v1
+kind: Secret
+metadata:
+ name: prod-kubeconfig
+type: Opaque
+stringData:
+ value.yaml: |
+ apiVersion: v1
+ kind: Config
+ # ...omitted for brevity
+```
+
+**Note:** The KubeConfig should be self-contained and not rely on binaries, the
+environment, or credential files from the helm-controller Pod. This matches the
+constraints of KubeConfigs from current Cluster API providers. KubeConfigs with
+`cmd-path` in them likely won't work without a custom, per-provider installation
+of helm-controller. For more information, see
+[remote clusters/Cluster-API](#remote-cluster-api-clusters).
+
+#### Secret-less authentication
+
+The field `.spec.kubeConfig.configMapRef.name` can be used to specify the
+name of a ConfigMap in the same namespace as the HelmRelease containing
+parameters for secret-less authentication via workload identity. The
+supported keys inside the `.data` field of the ConfigMap are:
+
+- `.data.provider`: The provider to use. One of `aws`, `azure`, `gcp`,
+ or `generic`. Required. The `aws` provider is used for connecting to
+ remote EKS clusters, `azure` for AKS, `gcp` for GKE, and `generic`
+ for Kubernetes OIDC authentication between clusters. For the
+ `generic` provider, the remote cluster must be configured to trust
+ the OIDC issuer of the cluster where Flux is running.
+- `.data.cluster`: The fully qualified resource name of the Kubernetes
+ cluster in the cloud provider API. Not used by the `generic`
+ provider. Required when one of `.data.address` or `.data["ca.crt"]` is
+ not set, or if the provider is `aws` (required for defining a region).
+- `.data.address`: The address of the Kubernetes API server. Required
+ for `generic`. For the other providers, if not specified, the
+ first address in the cluster resource will be used, and if
+ specified, it must match one of the addresses in the cluster
+ resource.
+ If `audiences` is not set, will be used as the audience for the
+ `generic` provider.
+- `.data["ca.crt"]`: The optional PEM-encoded CA certificate for the
+ Kubernetes API server. If not set, the controller will use the
+ CA certificate from the cluster resource.
+- `.data.audiences`: The optional audiences as a list of
+ line-break-separated strings for the Kubernetes ServiceAccount token.
+ Defaults to the address for the `generic` provider, or to specific
+ values for the other providers depending on the provider.
+- `.data.serviceAccountName`: The optional name of the Kubernetes
+ ServiceAccount in the same namespace that should be used
+ for authentication. If not specified, the controller
+ ServiceAccount will be used. Not confuse with the ServiceAccount
+ used for impersonation, which is specified with
+ [`.spec.serviceAccountName`](#service-account-reference) directly
+ in the HelmRelease spec and must exist in the target remote cluster.
+
+The `.data.cluster` field, when specified, must have the following formats:
+
+- `aws`: `arn::eks:::cluster/`
+- `azure`: `/subscriptions//resourceGroups//providers/Microsoft.ContainerService/managedClusters/`
+- `gcp`: `projects//locations//clusters/`
+
+For complete guides on workload identity and setting up permissions for
+this feature, see the following docs:
+
+- [EKS](/flux/integrations/aws/#for-amazon-elastic-kubernetes-service)
+- [AKS](/flux/integrations/azure/#for-azure-kubernetes-service)
+- [GKE](/flux/integrations/gcp/#for-google-kubernetes-engine)
+- [Generic](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#configuring-the-api-server)
+
+Example for an EKS cluster:
+
+```yaml
+apiVersion: helm.toolkit.fluxcd.io/v1
+kind: HelmRelease
+metadata:
+ name: backend
+ namespace: apps
+spec:
+ ... # other fields omitted for brevity
+ kubeConfig:
+ configMapRef:
+ name: kubeconfig
+ serviceAccountName: apps-sa # optional. must exist in the target cluster. user for impersonation
+---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: kubeconfig
+ namespace: apps
+data:
+ provider: aws
+ cluster: arn:aws:eks:eu-central-1:123456789012:cluster/my-cluster
+ serviceAccountName: apps-iam-role # optional. maps to an AWS IAM Role. used for authentication
+```
+
+### Interval
+
+`.spec.interval` is a required field that specifies the interval at which the
+HelmRelease is reconciled, i.e. the controller ensures the current Helm release
+matches the desired state.
+
+After successfully reconciling the object, the controller requeues it for
+inspection at the specified interval. The value must be in a [Go recognized
+duration string format](https://pkg.go.dev/time#ParseDuration), e.g. `15m0s`
+to reconcile the object every fifteen minutes.
+
+If the `.metadata.generation` of a resource changes (due to e.g. a change to
+the spec) or the HelmChart revision changes (which generates a Kubernetes
+Event), or a ConfigMap/Secret referenced in `valuesFrom` changes,
+this is handled instantly outside the interval window.
+
+**Note:** The controller can be configured to apply a jitter to the interval in
+order to distribute the load more evenly when multiple HelmRelease objects are
+set up with the same interval. For more information, please refer to the
+[helm-controller configuration options](https://fluxcd.io/flux/components/helm/options/).
+
+### Timeout
+
+`.spec.timeout` is an optional field to specify a timeout for a Helm action like
+install, upgrade or rollback. The value must be in a
+[Go recognized duration string format](https://pkg.go.dev/time#ParseDuration),
+e.g. `5m30s` for a timeout of five minutes and thirty seconds. The default
+value is `5m0s`.
+
+### Suspend
+
+`.spec.suspend` is an optional field to suspend the reconciliation of a
+HelmRelease. When set to `true`, the controller will stop reconciling the
+HelmRelease, and changes to the resource or the Helm chart will not result in
+a new Helm release. When the field is set to `false` or removed, it will
+resume.
+
+## Working with HelmReleases
+
+### Recommended settings
+
+When deploying applications to production environments, it is recommended
+to use OCI-based Helm charts with OCIRepository as `chartRef`, and
+to configure the following fields, while adjusting them to your desires for
+responsiveness:
+
+```yaml
+apiVersion: source.toolkit.fluxcd.io/v1
+kind: OCIRepository
+metadata:
+ name: webapp-chart
+ namespace: apps
+spec:
+ interval: 5m # check for new versions every 5 minutes and trigger an upgrade
+ url: oci://ghcr.io/org/charts/webapp
+ secretRef:
+ name: registry-auth # Image pull secret with read-only access
+ layerSelector: # select the Helm chart layer
+ mediaType: "application/vnd.cncf.helm.chart.content.v1.tar+gzip"
+ operation: copy
+ ref:
+ semver: "*" # track the latest stable version
+---
+apiVersion: helm.toolkit.fluxcd.io/v2
+kind: HelmRelease
+metadata:
+ name: webapp
+ namespace: apps
+spec:
+ releaseName: webapp
+ chartRef:
+ kind: OCIRepository
+ name: webapp-chart
+ interval: 30m # run drift detection every 30 minutes
+ driftDetection:
+ mode: enabled # undo kubectl edits and other unintended changes
+ install:
+ strategy:
+ name: RetryOnFailure # retry failed installations instead of uninstalling
+ retryInterval: 5m # retry failed installations every five minutes
+ upgrade:
+ crds: CreateReplace # update CRDs when upgrading
+ strategy:
+ name: RetryOnFailure # retry failed upgrades instead of rollback
+ retryInterval: 5m # retry failed upgrades every five minutes
+ # All ConfigMaps and Secrets referenced in valuesFrom should
+ # be labelled with `reconcile.fluxcd.io/watch: Enabled`
+ valuesFrom:
+ - kind: ConfigMap
+ name: webapp-values
+ - kind: Secret
+ name: webapp-secret-values
+```
+
+Note that the `RetryOnFailure` strategy is suitable for statefulsets
+and other workloads that cannot tolerate rollbacks and have a high rollout duration
+susceptible to health check timeouts and transient capacity errors.
+
+For stateless workloads and applications that can tolerate rollbacks, the
+`RemediateOnFailure` strategy may be more suitable, as it will ensure that
+the last known good state is restored in case of a failure.
+
+### Configuring failure handling
+
+From time to time, a Helm installation, upgrade, or accompanying [Helm test](#test-configuration)
+may fail. When this happens, by default no action is taken, and the release is
+left in a failed state. However, several automatic failure remediation options
+can be set via [`.spec.install.remediation`](#install-remediation) and
+[`.spec.upgrade.remediation`](#upgrade-remediation).
+
+By configuring the `.retries` field for the respective action, the controller
+will first remediate the failure by performing a Helm rollback or uninstall, and
+then reattempt the action. It will repeat this process until the `.retries`
+are exhausted, or the action succeeds.
+
+Once the `.retries` are exhausted, the controller will stop attempting to
+remediate the failure, and the Helm release will be left in a failed state.
+To ensure the Helm release is brought back to the last known good state or
+uninstalled, `.remediateLastFailure` can be set to `true`.
+For Helm upgrades, this defaults to `true` if at least one retry is configured.
+
+When a new release configuration or Helm chart is detected, the controller will
+reset the failure counters and attempt to install or upgrade the release again.
+
+**Note:** In addition to the automatic failure remediation options, the
+controller can be instructed to [force a Helm release](#forcing-a-release) or
+to [retry a failed Helm release](#resetting-remediation-retries)
+
+### Controlling the lifecycle of Custom Resource Definitions
+
+Helm does support [the installation of Custom Resource Definitions](https://helm.sh/docs/chart_best_practices/custom_resource_definitions/#method-1-let-helm-do-it-for-you)
+(CRDs) as part of a chart. However, it has no native support for
+[upgrading CRDs](https://helm.sh/docs/chart_best_practices/custom_resource_definitions/#some-caveats-and-explanations):
+
+> There is no support at this time for upgrading or deleting CRDs using Helm.
+> This was an explicit decision after much community discussion due to the
+> danger for unintentional data loss. Furthermore, there is currently no
+> community consensus around how to handle CRDs and their lifecycle. As this
+> evolves, Helm will add support for those use cases.
+
+If you write your own Helm charts, you can work around this limitation by
+putting your CRDs into the templates instead of the `crds/` directory, or by
+factoring them out into a separate Helm chart as suggested by the [official Helm
+documentation](https://helm.sh/docs/chart_best_practices/custom_resource_definitions/#method-2-separate-charts).
+
+However, if you use a third-party Helm chart that installs CRDs, not being able
+to upgrade the CRDs via HelmRelease objects might become a cumbersome limitation
+within your GitOps workflow. Therefore, Flux allows you to opt in to upgrading
+CRDs by setting the `.crds` policy in the [`.spec.install`](#install-configuration)
+and [`.spec.upgrade`](#upgrade-configuration) configurations.
+
+The following policy values are supported:
+
+- `Skip`: Skip the installation or upgrade of CRDs. This is the default value
+ for `.spec.upgrade.crds`.
+- `Create`: Create CRDs if they do not exist, but do not upgrade or delete them.
+ This is the default value for `.spec.install.crds`.
+- `CreateReplace`: Create new CRDs, update (replace) existing ones, but **do
+ not** delete CRDs which no longer exist in the current Helm chart.
+
+For example, if you want to update CRDs when installing and upgrading a Helm
+chart, you can set the `.spec.install.crds` and `.spec.upgrade.crds` policies to
+`CreateReplace`:
+
+```yaml
+---
+apiVersion: helm.toolkit.fluxcd.io/v2
+kind: HelmRelease
+metadata:
+ name: my-operator
+ namespace: default
+spec:
+ interval: 15m
+ chart:
+ spec:
+ chart: my-operator
+ version: "1.0.1"
+ sourceRef:
+ kind: HelmRepository
+ name: my-operator-repo
+ interval: 5m
+ install:
+ crds: CreateReplace
+ upgrade:
+ crds: CreateReplace
+```
+
+### Role-based access control
+
+By default, a HelmRelease runs under the cluster admin account and can create,
+modify, and delete cluster level objects (ClusterRoles, ClusterRoleBindings,
+CustomResourceDefinitions, etc.) and namespaced objects (Deployments, Ingresses,
+etc.)
+
+For certain HelmReleases, a cluster administrator may wish to restrict the
+permissions of the HelmRelease to a specific namespace or to a specific set of
+namespaced objects. To restrict a HelmRelease, one can assign a Service Account
+under which the reconciliation is performed using
+[`.spec.serviceAccountName`](#service-account-reference).
+
+Assuming you want to restrict a group of HelmReleases to a single namespace,
+you can create a Service Account with a RoleBinding that grants access only to
+that namespace.
+
+For example, the following Service Account and RoleBinding restricts the
+HelmRelease to the `webapp` namespace:
+
+```yaml
+---
+apiVersion: v1
+kind: Namespace
+metadata:
+ name: webapp
+---
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ name: webapp-reconciler
+ namespace: webapp
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
+metadata:
+ name: webapp-reconciler
+ namespace: webapp
+rules:
+ - apiGroups: ["*"]
+ resources: ["*"]
+ verbs: ["*"]
+---
+ apiVersion: rbac.authorization.k8s.io/v1
+ kind: RoleBinding
+ metadata:
+ name: webapp-reconciler
+ namespace: webapp
+ roleRef:
+ apiGroup: rbac.authorization.k8s.io
+ kind: Role
+ name: webapp-reconciler
+ subjects:
+ - kind: ServiceAccount
+ name: webapp-reconciler
+ namespace: webapp
+```
+
+**Note:** The above resources are not created by the helm-controller, but should
+be created by a cluster administrator and preferably be managed by a
+[Kustomization](https://fluxcd.io/flux/components/kustomize/kustomizations/).
+
+The Service Account can then be referenced in the HelmRelease:
+
+```yaml
+---
+apiVersion: helm.toolkit.fluxcd.io/v2
+kind: HelmRelease
+metadata:
+ name: podinfo
+ namespace: webapp
+spec:
+ serviceAccountName: webapp-reconciler
+ interval: 15m
+ chart:
+ spec:
+ chart: podinfo
+ sourceRef:
+ kind: HelmRepository
+ name: podinfo
+```
+
+When the controller reconciles the `podinfo` HelmRelease, it will impersonate
+the `webapp-reconciler` Service Account. If the chart contains cluster level
+objects like CustomResourceDefinitions, the reconciliation will fail since the
+account it runs under has no permissions to alter objects outside the
+`webapp` namespace.
+
+#### Enforcing impersonation
+
+On multi-tenant clusters, platform admins can enforce impersonation with the
+`--default-service-account` flag.
+
+When the flag is set, HelmReleases which do not have a `.spec.serviceAccountName`
+specified will use the Service Account name provided by
+`--default-service-account=` in the namespace of the HelmRelease object.
+
+For further best practices on securing helm-controller, see our
+[best practices guide](https://fluxcd.io/flux/security/best-practices).
+
+### Remote Cluster API clusters
+
+Using a [`.spec.kubeConfig` reference](#kubeconfig-remote-clusters), it is possible
+to manage the full lifecycle of Helm releases on remote clusters.
+This composes well with Cluster-API bootstrap providers such as CAPBK (kubeadm),
+CAPA (AWS), and others.
+
+To reconcile a HelmRelease to a CAPI controlled cluster, put the HelmRelease in
+the same namespace as your Cluster object, and set the
+`.spec.kubeConfig.secretRef.name` to `-kubeconfig`:
+
+```yaml
+---
+apiVersion: cluster.x-k8s.io/v1alpha3
+kind: Cluster
+metadata:
+ name: stage # the kubeconfig Secret will contain the Cluster name
+ namespace: capi-stage
+spec:
+ clusterNetwork:
+ pods:
+ cidrBlocks:
+ - 10.100.0.0/16
+ serviceDomain: stage-cluster.local
+ services:
+ cidrBlocks:
+ - 10.200.0.0/12
+ controlPlaneRef:
+ apiVersion: controlplane.cluster.x-k8s.io/v1alpha3
+ kind: KubeadmControlPlane
+ name: stage-control-plane
+ namespace: capi-stage
+ infrastructureRef:
+ apiVersion: infrastructure.cluster.x-k8s.io/v1alpha3
+ kind: DockerCluster
+ name: stage
+ namespace: capi-stage
+---
+# ... unrelated Cluster API objects omitted for brevity ...
+---
+apiVersion: helm.toolkit.fluxcd.io/v2
+kind: HelmRelease
+metadata:
+ name: kube-prometheus-stack
+ namespace: capi-stage
+spec:
+ kubeConfig:
+ secretRef:
+ name: stage-kubeconfig # Cluster API creates this for the matching Cluster
+ chart:
+ spec:
+ chart: prometheus
+ version: ">=4.0.0 <5.0.0"
+ sourceRef:
+ kind: HelmRepository
+ name: prometheus-community
+ install:
+ remediation:
+ retries: -1
+```
+
+The Cluster and HelmRelease can be created at the same time as long as the
+[install remediation configuration](#install-remediation) is set to a
+forgiving number of `.retries`. The HelmRelease will then eventually succeed
+in installing the Helm chart once the cluster is available.
+
+If you want to target clusters created by other means than Cluster-API, you can
+create a Service Account with the necessary permissions on the target cluster,
+generate a KubeConfig for that account, and then create a Secret on the cluster
+where helm-controller is running. For example:
+
+```shell
+kubectl -n default create secret generic prod-kubeconfig \
+ --from-file=value.yaml=./kubeconfig
+```
+
+### Triggering a reconcile
+
+To manually tell the helm-controller to reconcile a HelmRelease outside the
+[specified interval window](#interval), it can be annotated with
+`reconcile.fluxcd.io/requestedAt: `.
+
+Annotating the resource queues the HelmRelease for reconciliation if the
+`` differs from the last value the controller acted on, as
+reported in `.status.lastHandledReconcileAt`.
+
+Using `kubectl`:
+
+```sh
+kubectl annotate --field-manager=flux-client-side-apply --overwrite helmrelease/ reconcile.fluxcd.io/requestedAt="$(date +%s)"
+```
+
+Using `flux`:
+
+```sh
+flux reconcile helmrelease
+```
+
+### Forcing a release
+
+To instruct the helm-controller to forcefully perform a Helm install or
+upgrade without making changes to the spec, it can be annotated with
+`reconcile.fluxcd.io/forceAt: ` while simultaneously
+[triggering a reconcile](#triggering-a-reconcile) with the same value.
+
+Annotating the resource forces a one-off Helm install or upgrade if the
+`` differs from the last value the controller acted on, as
+reported in `.status.lastHandledForceAt` and `.status.lastHandledReconcileAt`.
+
+Using `kubectl`:
+
+```sh
+TOKEN="$(date +%s)"; \
+kubectl annotate --field-manager=flux-client-side-apply --overwrite helmrelease/ \
+"reconcile.fluxcd.io/requestedAt=$TOKEN" \
+"reconcile.fluxcd.io/forceAt=$TOKEN"
+```
+
+Using `flux`:
+
+```sh
+flux reconcile helmrelease --force
+```
+
+### Resetting remediation retries
+
+To instruct the helm-controller to reset the number of retries while
+attempting to perform a Helm release, it can be annotated with
+`reconcile.fluxcd.io/resetAt: ` while simultaneously
+[triggering a reconcile](#triggering-a-reconcile) with the same value.
+
+Annotating the resource resets the failure counts on the object if the
+`` differs from the last value the controller acted on, as
+reported in `.status.lastHandledResetAt` and `.status.lastHandledReconcileAt`.
+This effectively allows it to continue to attempt to perform a Helm release
+based on the [install](#install-remediation) or [upgrade](#upgrade-remediation)
+remediation configuration.
+
+Using `kubectl`:
+
+```sh
+TOKEN="$(date +%s)"; \
+kubectl annotate --field-manager=flux-client-side-apply --overwrite helmrelease/ \
+"reconcile.fluxcd.io/requestedAt=$TOKEN" \
+"reconcile.fluxcd.io/resetAt=$TOKEN"
+```
+
+Using `flux`:
+
+```sh
+flux reconcile helmrelease --reset
+```
+
+### Handling failed uninstall
+
+At times, a Helm uninstall may fail due to the resource deletion taking a long
+time, resources getting stuck in deleting phase due to some resource delete
+policy in the cluster or some failing delete hooks. Depending on the scenario,
+this can be handled in a few different ways.
+
+For resources that take long to delete but are certain to get deleted without
+any intervention, failed uninstall will be retried until they succeeds. The
+HelmRelease object will remain in a failed state until the uninstall succeeds.
+Once uninstall is successful, the HelmRelease object will get deleted.
+
+If resources get stuck at deletion due to some dependency on some other
+resource or policy, the controller will keep retrying to delete the resources.
+The HelmRelease object will remain in a failed state. Once the cause of resource
+deletion issue is resolved by intervention, HelmRelease uninstallation will
+succeed and the HelmRelease object will get deleted. In case the cause of the
+deletion issue can't be resolved, the HelmRelease can be force deleted by
+manually deleting the [Helm storage
+secret](https://helm.sh/docs/topics/advanced/#storage-backends) from the
+respective release namespace. When the controller retries uninstall and cannot
+find the release, it assumes that the release has been deleted, Helm uninstall
+succeeds and the HelmRelease object gets deleted. This leaves behind all the
+release resources. They have to be manually deleted.
+
+If a chart with pre-delete hooks fail, the controller will re-run the hooks
+until they succeed and unblock the uninstallation. The Helm uninstall error
+will be present in the status of HelmRelease. This can be used to identify which
+hook is failing. If the hook failure persists, to run uninstall without the
+hooks, equivalent of running `helm uninstall --no-hooks`, update the HelmRelease
+to set `.spec.uninstall.disableHooks` to `true`.
+
+```yaml
+apiVersion: helm.toolkit.fluxcd.io/v2
+kind: HelmRelease
+...
+spec:
+ ...
+ uninstall:
+ disableHooks: true
+```
+
+In the next reconciliation, the controller will run Helm uninstall without the
+hooks. On success, the HelmRelease will get deleted. Otherwise, check the status
+of the HelmRelease for other failure that may be blocking the uninstall.
+
+In case of charts with post-delete hooks, since the hook runs after the deletion
+of the resources and the Helm storage, the hook failure will result in an
+initial uninstall failure. In the subsequent reconciliation to retry uninstall,
+since the Helm storage for the release got deleted, uninstall will succeed and
+the HelmRelease object will get deleted.
+
+Any leftover pre or post-delete hook resources have to be manually deleted.
+
+### Waiting for `Ready`
+
+When a change is applied, it is possible to wait for the HelmRelease to reach a
+`Ready` state using `kubectl`:
+
+```sh
+kubectl wait helmrelease/ --for=condition=ready --timeout=5m
+```
+
+### Suspending and resuming
+
+When you find yourself in a situation where you temporarily want to pause the
+reconciliation of a HelmRelease, you can suspend it using the
+[`.spec.suspend` field](#suspend).
+
+#### Suspend a HelmRelease
+
+In your YAML declaration:
+
+```yaml
+---
+apiVersion: helm.toolkit.fluxcd.io/v2
+kind: HelmRelease
+metadata:
+ name:
+spec:
+ suspend: true
+```
+
+Using `kubectl`:
+
+```sh
+kubectl patch helmrelease --field-manager=flux-client-side-apply -p '{\"spec\": {\"suspend\" : true }}'
+```
+
+Using `flux`:
+
+```sh
+flux suspend helmrelease
+```
+
+##### Resume a HelmRelease
+
+In your YAML declaration, comment out (or remove) the field:
+
+```yaml
+---
+apiVersion: helm.toolkit.fluxcd.io/v2
+kind: HelmRelease
+metadata:
+ name:
+spec:
+ # suspend: true
+```
+
+**Note:** Setting the field value to `false` has the same effect as removing
+it, but does not allow for "hot patching" using e.g. `kubectl` while practicing
+GitOps; as the manually applied patch would be overwritten by the declared
+state in Git.
+
+Using `kubectl`:
+
+```sh
+kubectl patch helmrelease --field-manager=flux-client-side-apply -p '{\"spec\" : {\"suspend\" : false }}'
+```
+
+Using `flux`:
+
+```sh
+flux resume helmrelease
+```
+
+### Debugging a HelmRelease
+
+There are several ways to gather information about a HelmRelease for debugging
+purposes.
+
+#### Describe the HelmRelease
+
+Describing a HelmRelease using `kubectl describe helmrelease `
+displays the latest recorded information for the resource in the Status and
+Events sections:
+
+```console
+...
+Status:
+ Conditions:
+ Last Transition Time: 2023-12-06T18:23:21Z
+ Message: Failed to install after 1 attempt(s)
+ Observed Generation: 1
+ Reason: RetriesExceeded
+ Status: True
+ Type: Stalled
+ Last Transition Time: 2023-12-06T18:23:21Z
+ Message: Helm test failed for release podinfo/podinfo.v1 with chart podinfo@6.5.3: 1 error occurred:
+ * pod podinfo-fault-test-a0tew failed
+ Observed Generation: 1
+ Reason: TestFailed
+ Status: False
+ Type: Ready
+ Last Transition Time: 2023-12-06T18:23:16Z
+ Message: Helm install succeeded for release podinfo/podinfo.v1 with chart podinfo@6.5.3
+ Observed Generation: 1
+ Reason: InstallSucceeded
+ Status: True
+ Type: Released
+ Last Transition Time: 2023-12-06T18:23:21Z
+ Message: Helm test failed for release podinfo/podinfo.v1 with chart podinfo@6.5.3: 1 error occurred:
+ * pod podinfo-fault-test-a0tew failed
+ Observed Generation: 1
+ Reason: TestFailed
+ Status: False
+ Type: TestSuccess
+...
+ History:
+ Chart Name: podinfo
+ Chart Version: 6.5.3
+ Config Digest: sha256:2598fd0e8c65bae746c6686a61c2b2709f47ba8ed5c36450ae1c30aea9c88e9f
+ Digest: sha256:24f31c6f2f3da97b217a794b5fb9234818296c971ff9f849144bf07438976e4d
+ First Deployed: 2023-12-06T18:23:12Z
+ Last Deployed: 2023-12-06T18:23:12Z
+ Name: podinfo
+ Namespace: default
+ Status: deployed
+ Test Hooks:
+ podinfo-fault-test-a0tew:
+ Last Completed: 2023-12-06T18:23:21Z
+ Last Started: 2023-12-06T18:23:16Z
+ Phase: Failed
+ podinfo-grpc-test-rzg5v:
+ podinfo-jwt-test-7k1hv:
+ Podinfo - Service - Test - Bgoeg:
+ Version: 1
+...
+Events:
+ Type Reason Age From Message
+ ---- ------ ---- ---- -------
+ Normal HelmChartCreated 88s helm-controller Created HelmChart/podinfo/podinfo-podinfo with SourceRef 'HelmRepository/podinfo/podinfo'
+ Normal HelmChartInSync 88s helm-controller HelmChart/podinfo/podinfo-podinfo with SourceRef 'HelmRepository/podinfo/podinfo' is in-sync
+ Normal InstallSucceeded 83s helm-controller Helm install succeeded for release podinfo/podinfo.v1 with chart podinfo@6.5.3
+ Warning TestFailed 78s helm-controller Helm test failed for release podinfo/podinfo.v1 with chart podinfo@6.5.3: 1 error occurred:
+ * pod podinfo-fault-test-a0tew failed
+```
+
+#### Trace emitted Events
+
+To view events for specific HelmRelease(s), `kubectl events` can be used in
+combination with `--for` to list the Events for specific objects. For example,
+running
+
+```shell
+kubectl events --for HelmRelease/
+```
+
+lists
+
+```shell
+LAST SEEN TYPE REASON OBJECT MESSAGE
+88s Normal HelmChartCreated HelmRelease/podinfo Created HelmChart/podinfo/podinfo-podinfo with SourceRef 'HelmRepository/podinfo/podinfo'
+88s Normal HelmChartInSync HelmRelease/podinfo HelmChart/podinfo/podinfo-podinfo with SourceRef 'HelmRepository/podinfo/podinfo' is in-sync
+83s Normal InstallSucceeded HelmRelease/podinfo Helm install succeeded for release podinfo/podinfo.v1 with chart podinfo@6.5.3
+78s Warning TestFailed HelmRelease/podinfo Helm test failed for release podinfo/podinfo.v1 with chart podinfo@6.5.3: 1 error occurred:
+ * pod podinfo-fault-test-a0tew failed
+```
+
+Besides being reported in Events, the controller may also log reconciliation
+errors. The Flux CLI offers commands for filtering the logs for a specific
+HelmRelease, e.g. `flux logs --level=error --kind=HelmRelease --name=.`
+
+#### Rendering the final Values locally
+
+When using multiple [values references](#values-references) in a
+HelmRelease, it can be useful to inspect the final values computed from the various sources.
+This can be done by pointing the Flux CLI to the in-cluster HelmRelease object:
+
+```shell
+flux debug hr -n --show-values
+```
+
+The command will output the final values by merging the in-line values from the HelmRelease
+with the values from the referenced ConfigMaps and/or Secrets.
+
+**Note:** The debug command will print sensitive information if Kubernetes Secrets
+are referenced in the HelmRelease `.spec.valuesFrom` field, so exercise caution
+when using this command.
+
+### Reacting immediately to configuration dependencies
+
+To trigger a Helm release upgrade when changes occur in referenced
+Secrets or ConfigMaps, you can set the following label on the
+Secret or ConfigMap:
+
+```yaml
+metadata:
+ labels:
+ reconcile.fluxcd.io/watch: Enabled
+```
+
+An alternative to labeling every Secret or ConfigMap is
+setting the `--watch-configs-label-selector=owner!=helm`
+[flag](https://fluxcd.io/flux/components/helm/options/#flags)
+in helm-controller, which allows watching all Secrets and
+ConfigMaps except for Helm storage Secrets.
+
+**Note**: An upgrade will be triggered for an event on a referenced
+Secret/ConfigMap even if it's marked as optional in the `.spec.valuesFrom`
+field, including deletion events.
+
+## HelmRelease Status
+
+### Events
+
+The controller emits Kubernetes Events to report the result of each Helm action
+performed for a HelmRelease. These events can be used to monitor the progress
+of the HelmRelease and can be forwarded to external systems using
+[notification-controller alerts](https://fluxcd.io/flux/monitoring/alerts/).
+
+The controller annotates the events with the Helm chart version, app version,
+and with the chart OCI digest if available.
+
+#### Event example
+
+```yaml
+apiVersion: v1
+kind: Event
+metadata:
+ annotations:
+ helm.toolkit.fluxcd.io/app-version: 6.6.1
+ helm.toolkit.fluxcd.io/revision: 6.6.1+0cc9a8446c95
+ helm.toolkit.fluxcd.io/oci-digest: sha256:0cc9a8446c95009ef382f5eade883a67c257f77d50f84e78ecef2aac9428d1e5
+ creationTimestamp: "2024-05-07T05:02:34Z"
+ name: podinfo.17cd1c4e15d474bb
+ namespace: default
+firstTimestamp: "2024-05-07T05:02:34Z"
+involvedObject:
+ apiVersion: helm.toolkit.fluxcd.io/v2
+ kind: HelmRelease
+ name: podinfo
+ namespace: default
+lastTimestamp: "2024-05-07T05:02:34Z"
+message: 'Helm test succeeded for release podinfo/podinfo.v2 with chart podinfo@6.6.1+0cc9a8446c95:
+ 3 test hooks completed successfully'
+reason: TestSucceeded
+source:
+ component: helm-controller
+type: Normal
+```
+
+### History
+
+The HelmRelease shows the history of Helm releases it has performed up to the
+previous successful release as a list in the `.status.history` of the resource.
+The history is ordered by the time of the release, with the most recent release
+first.
+
+When [Helm tests](#test-configuration) are enabled, the history will also
+include the status of the tests which were run for each release.
+
+#### History example
+
+```yaml
+---
+apiVersion: helm.toolkit.fluxcd.io/v2
+kind: HelmRelease
+metadata:
+ name:
+status:
+ history:
+ - appVersion: 6.6.1
+ chartName: podinfo
+ chartVersion: 6.6.1+0cc9a8446c95
+ configDigest: sha256:e15c415d62760896bd8bec192a44c5716dc224db9e0fc609b9ac14718f8f9e56
+ digest: sha256:e59349a6d8cf01d625de9fe73efd94b5e2a8cc8453d1b893ec367cfa2105bae9
+ firstDeployed: "2024-05-07T04:54:21Z"
+ lastDeployed: "2024-05-07T04:54:55Z"
+ name: podinfo
+ namespace: podinfo
+ ociDigest: sha256:0cc9a8446c95009ef382f5eade883a67c257f77d50f84e78ecef2aac9428d1e5
+ status: deployed
+ action: upgrade
+ testHooks:
+ podinfo-grpc-test-goyey:
+ lastCompleted: "2024-05-07T04:55:11Z"
+ lastStarted: "2024-05-07T04:55:09Z"
+ phase: Succeeded
+ version: 2
+ - appVersion: 6.6.0
+ chartName: podinfo
+ chartVersion: 6.6.0+cdd538a0167e
+ configDigest: sha256:e15c415d62760896bd8bec192a44c5716dc224db9e0fc609b9ac14718f8f9e56
+ digest: sha256:9be0d34ced6b890a72026749bc0f1f9e3c1a89673e17921bbcc0f27774f31c3a
+ firstDeployed: "2024-05-07T04:54:21Z"
+ lastDeployed: "2024-05-07T04:54:21Z"
+ name: podinfo
+ namespace: podinfo
+ ociDigest: sha256:cdd538a0167e4b51152b71a477e51eb6737553510ce8797dbcc537e1342311bb
+ status: superseded
+ action: install
+ testHooks:
+ podinfo-grpc-test-q0ucx:
+ lastCompleted: "2024-05-07T04:54:25Z"
+ lastStarted: "2024-05-07T04:54:23Z"
+ phase: Succeeded
+ version: 1
+```
+
+### Inventory
+
+The HelmRelease reports the list of Kubernetes resource objects that have been
+applied by the Helm release in `.status.inventory`. This can be used to
+identify which objects are managed by the HelmRelease. The inventory records
+are in the format `___`.
+
+The inventory includes all resources from the rendered manifests, as well as
+CRDs from the chart's `crds/` directory. Helm hooks are not included in the
+inventory, as they are not considered part of the release by Helm.
+
+#### Inventory example
+
+```yaml
+---
+apiVersion: helm.toolkit.fluxcd.io/v2
+kind: HelmRelease
+metadata:
+ name:
+status:
+ inventory:
+ entries:
+ - id: default_podinfo__Service
+ v: v1
+ - id: default_podinfo_apps_Deployment
+ v: v1
+ - id: default_podinfo_autoscaling_HorizontalPodAutoscaler
+ v: v2
+```
+
+### Conditions
+
+A HelmRelease enters various states during its lifecycle, reflected as
+[Kubernetes Conditions](https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#typical-status-properties).
+It can be [reconciling](#reconciling-helmrelease) when it is being processed by
+the controller, it can be [ready](#ready-helmrelease) when the Helm release is
+installed and up-to-date, it can [fail](#failed-helmrelease) during
+reconciliation, or it can be [drifted](#drifted-helmrelease) if the
+drift detection mode is set to enabled/warn and there is a drift.
+
+The HelmRelease API is compatible with the [kstatus specification](https://github.com/kubernetes-sigs/cli-utils/tree/master/pkg/kstatus),
+and reports `Reconciling` and `Stalled` conditions where applicable to provide
+better (timeout) support to solutions polling the HelmRelease to become `Ready`.
+
+#### Reconciling HelmRelease
+
+The helm-controller marks the HelmRepository as _reconciling_ when it is working
+on re-assessing the Helm release state, or working on a Helm action such as
+installing or upgrading the release.
+
+This can be due to one of the following reasons (without this being an
+exhaustive list):
+
+- The desired state of the HelmRelease has changed, and the controller is
+ working on installing or upgrading the Helm release.
+- The generation of the HelmRelease is newer than the [Observed
+ Generation](#observed-generation).
+- The HelmRelease has been installed or upgraded, but the [Helm
+ test](#test-configuration) is still running.
+- The HelmRelease is installed or upgraded, but the controller is working on
+ [detecting](#drift-detection) or [correcting](#drift-correction) drift.
+
+When the HelmRelease is "reconciling", the `Ready` Condition status becomes
+`Unknown` when the controller is working on a Helm install or upgrade, and the
+controller adds a Condition with the following attributes to the HelmRelease's
+`.status.conditions`:
+
+- `type: Reconciling`
+- `status: "True"`
+- `reason: Progressing` | `reason: ProgressingWithRetry`
+
+The Condition `message` is updated during the course of the reconciliation to
+report the Helm action being performed at any particular moment.
+
+The Condition has a ["negative polarity"](https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#typical-status-properties),
+and is only present on the HelmRelease while the status is `"True"`.
+
+#### Ready HelmRelease
+
+The helm-controller marks the HelmRelease as _ready_ when it has the following
+characteristics:
+
+- The Helm release is installed and up-to-date. This means that the Helm
+ release has been installed or upgraded, the release's chart has the same
+ version as the [Helm chart referenced by the HelmRelease](#chart-template),
+ and the [values](#values) used to install or upgrade the release have not
+ changed.
+- The Helm release has passed any [Helm tests](#test-configuration) that are
+ enabled.
+- The HelmRelease is not being [reconciled](#reconciling-helmrelease).
+
+When the HelmRelease is "ready", the controller sets a Condition with the
+following attributes in the HelmRelease's `.status.conditions`:
+
+- `type: Ready`
+- `status: "True"`
+- `reason: InstallSucceeded` | `reason: UpgradeSucceeded` | `reason: TestSucceeded`
+
+This `Ready` Condition will retain a status value of `"True"` until the
+HelmRelease is marked as reconciling, or e.g. an [error occurs](#failed-helmrelease)
+due to a failed Helm action.
+
+When a Helm install or upgrade has completed, the controller sets a Condition
+with the following attributes in the HelmRelease's `.status.conditions`:
+
+- `type: Released`
+- `status: "True"`
+- `reason: InstallSucceeded` | `reason: UpgradeSucceeded`
+
+The `Released` Condition will retain a status value of `"True"` until the
+next Helm install or upgrade has completed.
+
+When [Helm tests are enabled](#test-configuration) and completed successfully,
+the controller sets a Condition with the following attributes in the
+HelmRelease's `.status.conditions`:
+
+- `type: TestSuccess`
+- `status: "True"`
+- `reason: TestSucceeded`
+
+The `TestSuccess` Condition will retain a status value of `"True"` until the
+next Helm install or upgrade occurs, or the Helm tests are disabled.
+
+#### Drifted HelmRelease
+
+The helm-controller marks the HelmRelease as _drifted_ when it has the following
+characteristics:
+
+- The HelmRelease have drift detection mode set to enabled or warn.
+- There is a drift detected against the cluster state.
+
+When the HelmRelease is "drifted", the controller sets a Condition with the
+following attributes in the HelmRelease's `.status.conditions`:
+
+- `type: Drifted`
+- `status: "True"`
+- `reason: DriftDetected`
+
+When the HelmRelease have drift detection mode set to enabled or warn there
+and there is no drift, the controller sets a Condition with the following
+attributes in the HelmRelease's `.status.conditions`:
+
+- `type: Drifted`
+- `status: "False"`
+- `reason: NoDriftDetected`
+
+#### Failed HelmRelease
+
+The helm-controller may get stuck trying to determine state or produce a Helm
+release without completing. This can occur due to some of the following factors:
+
+- The HelmChart does not have an Artifact, or is not ready.
+- The HelmRelease's dependencies are not ready.
+- The composition of [values references](#values-references) and [inline values](#inline-values)
+ failed due to a misconfiguration.
+- The Helm action (install, upgrade, rollback, uninstall) failed.
+- The Helm action succeeded, but the [Helm test](#test-configuration) failed.
+
+When the failure is due to an error during a Helm install or upgrade, a
+Condition with the following attributes is added:
+
+- `type: Released`
+- `status: "False"`
+- `reason: InstallFailed` | `reason: UpgradeFailed`
+
+In case the failure is due to an error during a Helm test, a Condition with the
+following attributes is added:
+
+- `type: TestSuccess`
+- `status: "False"`
+- `reason: TestFailed`
+
+This `TestSuccess` Condition will only count as a failure when the Helm test
+results have [not been ignored](#configuring-failure-handling).
+
+When the failure has resulted in a rollback or uninstall, a Condition with the
+following attributes is added:
+
+- `type: Remediated`
+- `status: "True"`
+- `reason: RollbackSucceeded` | `reason: UninstallSucceeded` | `reason: RollbackFailed` | `reason: UninstallFailed`
+
+This `Remediated` Condition will retain a status value of `"True"` until the
+next Helm install or upgrade has completed.
+
+When the HelmRelease is "failing", the controller sets a Condition with the
+following attributes in the HelmRelease's `.status.conditions`:
+
+- `type: Ready`
+- `status: "False"`
+- `reason: InstallFailed` | `reason: UpgradeFailed` | `reason: TestFailed` | `reason: RollbackSucceeded` | `reason: UninstallSucceeded` | `reason: RollbackFailed` | `reason: UninstallFailed` | `reason: `
+
+Note that a HelmRelease can be [reconciling](#reconciling-helmrelease) while
+failing at the same time. For example, due to a new release attempt after
+remediating a failed Helm action. When a reconciliation fails, the `Reconciling`
+Condition reason would be `ProgressingWithRetry`. When the reconciliation is
+performed again after the failure, the reason is updated to `Progressing`.
+
+### Storage Namespace
+
+The helm-controller reports the active storage namespace in the
+`.status.storageNamespace` field.
+
+When the [`.spec.storageNamespace`](#storage-namespace) is changed, the
+controller will use the namespace from the Status to perform a Helm uninstall
+for the release in the old storage namespace, before performing a Helm install
+using the new storage namespace.
+
+### Failure Counters
+
+The helm-controller reports the number of failures it encountered for a
+HelmRelease in the `.status.failures`, `.status.installFailures` and
+`.status.upgradeFailures` fields.
+
+The `.status.failures` field is a general counter for all failures, while the
+`.status.installFailures` and `.status.upgradeFailures` fields are counters
+which are specific to the Helm install and upgrade actions respectively.
+The latter two counters are used to determine if the controller is allowed to
+retry an action when [install](#install-remediation) or [upgrade](#upgrade-remediation)
+remediation is enabled.
+
+The counters are reset when a new configuration is applied to the HelmRelease,
+the [values](#values) change, or when a new Helm chart version is discovered.
+In addition, they can be [reset using an annotation](#resetting-remediation-retries).
+
+### Observed Generation
+
+The helm-controller reports an observed generation in the HelmRelease's
+`.status.observedGeneration`. The observed generation is the latest
+`.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
+attempted to perform a Helm install or upgrade with in the
+`.status.lastAttemptedConfigDigest` field.
+
+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
+to perform a Helm install or upgrade with in the
+`.status.lastAttemptedRevision` field.
+
+The revision is used by the controller to determine if it should reset the
+[failure counters](#failure-counters) due to a change in the chart version.
+
+### Last Attempted Revision Digest
+
+The helm-controller reports the OCI artifact digest of the Helm chart it last attempted
+to perform a Helm install or upgrade with in the
+`.status.lastAttemptedRevisionDigest` field.
+
+This field is present in status only when `.spec.chartRef.type` is set to `OCIRepository`.
+
+### Last Attempted Release Action
+
+The helm-controller reports the last Helm release action it attempted to
+perform in the `.status.lastAttemptedReleaseAction` field. The possible values
+are `install` and `upgrade`.
+
+This field is used by the controller to determine the active remediation
+strategy for the HelmRelease.
+
+### Last Attempted Release Action Duration
+
+The helm-controller reports the duration of the last Helm release action it
+attempted to perform in the `.status.lastAttemptedReleaseActionDuration` field.
+
+### Last Handled Reconcile At
+
+The helm-controller reports the last `reconcile.fluxcd.io/requestedAt`
+annotation value it acted on in the `.status.lastHandledReconcileAt` field.
+
+For practical information about this field, see
+[triggering a reconcile](#triggering-a-reconcile).
+
+### Last Handled Force At
+
+The helm-controller reports the last `reconcile.fluxcd.io/forceAt`
+annotation value it acted on in the `.status.lastHandledForceAt` field.
+
+For practical information about this field, see
+[forcing a release](#forcing-a-release).
+
+### Last Handled Reset At
+
+The helm-controller reports the last `reconcile.fluxcd.io/resetAt`
+annotation value it acted on in the `.status.lastHandledResetAt` field.
+
+For practical information about this field, see
+[resetting remediation retries](#resetting-remediation-retries).
diff --git a/docs/spec/v2beta1/helmreleases.md b/docs/spec/v2beta1/helmreleases.md
index 6f9561295..556291d2f 100644
--- a/docs/spec/v2beta1/helmreleases.md
+++ b/docs/spec/v2beta1/helmreleases.md
@@ -1027,7 +1027,7 @@ spec:
## Role-based access control
By default, a `HelmRelease` runs under the cluster admin account and can create, modify, delete cluster level objects
-(cluster roles, cluster role binding, CRDs, etc) and namespeced objects (deployments, ingresses, etc).
+(cluster roles, cluster role binding, CRDs, etc) and namespaced objects (deployments, ingresses, etc).
For certain `HelmReleases` a cluster admin may wish to control what types of Kubernetes objects can
be reconciled and under which namespace.
To restrict a `HelmRelease`, one can assign a service account under which the reconciliation is performed.
diff --git a/docs/spec/v2beta2/README.md b/docs/spec/v2beta2/README.md
new file mode 100644
index 000000000..551b435fe
--- /dev/null
+++ b/docs/spec/v2beta2/README.md
@@ -0,0 +1,16 @@
+# helm.toolkit.fluxcd.io/v2beta2
+
+This is the v2beta2 API specification for declaratively managing Helm chart
+releases with Kubernetes manifests.
+
+## Specification
+
+- [HelmRelease CRD](helmreleases.md)
+ + [Example](helmreleases.md#example)
+ + [Writing a HelmRelease spec](helmreleases.md#writing-a-helmrelease-spec)
+ + [Working with HelmReleases](helmreleases.md#working-with-helmreleases)
+ + [HelmRelease Status](helmreleases.md#helmrelease-status)
+
+## Implementation
+
+* [helm-controller](https://github.com/fluxcd/helm-controller/)
diff --git a/docs/spec/v2beta2/helmreleases.md b/docs/spec/v2beta2/helmreleases.md
new file mode 100644
index 000000000..830b9630a
--- /dev/null
+++ b/docs/spec/v2beta2/helmreleases.md
@@ -0,0 +1,1689 @@
+# Helm Releases
+
+
+
+The `HelmRelease` API allows for controller-driven reconciliation of Helm
+releases via Helm actions such as install, upgrade, test, uninstall, and
+rollback. In addition to this, it detects and corrects cluster state drift
+from the desired release state.
+
+## Example
+
+The following is an example of a HelmRelease which installs the
+[podinfo Helm chart](https://github.com/stefanprodan/podinfo/tree/master/charts/podinfo).
+
+```yaml
+---
+apiVersion: source.toolkit.fluxcd.io/v1beta2
+kind: HelmRepository
+metadata:
+ name: podinfo
+ namespace: default
+spec:
+ interval: 5m
+ url: https://stefanprodan.github.io/podinfo
+---
+apiVersion: helm.toolkit.fluxcd.io/v2beta2
+kind: HelmRelease
+metadata:
+ name: podinfo
+ namespace: default
+spec:
+ interval: 10m
+ timeout: 5m
+ chart:
+ spec:
+ chart: podinfo
+ version: '6.5.*'
+ sourceRef:
+ kind: HelmRepository
+ name: podinfo
+ interval: 5m
+ releaseName: podinfo
+ install:
+ remediation:
+ retries: 3
+ upgrade:
+ remediation:
+ retries: 3
+ test:
+ enable: true
+ driftDetection:
+ mode: enabled
+ ignore:
+ - paths: ["/spec/replicas"]
+ target:
+ kind: Deployment
+ values:
+ replicaCount: 2
+```
+
+In the above example:
+
+- A [HelmRepository](https://fluxcd.io/flux/components/source/helmrepositories/)
+ named `podinfo` is created, pointing to the Helm repository from which the
+ podinfo chart can be installed.
+- A HelmRelease named `podinfo` is created, that will create a [HelmChart](https://fluxcd.io/flux/components/source/helmcharts/) object
+ from [the `.spec.chart`](#chart-template) and watch it for Artifact changes.
+- The controller will fetch the chart from the HelmChart's Artifact and use it
+ together with the `.spec.releaseName` and `.spec.values` to confirm if the
+ Helm release exists and is up-to-date.
+- If the Helm release does not exist, is not up-to-date, or has not observed to
+ be made by the controller based on [the HelmRelease's history](#history), then
+ the controller will install or upgrade the release. If this fails, it is
+ allowed to retry the operation a number of times while requeueing between
+ attempts, as defined by the respective [remediation configurations](#configuring-failure-handling).
+- If the [Helm tests](#test-configuration) for the release have not been run
+ before for this release, the HelmRelease will run them.
+- When the Helm release in storage is up-to-date, the controller will check if
+ the release in the cluster has drifted from the desired state, as defined by
+ the [drift detection configuration](#drift-detection). If it has, the
+ controller will [correct the drift](#drift-correction) by re-applying the
+ desired state.
+- The controller will repeat the above steps at the interval defined by
+ `.spec.interval`, or when the configuration changes in a way that affects the
+ desired state of the Helm release (e.g. a new chart version or values).
+
+You can run this example by saving the manifest into `podinfo.yaml`.
+
+1. Apply the resource on the cluster:
+
+ ```sh
+ kubectl apply -f podinfo.yaml
+ ```
+
+2. Run `kubectl get helmrelease` to see the HelmRelease:
+
+ ```console
+ NAME AGE READY STATUS
+ podinfo 15s True Helm test succeeded for release default/podinfo.v1 with chart podinfo@6.5.3: 3 test hooks completed successfully
+ ```
+
+3. Run `kubectl describe helmrelease podinfo` to see the [Conditions](#conditions)
+ and [History](#history) in the HelmRelease's Status:
+
+ ```console
+ ...
+ Status:
+ Conditions:
+ Last Transition Time: 2023-12-04T14:17:47Z
+ Message: Helm test succeeded for release default/podinfo.v1 with chart podinfo@6.5.3: 3 test hooks completed successfully
+ Observed Generation: 1
+ Reason: TestSucceeded
+ Status: True
+ Type: Ready
+ Last Transition Time: 2023-12-04T14:17:39Z
+ Message: Helm install succeeded for release default/podinfo.v1 with chart podinfo@6.5.3
+ Observed Generation: 1
+ Reason: InstallSucceeded
+ Status: True
+ Type: Released
+ Last Transition Time: 2023-12-04T14:17:47Z
+ Message: Helm test succeeded for release default/podinfo.v1 with chart podinfo@6.5.3: 3 test hooks completed successfully
+ Observed Generation: 1
+ Reason: TestSucceeded
+ Status: True
+ Type: TestSuccess
+ Helm Chart: default/default-podinfo
+ History:
+ Chart Name: podinfo
+ Chart Version: 6.5.3
+ Config Digest: sha256:e15c415d62760896bd8bec192a44c5716dc224db9e0fc609b9ac14718f8f9e56
+ Digest: sha256:e59aeb8b854f42e44756c2ef552a073051f1fc4f90e68aacbae7f824139580bc
+ First Deployed: 2023-12-04T14:17:35Z
+ Last Deployed: 2023-12-04T14:17:35Z
+ Name: podinfo
+ Namespace: default
+ Status: deployed
+ Test Hooks:
+ Podinfo - Grpc - Test - Scyhk:
+ Last Completed: 2023-12-04T14:17:42Z
+ Last Started: 2023-12-04T14:17:39Z
+ Phase: Succeeded
+ Podinfo - Jwt - Test - Scddu:
+ Last Completed: 2023-12-04T14:17:45Z
+ Last Started: 2023-12-04T14:17:42Z
+ Phase: Succeeded
+ Podinfo - Service - Test - Uibss:
+ Last Completed: 2023-12-04T14:17:47Z
+ Last Started: 2023-12-04T14:17:45Z
+ Phase: Succeeded
+ Version: 1
+ Last Applied Revision: 6.5.3
+ Last Attempted Config Digest: sha256:e15c415d62760896bd8bec192a44c5716dc224db9e0fc609b9ac14718f8f9e56
+ Last Attempted Generation: 1
+ Last Attempted Release Action: install
+ Last Attempted Revision: 6.5.3
+ Observed Generation: 1
+ Storage Namespace: default
+ Events:
+ Type Reason Age From Message
+ ---- ------ ---- ---- -------
+ Normal HelmChartCreated 23s helm-controller Created HelmChart/default/default-podinfo with SourceRef 'HelmRepository/default/podinfo'
+ Normal HelmChartInSync 22s helm-controller HelmChart/default/default-podinfo with SourceRef 'HelmRepository/default/podinfo' is in-sync
+ Normal InstallSucceeded 18s helm-controller Helm install succeeded for release default/podinfo.v1 with chart podinfo@6.5.3
+ Normal TestSucceeded 10s helm-controller Helm test succeeded for release default/podinfo.v1 with chart podinfo@6.5.3: 3 test hooks completed successfully
+ ```
+
+## Writing a HelmRelease spec
+
+As with all other Kubernetes config, a HelmRelease needs `apiVersion`,
+`kind`, and `metadata` fields. The name of a HelmRelease object must be a
+valid [DNS subdomain name](https://kubernetes.io/docs/concepts/overview/working-with-objects/names#dns-subdomain-names).
+
+A HelmRelease also needs a
+[`.spec` section](https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#spec-and-status).
+
+### Chart template
+
+`.spec.chart` is an optional field used by the helm-controller as a template to
+create a new [HelmChart resource](https://fluxcd.io/flux/components/source/helmcharts/).
+
+The spec for the HelmChart is provided via `.spec.chart.spec`, refer to
+[writing a HelmChart spec](https://fluxcd.io/flux/components/source/helmcharts/#writing-a-helmchart-spec)
+for in-depth information.
+
+Annotations and labels can be added by configuring the respective
+`.spec.chart.metadata` fields.
+
+The HelmChart is created in the same namespace as the `.sourceRef`, with a name
+matching the HelmRelease's `<.metadata.namespace>-<.metadata.name>`, and will
+be reported in `.status.helmChart`.
+
+The chart version of the last release attempt is reported in
+`.status.lastAttemptedRevision`. The controller will automatically perform a
+Helm release when the HelmChart produces a new chart (version).
+
+**Warning:** Changing the `.spec.chart` to a Helm chart with a different name
+(as specified in the chart's `Chart.yaml`) will cause the controller to
+uninstall any previous release before installing the new one.
+
+**Note:** On multi-tenant clusters, platform admins can disable cross-namespace
+references with the `--no-cross-namespace-refs=true` flag. When this flag is
+set, the HelmRelease can only refer to Sources in the same namespace as the
+HelmRelease object.
+
+### Chart reference
+
+`.spec.chartRef` is an optional field used to refer to an [OCIRepository resource](https://fluxcd.io/flux/components/source/ocirepositories/) or a [HelmChart resource](https://fluxcd.io/flux/components/source/helmcharts/)
+from which to fetch the Helm chart. The chart is fetched by the controller with the
+information provided by `.status.artifact` of the referenced resource.
+
+For a referenced resource of `kind OCIRepository`, the chart version of the last
+release attempt is reported in `.status.lastAttemptedRevision`. The version is in
+the format `+`. The digest of the OCI artifact is appended
+to the version to ensure that a change in the artifact content triggers a new release.
+The controller will automatically perform a Helm upgrade when the `OCIRepository`
+detects a new digest in the OCI artifact stored in registry, even if the version
+inside `Chart.yaml` is unchanged.
+
+**Warning:** One of `.spec.chart` or `.spec.chartRef` must be set, but not both.
+When switching from `.spec.chart` to `.spec.chartRef`, the controller will perform
+an Helm upgrade and will garbage collect the old HelmChart object.
+
+**Note:** On multi-tenant clusters, platform admins can disable cross-namespace
+references with the `--no-cross-namespace-refs=true` controller flag. When this flag is
+set, the HelmRelease can only refer to OCIRepositories in the same namespace as the
+HelmRelease object.
+
+#### OCIRepository reference example
+
+```yaml
+apiVersion: source.toolkit.fluxcd.io/v1beta2
+kind: OCIRepository
+metadata:
+ name: podinfo
+ namespace: default
+spec:
+ interval: 30s
+ url: oci://ghcr.io/stefanprodan/charts/podinfo
+ ref:
+ tag: 6.6.0
+---
+apiVersion: helm.toolkit.fluxcd.io/v2beta2
+kind: HelmRelease
+metadata:
+ name: podinfo
+ namespace: default
+spec:
+ interval: 10m
+ chartRef:
+ kind: OCIRepository
+ name: podinfo
+ namespace: default
+ values:
+ replicaCount: 2
+```
+
+#### HelmChart reference example
+
+```yaml
+apiVersion: source.toolkit.fluxcd.io/v1beta2
+kind: HelmChart
+metadata:
+ name: podinfo
+ namespace: default
+spec:
+ interval: 5m0s
+ chart: podinfo
+ reconcileStrategy: ChartVersion
+ sourceRef:
+ kind: HelmRepository
+ name: podinfo
+ version: '5.*'
+---
+apiVersion: helm.toolkit.fluxcd.io/v2beta2
+kind: HelmRelease
+metadata:
+ name: podinfo
+ namespace: default
+spec:
+ interval: 10m
+ chartRef:
+ kind: HelmChart
+ name: podinfo
+ namespace: default
+ values:
+ replicaCount: 2
+```
+
+### Release name
+
+`.spec.releaseName` is an optional field used to specify the name of the Helm
+release. It defaults to a composition of `[-]`.
+
+**Warning:** Changing the release name of a HelmRelease which has already been
+installed will not rename the release. Instead, the existing release will be
+uninstalled before installing a new release with the new name.
+
+**Note:** When the composition exceeds the maximum length of 53 characters, the
+name is shortened by hashing the release name with SHA-256. The resulting name
+is then composed of the first 40 characters of the release name, followed by a
+dash (`-`), followed by the first 12 characters of the hash. For example,
+`a-very-lengthy-target-namespace-with-a-nice-object-name` becomes
+`a-very-lengthy-target-namespace-with-a-nic-97af5d7f41f3`.
+
+### Target namespace
+
+`.spec.targetNamespace` is an optional field used to specify the namespace to
+which the Helm release is made. It defaults to the namespace of the
+HelmRelease.
+
+**Warning:** Changing the target namespace of a HelmRelease which has already
+been installed will not move the release to the new namespace. Instead, the
+existing release will be uninstalled before installing a new release in the new
+target namespace.
+
+### Storage namespace
+
+`.spec.storageNamespace` is an optional field used to specify the namespace
+in which Helm stores release information. It defaults to the namespace of the
+HelmRelease.
+
+**Warning:** Changing the storage namespace of a HelmRelease which has already
+been installed will not move the release to the new namespace. Instead, the
+existing release will be uninstalled before installing a new release in the new
+storage namespace.
+
+**Note:** When making use of the Helm CLI and attempting to make use of
+`helm get` commands to inspect a release, the `-n` flag should target the
+storage namespace of the HelmRelease.
+
+### Service Account reference
+
+`.spec.serviceAccountName` is an optional field used to specify the
+Service Account to be impersonated while reconciling the HelmRelease.
+For more information, refer to [Role-based access control](#role-based-access-control).
+
+### Persistent client
+
+`.spec.persistentClient` is an optional field to instruct the controller to use
+a persistent Kubernetes client for this release. If specified, the client will
+be reused for the duration of the reconciliation, instead of being created and
+destroyed for each (step of a) Helm action. If not set, it defaults to `true.`
+
+**Note:** This method generally boosts performance but could potentially cause
+complications with specific Helm charts. For instance, charts creating Custom
+Resource Definitions outside Helm's CRD lifecycle hooks during installation
+might face issues where these resources are not recognized as available,
+especially by post-install hooks.
+
+### Max history
+
+`.spec.maxHistory` is an optional field to configure the number of release
+revisions saved by Helm. If not set, it defaults to `5`.
+
+**Note:** Although setting this to `0` for an unlimited number of revisions is
+permissible, it is advised against due to performance reasons.
+
+### Dependencies
+
+`.spec.dependsOn` is an optional list to refer to other HelmRelease objects
+which the HelmRelease depends on. If specified, the HelmRelease is only allowed
+to proceed after the referred HelmReleases are ready, i.e. have the `Ready`
+condition marked as `True`.
+
+This is helpful when there is a need to make sure other resources exist before
+the workloads defined in a HelmRelease are released. For example, before
+installing objects of a certain Custom Resource kind, the Custom Resource
+Defintions and the related controller must exist in the cluster.
+
+```yaml
+---
+apiVersion: helm.toolkit.fluxcd.io/v2beta2
+kind: HelmRelease
+metadata:
+ name: backend
+ namespace: default
+spec:
+ # ...omitted for brevity
+---
+apiVersion: helm.toolkit.fluxcd.io/v2beta2
+kind: HelmRelease
+metadata:
+ name: frontend
+ namespace: default
+spec:
+ # ...omitted for brevity
+ dependsOn:
+ - name: backend
+```
+
+**Note:** This does not account for upgrade ordering. Kubernetes only allows
+applying one resource (HelmRelease in this case) at a time, so there is no
+way for the controller to know when a dependency HelmRelease may be updated.
+Also, circular dependencies between HelmRelease resources must be avoided,
+otherwise the interdependent HelmRelease resources will never be reconciled.
+
+### Values
+
+The values for the Helm release can be specified in two ways:
+
+- [Values references](#values-references)
+- [Inline values](#inline-values)
+
+Changes to the combined values will trigger a new Helm release.
+
+#### Values references
+
+`.spec.valuesFrom` is an optional list to refer to ConfigMap and Secret
+resources from which to take values. The values are merged in the order given,
+with the later values overwriting earlier, and then [inline values](#inline-values)
+overwriting those. When `targetPath` is set, it will overwrite everything before,
+including inline values.
+
+An item on the list offers the following subkeys:
+
+- `kind`: Kind of the values referent, supported values are `ConfigMap` and
+ `Secret`.
+- `name`: The `.metadata.name` of the values referent, in the same namespace as
+ the HelmRelease.
+- `valuesKey` (Optional): The `.data` key where the values.yaml or a specific
+ value can be found. Defaults to `values.yaml` when omitted.
+- `targetPath` (Optional): The YAML dot notation path at which the value should
+ be merged. When set, the valuesKey is expected to be a single flat value.
+ Defaults to empty when omitted, which results in the values getting merged at
+ the root.
+- `optional` (Optional): Whether this values reference is optional. When
+ `true`, a not found error for the values reference is ignored, but any
+ `valuesKey`, `targetPath` or transient error will still result in a
+ reconciliation failure. Defaults to `false` when omitted.
+
+```yaml
+spec:
+ valuesFrom:
+ - kind: ConfigMap
+ name: prod-env-values
+ valuesKey: values-prod.yaml
+ - kind: Secret
+ name: prod-tls-values
+ valuesKey: crt
+ targetPath: tls.crt
+ optional: true
+```
+
+**Note:** The `targetPath` supports the same formatting as you would supply as
+an argument to the `helm` binary using `--set [path]=[value]`. In addition to
+this, the referred value can contain the same value formats (e.g. `{a,b,c}` for
+a list). You can read more about the available formats and limitations in the
+[Helm documentation](https://helm.sh/docs/intro/using_helm/#the-format-and-limitations-of---set).
+
+For JSON strings, the [limitations are the same as while using `helm`](https://github.com/helm/helm/issues/5618)
+and require you to escape the full JSON string (including `=`, `[`, `,`, `.`).
+
+#### Inline values
+
+`.spec.values` is an optional field to inline values within a HelmRelease. When
+[values references](#values-references) are defined, inline values are merged
+with the values from these references, overwriting any existing ones.
+
+```yaml
+spec:
+ values:
+ replicaCount: 2
+```
+
+### Install configuration
+
+`.spec.install` is an optional field to specify the configuration for the
+controller to use when running a [Helm install action](https://helm.sh/docs/helm/helm_install/).
+
+The field offers the following subfields:
+
+- `.timeout` (Optional): The time to wait for any individual Kubernetes
+ operation (like Jobs for hooks) during the installation of the chart.
+ Defaults to the [global timeout value](#timeout).
+- `.crds` (Optional): The Custom Resource Definition install policy to use.
+ Valid values are `Skip`, `Create` and `CreateReplace`. Default is `Create`,
+ which will create Custom Resource Definitions when they do not exist. Refer
+ to [Custom Resource Definition lifecycle](#controlling-the-lifecycle-of-custom-resource-definitions)
+ for more information.
+- `.replace` (Optional): Instructs Helm to re-use the [release name](#release-name),
+ but only if that name is a deleted release which remains in the history.
+ Defaults to `false`.
+- `.createNamespace` (Optional): Instructs Helm to create the [target namespace](#target-namespace)
+ if it does not exist. On uninstall, the created namespace will not be garbage
+ collected. Defaults to `false`.
+- `.disableHooks` (Optional): Prevents [chart hooks](https://helm.sh/docs/topics/charts_hooks/)
+ from running during the installation of the chart. Defaults to `false`.
+- `.disableOpenAPIValidation` (Optional): Prevents Helm from validating the
+ rendered templates against the Kubernetes OpenAPI Schema. Defaults to `false`.
+- `.disableWait` (Optional): Disables waiting for resources to be ready after
+ the installation of the chart. Defaults to `false`.
+- `.disableWaitForJobs` (Optional): Disables waiting for any Jobs to complete
+ after the installation of the chart. Defaults to `false`.
+
+#### Install remediation
+
+`.spec.install.remediation` is an optional field to configure the remediation
+strategy to use when the installation of a Helm chart fails.
+
+The field offers the following subfields:
+
+- `.retries` (Optional): The number of retries that should be attempted on
+ failures before bailing. Remediation, using an [uninstall](#uninstall-configuration),
+ is performed between each attempt. Defaults to `0`, a negative integer equals
+ to an infinite number of retries.
+- `.ignoreTestFailures` (Optional): Instructs the controller to not remediate
+ when a [Helm test](#test-configuration) failure occurs. Defaults to
+ `.spec.test.ignoreFailures`.
+- `.remediateLastFailure` (Optional): Instructs the controller to remediate the
+ last failure when no retries remain. Defaults to `false`.
+
+### Upgrade configuration
+
+`.spec.upgrade` is an optional field to specify the configuration for the
+controller to use when running a [Helm upgrade action](https://helm.sh/docs/helm/helm_upgrade/).
+
+The field offers the following subfields:
+
+- `.timeout` (Optional): The time to wait for any individual Kubernetes
+ operation (like Jobs for hooks) during the upgrade of the release.
+ Defaults to the [global timeout value](#timeout).
+- `.crds` (Optional): The Custom Resource Definition upgrade policy to use.
+ Valid values are `Skip`, `Create` and `CreateReplace`. Default is `Skip`.
+ Refer to [Custom Resource Definition lifecycle](#controlling-the-lifecycle-of-custom-resource-definitions)
+ for more information.
+- `.cleanupOnFail` (Optional): Allows deletion of new resources created during
+ the upgrade of the release when it fails. Defaults to `false`.
+- `.disableHooks` (Optional): Prevents [chart hooks](https://helm.sh/docs/topics/charts_hooks/)
+ from running during the upgrade of the release. Defaults to `false`.
+- `.disableOpenAPIValidation` (Optional): Prevents Helm from validating the
+ rendered templates against the Kubernetes OpenAPI Schema. Defaults to `false`.
+- `.disableWait` (Optional): Disables waiting for resources to be ready after
+ upgrading the release. Defaults to `false`.
+- `.disableWaitForJobs` (Optional): Disables waiting for any Jobs to complete
+ after upgrading the release. Defaults to `false`.
+- `.force` (Optional): Forces resource updates through a replacement strategy.
+ Defaults to `false`.
+- `.preserveValues` (Optional): Instructs Helm to re-use the values from the
+ last release while merging in overrides from [values](#values). Setting
+ this flag makes the HelmRelease non-declarative. Defaults to `false`.
+
+#### Upgrade remediation
+
+`.spec.upgrade.remediation` is an optional field to configure the remediation
+strategy to use when the upgrade of a Helm release fails.
+
+The field offers the following subfields:
+
+- `.retries` (Optional): The number of retries that should be attempted on
+ failures before bailing. Remediation, using the `.strategy`, is performed
+ between each attempt. Defaults to `0`, a negative integer equals to an
+ infinite number of retries.
+- `.strategy` (Optional): The remediation strategy to use when a Helm upgrade
+ fails. Valid values are `rollback` and `uninstall`. Defaults to `rollback`.
+- `.ignoreTestFailures` (Optional): Instructs the controller to not remediate
+ when a [Helm test](#test-configuration) failure occurs. Defaults to
+ `.spec.test.ignoreFailures`.
+- `.remediateLastFailure` (Optional): Instructs the controller to remediate the
+ last failure when no retries remain. Defaults to `false` unless `.retries` is
+ greater than `0`.
+
+### Test configuration
+
+`.spec.test` is an optional field to specify the configuration values for the
+[Helm test action](https://helm.sh/docs/helm/helm_test/).
+
+To make the controller run the [Helm tests available for the chart](https://helm.sh/docs/topics/chart_tests/)
+after a successful Helm install or upgrade, `.spec.test.enable` can be set to
+`true`. When enabled, the test results will be available in the
+[`.status.history`](#history) field and emitted as a Kubernetes Event.
+
+By default, when tests are enabled, failures in tests are considered release
+failures, and thus are subject to the triggering Helm action's remediation
+configuration. However, test failures can be ignored by setting
+`.spec.test.ignoreFailures` to `true`. In this case, no remediation action
+will be taken, and the test failure will not affect the `Ready` status
+condition. This can be overridden per Helm action by setting the respective
+[install](#install-configuration) or [upgrade](#upgrade-configuration)
+configuration option.
+
+```yaml
+spec:
+ test:
+ enable: true
+ ignoreFailures: true
+```
+
+#### Filtering tests
+
+`.spec.test.filters` is an optional list to include or exclude specific tests
+from being run.
+
+```yaml
+spec:
+ test:
+ enable: true
+ filters:
+ - name: my-release-test-connection
+ exclude: false
+ - name: my-release-test-migration
+ exclude: true
+```
+
+### Rollback configuration
+
+`.spec.rollback` is an optional field to specify the configuration values for
+a [Helm rollback action](https://helm.sh/docs/helm/helm_rollback/). This
+configuration applies when the [upgrade remediation strategy](#upgrade-remediation)
+is set to `rollback`.
+
+The field offers the following subfields:
+
+- `.timeout` (Optional): The time to wait for any individual Kubernetes
+ operation (like Jobs for hooks) during the rollback of the release.
+ Defaults to the [global timeout value](#timeout).
+- `.cleanupOnFail` (Optional): Allows deletion of new resources created during
+ the rollback of the release when it fails. Defaults to `false`.
+- `.disableHooks` (Optional): Prevents [chart hooks](https://helm.sh/docs/topics/charts_hooks/)
+ from running during the rollback of the release. Defaults to `false`.
+- `.disableWait` (Optional): Disables waiting for resources to be ready after
+ rolling back the release. Defaults to `false`.
+- `.disableWaitForJobs` (Optional): Disables waiting for any Jobs to complete
+ after rolling back the release. Defaults to `false`.
+- `.force` (Optional): Forces resource updates through a replacement strategy.
+ Defaults to `false`.
+- `.recreate` (Optional): Performs Pod restarts if applicable. Defaults to
+ `false`.
+
+### Uninstall configuration
+
+`.spec.uninstall` is an optional field to specify the configuration values for
+a [Helm uninstall action](https://helm.sh/docs/helm/helm_uninstall/). This
+configuration applies to the [install remediation](#install-remediation), and
+when the [upgrade remediation strategy](#upgrade-remediation) is set to
+`uninstall`.
+
+The field offers the following subfields:
+
+- `.timeout` (Optional): The time to wait for any individual Kubernetes
+ operation (like Jobs for hooks) during the uninstalltion of the release.
+ Defaults to the [global timeout value](#timeout).
+- `.deletionPropagation` (Optional): The [deletion propagation policy](https://kubernetes.io/docs/tasks/administer-cluster/use-cascading-deletion/)
+ when a Helm uninstall is performed. Valid values are `background`,
+ `foreground` and `orphan`. Defaults to `background`.
+- `.disableHooks` (Optional): Prevents [chart hooks](https://helm.sh/docs/topics/charts_hooks/)
+ from running during the uninstallation of the release. Defaults to `false`.
+- `.disableWait` (Optional): Disables waiting for resources to be deleted after
+ uninstalling the release. Defaults to `false`.
+- `.keepHistory` (Optional): Instructs Helm to remove all associated resources
+ and mark the release as deleted, but to retain the release history. Defaults
+ to `false`.
+
+### Drift detection
+
+`.spec.driftDetection` is an optional field to enable the detection (and
+correction) of cluster-state drift compared to the manifest from the Helm
+storage.
+
+When `.spec.driftDetection.mode` is set to `warn` or `enabled`, and the
+desired state of the HelmRelease is in-sync with the Helm release object in
+the storage, the controller will compare the manifest from the Helm storage
+with the current state of the cluster using a
+[server-side dry-run apply](https://kubernetes.io/docs/reference/using-api/server-side-apply/).
+
+If this comparison detects a drift (either due to a resource being created
+or modified during the dry-run), the controller will emit a Kubernetes Event
+with a short summary of the detected changes. In addition, a more extensive
+[JSON Patch](https://datatracker.ietf.org/doc/html/rfc6902) summary is logged
+to the controller logs (with `--log-level=debug`).
+
+#### Drift correction
+
+Furthermore, when `.spec.driftDetection.mode` is set to `enabled`, the
+controller will attempt to correct the drift by creating and patching the
+resources based on the server-side dry-run apply result.
+
+At the end of the correction attempt, it will emit a Kubernetes Event with a
+summary of the changes it made and any failures it encountered. In case of a
+failure, it will continue to detect and correct drift until the desired state
+has been reached, or a new Helm action is triggered (due to e.g. a change to
+the spec).
+
+#### Ignore rules
+
+`.spec.driftDetection.ignore` is an optional field to provide
+[JSON Pointers](https://datatracker.ietf.org/doc/html/rfc6901) to ignore while
+detecting and correcting drift. This can for example be useful when Horizontal
+Pod Autoscaling is enabled for a Deployment, or when a Helm chart has hooks
+which mutate a resource.
+
+```yaml
+spec:
+ driftDetection:
+ mode: enabled
+ ignore:
+ - paths: ["/spec/replicas"]
+```
+
+**Note:** It is possible to achieve a likewise behavior as using
+[ignore annotations](#ignore-annotation) by configuring a JSON Pointer
+targeting a whole document (`""`).
+
+To ignore `.paths` in a specific target resource, a `.target` selector can be
+applied to the ignored paths.
+
+```yaml
+spec:
+ driftDetection:
+ ignore:
+ - paths: ["/spec/replicas"]
+ target:
+ kind: Deployment
+```
+
+The following `.target` selectors are available, defining multiple fields
+causes the selector to be more specific:
+
+- `group` (Optional): Matches the `.apiVersion` group of resources while
+ offering support for regular expressions. For example, `apps`,
+ `helm.toolkit.fluxcd.io` or `.*.toolkit.fluxcd.io`.
+- `version` (Optional): Matches the `.apiVersion` version of resources while
+ offering support for regular expressions. For example, `v1`, `v2beta2` or
+ `v2beta[\d]`.
+- `kind` (Optional): Matches the `.kind` of resources while offering support
+ for regular expressions. For example, `Deployment`, `HelmRelelease` or
+ `(HelmRelease|HelmChart)`.
+- `name` (Optional): Matches the `.metadata.name` of resources while offering
+ support for regular expressions. For example, `podinfo` or `podinfo.*`.
+- `namespace` (Optional): Matches the `.metadata.namespace` of resources while
+ offering support for regular expressions. For example, `my-release-ns` or
+ `.*-system`.
+- `annotationSelector` (Optional): Matches the `.metadata.annotations` of
+ resources using a [label selector expression](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#label-selectors).
+ For example, `environment = production` or `environment notin (staging)`.
+- `labelSelector` (Optional): Matches the `.metadata.labels` of resources
+ using a [label selector expression](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#label-selectors).
+ For example, `environment = production` or `environment notin (staging)`.
+
+#### Ignore annotation
+
+To exclude certain resources from the comparison, they can be labeled or
+annotated with `helm.toolkit.fluxcd.io/driftDetection: disabled`. Using
+[post-renderers](#post-renderers), this can be applied to any resource
+rendered by Helm.
+
+```yaml
+spec:
+ postRenderers:
+ - kustomize:
+ patches:
+ - target:
+ version: v1
+ kind: Deployment
+ name: my-app
+ patch: |
+ - op: add
+ path: /metadata/annotations/helm.toolkit.fluxcd.io~1driftDetection
+ value: disabled
+```
+
+**Note:** In many cases, it may be better (and easier) to configure an [ignore
+rule](#ignore-rules) to ignore (a portion of) a resource.
+
+### Post renderers
+
+`.spec.postRenderers` is an optional list to provide [post rendering](https://helm.sh/docs/topics/advanced/#post-rendering)
+capabilities using the following built-in Kustomize directives:
+
+- [patches](https://kubectl.docs.kubernetes.io/references/kustomize/kustomization/patches/) (`kustomize.patches`)
+- [images](https://kubectl.docs.kubernetes.io/references/kustomize/kustomization/images/) (`kustomize.images`)
+
+Post renderers are applied in the order given, and persisted by Helm to the
+manifest for the release in the storage.
+
+**Note:** [Helm has a limitation at present](https://github.com/helm/helm/issues/7891),
+which prevents post renderers from being applied to chart hooks.
+
+```yaml
+spec:
+ postRenderers:
+ - kustomize:
+ patches:
+ - target:
+ version: v1
+ kind: Deployment
+ name: metrics-server
+ patch: |
+ - op: add
+ path: /metadata/labels/environment
+ value: production
+ images:
+ - name: docker.io/bitnami/metrics-server
+ newName: docker.io/bitnami/metrics-server
+ newTag: 0.4.1-debian-10-r54
+```
+
+### KubeConfig reference
+
+`.spec.kubeConfig.secretRef.name` is an optional field to specify the name of
+a Secret containing a KubeConfig. If specified, the Helm operations will be
+targeted at the default cluster specified in this KubeConfig instead of using
+the in-cluster Service Account.
+
+The Secret defined in the `.secretRef` must exist in the same namespace as the
+HelmRelease. On every reconciliation, the KubeConfig bytes will be loaded from
+the `.secretRef.key` (default: `value` or `value.yaml`) of the Secret's data,
+and the Secret can thus be regularly updated if cluster access tokens have to
+rotate due to expiration.
+
+```yaml
+---
+apiVersion: v1
+kind: Secret
+metadata:
+ name: prod-kubeconfig
+type: Opaque
+stringData:
+ value.yaml: |
+ apiVersion: v1
+ kind: Config
+ # ...omitted for brevity
+```
+
+**Note:** The KubeConfig should be self-contained and not rely on binaries, the
+environment, or credential files from the helm-controller Pod. This matches the
+constraints of KubeConfigs from current Cluster API providers. KubeConfigs with
+`cmd-path` in them likely won't work without a custom, per-provider installation
+of helm-controller.
+
+When both `.spec.kubeConfig` and a [Service Account reference](#service-account-reference)
+are specified, the controller will impersonate the Service Account on the
+target cluster.
+
+The Helm storage is stored on the remote cluster in a namespace that equals to
+the namespace of the HelmRelease, or the [configured storage namespace](#storage-namespace).
+The release itself is made in a namespace that equals to the namespace of the
+HelmRelease, or the [configured target namespace](#target-namespace). The
+namespaces are expected to exist, with the exception that the target namespace
+can be created on demand by Helm when namespace creation is [configured during
+install](#install-configuration).
+
+Other references to Kubernetes resources in the HelmRelease, like [values
+references](#values-references), are expected to exist on the reconciling
+cluster.
+
+### Interval
+
+`.spec.interval` is a required field that specifies the interval at which the
+HelmRelease is reconciled, i.e. the controller ensures the current Helm release
+matches the desired state.
+
+After successfully reconciling the object, the controller requeues it for
+inspection at the specified interval. The value must be in a [Go recognized
+duration string format](https://pkg.go.dev/time#ParseDuration), e.g. `10m0s`
+to reconcile the object every ten minutes.
+
+If the `.metadata.generation` of a resource changes (due to e.g. a change to
+the spec) or the HelmChart revision changes (which generates a Kubernetes
+Event), this is handled instantly outside the interval window.
+
+**Note:** The controller can be configured to apply a jitter to the interval in
+order to distribute the load more evenly when multiple HelmRelease objects are
+set up with the same interval. For more information, please refer to the
+[helm-controller configuration options](https://fluxcd.io/flux/components/helm/options/).
+
+### Timeout
+
+`.spec.timeout` is an optional field to specify a timeout for a Helm action like
+install, upgrade or rollback. The value must be in a
+[Go recognized duration string format](https://pkg.go.dev/time#ParseDuration),
+e.g. `5m30s` for a timeout of five minutes and thirty seconds. The default
+value is `5m0s`.
+
+### Suspend
+
+`.spec.suspend` is an optional field to suspend the reconciliation of a
+HelmRelease. When set to `true`, the controller will stop reconciling the
+HelmRelease, and changes to the resource or the Helm chart will not result in
+a new Helm release. When the field is set to `false` or removed, it will
+resume.
+
+## Working with HelmReleases
+
+### Configuring failure handling
+
+From time to time, a Helm installation, upgrade, or accompanying [Helm test](#test-configuration)
+may fail. When this happens, by default no action is taken, and the release is
+left in a failed state. However, several automatic failure remediation options
+can be set via [`.spec.install.remediation`](#install-remediation) and
+[`.spec.upgrade.remediation`](#upgrade-remediation).
+
+By configuring the `.retries` field for the respective action, the controller
+will first remediate the failure by performing a Helm rollback or uninstall, and
+then reattempt the action. It will repeat this process until the `.retries`
+are exhausted, or the action succeeds.
+
+Once the `.retries` are exhausted, the controller will stop attempting to
+remediate the failure, and the Helm release will be left in a failed state.
+To ensure the Helm release is brought back to the last known good state or
+uninstalled, `.remediateLastFailure` can be set to `true`.
+For Helm upgrades, this defaults to `true` if at least one retry is configured.
+
+When a new release configuration or Helm chart is detected, the controller will
+reset the failure counters and attempt to install or upgrade the release again.
+
+**Note:** In addition to the automatic failure remediation options, the
+controller can be instructed to [force a Helm release](#forcing-a-release) or
+to [retry a failed Helm release](#resetting-remediation-retries)
+
+### Controlling the lifecycle of Custom Resource Definitions
+
+Helm does support [the installation of Custom Resource Definitions](https://helm.sh/docs/chart_best_practices/custom_resource_definitions/#method-1-let-helm-do-it-for-you)
+(CRDs) as part of a chart. However, it has no native support for
+[upgrading CRDs](https://helm.sh/docs/chart_best_practices/custom_resource_definitions/#some-caveats-and-explanations):
+
+> There is no support at this time for upgrading or deleting CRDs using Helm.
+> This was an explicit decision after much community discussion due to the
+> danger for unintentional data loss. Furthermore, there is currently no
+> community consensus around how to handle CRDs and their lifecycle. As this
+> evolves, Helm will add support for those use cases.
+
+If you write your own Helm charts, you can work around this limitation by
+putting your CRDs into the templates instead of the `crds/` directory, or by
+factoring them out into a separate Helm chart as suggested by the [official Helm
+documentation](https://helm.sh/docs/chart_best_practices/custom_resource_definitions/#method-2-separate-charts).
+
+However, if you use a third-party Helm chart that installs CRDs, not being able
+to upgrade the CRDs via HelmRelease objects might become a cumbersome limitation
+within your GitOps workflow. Therefore, Flux allows you to opt in to upgrading
+CRDs by setting the `.crds` policy in the [`.spec.install`](#install-configuration)
+and [`.spec.upgrade`](#upgrade-configuration) configurations.
+
+The following policy values are supported:
+
+- `Skip`: Skip the installation or upgrade of CRDs. This is the default value
+ for `.spec.upgrade.crds`.
+- `Create`: Create CRDs if they do not exist, but do not upgrade or delete them.
+ This is the default value for `.spec.install.crds`.
+- `CreateReplace`: Create new CRDs, update (replace) existing ones, but **do
+ not** delete CRDs which no longer exist in the current Helm chart.
+
+For example, if you want to update CRDs when installing and upgrading a Helm
+chart, you can set the `.spec.install.crds` and `.spec.upgrade.crds` policies to
+`CreateReplace`:
+
+```yaml
+---
+apiVersion: helm.toolkit.fluxcd.io/v2beta2
+kind: HelmRelease
+metadata:
+ name: my-operator
+ namespace: default
+spec:
+ interval: 10m
+ chart:
+ spec:
+ chart: my-operator
+ version: "1.0.1"
+ sourceRef:
+ kind: HelmRepository
+ name: my-operator-repo
+ interval: 5m
+ install:
+ crds: CreateReplace
+ upgrade:
+ crds: CreateReplace
+```
+
+### Role-based access control
+
+By default, a HelmRelease runs under the cluster admin account and can create,
+modify, and delete cluster level objects (ClusterRoles, ClusterRoleBindings,
+CustomResourceDefinitions, etc.) and namespaced objects (Deployments, Ingresses,
+etc.)
+
+For certain HelmReleases, a cluster administrator may wish to restrict the
+permissions of the HelmRelease to a specific namespace or to a specific set of
+namespaced objects. To restrict a HelmRelease, one can assign a Service Account
+under which the reconciliation is performed using
+[`.spec.serviceAccountName`](#service-account-reference).
+
+Assuming you want to restrict a group of HelmReleases to a single namespace,
+you can create a Service Account with a RoleBinding that grants access only to
+that namespace.
+
+For example, the following Service Account and RoleBinding restricts the
+HelmRelease to the `webapp` namespace:
+
+```yaml
+---
+apiVersion: v1
+kind: Namespace
+metadata:
+ name: webapp
+---
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ name: webapp-reconciler
+ namespace: webapp
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
+metadata:
+ name: webapp-reconciler
+ namespace: webapp
+rules:
+ - apiGroups: ["*"]
+ resources: ["*"]
+ verbs: ["*"]
+---
+ apiVersion: rbac.authorization.k8s.io/v1
+ kind: RoleBinding
+ metadata:
+ name: webapp-reconciler
+ namespace: webapp
+ roleRef:
+ apiGroup: rbac.authorization.k8s.io
+ kind: Role
+ name: webapp-reconciler
+ subjects:
+ - kind: ServiceAccount
+ name: webapp-reconciler
+ namespace: webapp
+```
+
+**Note:** The above resources are not created by the helm-controller, but should
+be created by a cluster administrator and preferably be managed by a
+[Kustomization](https://fluxcd.io/flux/components/kustomize/kustomizations/).
+
+The Service Account can then be referenced in the HelmRelease:
+
+```yaml
+---
+apiVersion: helm.toolkit.fluxcd.io/v2beta2
+kind: HelmRelease
+metadata:
+ name: podinfo
+ namespace: webapp
+spec:
+ serviceAccountName: webapp-reconciler
+ interval: 5m
+ chart:
+ spec:
+ chart: podinfo
+ sourceRef:
+ kind: HelmRepository
+ name: podinfo
+```
+
+When the controller reconciles the `podinfo` HelmRelease, it will impersonate
+the `webapp-reconciler` Service Account. If the chart contains cluster level
+objects like CustomResourceDefinitions, the reconciliation will fail since the
+account it runs under has no permissions to alter objects outside the
+`webapp` namespace.
+
+#### Enforcing impersonation
+
+On multi-tenant clusters, platform admins can enforce impersonation with the
+`--default-service-account` flag.
+
+When the flag is set, HelmReleases which do not have a `.spec.serviceAccountName`
+specified will use the Service Account name provided by
+`--default-service-account=` in the namespace of the HelmRelease object.
+
+For further best practices on securing helm-controller, see our
+[best practices guide](https://fluxcd.io/flux/security/best-practices).
+
+### Remote clusters / Cluster-API
+
+Using a [`.spec.kubeConfig` reference](#kubeconfig-reference), it is possible
+to manage the full lifecycle of Helm releases on remote clusters.
+This composes well with Cluster-API bootstrap providers such as CAPBK (kubeadm),
+CAPA (AWS), and others.
+
+To reconcile a HelmRelease to a CAPI controlled cluster, put the HelmRelease in
+the same namespace as your Cluster object, and set the
+`.spec.kubeConfig.secretRef.name` to `-kubeconfig`:
+
+```yaml
+---
+apiVersion: cluster.x-k8s.io/v1alpha3
+kind: Cluster
+metadata:
+ name: stage # the kubeconfig Secret will contain the Cluster name
+ namespace: capi-stage
+spec:
+ clusterNetwork:
+ pods:
+ cidrBlocks:
+ - 10.100.0.0/16
+ serviceDomain: stage-cluster.local
+ services:
+ cidrBlocks:
+ - 10.200.0.0/12
+ controlPlaneRef:
+ apiVersion: controlplane.cluster.x-k8s.io/v1alpha3
+ kind: KubeadmControlPlane
+ name: stage-control-plane
+ namespace: capi-stage
+ infrastructureRef:
+ apiVersion: infrastructure.cluster.x-k8s.io/v1alpha3
+ kind: DockerCluster
+ name: stage
+ namespace: capi-stage
+---
+# ... unrelated Cluster API objects omitted for brevity ...
+---
+apiVersion: helm.toolkit.fluxcd.io/v2beta2
+kind: HelmRelease
+metadata:
+ name: kube-prometheus-stack
+ namespace: capi-stage
+spec:
+ kubeConfig:
+ secretRef:
+ name: stage-kubeconfig # Cluster API creates this for the matching Cluster
+ chart:
+ spec:
+ chart: prometheus
+ version: ">=4.0.0 <5.0.0"
+ sourceRef:
+ kind: HelmRepository
+ name: prometheus-community
+ install:
+ remediation:
+ retries: -1
+```
+
+The Cluster and HelmRelease can be created at the same time as long as the
+[install remediation configuration](#install-remediation) is set to a
+forgiving number of `.retries`. The HelmRelease will then eventually succeed
+in installing the Helm chart once the cluster is available.
+
+If you want to target clusters created by other means than Cluster-API, you can
+create a Service Account with the necessary permissions on the target cluster,
+generate a KubeConfig for that account, and then create a Secret on the cluster
+where helm-controller is running. For example:
+
+```shell
+kubectl -n default create secret generic prod-kubeconfig \
+ --from-file=value.yaml=./kubeconfig
+```
+
+### Triggering a reconcile
+
+To manually tell the helm-controller to reconcile a HelmRelease outside the
+[specified interval window](#interval), it can be annotated with
+`reconcile.fluxcd.io/requestedAt: `.
+
+Annotating the resource queues the HelmRelease for reconciliation if the
+`` differs from the last value the controller acted on, as
+reported in `.status.lastHandledReconcileAt`.
+
+Using `kubectl`:
+
+```sh
+kubectl annotate --field-manager=flux-client-side-apply --overwrite helmrelease/ reconcile.fluxcd.io/requestedAt="$(date +%s)"
+```
+
+Using `flux`:
+
+```sh
+flux reconcile helmrelease
+```
+
+### Forcing a release
+
+To instruct the helm-controller to forcefully perform a Helm install or
+upgrade without making changes to the spec, it can be annotated with
+`reconcile.fluxcd.io/forceAt: ` while simultaneously
+[triggering a reconcile](#triggering-a-reconcile) with the same value.
+
+Annotating the resource forces a one-off Helm install or upgrade if the
+`` differs from the last value the controller acted on, as
+reported in `.status.lastHandledForceAt` and `.status.lastHandledReconcileAt`.
+
+Using `kubectl`:
+
+```sh
+TOKEN="$(date +%s)"; \
+kubectl annotate --field-manager=flux-client-side-apply --overwrite helmrelease/ \
+"reconcile.fluxcd.io/requestedAt=$TOKEN" \
+"reconcile.fluxcd.io/forceAt=$TOKEN"
+```
+
+Using `flux`:
+
+```sh
+flux reconcile helmrelease --force
+```
+
+### Resetting remediation retries
+
+To instruct the helm-controller to reset the number of retries while
+attempting to perform a Helm release, it can be annotated with
+`reconcile.fluxcd.io/resetAt: ` while simultaneously
+[triggering a reconcile](#triggering-a-reconcile) with the same value.
+
+Annotating the resource resets the failure counts on the object if the
+`` differs from the last value the controller acted on, as
+reported in `.status.lastHandledResetAt` and `.status.lastHandledReconcileAt`.
+This effectively allows it to continue to attempt to perform a Helm release
+based on the [install](#install-remediation) or [upgrade](#upgrade-remediation)
+remediation configuration.
+
+Using `kubectl`:
+
+```sh
+TOKEN="$(date +%s)"; \
+kubectl annotate --field-manager=flux-client-side-apply --overwrite helmrelease/ \
+"reconcile.fluxcd.io/requestedAt=$TOKEN" \
+"reconcile.fluxcd.io/resetAt=$TOKEN"
+```
+
+Using `flux`:
+
+```sh
+flux reconcile helmrelease --reset
+```
+
+### Waiting for `Ready`
+
+When a change is applied, it is possible to wait for the HelmRelease to reach a
+`Ready` state using `kubectl`:
+
+```sh
+kubectl wait helmrelease/ --for=condition=ready --timeout=5m
+```
+
+### Suspending and resuming
+
+When you find yourself in a situation where you temporarily want to pause the
+reconciliation of a HelmRelease, you can suspend it using the
+[`.spec.suspend` field](#suspend).
+
+#### Suspend a HelmRelease
+
+In your YAML declaration:
+
+```yaml
+---
+apiVersion: helm.toolkit.fluxcd.io/v2beta2
+kind: HelmRelease
+metadata:
+ name:
+spec:
+ suspend: true
+```
+
+Using `kubectl`:
+
+```sh
+kubectl patch helmrelease --field-manager=flux-client-side-apply -p '{\"spec\": {\"suspend\" : true }}'
+```
+
+Using `flux`:
+
+```sh
+flux suspend helmrelease
+```
+
+##### Resume a HelmRelease
+
+In your YAML declaration, comment out (or remove) the field:
+
+```yaml
+---
+apiVersion: helm.toolkit.fluxcd.io/v2beta2
+kind: HelmRelease
+metadata:
+ name:
+spec:
+ # suspend: true
+```
+
+**Note:** Setting the field value to `false` has the same effect as removing
+it, but does not allow for "hot patching" using e.g. `kubectl` while practicing
+GitOps; as the manually applied patch would be overwritten by the declared
+state in Git.
+
+Using `kubectl`:
+
+```sh
+kubectl patch helmrelease --field-manager=flux-client-side-apply -p '{\"spec\" : {\"suspend\" : false }}'
+```
+
+Using `flux`:
+
+```sh
+flux resume helmrelease
+```
+
+### Debugging a HelmRelease
+
+There are several ways to gather information about a HelmRelease for debugging
+purposes.
+
+#### Describe the HelmRelease
+
+Describing a HelmRelease using `kubectl describe helmrelease `
+displays the latest recorded information for the resource in the Status and
+Events sections:
+
+```console
+...
+Status:
+ Conditions:
+ Last Transition Time: 2023-12-06T18:23:21Z
+ Message: Failed to install after 1 attempt(s)
+ Observed Generation: 1
+ Reason: RetriesExceeded
+ Status: True
+ Type: Stalled
+ Last Transition Time: 2023-12-06T18:23:21Z
+ Message: Helm test failed for release podinfo/podinfo.v1 with chart podinfo@6.5.3: 1 error occurred:
+ * pod podinfo-fault-test-a0tew failed
+ Observed Generation: 1
+ Reason: TestFailed
+ Status: False
+ Type: Ready
+ Last Transition Time: 2023-12-06T18:23:16Z
+ Message: Helm install succeeded for release podinfo/podinfo.v1 with chart podinfo@6.5.3
+ Observed Generation: 1
+ Reason: InstallSucceeded
+ Status: True
+ Type: Released
+ Last Transition Time: 2023-12-06T18:23:21Z
+ Message: Helm test failed for release podinfo/podinfo.v1 with chart podinfo@6.5.3: 1 error occurred:
+ * pod podinfo-fault-test-a0tew failed
+ Observed Generation: 1
+ Reason: TestFailed
+ Status: False
+ Type: TestSuccess
+...
+ History:
+ Chart Name: podinfo
+ Chart Version: 6.5.3
+ Config Digest: sha256:2598fd0e8c65bae746c6686a61c2b2709f47ba8ed5c36450ae1c30aea9c88e9f
+ Digest: sha256:24f31c6f2f3da97b217a794b5fb9234818296c971ff9f849144bf07438976e4d
+ First Deployed: 2023-12-06T18:23:12Z
+ Last Deployed: 2023-12-06T18:23:12Z
+ Name: podinfo
+ Namespace: default
+ Status: deployed
+ Test Hooks:
+ podinfo-fault-test-a0tew:
+ Last Completed: 2023-12-06T18:23:21Z
+ Last Started: 2023-12-06T18:23:16Z
+ Phase: Failed
+ podinfo-grpc-test-rzg5v:
+ podinfo-jwt-test-7k1hv:
+ Podinfo - Service - Test - Bgoeg:
+ Version: 1
+...
+Events:
+ Type Reason Age From Message
+ ---- ------ ---- ---- -------
+ Normal HelmChartCreated 88s helm-controller Created HelmChart/podinfo/podinfo-podinfo with SourceRef 'HelmRepository/podinfo/podinfo'
+ Normal HelmChartInSync 88s helm-controller HelmChart/podinfo/podinfo-podinfo with SourceRef 'HelmRepository/podinfo/podinfo' is in-sync
+ Normal InstallSucceeded 83s helm-controller Helm install succeeded for release podinfo/podinfo.v1 with chart podinfo@6.5.3
+ Warning TestFailed 78s helm-controller Helm test failed for release podinfo/podinfo.v1 with chart podinfo@6.5.3: 1 error occurred:
+ * pod podinfo-fault-test-a0tew failed
+```
+
+#### Trace emitted Events
+
+To view events for specific HelmRelease(s), `kubectl events` can be used in
+combination with `--for` to list the Events for specific objects. For example,
+running
+
+```shell
+kubectl events --for HelmRelease/
+```
+
+lists
+
+```shell
+LAST SEEN TYPE REASON OBJECT MESSAGE
+88s Normal HelmChartCreated HelmRelease/podinfo Created HelmChart/podinfo/podinfo-podinfo with SourceRef 'HelmRepository/podinfo/podinfo'
+88s Normal HelmChartInSync HelmRelease/podinfo HelmChart/podinfo/podinfo-podinfo with SourceRef 'HelmRepository/podinfo/podinfo' is in-sync
+83s Normal InstallSucceeded HelmRelease/podinfo Helm install succeeded for release podinfo/podinfo.v1 with chart podinfo@6.5.3
+78s Warning TestFailed HelmRelease/podinfo Helm test failed for release podinfo/podinfo.v1 with chart podinfo@6.5.3: 1 error occurred:
+ * pod podinfo-fault-test-a0tew failed
+```
+
+Besides being reported in Events, the controller may also log reconciliation
+errors. The Flux CLI offers commands for filtering the logs for a specific
+HelmRelease, e.g. `flux logs --level=error --kind=HelmRelease --name=.`
+
+## HelmRelease Status
+
+### History
+
+The HelmRelease shows the history of Helm releases it has performed up to the
+previous successful release as a list in the `.status.history` of the resource.
+The history is ordered by the time of the release, with the most recent release
+first.
+
+When [Helm tests](#test-configuration) are enabled, the history will also
+include the status of the tests which were run for each release.
+
+#### History example
+
+```yaml
+---
+apiVersion: helm.toolkit.fluxcd.io/v2beta2
+kind: HelmRelease
+metadata:
+ name:
+status:
+ history:
+ - chartName: podinfo
+ chartVersion: 6.5.3
+ configDigest: sha256:803f06d4673b07668ff270301ca54ca5829da3133c1219f47bd9f52a60b22f9f
+ digest: sha256:3036cf7c06fd35b8ccb15c426fed9ce8a059a0a4befab1a47170b6e962c4d784
+ firstDeployed: '2023-12-06T20:38:47Z'
+ lastDeployed: '2023-12-06T20:52:06Z'
+ name: podinfo
+ namespace: podinfo
+ status: deployed
+ testHooks:
+ podinfo-grpc-test-qulpw:
+ lastCompleted: '2023-12-06T20:52:09Z'
+ lastStarted: '2023-12-06T20:52:07Z'
+ phase: Succeeded
+ podinfo-jwt-test-xe0ch:
+ lastCompleted: '2023-12-06T20:52:12Z'
+ lastStarted: '2023-12-06T20:52:09Z'
+ phase: Succeeded
+ podinfo-service-test-eh6x2:
+ lastCompleted: '2023-12-06T20:52:14Z'
+ lastStarted: '2023-12-06T20:52:12Z'
+ phase: Succeeded
+ version: 3
+ - chartName: podinfo
+ chartVersion: 6.5.3
+ configDigest: sha256:e15c415d62760896bd8bec192a44c5716dc224db9e0fc609b9ac14718f8f9e56
+ digest: sha256:858b157a63889b25379e287e24a9b38beb09a8ae21f31ae2cf7ad53d70744375
+ firstDeployed: '2023-12-06T20:38:47Z'
+ lastDeployed: '2023-12-06T20:39:02Z'
+ name: podinfo
+ namespace: podinfo
+ status: superseded
+ testHooks:
+ podinfo-grpc-test-aiuee:
+ lastCompleted: '2023-12-06T20:39:04Z'
+ lastStarted: '2023-12-06T20:39:02Z'
+ phase: Succeeded
+ podinfo-jwt-test-dme3b:
+ lastCompleted: '2023-12-06T20:39:07Z'
+ lastStarted: '2023-12-06T20:39:04Z'
+ phase: Succeeded
+ podinfo-service-test-fgvte:
+ lastCompleted: '2023-12-06T20:39:09Z'
+ lastStarted: '2023-12-06T20:39:07Z'
+ phase: Succeeded
+ version: 2
+```
+
+### Conditions
+
+A HelmRelease enters various states during its lifecycle, reflected as
+[Kubernetes Conditions](https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#typical-status-properties).
+It can be [reconciling](#reconciling-helmrelease) when it is being processed by
+the controller, it can be [ready](#ready-helmrelease) when the Helm release is
+installed and up-to-date, or it can [fail](#failed-helmrelease) during
+reconciliation.
+
+The HelmRelease API is compatible with the [kstatus specification](https://github.com/kubernetes-sigs/cli-utils/tree/master/pkg/kstatus),
+and reports `Reconciling` and `Stalled` conditions where applicable to provide
+better (timeout) support to solutions polling the HelmRelease to become `Ready`.
+
+#### Reconciling HelmRelease
+
+The helm-controller marks the HelmRepository as _reconciling_ when it is working
+on re-assessing the Helm release state, or working on a Helm action such as
+installing or upgrading the release.
+
+This can be due to one of the following reasons (without this being an
+exhaustive list):
+
+- The desired state of the HelmRelease has changed, and the controller is
+ working on installing or upgrading the Helm release.
+- The generation of the HelmRelease is newer than the [Observed
+ Generation](#observed-generation).
+- The HelmRelease has been installed or upgraded, but the [Helm
+ test](#test-configuration) is still running.
+- The HelmRelease is installed or upgraded, but the controller is working on
+ [detecting](#drift-detection) or [correcting](#drift-correction) drift.
+
+When the HelmRelease is "reconciling", the `Ready` Condition status becomes
+`Unknown` when the controller is working on a Helm install or upgrade, and the
+controller adds a Condition with the following attributes to the HelmRelease's
+`.status.conditions`:
+
+- `type: Reconciling`
+- `status: "True"`
+- `reason: Progressing` | `reason: ProgressingWithRetry`
+
+The Condition `message` is updated during the course of the reconciliation to
+report the Helm action being performed at any particular moment.
+
+The Condition has a ["negative polarity"](https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#typical-status-properties),
+and is only present on the HelmRelease while the status is `"True"`.
+
+#### Ready HelmRelease
+
+The helm-controller marks the HelmRelease as _ready_ when it has the following
+characteristics:
+
+- The Helm release is installed and up-to-date. This means that the Helm
+ release has been installed or upgraded, the release's chart has the same
+ version as the [Helm chart referenced by the HelmRelease](#chart-template),
+ and the [values](#values) used to install or upgrade the release have not
+ changed.
+- The Helm release has passed any [Helm tests](#test-configuration) that are
+ enabled.
+- The HelmRelease is not being [reconciled](#reconciling-helmrelease).
+
+When the HelmRelease is "ready", the controller sets a Condition with the
+following attributes in the HelmRelease's `.status.conditions`:
+
+- `type: Ready`
+- `status: "True"`
+- `reason: InstallSucceeded` | `reason: UpgradeSucceeded` | `reason: TestSucceeded`
+
+This `Ready` Condition will retain a status value of `"True"` until the
+HelmRelease is marked as reconciling, or e.g. an [error occurs](#failed-helmrelease)
+due to a failed Helm action.
+
+When a Helm install or upgrade has completed, the controller sets a Condition
+with the following attributes in the HelmRelease's `.status.conditions`:
+
+- `type: Released`
+- `status: "True"`
+- `reason: InstallSucceeded` | `reason: UpgradeSucceeded`
+
+The `Released` Condition will retain a status value of `"True"` until the
+next Helm install or upgrade has completed.
+
+When [Helm tests are enabled](#test-configuration) and completed successfully,
+the controller sets a Condition with the following attributes in the
+HelmRelease's `.status.conditions`:
+
+- `type: TestSuccess`
+- `status: "True"`
+- `reason: TestSucceeded`
+
+The `TestSuccess` Condition will retain a status value of `"True"` until the
+next Helm install or upgrade occurs, or the Helm tests are disabled.
+
+#### Failed HelmRelease
+
+The helm-controller may get stuck trying to determine state or produce a Helm
+release without completing. This can occur due to some of the following factors:
+
+- The HelmChart does not have an Artifact, or is not ready.
+- The HelmRelease's dependencies are not ready.
+- The composition of [values references](#values-references) and [inline values](#inline-values)
+ failed due to a misconfiguration.
+- The Helm action (install, upgrade, rollback, uninstall) failed.
+- The Helm action succeeded, but the [Helm test](#test-configuration) failed.
+
+When the failure is due to an error during a Helm install or upgrade, a
+Condition with the following attributes is added:
+
+- `type: Released`
+- `status: "False"`
+- `reason: InstallFailed` | `reason: UpgradeFailed`
+
+In case the failure is due to an error during a Helm test, a Condition with the
+following attributes is added:
+
+- `type: TestSuccess`
+- `status: "False"`
+- `reason: TestFailed`
+
+This `TestSuccess` Condition will only count as a failure when the Helm test
+results have [not been ignored](#configuring-failure-handling).
+
+When the failure has resulted in a rollback or uninstall, a Condition with the
+following attributes is added:
+
+- `type: Remediated`
+- `status: "True"`
+- `reason: RollbackSucceeded` | `reason: UninstallSucceeded` | `reason: RollbackFailed` | `reason: UninstallFailed`
+
+This `Remediated` Condition will retain a status value of `"True"` until the
+next Helm install or upgrade has completed.
+
+When the HelmRelease is "failing", the controller sets a Condition with the
+following attributes in the HelmRelease's `.status.conditions`:
+
+- `type: Ready`
+- `status: "False"`
+- `reason: InstallFailed` | `reason: UpgradeFailed` | `reason: TestFailed` | `reason: RollbackSucceeded` | `reason: UninstallSucceeded` | `reason: RollbackFailed` | `reason: UninstallFailed` | `reason: `
+
+Note that a HelmRelease can be [reconciling](#reconciling-helmrelease) while
+failing at the same time. For example, due to a new release attempt after
+remediating a failed Helm action. When a reconciliation fails, the `Reconciling`
+Condition reason would be `ProgressingWithRetry`. When the reconciliation is
+performed again after the failure, the reason is updated to `Progressing`.
+
+### Storage Namespace
+
+The helm-controller reports the active storage namespace in the
+`.status.storageNamespace` field.
+
+When the [`.spec.storageNamespace`](#storage-namespace) is changed, the
+controller will use the namespace from the Status to perform a Helm uninstall
+for the release in the old storage namespace, before performing a Helm install
+using the new storage namespace.
+
+### Failure Counters
+
+The helm-controller reports the number of failures it encountered for a
+HelmRelease in the `.status.failures`, `.status.installFailures` and
+`.status.upgradeFailures` fields.
+
+The `.status.failures` field is a general counter for all failures, while the
+`.status.installFailures` and `.status.upgradeFailures` fields are counters
+which are specific to the Helm install and upgrade actions respectively.
+The latter two counters are used to determine if the controller is allowed to
+retry an action when [install](#install-remediation) or [upgrade](#upgrade-remediation)
+remediation is enabled.
+
+The counters are reset when a new configuration is applied to the HelmRelease,
+the [values](#values) change, or when a new Helm chart version is discovered.
+In addition, they can be [reset using an annotation](#resetting-remediation-retries).
+
+### Observed Generation
+
+The helm-controller reports an observed generation in the HelmRelease's
+`.status.observedGeneration`. The observed generation is the latest
+`.metadata.generation` which resulted in either a [ready state](#ready-helmrelease),
+or stalled due to error it can not recover from without human intervention.
+
+### Last Attempted Config Digest
+
+The helm-controller reports the digest for the [values](#values) it last
+attempted to perform a Helm install or upgrade with in the
+`.status.lastAttemptedConfigDigest` field.
+
+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
+to perform a Helm install or upgrade with in the
+`.status.lastAttemptedRevision` field.
+
+The revision is used by the controller to determine if it should reset the
+[failure counters](#failure-counters) due to a change in the chart version.
+
+### Last Attempted Release Action
+
+The helm-controller reports the last Helm release action it attempted to
+perform in the `.status.lastAttemptedReleaseAction` field. The possible values
+are `install` and `upgrade`.
+
+This field is used by the controller to determine the active remediation
+strategy for the HelmRelease.
+
+### Last Handled Reconcile At
+
+The helm-controller reports the last `reconcile.fluxcd.io/requestedAt`
+annotation value it acted on in the `.status.lastHandledReconcileAt` field.
+
+For practical information about this field, see
+[triggering a reconcile](#triggering-a-reconcile).
+
+### Last Handled Force At
+
+The helm-controller reports the last `reconcile.fluxcd.io/forceAt`
+annotation value it acted on in the `.status.lastHandledForceAt` field.
+
+For practical information about this field, see
+[forcing a release](#forcing-a-release).
+
+### Last Handled Reset At
+
+The helm-controller reports the last `reconcile.fluxcd.io/resetAt`
+annotation value it acted on in the `.status.lastHandledResetAt` field.
+
+For practical information about this field, see
+[resetting remediation retries](#resetting-remediation-retries).
diff --git a/go.mod b/go.mod
index c35f1f1fe..07f10cf7c 100644
--- a/go.mod
+++ b/go.mod
@@ -1,172 +1,238 @@
module github.com/fluxcd/helm-controller
-go 1.20
+go 1.26.0
replace github.com/fluxcd/helm-controller/api => ./api
+// TODO: Remove this after Helm 4.1.4 comes out.
+// This fixes the following bugs:
+// - https://github.com/helm/helm/issues/31867
+// - https://github.com/helm/helm/issues/31935
+replace helm.sh/helm/v4 => github.com/fluxcd/helm/v4 v4.1.4-flux.1
+
// Replace digest lib to master to gather access to BLAKE3.
// xref: https://github.com/opencontainers/go-digest/pull/66
-replace github.com/opencontainers/go-digest => github.com/opencontainers/go-digest v1.0.1-0.20220411205349-bde1400a84be
+replace github.com/opencontainers/go-digest => github.com/opencontainers/go-digest v1.0.1-0.20231025023718-d50d2fec9c98
-// Pin kustomize to v5.0.3
+// Pin kustomize to v5.7.1
replace (
- sigs.k8s.io/kustomize/api => sigs.k8s.io/kustomize/api v0.13.4
- sigs.k8s.io/kustomize/kyaml => sigs.k8s.io/kustomize/kyaml v0.14.2
+ sigs.k8s.io/kustomize/api => sigs.k8s.io/kustomize/api v0.20.1
+ sigs.k8s.io/kustomize/kyaml => sigs.k8s.io/kustomize/kyaml v0.20.1
)
require (
- github.com/fluxcd/helm-controller/api v0.36.0
- github.com/fluxcd/pkg/apis/acl v0.1.0
- github.com/fluxcd/pkg/apis/event v0.5.2
- github.com/fluxcd/pkg/apis/kustomize v1.1.1
- github.com/fluxcd/pkg/apis/meta v1.1.2
- github.com/fluxcd/pkg/runtime v0.42.0
- github.com/fluxcd/pkg/ssa v0.32.0
- github.com/fluxcd/source-controller/api v1.1.0
- github.com/go-logr/logr v1.2.4
- github.com/google/go-cmp v0.5.9
- github.com/hashicorp/go-retryablehttp v0.7.4
- github.com/onsi/gomega v1.27.10
- github.com/opencontainers/go-digest v1.0.0
- github.com/opencontainers/go-digest/blake3 v0.0.0-20230815154656-802ce17c4f59
- github.com/spf13/pflag v1.0.5
- gopkg.in/yaml.v2 v2.4.0
- helm.sh/helm/v3 v3.12.3
- k8s.io/api v0.27.4
- k8s.io/apiextensions-apiserver v0.27.4
- k8s.io/apimachinery v0.27.4
- k8s.io/cli-runtime v0.27.4
- k8s.io/client-go v0.27.4
- k8s.io/utils v0.0.0-20230505201702-9f6742963106
- sigs.k8s.io/cli-utils v0.35.0
- sigs.k8s.io/controller-runtime v0.15.1
- sigs.k8s.io/kustomize/api v0.13.4
- sigs.k8s.io/yaml v1.3.0
+ github.com/Masterminds/semver/v3 v3.4.0
+ github.com/fluxcd/cli-utils v0.37.2-flux.1
+ github.com/fluxcd/helm-controller/api v1.5.0
+ github.com/fluxcd/pkg/apis/acl v0.9.0
+ github.com/fluxcd/pkg/apis/event v0.25.0
+ github.com/fluxcd/pkg/apis/kustomize v1.16.0
+ github.com/fluxcd/pkg/apis/meta v1.26.0
+ github.com/fluxcd/pkg/auth v0.40.0
+ github.com/fluxcd/pkg/cache v0.13.0
+ github.com/fluxcd/pkg/chartutil v1.23.0
+ github.com/fluxcd/pkg/runtime v0.103.0
+ github.com/fluxcd/pkg/ssa v0.70.0
+ github.com/fluxcd/pkg/testserver v0.13.0
+ github.com/fluxcd/source-controller/api v1.8.0
+ github.com/go-logr/logr v1.4.3
+ github.com/google/cel-go v0.26.1
+ github.com/google/go-cmp v0.7.0
+ github.com/hashicorp/go-retryablehttp v0.7.8
+ github.com/mitchellh/copystructure v1.2.0
+ github.com/onsi/gomega v1.39.1
+ github.com/opencontainers/go-digest v1.0.1-0.20231025023718-d50d2fec9c98
+ github.com/opencontainers/go-digest/blake3 v0.0.0-20250116041648-1e56c6daea3b
+ github.com/spf13/pflag v1.0.10
+ github.com/wI2L/jsondiff v0.7.0
+ go.uber.org/zap v1.27.1
+ golang.org/x/text v0.34.0
+ helm.sh/helm/v4 v4.1.3
+ k8s.io/api v0.35.2
+ k8s.io/apiextensions-apiserver v0.35.2
+ k8s.io/apimachinery v0.35.2
+ k8s.io/cli-runtime v0.35.2
+ k8s.io/client-go v0.35.2
+ k8s.io/kubectl v0.35.2
+ k8s.io/utils v0.0.0-20251002143259-bc988d571ff4
+ sigs.k8s.io/controller-runtime v0.23.3
+ sigs.k8s.io/kustomize/api v0.21.1
+ sigs.k8s.io/kustomize/kyaml v0.21.1
+ sigs.k8s.io/yaml v1.6.0
)
require (
- github.com/AdaLogics/go-fuzz-headers v0.0.0-20230106234847-43070de90fa1 // indirect
- github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
- github.com/BurntSushi/toml v1.2.1 // indirect
+ cel.dev/expr v0.24.0 // indirect
+ cloud.google.com/go/auth v0.18.0 // indirect
+ cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
+ cloud.google.com/go/compute/metadata v0.9.0 // indirect
+ dario.cat/mergo v1.0.1 // indirect
+ github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 // indirect
+ github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 // indirect
+ github.com/Azure/azure-sdk-for-go/sdk/containers/azcontainerregistry v0.2.3 // indirect
+ github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect
+ github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerservice/armcontainerservice v1.0.0 // indirect
+ github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
+ github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 // indirect
+ github.com/BurntSushi/toml v1.6.0 // indirect
github.com/MakeNowJust/heredoc v1.0.0 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
- github.com/Masterminds/semver/v3 v3.2.1 // indirect
- github.com/Masterminds/sprig/v3 v3.2.3 // indirect
+ github.com/Masterminds/sprig/v3 v3.3.0 // indirect
github.com/Masterminds/squirrel v1.5.4 // indirect
- github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 // indirect
+ github.com/ProtonMail/go-crypto v1.3.0 // indirect
+ github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
+ github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
+ github.com/aws/aws-sdk-go-v2 v1.41.1 // indirect
+ github.com/aws/aws-sdk-go-v2/config v1.32.7 // indirect
+ github.com/aws/aws-sdk-go-v2/credentials v1.19.7 // indirect
+ github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 // indirect
+ github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 // indirect
+ github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 // indirect
+ github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect
+ github.com/aws/aws-sdk-go-v2/service/ecr v1.55.1 // indirect
+ github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.38.9 // indirect
+ github.com/aws/aws-sdk-go-v2/service/eks v1.77.0 // indirect
+ github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 // indirect
+ github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 // indirect
+ github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 // indirect
+ github.com/aws/aws-sdk-go-v2/service/sso v1.30.9 // indirect
+ github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.13 // indirect
+ github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 // indirect
+ github.com/aws/smithy-go v1.24.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
- github.com/cespare/xxhash/v2 v2.2.0 // indirect
+ github.com/blang/semver/v4 v4.0.0 // indirect
+ github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/chai2010/gettext-go v1.0.2 // indirect
- github.com/containerd/containerd v1.7.0 // indirect
- github.com/cyphar/filepath-securejoin v0.2.4 // indirect
- github.com/davecgh/go-spew v1.1.1 // indirect
- github.com/docker/cli v23.0.1+incompatible // indirect
- github.com/docker/distribution v2.8.2+incompatible // indirect
- github.com/docker/docker v23.0.3+incompatible // indirect
- github.com/docker/docker-credential-helpers v0.7.0 // indirect
- github.com/docker/go-connections v0.4.0 // indirect
- github.com/docker/go-metrics v0.0.1 // indirect
- github.com/docker/go-units v0.5.0 // indirect
- github.com/emicklei/go-restful/v3 v3.10.1 // indirect
- github.com/evanphx/json-patch v5.6.0+incompatible // indirect
- github.com/evanphx/json-patch/v5 v5.6.0 // indirect
+ github.com/cloudflare/circl v1.6.3 // indirect
+ github.com/cyphar/filepath-securejoin v0.6.1 // indirect
+ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
+ github.com/docker/cli v29.2.0+incompatible // indirect
+ github.com/docker/docker-credential-helpers v0.9.3 // indirect
+ github.com/dylibso/observe-sdk/go v0.0.0-20240819160327-2d926c5d788a // indirect
+ github.com/emicklei/go-restful/v3 v3.13.0 // indirect
+ github.com/evanphx/json-patch v5.9.11+incompatible // indirect
+ github.com/evanphx/json-patch/v5 v5.9.11 // indirect
github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect
- github.com/fatih/color v1.13.0 // indirect
- github.com/fsnotify/fsnotify v1.6.0 // indirect
- github.com/go-errors/errors v1.4.2 // indirect
- github.com/go-gorp/gorp/v3 v3.0.5 // indirect
+ github.com/extism/go-sdk v1.7.1 // indirect
+ github.com/fatih/color v1.18.0 // indirect
+ github.com/felixge/httpsnoop v1.0.4 // indirect
+ github.com/fsnotify/fsnotify v1.9.0 // indirect
+ github.com/fxamacker/cbor/v2 v2.9.0 // indirect
+ github.com/go-errors/errors v1.5.1 // indirect
+ github.com/go-gorp/gorp/v3 v3.1.0 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
- github.com/go-logr/zapr v1.2.4 // indirect
- github.com/go-openapi/jsonpointer v0.19.6 // indirect
- github.com/go-openapi/jsonreference v0.20.1 // indirect
- github.com/go-openapi/swag v0.22.3 // indirect
+ github.com/go-logr/zapr v1.3.0 // indirect
+ github.com/go-openapi/jsonpointer v0.22.0 // indirect
+ github.com/go-openapi/jsonreference v0.21.1 // indirect
+ github.com/go-openapi/swag v0.24.1 // indirect
+ github.com/go-openapi/swag/cmdutils v0.24.0 // indirect
+ github.com/go-openapi/swag/conv v0.24.0 // indirect
+ github.com/go-openapi/swag/fileutils v0.24.0 // indirect
+ github.com/go-openapi/swag/jsonname v0.24.0 // indirect
+ github.com/go-openapi/swag/jsonutils v0.24.0 // indirect
+ github.com/go-openapi/swag/loading v0.24.0 // indirect
+ github.com/go-openapi/swag/mangling v0.24.0 // indirect
+ github.com/go-openapi/swag/netutils v0.24.0 // indirect
+ github.com/go-openapi/swag/stringutils v0.24.0 // indirect
+ github.com/go-openapi/swag/typeutils v0.24.0 // indirect
+ github.com/go-openapi/swag/yamlutils v0.24.0 // indirect
github.com/gobwas/glob v0.2.3 // indirect
- github.com/gogo/protobuf v1.3.2 // indirect
- github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
- github.com/golang/protobuf v1.5.3 // indirect
- github.com/google/btree v1.1.2 // indirect
- github.com/google/gnostic v0.6.9 // indirect
- github.com/google/gofuzz v1.2.0 // indirect
- github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
- github.com/google/uuid v1.3.0 // indirect
- github.com/gorilla/mux v1.8.0 // indirect
+ github.com/gofrs/flock v0.13.0 // indirect
+ github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
+ github.com/google/btree v1.1.3 // indirect
+ github.com/google/gnostic-models v0.7.0 // indirect
+ github.com/google/go-containerregistry v0.20.7 // indirect
+ github.com/google/s2a-go v0.1.9 // indirect
+ github.com/google/uuid v1.6.0 // indirect
+ github.com/googleapis/enterprise-certificate-proxy v0.3.11 // indirect
+ github.com/googleapis/gax-go/v2 v2.16.0 // indirect
github.com/gosuri/uitable v0.0.4 // indirect
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect
- github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
- github.com/hashicorp/go-multierror v1.1.1 // indirect
- github.com/huandu/xstrings v1.4.0 // indirect
- github.com/imdario/mergo v0.3.13 // indirect
- github.com/inconshreveable/mousetrap v1.0.1 // indirect
- github.com/jmoiron/sqlx v1.3.5 // indirect
+ github.com/huandu/xstrings v1.5.0 // indirect
+ github.com/ianlancetaylor/demangle v0.0.0-20240805132620-81f5be970eca // indirect
+ github.com/inconshreveable/mousetrap v1.1.0 // indirect
+ github.com/jmoiron/sqlx v1.4.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
- github.com/klauspost/compress v1.16.0 // indirect
+ github.com/klauspost/cpuid/v2 v2.2.5 // indirect
+ github.com/kylelemons/godebug v1.1.0 // indirect
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
github.com/lib/pq v1.10.9 // indirect
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect
- github.com/mailru/easyjson v0.7.7 // indirect
+ github.com/mailru/easyjson v0.9.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
- github.com/mattn/go-isatty v0.0.17 // indirect
+ github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.9 // indirect
- github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
- github.com/mitchellh/copystructure v1.2.0 // indirect
+ github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
- github.com/moby/locker v1.0.1 // indirect
- github.com/moby/spdystream v0.2.0 // indirect
- github.com/moby/term v0.0.0-20221205130635-1aeaba878587 // indirect
+ github.com/moby/term v0.5.2 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
- github.com/modern-go/reflect2 v1.0.2 // indirect
+ github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect
- github.com/morikuni/aec v1.0.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
- github.com/opencontainers/image-spec v1.1.0-rc2.0.20221005185240-3a7f492d3f1b // indirect
+ github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
+ github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/pkg/errors v0.9.1 // indirect
- github.com/prometheus/client_golang v1.16.0 // indirect
- github.com/prometheus/client_model v0.4.0 // indirect
- github.com/prometheus/common v0.42.0 // indirect
- github.com/prometheus/procfs v0.10.1 // indirect
- github.com/rubenv/sql-migrate v1.3.1 // indirect
+ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
+ github.com/prometheus/client_golang v1.23.2 // indirect
+ github.com/prometheus/client_model v0.6.2 // indirect
+ github.com/prometheus/common v0.67.5 // indirect
+ github.com/prometheus/procfs v0.19.2 // indirect
+ github.com/rubenv/sql-migrate v1.8.1 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
- github.com/shopspring/decimal v1.3.1 // indirect
- github.com/sirupsen/logrus v1.9.0 // indirect
- github.com/spf13/cast v1.5.0 // indirect
- github.com/spf13/cobra v1.6.1 // indirect
- github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
- github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
- github.com/xeipuuv/gojsonschema v1.2.0 // indirect
- github.com/xlab/treeprint v1.1.0 // indirect
- github.com/zeebo/blake3 v0.1.1 // indirect
- go.opentelemetry.io/otel v1.14.0 // indirect
- go.opentelemetry.io/otel/trace v1.14.0 // indirect
- go.starlark.net v0.0.0-20221028183056-acb66ad56dd2 // indirect
- go.uber.org/multierr v1.10.0 // indirect
- go.uber.org/zap v1.25.0 // indirect
- golang.org/x/crypto v0.11.0 // indirect
- golang.org/x/net v0.13.0 // indirect
- golang.org/x/oauth2 v0.5.0 // indirect
- golang.org/x/sync v0.3.0 // indirect
- golang.org/x/sys v0.10.0 // indirect
- golang.org/x/term v0.10.0 // indirect
- golang.org/x/text v0.11.0 // indirect
- golang.org/x/time v0.3.0 // indirect
- gomodules.xyz/jsonpatch/v2 v2.3.0 // indirect
- google.golang.org/appengine v1.6.7 // indirect
- google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4 // indirect
- google.golang.org/grpc v1.53.0 // indirect
- google.golang.org/protobuf v1.30.0 // indirect
+ github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect
+ github.com/shopspring/decimal v1.4.0 // indirect
+ github.com/sirupsen/logrus v1.9.3 // indirect
+ github.com/spf13/cast v1.7.0 // indirect
+ github.com/spf13/cobra v1.10.2 // indirect
+ github.com/stoewer/go-strcase v1.3.0 // indirect
+ github.com/stretchr/testify v1.11.1 // indirect
+ github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834 // indirect
+ github.com/tetratelabs/wazero v1.11.0 // indirect
+ github.com/tidwall/gjson v1.18.0 // indirect
+ github.com/tidwall/match v1.1.1 // indirect
+ github.com/tidwall/pretty v1.2.1 // indirect
+ github.com/tidwall/sjson v1.2.5 // indirect
+ github.com/x448/float16 v0.8.4 // indirect
+ github.com/xlab/treeprint v1.2.0 // indirect
+ github.com/zeebo/blake3 v0.2.3 // indirect
+ go.opentelemetry.io/auto/sdk v1.2.1 // indirect
+ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 // indirect
+ go.opentelemetry.io/otel v1.40.0 // indirect
+ go.opentelemetry.io/otel/metric v1.40.0 // indirect
+ go.opentelemetry.io/otel/trace v1.40.0 // indirect
+ go.opentelemetry.io/proto/otlp v1.9.0 // indirect
+ go.uber.org/multierr v1.11.0 // indirect
+ go.yaml.in/yaml/v2 v2.4.3 // indirect
+ go.yaml.in/yaml/v3 v3.0.4 // indirect
+ golang.org/x/crypto v0.47.0 // indirect
+ golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b // indirect
+ golang.org/x/net v0.49.0 // indirect
+ golang.org/x/oauth2 v0.34.0 // indirect
+ golang.org/x/sync v0.19.0 // indirect
+ golang.org/x/sys v0.40.0 // indirect
+ golang.org/x/term v0.39.0 // indirect
+ golang.org/x/time v0.14.0 // indirect
+ gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect
+ google.golang.org/api v0.261.0 // indirect
+ google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 // indirect
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect
+ google.golang.org/grpc v1.78.0 // indirect
+ google.golang.org/protobuf v1.36.11 // indirect
+ gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
- k8s.io/apiserver v0.27.4 // indirect
- k8s.io/component-base v0.27.4 // indirect
- k8s.io/klog/v2 v2.100.1 // indirect
- k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f // indirect
- k8s.io/kubectl v0.27.3 // indirect
- oras.land/oras-go v1.2.3 // indirect
- sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
- sigs.k8s.io/kustomize/kyaml v0.14.2 // indirect
- sigs.k8s.io/structured-merge-diff/v4 v4.3.0 // indirect
+ gotest.tools/v3 v3.4.0 // indirect
+ k8s.io/apiserver v0.35.2 // indirect
+ k8s.io/component-base v0.35.2 // indirect
+ k8s.io/klog/v2 v2.130.1 // indirect
+ k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect
+ oras.land/oras-go/v2 v2.6.0 // indirect
+ sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
+ sigs.k8s.io/randfill v1.0.0 // indirect
+ sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 // indirect
)
diff --git a/go.sum b/go.sum
index c97ac7a37..1a5dc890d 100644
--- a/go.sum
+++ b/go.sum
@@ -1,1138 +1,618 @@
-cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
-cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
-cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
-cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
-cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
-cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
-cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
-cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
-cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
-cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
-cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
-cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
-cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
-cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
-cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
-cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
-cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
-cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg=
-cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8=
-cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0=
-cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
-cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
-cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
-cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
-cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
-cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
-cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
-cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
-cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
-cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
-cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
-cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
-cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
-cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
-cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
-cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
-cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
-cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
-dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
-github.com/AdaLogics/go-fuzz-headers v0.0.0-20230106234847-43070de90fa1 h1:EKPd1INOIyr5hWOWhvpmQpY6tKjeG0hT1s3AMC/9fic=
-github.com/AdaLogics/go-fuzz-headers v0.0.0-20230106234847-43070de90fa1/go.mod h1:VzwV+t+dZ9j/H867F1M2ziD+yLHtB46oM35FxxMJ4d0=
-github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
-github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
-github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
-github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak=
-github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
-github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
-github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
+cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY=
+cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw=
+cloud.google.com/go/auth v0.18.0 h1:wnqy5hrv7p3k7cShwAU/Br3nzod7fxoqG+k0VZ+/Pk0=
+cloud.google.com/go/auth v0.18.0/go.mod h1:wwkPM1AgE1f2u6dG443MiWoD8C3BtOywNsUMcUTVDRo=
+cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
+cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
+cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
+cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
+dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
+dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
+filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
+filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
+github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk=
+github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
+github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 h1:fou+2+WFTib47nS+nz/ozhEBnvU96bKHy6LjRsY4E28=
+github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0/go.mod h1:t76Ruy8AHvUAC8GfMWJMa0ElSbuIcO03NLpynfbgsPA=
+github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4=
+github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1/go.mod h1:IYus9qsFobWIc2YVwe/WPjcnyCkPKtnHAqUYeebc8z0=
+github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY=
+github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8=
+github.com/Azure/azure-sdk-for-go/sdk/containers/azcontainerregistry v0.2.3 h1:ldKsKtEIblsgsr6mPwrd9yRntoX6uLz/K89wsldwx/k=
+github.com/Azure/azure-sdk-for-go/sdk/containers/azcontainerregistry v0.2.3/go.mod h1:MAm7bk0oDLmD8yIkvfbxPW04fxzphPyL+7GzwHxOp6Y=
+github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA=
+github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI=
+github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerservice/armcontainerservice v1.0.0 h1:figxyQZXzZQIcP3njhC68bYUiTw45J8/SsHaLW8Ax0M=
+github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerservice/armcontainerservice v1.0.0/go.mod h1:TmlMW4W5OvXOmOyKNnor8nlMMiO1ctIyzmHme/VHsrA=
+github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
+github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
+github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM=
+github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE=
+github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs=
+github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk=
+github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
+github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
+github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
+github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
-github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
-github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
-github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
-github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
-github.com/Masterminds/sprig/v3 v3.2.1/go.mod h1:UoaO7Yp8KlPnJIYWTFkMaqPUYKTfGFPhxNuwnnxkKlk=
-github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA=
-github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM=
+github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
+github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
+github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs=
+github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0=
github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM=
github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10=
-github.com/Microsoft/go-winio v0.6.0 h1:slsWYD/zyx7lCXoZVlvQrj0hPTM1HI4+v1sIda2yDvg=
-github.com/Microsoft/hcsshim v0.10.0-rc.7 h1:HBytQPxcv8Oy4244zbQbe6hnOnx544eL5QPUqhJldz8=
-github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
-github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d h1:UrqY+r/OJnIp5u0s1SbQ8dVfLCZJsnvazdBP5hS4iRs=
-github.com/a8m/expect v1.0.0/go.mod h1:4IwSCMumY49ScypDnjNbYEjgVeqy1/U2cEs3Lat96eA=
-github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
-github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
-github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
-github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
-github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
-github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
-github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
-github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
-github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
-github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 h1:4daAzAu0S6Vi7/lbWECcX0j45yZReDZ56BQsrVBOEEY=
-github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg=
-github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
-github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A=
-github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
-github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
+github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw=
+github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
+github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ=
+github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw=
+github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
+github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
+github.com/aws/aws-sdk-go-v2 v1.41.1 h1:ABlyEARCDLN034NhxlRUSZr4l71mh+T5KAeGh6cerhU=
+github.com/aws/aws-sdk-go-v2 v1.41.1/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0=
+github.com/aws/aws-sdk-go-v2/config v1.32.7 h1:vxUyWGUwmkQ2g19n7JY/9YL8MfAIl7bTesIUykECXmY=
+github.com/aws/aws-sdk-go-v2/config v1.32.7/go.mod h1:2/Qm5vKUU/r7Y+zUk/Ptt2MDAEKAfUtKc1+3U1Mo3oY=
+github.com/aws/aws-sdk-go-v2/credentials v1.19.7 h1:tHK47VqqtJxOymRrNtUXN5SP/zUTvZKeLx4tH6PGQc8=
+github.com/aws/aws-sdk-go-v2/credentials v1.19.7/go.mod h1:qOZk8sPDrxhf+4Wf4oT2urYJrYt3RejHSzgAquYeppw=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 h1:I0GyV8wiYrP8XpA70g1HBcQO1JlQxCMTW9npl5UbDHY=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17/go.mod h1:tyw7BOl5bBe/oqvoIeECFJjMdzXoa/dfVz3QQ5lgHGA=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 h1:xOLELNKGp2vsiteLsvLPwxC+mYmO6OZ8PYgiuPJzF8U=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17/go.mod h1:5M5CI3D12dNOtH3/mk6minaRwI2/37ifCURZISxA/IQ=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 h1:WWLqlh79iO48yLkj1v3ISRNiv+3KdQoZ6JWyfcsyQik=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17/go.mod h1:EhG22vHRrvF8oXSTYStZhJc1aUgKtnJe+aOiFEV90cM=
+github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=
+github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc=
+github.com/aws/aws-sdk-go-v2/service/ecr v1.55.1 h1:B7f9R99lCF83XlolTg6d6Lvghyto+/VU83ZrneAVfK8=
+github.com/aws/aws-sdk-go-v2/service/ecr v1.55.1/go.mod h1:cpYRXx5BkmS3mwWRKPbWSPKmyAUNL7aLWAPiiinwk/U=
+github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.38.9 h1:WxoqdNfGWj668u/NX7qBMPevmJu14LYNMMTRZthoclc=
+github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.38.9/go.mod h1:4oMS/bVKMnYIIBgkcHPoru4DVeMGutHv03FZUTjvsvI=
+github.com/aws/aws-sdk-go-v2/service/eks v1.77.0 h1:Z5mTpmbJKU7jEM7xoXI5tO4Nm0JUZSgVSFkpYuu6Ic0=
+github.com/aws/aws-sdk-go-v2/service/eks v1.77.0/go.mod h1:Qg678m+87sCuJhcsZojenz8mblYG+Tq86V4m3hjVz0s=
+github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E=
+github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 h1:RuNSMoozM8oXlgLG/n6WLaFGoea7/CddrCfIiSA+xdY=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17/go.mod h1:F2xxQ9TZz5gDWsclCtPQscGpP0VUOc8RqgFM3vDENmU=
+github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 h1:VrhDvQib/i0lxvr3zqlUwLwJP4fpmpyD9wYG1vfSu+Y=
+github.com/aws/aws-sdk-go-v2/service/signin v1.0.5/go.mod h1:k029+U8SY30/3/ras4G/Fnv/b88N4mAfliNn08Dem4M=
+github.com/aws/aws-sdk-go-v2/service/sso v1.30.9 h1:v6EiMvhEYBoHABfbGB4alOYmCIrcgyPPiBE1wZAEbqk=
+github.com/aws/aws-sdk-go-v2/service/sso v1.30.9/go.mod h1:yifAsgBxgJWn3ggx70A3urX2AN49Y5sJTD1UQFlfqBw=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.13 h1:gd84Omyu9JLriJVCbGApcLzVR3XtmC4ZDPcAI6Ftvds=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.13/go.mod h1:sTGThjphYE4Ohw8vJiRStAcu3rbjtXRsdNB0TvZ5wwo=
+github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 h1:5fFjR/ToSOzB2OQ/XqWpZBmNvmP/pJ1jOWYlFDJTjRQ=
+github.com/aws/aws-sdk-go-v2/service/sts v1.41.6/go.mod h1:qgFDZQSD/Kys7nJnVqYlWKnh0SSdMjAi0uSwON4wgYQ=
+github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk=
+github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
-github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
-github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM=
+github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
+github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
github.com/bshuster-repo/logrus-logstash-hook v1.0.0 h1:e+C0SB5R1pu//O4MQ3f9cFuPGoOVeF2fE4Og9otCc70=
-github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
-github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd h1:rFt+Y/IK1aEZkEHchZRSq9OQbsSzIT/OrI8YFFmRIng=
-github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b h1:otBG+dV+YK+Soembjv71DPz3uX/V/6MMlSyD9JBQ6kQ=
-github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0 h1:nvj0OLI3YqYXer/kZD8Ri1aaunCxIEsOst1BVJswV0o=
-github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
-github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
-github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
-github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/bshuster-repo/logrus-logstash-hook v1.0.0/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk=
+github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
+github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
+github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
+github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chai2010/gettext-go v1.0.2 h1:1Lwwip6Q2QGsAdl/ZKPCwTe9fe0CjlUbqj5bFNSjIRk=
github.com/chai2010/gettext-go v1.0.2/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA=
-github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
-github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
-github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
-github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
-github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
-github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
-github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
-github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
-github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM=
-github.com/containerd/containerd v1.7.0 h1:G/ZQr3gMZs6ZT0qPUZ15znx5QSdQdASW11nXTLTM2Pg=
-github.com/containerd/containerd v1.7.0/go.mod h1:QfR7Efgb/6X2BDpTPJRvPTYDE9rsF0FsXX9J8sIs/sc=
-github.com/containerd/continuity v0.3.0 h1:nisirsYROK15TAMVukJOUyGJjz4BNQJBVsNvAXZJ/eg=
-github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
-github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
-github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
-github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
-github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
-github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
-github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
-github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
-github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
-github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
+github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
+github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
+github.com/coreos/go-oidc v2.3.0+incompatible h1:+5vEsrgprdLjjQ9FzIKAzQz1wwPD+83hQRfUIPh7rO0=
+github.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc=
+github.com/coreos/go-oidc/v3 v3.17.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8=
+github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
+github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
+github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
-github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg=
-github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
+github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
+github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE=
+github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/denisenkom/go-mssqldb v0.9.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
-github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
-github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
-github.com/distribution/distribution/v3 v3.0.0-20221208165359-362910506bc2 h1:aBfCb7iqHmDEIp6fBvC/hQUddQfg+3qdYjwzaiP9Hnc=
-github.com/docker/cli v23.0.1+incompatible h1:LRyWITpGzl2C9e9uGxzisptnxAn1zfZKXy13Ul2Q5oM=
-github.com/docker/cli v23.0.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
-github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8=
-github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
-github.com/docker/docker v23.0.3+incompatible h1:9GhVsShNWz1hO//9BNg/dpMnZW25KydO4wtVxWAIbho=
-github.com/docker/docker v23.0.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
-github.com/docker/docker-credential-helpers v0.7.0 h1:xtCHsjxogADNZcdv1pKUHXryefjlVRqWqIhk/uXJp0A=
-github.com/docker/docker-credential-helpers v0.7.0/go.mod h1:rETQfLdHNT3foU5kuNkFR1R1V12OJRRO5lzt2D1b5X0=
-github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
-github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
+github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
+github.com/distribution/distribution/v3 v3.0.0 h1:q4R8wemdRQDClzoNNStftB2ZAfqOiN6UX90KJc4HjyM=
+github.com/distribution/distribution/v3 v3.0.0/go.mod h1:tRNuFoZsUdyRVegq8xGNeds4KLjwLCRin/tTo6i1DhU=
+github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
+github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
+github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
+github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
+github.com/docker/cli v29.2.0+incompatible h1:9oBd9+YM7rxjZLfyMGxjraKBKE4/nVyvVfN4qNl9XRM=
+github.com/docker/cli v29.2.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
+github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8=
+github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo=
github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c h1:+pKlWGMw7gf6bQ+oDZB4KHQFypsfjYlq/C4rfL7D3g8=
+github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA=
github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8=
github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw=
-github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
-github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
-github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1 h1:ZClxb8laGDf5arXfYcAtECDFgAgHklGI8CxgjHnXKJ4=
-github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
-github.com/emicklei/go-restful/v3 v3.10.1 h1:rc42Y5YTp7Am7CS630D7JmhRjq4UlEUuEKfrDac4bSQ=
-github.com/emicklei/go-restful/v3 v3.10.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
-github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
-github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
-github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
-github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
-github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
-github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
-github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
-github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
-github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U=
-github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
-github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJCLunww=
-github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4=
+github.com/dylibso/observe-sdk/go v0.0.0-20240819160327-2d926c5d788a h1:UwSIFv5g5lIvbGgtf3tVwC7Ky9rmMFBp0RMs+6f6YqE=
+github.com/dylibso/observe-sdk/go v0.0.0-20240819160327-2d926c5d788a/go.mod h1:C8DzXehI4zAbrdlbtOByKX6pfivJTBiV9Jjqv56Yd9Q=
+github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes=
+github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
+github.com/evanphx/json-patch v5.9.11+incompatible h1:ixHHqfcGvxhWkniF1tWxBHA0yb4Z+d1UQi45df52xW8=
+github.com/evanphx/json-patch v5.9.11+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
+github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU=
+github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM=
github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f h1:Wl78ApPPB2Wvf/TIe2xdyJxTlb6obmF18d8QdkxNDu4=
github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f/go.mod h1:OSYXu++VVOHnXeitef/D8n/6y4QV8uLHSFXX4NeXMGc=
-github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
-github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
-github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
-github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
-github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk=
-github.com/flowstack/go-jsonschema v0.1.1/go.mod h1:yL7fNggx1o8rm9RlgXv7hTBWxdBM0rVwpMwimd3F3N0=
-github.com/fluxcd/pkg/apis/acl v0.1.0 h1:EoAl377hDQYL3WqanWCdifauXqXbMyFuK82NnX6pH4Q=
-github.com/fluxcd/pkg/apis/acl v0.1.0/go.mod h1:zfEZzz169Oap034EsDhmCAGgnWlcWmIObZjYMusoXS8=
-github.com/fluxcd/pkg/apis/event v0.5.2 h1:WtnCOeWglf7wR3dpyiWxb1JtYkw1G5OXcERb1QopFpA=
-github.com/fluxcd/pkg/apis/event v0.5.2/go.mod h1:5l6SSxVTkqrXrYjgEqAajOOHkl4x0TPocAuSdu+3AEs=
-github.com/fluxcd/pkg/apis/kustomize v1.1.1 h1:MSGn4z0R9PptmoPFHnx2nEZ8Jtl1sKfw0cuDQY2HYwM=
-github.com/fluxcd/pkg/apis/kustomize v1.1.1/go.mod h1:0pCu0ecIY+ZM0iE/hOHYwCMZ3b0SpBrjJ1SH3FFyYdE=
-github.com/fluxcd/pkg/apis/meta v1.1.2 h1:Unjo7hxadtB2dvGpeFqZZUdsjpRA08YYSBb7dF2WIAM=
-github.com/fluxcd/pkg/apis/meta v1.1.2/go.mod h1:BHQyRHCskGMEDf6kDGbgQ+cyiNpUHbLsCOsaMYM2maI=
-github.com/fluxcd/pkg/runtime v0.42.0 h1:a5DQ/f90YjoHBmiXZUpnp4bDSLORjInbmqP7K11L4uY=
-github.com/fluxcd/pkg/runtime v0.42.0/go.mod h1:p6A3xWVV8cKLLQW0N90GehKgGMMmbNYv+OSJ/0qB0vg=
-github.com/fluxcd/pkg/ssa v0.32.0 h1:RBqs9DNrbJkFHjpfsiKilyean7gwqWFspSBTLOaBIHs=
-github.com/fluxcd/pkg/ssa v0.32.0/go.mod h1:+Kf5euYAbvgJX645bo+IL7V/NlH0X7kGgFTr1W++I3c=
-github.com/fluxcd/source-controller/api v1.1.0 h1:JPtt9WTTqVNdJfPpea8q7fUWF/00kDihxbhISzcb0WE=
-github.com/fluxcd/source-controller/api v1.1.0/go.mod h1:ZLkaUd1KQIjtLPCvO63Ni5zpnSTVBAkeRgFBzMItbDQ=
-github.com/foxcpp/go-mockdns v1.0.0 h1:7jBqxd3WDWwi/6WhDvacvH1XsN3rOLXyHM1uhvIx6FI=
-github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
-github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps=
-github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
-github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
-github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
-github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
-github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
-github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
-github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
-github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
-github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
-github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
-github.com/go-gorp/gorp/v3 v3.0.5 h1:PUjzYdYu3HBOh8LE+UUmRG2P0IRDak9XMeGNvaeq4Ow=
-github.com/go-gorp/gorp/v3 v3.0.5/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpjH8TixEw=
-github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
-github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
-github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
-github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
-github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
+github.com/extism/go-sdk v1.7.1 h1:lWJos6uY+tRFdlIHR+SJjwFDApY7OypS/2nMhiVQ9Sw=
+github.com/extism/go-sdk v1.7.1/go.mod h1:IT+Xdg5AZM9hVtpFUA+uZCJMge/hbvshl8bwzLtFyKA=
+github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
+github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
+github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
+github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
+github.com/fluxcd/cli-utils v0.37.2-flux.1 h1:tQ588ghtRN+E+kHq415FddfqA9v4brn/1WWgrP6rQR0=
+github.com/fluxcd/cli-utils v0.37.2-flux.1/go.mod h1:LcWSu1NYET8d8U7O326RhEm5JkQXCMK6ITu4G1CT02c=
+github.com/fluxcd/helm/v4 v4.1.4-flux.1 h1:ntm0bY/1VrqfsJbxlmAVxryGJoFcM5AySHiow/K2268=
+github.com/fluxcd/helm/v4 v4.1.4-flux.1/go.mod h1:5dSo8rRgn3OTkDAc/k0Ipw5/Q+BlqKIKZwa0XwSiINI=
+github.com/fluxcd/pkg/apis/acl v0.9.0 h1:wBpgsKT+jcyZEcM//OmZr9RiF8klL3ebrDp2u2ThsnA=
+github.com/fluxcd/pkg/apis/acl v0.9.0/go.mod h1:TttNS+gocsGLwnvmgVi3/Yscwqrjc17+vhgYfqkfrV4=
+github.com/fluxcd/pkg/apis/event v0.25.0 h1:zdwytvDhG+fk+Ywl5DOtv7TklkrVgM21WHm1f+YhleE=
+github.com/fluxcd/pkg/apis/event v0.25.0/go.mod h1:TlK8HWYrTwl0raqBRC+ROoNpYW5fdVnwcwOBOx5Kzw8=
+github.com/fluxcd/pkg/apis/kustomize v1.16.0 h1:PhWXEhqQqsisIpwp1/wHvTvo+MO+GGzsBPoN0ZnRE3Y=
+github.com/fluxcd/pkg/apis/kustomize v1.16.0/go.mod h1:IZOy4CCtR/hxMGb7erK1RfbGnczVv4/dRBoVD37AywI=
+github.com/fluxcd/pkg/apis/meta v1.26.0 h1:dxP1FfBpTCYso6odzRcltVnnRuBb2VyhhgV0VX9YbUE=
+github.com/fluxcd/pkg/apis/meta v1.26.0/go.mod h1:c7o6mJGLCMvNrfdinGZehkrdZuFT9vZdZNrn66DtVD0=
+github.com/fluxcd/pkg/auth v0.40.0 h1:p6Kw6KH+z8oRqngKhmTt8ILKD/rC+8tP87a//kLZhi8=
+github.com/fluxcd/pkg/auth v0.40.0/go.mod h1:Oq/hIEKUMTbL2bv5blf+EhC/jXXJLsOjIMtJj/AtG3Y=
+github.com/fluxcd/pkg/cache v0.13.0 h1:MqtlgOwIVcGKKgV422e39O+KFSVMWuExKeRaMDBjJlk=
+github.com/fluxcd/pkg/cache v0.13.0/go.mod h1:0xRZ1hitrIFQ6pl68ke2wZLbIqA2VLzY78HpDo9DVxs=
+github.com/fluxcd/pkg/chartutil v1.23.0 h1:ohstQEVnrBIbN85FGu83hnmAohLl0PdOoPlsM6+cjyI=
+github.com/fluxcd/pkg/chartutil v1.23.0/go.mod h1:kFhmD6DwBgRsvC1ilINsomargMi2WbqvSndWQLikkLc=
+github.com/fluxcd/pkg/runtime v0.103.0 h1:J5y5GPhWdkyqIUBlaI1FP2N02TtZmsjbWhhZubuTSFk=
+github.com/fluxcd/pkg/runtime v0.103.0/go.mod h1:mbo2f3azo3yVQgm7XZGxQB6/2zvzQ5Wgtd8TjRRwwAw=
+github.com/fluxcd/pkg/ssa v0.70.0 h1:IBylYPiTK1IEdCC2DvjKXIhwQcbd5VufXA9WS3zO+tE=
+github.com/fluxcd/pkg/ssa v0.70.0/go.mod h1:6igtlt7/zF+nNFQpa5ZAkkvtpL6o36NRU39/PqqC+Bg=
+github.com/fluxcd/pkg/testserver v0.13.0 h1:xEpBcEYtD7bwvZ+i0ZmChxKkDo/wfQEV3xmnzVybSSg=
+github.com/fluxcd/pkg/testserver v0.13.0/go.mod h1:akRYv3FLQUsme15na9ihECRG6hBuqni4XEY9W8kzs8E=
+github.com/fluxcd/source-controller/api v1.8.0 h1:ndrYmcv6ZMcdQHFSUkOrFVDO7h16SfDBSw/DOqf/LPo=
+github.com/fluxcd/source-controller/api v1.8.0/go.mod h1:1O7+sMbqc1+3tPvjmtgFz+bASTl794Y9SxpebHDDSGA=
+github.com/foxcpp/go-mockdns v1.2.0 h1:omK3OrHRD1IWJz1FuFBCFquhXslXoF17OvBS6JPzZF0=
+github.com/foxcpp/go-mockdns v1.2.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk=
+github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
+github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
+github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
+github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
+github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
+github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
+github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk=
+github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
+github.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs=
+github.com/go-gorp/gorp/v3 v3.1.0/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpjH8TixEw=
+github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
+github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
-github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
-github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
+github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
+github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
-github.com/go-logr/zapr v1.2.4 h1:QHVo+6stLbfJmYGkQ7uGHUCu5hnAFAj6mDe6Ea0SeOo=
-github.com/go-logr/zapr v1.2.4/go.mod h1:FyHWQIzQORZ0QVE1BtVHv3cKtNLuXsbNLtpuhNapBOA=
-github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE=
-github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
-github.com/go-openapi/jsonreference v0.20.1 h1:FBLnyygC4/IZZr893oiomc9XaghoveYTrLC1F86HID8=
-github.com/go-openapi/jsonreference v0.20.1/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
-github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g=
-github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
-github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
-github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
-github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
-github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
-github.com/gobuffalo/logger v1.0.6 h1:nnZNpxYo0zx+Aj9RfMPBm+x9zAU2OayFh/xrAWi34HU=
-github.com/gobuffalo/logger v1.0.6/go.mod h1:J31TBEHR1QLV2683OXTAItYIg8pv2JMHnF/quuAbMjs=
-github.com/gobuffalo/packd v1.0.1 h1:U2wXfRr4E9DH8IdsDLlRFwTZTK7hLfq9qT/QHXGVe/0=
-github.com/gobuffalo/packd v1.0.1/go.mod h1:PP2POP3p3RXGz7Jh6eYEf93S7vA2za6xM7QT85L4+VY=
-github.com/gobuffalo/packr/v2 v2.8.3 h1:xE1yzvnO56cUC0sTpKR3DIbxZgB54AftTFMhB2XEWlY=
-github.com/gobuffalo/packr/v2 v2.8.3/go.mod h1:0SahksCVcx4IMnigTjiFuyldmTrdTctXsOdiU5KwbKc=
+github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ=
+github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg=
+github.com/go-openapi/jsonpointer v0.22.0 h1:TmMhghgNef9YXxTu1tOopo+0BGEytxA+okbry0HjZsM=
+github.com/go-openapi/jsonpointer v0.22.0/go.mod h1:xt3jV88UtExdIkkL7NloURjRQjbeUgcxFblMjq2iaiU=
+github.com/go-openapi/jsonreference v0.21.1 h1:bSKrcl8819zKiOgxkbVNRUBIr6Wwj9KYrDbMjRs0cDA=
+github.com/go-openapi/jsonreference v0.21.1/go.mod h1:PWs8rO4xxTUqKGu+lEvvCxD5k2X7QYkKAepJyCmSTT8=
+github.com/go-openapi/swag v0.24.1 h1:DPdYTZKo6AQCRqzwr/kGkxJzHhpKxZ9i/oX0zag+MF8=
+github.com/go-openapi/swag v0.24.1/go.mod h1:sm8I3lCPlspsBBwUm1t5oZeWZS0s7m/A+Psg0ooRU0A=
+github.com/go-openapi/swag/cmdutils v0.24.0 h1:KlRCffHwXFI6E5MV9n8o8zBRElpY4uK4yWyAMWETo9I=
+github.com/go-openapi/swag/cmdutils v0.24.0/go.mod h1:uxib2FAeQMByyHomTlsP8h1TtPd54Msu2ZDU/H5Vuf8=
+github.com/go-openapi/swag/conv v0.24.0 h1:ejB9+7yogkWly6pnruRX45D1/6J+ZxRu92YFivx54ik=
+github.com/go-openapi/swag/conv v0.24.0/go.mod h1:jbn140mZd7EW2g8a8Y5bwm8/Wy1slLySQQ0ND6DPc2c=
+github.com/go-openapi/swag/fileutils v0.24.0 h1:U9pCpqp4RUytnD689Ek/N1d2N/a//XCeqoH508H5oak=
+github.com/go-openapi/swag/fileutils v0.24.0/go.mod h1:3SCrCSBHyP1/N+3oErQ1gP+OX1GV2QYFSnrTbzwli90=
+github.com/go-openapi/swag/jsonname v0.24.0 h1:2wKS9bgRV/xB8c62Qg16w4AUiIrqqiniJFtZGi3dg5k=
+github.com/go-openapi/swag/jsonname v0.24.0/go.mod h1:GXqrPzGJe611P7LG4QB9JKPtUZ7flE4DOVechNaDd7Q=
+github.com/go-openapi/swag/jsonutils v0.24.0 h1:F1vE1q4pg1xtO3HTyJYRmEuJ4jmIp2iZ30bzW5XgZts=
+github.com/go-openapi/swag/jsonutils v0.24.0/go.mod h1:vBowZtF5Z4DDApIoxcIVfR8v0l9oq5PpYRUuteVu6f0=
+github.com/go-openapi/swag/loading v0.24.0 h1:ln/fWTwJp2Zkj5DdaX4JPiddFC5CHQpvaBKycOlceYc=
+github.com/go-openapi/swag/loading v0.24.0/go.mod h1:gShCN4woKZYIxPxbfbyHgjXAhO61m88tmjy0lp/LkJk=
+github.com/go-openapi/swag/mangling v0.24.0 h1:PGOQpViCOUroIeak/Uj/sjGAq9LADS3mOyjznmHy2pk=
+github.com/go-openapi/swag/mangling v0.24.0/go.mod h1:Jm5Go9LHkycsz0wfoaBDkdc4CkpuSnIEf62brzyCbhc=
+github.com/go-openapi/swag/netutils v0.24.0 h1:Bz02HRjYv8046Ycg/w80q3g9QCWeIqTvlyOjQPDjD8w=
+github.com/go-openapi/swag/netutils v0.24.0/go.mod h1:WRgiHcYTnx+IqfMCtu0hy9oOaPR0HnPbmArSRN1SkZM=
+github.com/go-openapi/swag/stringutils v0.24.0 h1:i4Z/Jawf9EvXOLUbT97O0HbPUja18VdBxeadyAqS1FM=
+github.com/go-openapi/swag/stringutils v0.24.0/go.mod h1:5nUXB4xA0kw2df5PRipZDslPJgJut+NjL7D25zPZ/4w=
+github.com/go-openapi/swag/typeutils v0.24.0 h1:d3szEGzGDf4L2y1gYOSSLeK6h46F+zibnEas2Jm/wIw=
+github.com/go-openapi/swag/typeutils v0.24.0/go.mod h1:q8C3Kmk/vh2VhpCLaoR2MVWOGP8y7Jc8l82qCTd1DYI=
+github.com/go-openapi/swag/yamlutils v0.24.0 h1:bhw4894A7Iw6ne+639hsBNRHg9iZg/ISrOVr+sJGp4c=
+github.com/go-openapi/swag/yamlutils v0.24.0/go.mod h1:DpKv5aYuaGm/sULePoeiG8uwMpZSfReo1HR3Ik0yaG8=
+github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
+github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
+github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
+github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
-github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
-github.com/godror/godror v0.24.2/go.mod h1:wZv/9vPiUib6tkoDl+AZ/QLf5YZgMravZ7jxH2eQWAE=
-github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
-github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
-github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
-github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
-github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
-github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
-github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
-github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
-github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
-github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
-github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
-github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
-github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
-github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
-github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
-github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
-github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
-github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
-github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
-github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
-github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
-github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
-github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
-github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
-github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
-github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
-github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
-github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
-github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
-github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
-github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
-github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
-github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
-github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
-github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
-github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=
-github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
-github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
-github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
-github.com/gomodule/redigo v1.8.2 h1:H5XSIre1MB5NbPYFp+i1NBbb5qN1W8Y8YAQoAYbkm8k=
-github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
-github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
-github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
-github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
-github.com/google/gnostic v0.6.9 h1:ZK/5VhkoX835RikCHpSUJV9a+S3e1zLh59YnyWeBW+0=
-github.com/google/gnostic v0.6.9/go.mod h1:Nm8234We1lq6iB9OmlgNv3nH91XLLVZHCDayfA3xq+E=
-github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
-github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
-github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
-github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=
+github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0=
+github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
+github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
+github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
+github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
+github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
+github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
+github.com/google/cel-go v0.26.1 h1:iPbVVEdkhTX++hpe3lzSk7D3G3QSYqLGoHOcEio+UXQ=
+github.com/google/cel-go v0.26.1/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PUIaryMM=
+github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo=
+github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
-github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
-github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
+github.com/google/go-containerregistry v0.20.7 h1:24VGNpS0IwrOZ2ms2P1QE3Xa5X9p4phx0aUgzYzHW6I=
+github.com/google/go-containerregistry v0.20.7/go.mod h1:Lx5LCZQjLH1QBaMPeGwsME9biPeo1lPx6lbGj/UmzgM=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
-github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
-github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
-github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
-github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
-github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
-github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
-github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
-github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
-github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
-github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
-github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
-github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
-github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
-github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
-github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec=
-github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
-github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
-github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
-github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
-github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
-github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
-github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
-github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4=
-github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
-github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
-github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
-github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
+github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 h1:z2ogiKUYzX5Is6zr/vP9vJGqPwcdqsWjOt+V8J7+bTc=
+github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI=
+github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
+github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/googleapis/enterprise-certificate-proxy v0.3.11 h1:vAe81Msw+8tKUxi2Dqh/NZMz7475yUvmRIkXr4oN2ao=
+github.com/googleapis/enterprise-certificate-proxy v0.3.11/go.mod h1:RFV7MUdlb7AgEq2v7FmMCfeSMCllAzWxFgRdusoGks8=
+github.com/googleapis/gax-go/v2 v2.16.0 h1:iHbQmKLLZrexmb0OSsNGTeSTS0HO4YvFOG8g5E4Zd0Y=
+github.com/googleapis/gax-go/v2 v2.16.0/go.mod h1:o1vfQjjNZn4+dPnRdl/4ZD7S9414Y4xA+a/6Icj6l14=
+github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE=
+github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w=
+github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
+github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/gosuri/uitable v0.0.4 h1:IG2xLKRvErL3uhY6e1BylFzG+aJiwQviDDTfOKeKTpY=
github.com/gosuri/uitable v0.0.4/go.mod h1:tKR86bXuXPZazfOTG1FIzvjIdXzd0mo4Vtn16vt0PJo=
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA=
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
-github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
-github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
-github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
-github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
-github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
-github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
-github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
-github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
-github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
-github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 h1:X+2YciYSxvMQK0UZ7sg45ZVabVZBeBuvMkmuI2V3Fak=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7/go.mod h1:lW34nIZuQ8UDPdkon5fmfp2l3+ZkQ2me/+oecHYLOII=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
-github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI=
-github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
-github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
-github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
-github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
-github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
-github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
-github.com/hashicorp/go-retryablehttp v0.7.4 h1:ZQgVdpTdAL7WpMIwLzCfbalOcSUdkDZnpUv3/+BxzFA=
-github.com/hashicorp/go-retryablehttp v0.7.4/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8=
-github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
-github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
-github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
-github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
-github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
-github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
-github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
-github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
-github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
-github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
-github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
-github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
-github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
-github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
-github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
-github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
-github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
-github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU=
-github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
-github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
-github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
-github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
-github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk=
-github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg=
-github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
-github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc=
-github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
-github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
-github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
-github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=
-github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
+github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
+github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
+github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48=
+github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw=
+github.com/hashicorp/golang-lru/arc/v2 v2.0.5 h1:l2zaLDubNhW4XO3LnliVj0GXO3+/CGNJAg1dcN2Fpfw=
+github.com/hashicorp/golang-lru/arc/v2 v2.0.5/go.mod h1:ny6zBSQZi2JxIeYcv7kt2sH2PXJtirBN7RDhRpxPkxU=
+github.com/hashicorp/golang-lru/v2 v2.0.5 h1:wW7h1TG88eUIJ2i69gaE3uNVtEPIagzhGvHgwfx2Vm4=
+github.com/hashicorp/golang-lru/v2 v2.0.5/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
+github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
+github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
+github.com/ianlancetaylor/demangle v0.0.0-20240805132620-81f5be970eca h1:T54Ema1DU8ngI+aef9ZhAhNGQhcRTrWxVeG07F+c/Rw=
+github.com/ianlancetaylor/demangle v0.0.0-20240805132620-81f5be970eca/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw=
+github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
+github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
+github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
+github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
-github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
-github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
-github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
-github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
-github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
-github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
-github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
-github.com/karrick/godirwalk v1.16.1 h1:DynhcF+bztK8gooS0+NDJFrdNZjJ3gzVzC545UNA9iw=
-github.com/karrick/godirwalk v1.16.1/go.mod h1:j4mkqPuvaLI8mp1DroR3P6ad7cyYd4c1qeJ3RV7ULlk=
-github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
-github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
-github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
-github.com/klauspost/compress v1.16.0 h1:iULayQNOReoYUe+1qtKOqw9CwJv3aNQu8ivo7lw1HU4=
-github.com/klauspost/compress v1.16.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
-github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
-github.com/kortschak/utter v1.0.1/go.mod h1:vSmSjbyrlKjjsL71193LmzBOKgwePk9DH6uFaWHIInc=
-github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
-github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
-github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
-github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
-github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
-github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
+github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU=
+github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k=
+github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
+github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
+github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
+github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg=
+github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
-github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
-github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
+github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw=
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o=
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk=
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw=
-github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
-github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0=
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE=
-github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
-github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
-github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
-github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
-github.com/markbates/errx v1.1.0 h1:QDFeR+UP95dO12JgW+tgi2UVfo0V8YBHiUIOaeBPiEI=
-github.com/markbates/errx v1.1.0/go.mod h1:PLa46Oex9KNbVDZhKel8v1OT7hD5JZ2eI7AHhA0wswc=
-github.com/markbates/oncer v1.0.0 h1:E83IaVAHygyndzPimgUYJjbshhDTALZyXxvk9FOlQRY=
-github.com/markbates/oncer v1.0.0/go.mod h1:Z59JA581E9GP6w96jai+TGqafHPW+cPfRxz2aSZ0mcI=
-github.com/markbates/safe v1.0.1 h1:yjZkbvRM6IzKj9tlu/zMJLS0n/V351OZWRnF3QfaUxI=
-github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0=
-github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
-github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
-github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
+github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
+github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
-github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
-github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
-github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
-github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
-github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
-github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
-github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
-github.com/mattn/go-oci8 v0.1.1/go.mod h1:wjDx6Xm9q7dFtHJvIlrI99JytznLw5wQ4R+9mNXJwGI=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
-github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
-github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI=
-github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
-github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
-github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
-github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
-github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
-github.com/miekg/dns v1.1.25 h1:dFwPR6SfLtrSwgDcIq2bcU/gVutB4sNApq2HBdqcakg=
-github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
-github.com/mitchellh/cli v1.1.5/go.mod h1:v8+iFts2sPIKUV1ltktPXMCC8fumSKFItNcD2cLtRR4=
-github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
+github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
+github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
+github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM=
+github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk=
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
-github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
+github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
-github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
-github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
-github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
-github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
-github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
-github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
-github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
-github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg=
-github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc=
-github.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8=
-github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c=
-github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78=
-github.com/moby/term v0.0.0-20221205130635-1aeaba878587 h1:HfkjXDfhgVaN5rmueG8cL8KKeFNecRCXFhaJ2qZ5SKA=
-github.com/moby/term v0.0.0-20221205130635-1aeaba878587/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
+github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
+github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
-github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
-github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
-github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8=
+github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0=
github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4=
-github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
-github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
-github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
-github.com/nelsam/hel/v2 v2.3.2/go.mod h1:1ZTGfU2PFTOd5mx22i5O0Lc2GY933lQ2wb/ggy+rL3w=
-github.com/nelsam/hel/v2 v2.3.3/go.mod h1:1ZTGfU2PFTOd5mx22i5O0Lc2GY933lQ2wb/ggy+rL3w=
-github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
-github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
-github.com/onsi/ginkgo/v2 v2.11.0 h1:WgqUCUt/lT6yXoQ8Wef0fsNn5cAuMK7+KT9UFRz2tcU=
-github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI=
-github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M=
-github.com/opencontainers/go-digest v1.0.1-0.20220411205349-bde1400a84be h1:f2PlhC9pm5sqpBZFvnAoKj+KzXRzbjFMA+TqXfJdgho=
-github.com/opencontainers/go-digest v1.0.1-0.20220411205349-bde1400a84be/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
-github.com/opencontainers/go-digest/blake3 v0.0.0-20230815154656-802ce17c4f59 h1:PHIYPK2sf+Wfnsy6Sj8oHjLmPpbybrYBjxzSZckHjDQ=
-github.com/opencontainers/go-digest/blake3 v0.0.0-20230815154656-802ce17c4f59/go.mod h1:jzLYw+a3sNsnN6aHKFejdYQRlfOsoGQEL2b8eTMKk7I=
-github.com/opencontainers/image-spec v1.1.0-rc2.0.20221005185240-3a7f492d3f1b h1:YWuSjZCQAPM8UUBLkYUk1e+rZcvWHJmFb6i6rM44Xs8=
-github.com/opencontainers/image-spec v1.1.0-rc2.0.20221005185240-3a7f492d3f1b/go.mod h1:3OVijpioIKYWTqjiG0zfF6wvoJ4fAXGbjdZuI2NgsRQ=
-github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
-github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
-github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
+github.com/onsi/ginkgo/v2 v2.28.1 h1:S4hj+HbZp40fNKuLUQOYLDgZLwNUVn19N3Atb98NCyI=
+github.com/onsi/ginkgo/v2 v2.28.1/go.mod h1:CLtbVInNckU3/+gC8LzkGUb9oF+e8W8TdUsxPwvdOgE=
+github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28=
+github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg=
+github.com/opencontainers/go-digest v1.0.1-0.20231025023718-d50d2fec9c98 h1:H55sU3giNgBkIvmAo0vI/AAFwVTwfWsf6MN3+9H6U8o=
+github.com/opencontainers/go-digest v1.0.1-0.20231025023718-d50d2fec9c98/go.mod h1:RqnyioA3pIEZMkSbOIcrw32YSgETfn/VrLuEikEdPNU=
+github.com/opencontainers/go-digest/blake3 v0.0.0-20250116041648-1e56c6daea3b h1:nAiL9bmUK4IzFrKoVMRykv0iYGdoit5vpbPaVCZ+fI4=
+github.com/opencontainers/go-digest/blake3 v0.0.0-20250116041648-1e56c6daea3b/go.mod h1:kqQaIc6bZstKgnGpL7GD5dWoLKbA6mH1Y9ULjGImBnM=
+github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
+github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI=
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
-github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 h1:Ii+DKncOVM8Cu1Hc+ETb5K+23HdAMvESYE3ZJ5b5cMI=
-github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
-github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
-github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
+github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
-github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
-github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
-github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=
-github.com/poy/onpar v0.0.0-20200406201722-06f95a1c68e8/go.mod h1:nSbFQvMj97ZyhFRSJYtut+msi4sOY6zJDGCdSc+/rZU=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/poy/onpar v1.1.2 h1:QaNrNiZx0+Nar5dLgTVp5mXkyoVFIbepjyEoGSnhbAY=
github.com/poy/onpar v1.1.2/go.mod h1:6X8FLNoxyr9kkmnlqpK6LSoiOtrO6MICtWwEuWkLjzg=
-github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
-github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
-github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
-github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g=
-github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8=
-github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc=
-github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
-github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
-github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
-github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY=
-github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU=
-github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
-github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
-github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
-github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc=
-github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM=
-github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc=
-github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
-github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
-github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
-github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
-github.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+PymziUAg=
-github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM=
-github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
-github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
-github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
-github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
-github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
-github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
-github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
-github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
-github.com/rubenv/sql-migrate v1.3.1 h1:Vx+n4Du8X8VTYuXbhNxdEUoh6wiJERA0GlWocR5FrbA=
-github.com/rubenv/sql-migrate v1.3.1/go.mod h1:YzG/Vh82CwyhTFXy+Mf5ahAiiEOpAlHurg+23VEzcsk=
-github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
+github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
+github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
+github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
+github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
+github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
+github.com/prometheus/otlptranslator v1.0.0 h1:s0LJW/iN9dkIH+EnhiD3BlkkP5QVIUVEoIwkU+A6qos=
+github.com/prometheus/otlptranslator v1.0.0/go.mod h1:vRYWnXvI6aWGpsdY/mOT/cbeVRBlPWtBNDb7kGR3uKM=
+github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
+github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
+github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5 h1:EaDatTxkdHG+U3Bk4EUr+DZ7fOGwTfezUiUJMaIcaho=
+github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5/go.mod h1:fyalQWdtzDBECAQFBJuQe5bzQ02jGd5Qcbgb97Flm7U=
+github.com/redis/go-redis/extra/redisotel/v9 v9.0.5 h1:EfpWLLCyXw8PSM2/XNJLjI3Pb27yVE+gIAfeqp8LUCc=
+github.com/redis/go-redis/extra/redisotel/v9 v9.0.5/go.mod h1:WZjPDy7VNzn77AAfnAfVjZNvfJTYfPetfZk5yoSTLaQ=
+github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM=
+github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA=
+github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
+github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
+github.com/rubenv/sql-migrate v1.8.1 h1:EPNwCvjAowHI3TnZ+4fQu3a915OpnQoPAjTXCGOy2U0=
+github.com/rubenv/sql-migrate v1.8.1/go.mod h1:BTIKBORjzyxZDS6dzoiw6eAFYJ1iNlGAtjn4LGeVjS8=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
-github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
-github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
-github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
-github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
-github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
-github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
-github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
-github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
-github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
-github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
-github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
-github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
-github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
-github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
-github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
-github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
-github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
-github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
-github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
-github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
-github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=
-github.com/spf13/cobra v0.0.6/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
-github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk=
-github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA=
-github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY=
-github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
-github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
-github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
-github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
-github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
-github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
-github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns=
-github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8=
+github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ=
+github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
+github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ=
+github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
+github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
+github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
+github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
+github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
+github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w=
+github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
+github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
+github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
+github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
+github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs=
+github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
-github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
+github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
-github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
-github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
-github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
-github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
-github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
-github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
-github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
-github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
-github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
-github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
-github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
-github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
-github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
-github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
-github.com/xlab/treeprint v1.1.0 h1:G/1DjNkPpfZCFt9CSh6b5/nY4VimlbHF3Rh4obvtzDk=
-github.com/xlab/treeprint v1.1.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0=
-github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
-github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
-github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
-github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
+github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
+github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834 h1:ZF+QBjOI+tILZjBaFj3HgFonKXUcwgJ4djLb6i42S3Q=
+github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834/go.mod h1:m9ymHTgNSEjuxvw8E7WWe4Pl4hZQHXONY8wE6dMLaRk=
+github.com/tetratelabs/wazero v1.11.0 h1:+gKemEuKCTevU4d7ZTzlsvgd1uaToIDtlQlmNbwqYhA=
+github.com/tetratelabs/wazero v1.11.0/go.mod h1:eV28rsN8Q+xwjogd7f4/Pp4xFxO7uOGbLcD/LzB1wiU=
+github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
+github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
+github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
+github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
+github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
+github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
+github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
+github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
+github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
+github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
+github.com/wI2L/jsondiff v0.7.0 h1:1lH1G37GhBPqCfp/lrs91rf/2j3DktX6qYAKZkLuCQQ=
+github.com/wI2L/jsondiff v0.7.0/go.mod h1:KAEIojdQq66oJiHhDyQez2x+sRit0vIzC9KeK0yizxM=
+github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
+github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
+github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ=
+github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
-github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
-github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
-github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
-github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43 h1:+lm10QQTNSBd8DVTNGHx7o/IKu9HYDvLMffDhbyLccI=
-github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50 h1:hlE8//ciYMztlGpl/VA+Zm1AcTPHYkHJPbHqE6WJUXE=
-github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f h1:ERexzlUfuTvpE74urLSbIQW0Z/6hF9t8U4NsJLaioAY=
github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY=
github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
-github.com/zeebo/blake3 v0.1.1 h1:Nbsts7DdKThRHHd+YNlqiGlRqGEF2bE2eXN+xQ1hsEs=
-github.com/zeebo/blake3 v0.1.1/go.mod h1:G9pM4qQwjRzF1/v7+vabMj/c5mWpGZ2Wzo3Eb4z0pb4=
-github.com/zeebo/pcg v1.0.0 h1:dt+dx+HvX8g7Un32rY9XWoYnd0NmKmrIzpHF7qiTDj0=
-github.com/zeebo/pcg v1.0.0/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
-go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
-go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=
-go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
-go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ=
-go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
-go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
-go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
-go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
-go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
-go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
-go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
-go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
-go.opentelemetry.io/otel v1.14.0 h1:/79Huy8wbf5DnIPhemGB+zEPVwnN6fuQybr/SRXa6hM=
-go.opentelemetry.io/otel v1.14.0/go.mod h1:o4buv+dJzx8rohcUeRmWUZhqupFvzWis188WlggnNeU=
-go.opentelemetry.io/otel/trace v1.14.0 h1:wp2Mmvj41tDsyAJXiWDWpfNsOiIyd38fy85pyKcFq/M=
-go.opentelemetry.io/otel/trace v1.14.0/go.mod h1:8avnQLK+CG77yNLUae4ea2JDQ6iT+gozhnZjy/rw9G8=
-go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
-go.starlark.net v0.0.0-20221028183056-acb66ad56dd2 h1:5/KzhcSqd4UgY51l17r7C5g/JiE6DRw1Vq7VJfQHuMc=
-go.starlark.net v0.0.0-20221028183056-acb66ad56dd2/go.mod h1:kIVgS18CjmEC3PqMd5kaJSGEifyV/CeB9x506ZJ1Vbk=
-go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
-go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
-go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
-go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=
-go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
-go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
-go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
-go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
-go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
-go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
-go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg=
-go.uber.org/zap v1.25.0 h1:4Hvk6GtkucQ790dqmj7l1eEnRdKm3k3ZUrUMS2d5+5c=
-go.uber.org/zap v1.25.0/go.mod h1:JIAUzQIH94IC4fOJQm7gMmBJP5k7wQfdcnYdPoEXJYk=
-golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
-golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+github.com/zeebo/blake3 v0.2.3 h1:TFoLXsjeXqRNFxSbk35Dk4YtszE/MQQGK10BH4ptoTg=
+github.com/zeebo/blake3 v0.2.3/go.mod h1:mjJjZpnsyIVtVgTOSpJ9vmRE4wgDeyt2HU3qXvvKCaQ=
+github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=
+github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
+go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
+go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
+go.opentelemetry.io/contrib/bridges/prometheus v0.65.0 h1:I/7S/yWobR3QHFLqHsJ8QOndoiFsj1VgHpQiq43KlUI=
+go.opentelemetry.io/contrib/bridges/prometheus v0.65.0/go.mod h1:jPF6gn3y1E+nozCAEQj3c6NZ8KY+tvAgSVfvoOJUFac=
+go.opentelemetry.io/contrib/exporters/autoexport v0.65.0 h1:2gApdml7SznX9szEKFjKjM4qGcGSvAybYLBY319XG3g=
+go.opentelemetry.io/contrib/exporters/autoexport v0.65.0/go.mod h1:0QqAGlbHXhmPYACG3n5hNzO5DnEqqtg4VcK5pr22RI0=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0=
+go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=
+go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=
+go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.16.0 h1:ZVg+kCXxd9LtAaQNKBxAvJ5NpMf7LpvEr4MIZqb0TMQ=
+go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.16.0/go.mod h1:hh0tMeZ75CCXrHd9OXRYxTlCAdxcXioWHFIpYw2rZu8=
+go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.16.0 h1:djrxvDxAe44mJUrKataUbOhCKhR3F8QCyWucO16hTQs=
+go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.16.0/go.mod h1:dt3nxpQEiSoKvfTVxp3TUg5fHPLhKtbcnN3Z1I1ePD0=
+go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.40.0 h1:NOyNnS19BF2SUDApbOKbDtWZ0IK7b8FJ2uAGdIWOGb0=
+go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.40.0/go.mod h1:VL6EgVikRLcJa9ftukrHu/ZkkhFBSo1lzvdBC9CF1ss=
+go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.40.0 h1:9y5sHvAxWzft1WQ4BwqcvA+IFVUJ1Ya75mSAUnFEVwE=
+go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.40.0/go.mod h1:eQqT90eR3X5Dbs1g9YSM30RavwLF725Ris5/XSXWvqE=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 h1:QKdN8ly8zEMrByybbQgv8cWBcdAarwmIPZ6FThrWXJs=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0/go.mod h1:bTdK1nhqF76qiPoCCdyFIV+N/sRHYXYCTQc+3VCi3MI=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 h1:DvJDOPmSWQHWywQS6lKL+pb8s3gBLOZUtw4N+mavW1I=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0/go.mod h1:EtekO9DEJb4/jRyN4v4Qjc2yA7AtfCBuz2FynRUWTXs=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0 h1:wVZXIWjQSeSmMoxF74LzAnpVQOAFDo3pPji9Y4SOFKc=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0/go.mod h1:khvBS2IggMFNwZK/6lEeHg/W57h/IX6J4URh57fuI40=
+go.opentelemetry.io/otel/exporters/prometheus v0.62.0 h1:krvC4JMfIOVdEuNPTtQ0ZjCiXrybhv+uOHMfHRmnvVo=
+go.opentelemetry.io/otel/exporters/prometheus v0.62.0/go.mod h1:fgOE6FM/swEnsVQCqCnbOfRV4tOnWPg7bVeo4izBuhQ=
+go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.16.0 h1:ivlbaajBWJqhcCPniDqDJmRwj4lc6sRT+dCAVKNmxlQ=
+go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.16.0/go.mod h1:u/G56dEKDDwXNCVLsbSrllB2o8pbtFLUC4HpR66r2dc=
+go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.40.0 h1:ZrPRak/kS4xI3AVXy8F7pipuDXmDsrO8Lg+yQjBLjw0=
+go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.40.0/go.mod h1:3y6kQCWztq6hyW8Z9YxQDDm0Je9AJoFar2G0yDcmhRk=
+go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.40.0 h1:MzfofMZN8ulNqobCmCAVbqVL5syHw+eB2qPRkCMA/fQ=
+go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.40.0/go.mod h1:E73G9UFtKRXrxhBsHtG00TB5WxX57lpsQzogDkqBTz8=
+go.opentelemetry.io/otel/log v0.16.0 h1:DeuBPqCi6pQwtCK0pO4fvMB5eBq6sNxEnuTs88pjsN4=
+go.opentelemetry.io/otel/log v0.16.0/go.mod h1:rWsmqNVTLIA8UnwYVOItjyEZDbKIkMxdQunsIhpUMes=
+go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=
+go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc=
+go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8=
+go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE=
+go.opentelemetry.io/otel/sdk/log v0.16.0 h1:e/b4bdlQwC5fnGtG3dlXUrNOnP7c8YLVSpSfEBIkTnI=
+go.opentelemetry.io/otel/sdk/log v0.16.0/go.mod h1:JKfP3T6ycy7QEuv3Hj8oKDy7KItrEkus8XJE6EoSzw4=
+go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw=
+go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg=
+go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw=
+go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=
+go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
+go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
+go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
+go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
+go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
+go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
+go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
+go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
+go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
+go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
+go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
+go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
-golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
-golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
-golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
-golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU=
-golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA=
-golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
-golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
-golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
-golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
-golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
-golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
-golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
-golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
-golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
-golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
-golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
-golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
-golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
-golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
-golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
-golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
-golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
-golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
-golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
-golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
-golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
-golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
-golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
-golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
-golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
-golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
-golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
-golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
-golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
-golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
-golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
-golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
+golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
+golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b h1:DXr+pvt3nC887026GRP39Ej11UATqWDmWuS99x26cD0=
+golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
-golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs=
-golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
+golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
-golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
-golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
-golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
-golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
-golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
-golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
-golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
-golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
-golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
-golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
-golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
-golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
-golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
-golang.org/x/net v0.13.0 h1:Nvo8UFsZ8X3BhAC9699Z1j7XQ3rsZnUUm7jfBEk1ueY=
-golang.org/x/net v0.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
-golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
-golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
-golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
-golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
-golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
-golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.5.0 h1:HuArIo48skDwlrvM3sEdHXElYslAMsf3KwRkkW4MC4s=
-golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I=
-golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
+golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
+golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
+golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
-golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
-golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
+golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20201014080544-cc95f250f6bc/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20221013171732-95e765b1cc43/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
-golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
-golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
-golang.org/x/term v0.0.0-20220526004731-065cf7ba2467/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
-golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
-golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
-golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c=
-golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=
-golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
+golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
+golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
+golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
-golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
-golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
-golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
-golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
-golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
-golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
-golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
-golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
-golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
-golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
+golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
+golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
+golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
-golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
-golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
-golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
-golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
-golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
-golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
-golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
-golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
-golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
-golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
-golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
-golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
-golang.org/x/tools v0.0.0-20200313205530-4303120df7d8/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
-golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
-golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
-golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
-golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
-golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
-golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
-golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
-golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
-golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
-golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
-golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
-golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
-golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
-golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
-golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
-golang.org/x/tools v0.9.3 h1:Gn1I8+64MsuTb/HpH+LmQtNas23LhUVr3rYZ0eKuaMM=
+golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
+golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-gomodules.xyz/jsonpatch/v2 v2.3.0 h1:8NFhfS6gzxNqjLIYnZxg319wZ5Qjnx4m/CcX+Klzazc=
-gomodules.xyz/jsonpatch/v2 v2.3.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY=
-google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
-google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
-google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
-google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
-google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
-google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
-google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
-google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
-google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
-google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
-google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
-google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
-google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
-google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
-google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
-google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
-google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
-google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
-google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
-google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU=
-google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94=
-google.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8=
-google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
-google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
-google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
-google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
-google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
-google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
-google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
-google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
-google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
-google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
-google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
-google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
-google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
-google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
-google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
-google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
-google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
-google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
-google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
-google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
-google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
-google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
-google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
-google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
-google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
-google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
-google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=
-google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
-google.golang.org/genproto v0.0.0-20220107163113-42d7afdf6368/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
-google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4 h1:DdoeryqhaXp1LtT/emMP1BRJPHHKFi5akj/nbx/zNTA=
-google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4/go.mod h1:NWraEVixdDnqcqQ30jipen1STv2r/n24Wb7twVTGR4s=
-google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
-google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
-google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
-google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
-google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
-google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
-google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
-google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
-google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
-google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
-google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
-google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
-google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
-google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
-google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
-google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
-google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
-google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
-google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
-google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
-google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
-google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
-google.golang.org/grpc v1.53.0 h1:LAv2ds7cmFV/XTS3XG1NneeENYrXGmorPxsBbptIjNc=
-google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw=
-google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
-google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
-google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
-google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
-google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
-google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
-google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
-google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
-google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
-google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
-google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
-google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
-google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
-google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
-google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
-gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
+gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0=
+gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY=
+gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
+gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
+google.golang.org/api v0.261.0 h1:3DoJ2GGibaCxNi1lhdScNMx9fTW87ujKHDgyHMMYdoA=
+google.golang.org/api v0.261.0/go.mod h1:nVH0ZK5C4tO0RdsMscleeTLY7I8m/Nt9IXxcXD2tfts=
+google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217 h1:GvESR9BIyHUahIb0NcTum6itIWtdoglGX+rnGxm2934=
+google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:yJ2HH4EHEDTd3JiLmhds6NkJ17ITVYOdV3m3VKOnws0=
+google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 h1:merA0rdPeUV3YIIfHHcH4qBkiQAc1nfCKSI7lB4cV2M=
+google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409/go.mod h1:fl8J1IvUjCilwZzQowmw2b7HQB2eAuYBabMXzWurF+I=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
+google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
+google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
+google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
+google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
-gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
+gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo=
+gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
-gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
-gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
-gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
-gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o=
-helm.sh/helm/v3 v3.12.3 h1:5y1+Sbty12t48T/t/CGNYUIME5BJ0WKfmW/sobYqkFg=
-helm.sh/helm/v3 v3.12.3/go.mod h1:KPKQiX9IP5HX7o5YnnhViMnNuKiL/lJBVQ47GHe1R0k=
-honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
-honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
-honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
-honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
-honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
-honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
-honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
-k8s.io/api v0.27.4 h1:0pCo/AN9hONazBKlNUdhQymmnfLRbSZjd5H5H3f0bSs=
-k8s.io/api v0.27.4/go.mod h1:O3smaaX15NfxjzILfiln1D8Z3+gEYpjEpiNA/1EVK1Y=
-k8s.io/apiextensions-apiserver v0.27.4 h1:ie1yZG4nY/wvFMIR2hXBeSVq+HfNzib60FjnBYtPGSs=
-k8s.io/apiextensions-apiserver v0.27.4/go.mod h1:KHZaDr5H9IbGEnSskEUp/DsdXe1hMQ7uzpQcYUFt2bM=
-k8s.io/apimachinery v0.27.4 h1:CdxflD4AF61yewuid0fLl6bM4a3q04jWel0IlP+aYjs=
-k8s.io/apimachinery v0.27.4/go.mod h1:XNfZ6xklnMCOGGFNqXG7bUrQCoR04dh/E7FprV6pb+E=
-k8s.io/apiserver v0.27.4 h1:ncZ0MBR9yQ/Gf34rtu1EK+HqT8In1YpfAUINu/Akvho=
-k8s.io/apiserver v0.27.4/go.mod h1:GDEFRfFZ4/l+pAvwYRnoSfz0K4j3TWiN4WsG2KnRteE=
-k8s.io/cli-runtime v0.27.4 h1:Zb0eci+58eHZNnoHhjRFc7W88s8dlG12VtIl3Nv2Hto=
-k8s.io/cli-runtime v0.27.4/go.mod h1:k9Z1xiZq2xNplQmehpDquLgc+rE+pubpO1cK4al4Mlw=
-k8s.io/client-go v0.27.4 h1:vj2YTtSJ6J4KxaC88P4pMPEQECWMY8gqPqsTgUKzvjk=
-k8s.io/client-go v0.27.4/go.mod h1:ragcly7lUlN0SRPk5/ZkGnDjPknzb37TICq07WhI6Xc=
-k8s.io/component-base v0.27.4 h1:Wqc0jMKEDGjKXdae8hBXeskRP//vu1m6ypC+gwErj4c=
-k8s.io/component-base v0.27.4/go.mod h1:hoiEETnLc0ioLv6WPeDt8vD34DDeB35MfQnxCARq3kY=
-k8s.io/klog/v2 v2.100.1 h1:7WCHKK6K8fNhTqfBhISHQ97KrnJNFZMcQvKp7gP/tmg=
-k8s.io/klog/v2 v2.100.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0=
-k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f h1:2kWPakN3i/k81b0gvD5C5FJ2kxm1WrQFanWchyKuqGg=
-k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f/go.mod h1:byini6yhqGC14c3ebc/QwanvYwhuMWF6yz2F8uwW8eg=
-k8s.io/kubectl v0.27.3 h1:HyC4o+8rCYheGDWrkcOQHGwDmyLKR5bxXFgpvF82BOw=
-k8s.io/kubectl v0.27.3/go.mod h1:g9OQNCC2zxT+LT3FS09ZYqnDhlvsKAfFq76oyarBcq4=
-k8s.io/utils v0.0.0-20230505201702-9f6742963106 h1:EObNQ3TW2D+WptiYXlApGNLVy0zm/JIBVY9i+M4wpAU=
-k8s.io/utils v0.0.0-20230505201702-9f6742963106/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
-oras.land/oras-go v1.2.3 h1:v8PJl+gEAntI1pJ/LCrDgsuk+1PKVavVEPsYIHFE5uY=
-oras.land/oras-go v1.2.3/go.mod h1:M/uaPdYklze0Vf3AakfarnpoEckvw0ESbRdN8Z1vdJg=
-rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
-rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
-rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
-sigs.k8s.io/cli-utils v0.35.0 h1:dfSJaF1W0frW74PtjwiyoB4cwdRygbHnC7qe7HF0g/Y=
-sigs.k8s.io/cli-utils v0.35.0/go.mod h1:ITitykCJxP1vaj1Cew/FZEaVJ2YsTN9Q71m02jebkoE=
-sigs.k8s.io/controller-runtime v0.15.1 h1:9UvgKD4ZJGcj24vefUFgZFP3xej/3igL9BsOUTb/+4c=
-sigs.k8s.io/controller-runtime v0.15.1/go.mod h1:7ngYvp1MLT+9GeZ+6lH3LOlcHkp/+tzA/fmHa4iq9kk=
-sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo=
-sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
-sigs.k8s.io/kustomize/api v0.13.4 h1:E38Hfx0G9R9v7vRgKshviPotJQETG0S2gD3JdHLCAsI=
-sigs.k8s.io/kustomize/api v0.13.4/go.mod h1:Bkaavz5RKK6ZzP0zgPrB7QbpbBJKiHuD3BB0KujY7Ls=
-sigs.k8s.io/kustomize/kyaml v0.14.2 h1:9WSwztbzwGszG1bZTziQUmVMrJccnyrLb5ZMKpJGvXw=
-sigs.k8s.io/kustomize/kyaml v0.14.2/go.mod h1:AN1/IpawKilWD7V+YvQwRGUvuUOOWpjsHu6uHwonSF4=
-sigs.k8s.io/structured-merge-diff/v4 v4.3.0 h1:UZbZAZfX0wV2zr7YZorDz6GXROfDFj6LvqCRm4VUVKk=
-sigs.k8s.io/structured-merge-diff/v4 v4.3.0/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08=
-sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
-sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=
+gotest.tools/v3 v3.4.0/go.mod h1:CtbdzLSsqVhDgMtKsx03ird5YTGB3ar27v0u/yKBW5g=
+k8s.io/api v0.35.2 h1:tW7mWc2RpxW7HS4CoRXhtYHSzme1PN1UjGHJ1bdrtdw=
+k8s.io/api v0.35.2/go.mod h1:7AJfqGoAZcwSFhOjcGM7WV05QxMMgUaChNfLTXDRE60=
+k8s.io/apiextensions-apiserver v0.35.2 h1:iyStXHoJZsUXPh/nFAsjC29rjJWdSgUmG1XpApE29c0=
+k8s.io/apiextensions-apiserver v0.35.2/go.mod h1:OdyGvcO1FtMDWQ+rRh/Ei3b6X3g2+ZDHd0MSRGeS8rU=
+k8s.io/apimachinery v0.35.2 h1:NqsM/mmZA7sHW02JZ9RTtk3wInRgbVxL8MPfzSANAK8=
+k8s.io/apimachinery v0.35.2/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns=
+k8s.io/apiserver v0.35.2 h1:rb52v0CZGEL0FkhjS+I6jHflAp7fZ4MIaKcEHX7wmDk=
+k8s.io/apiserver v0.35.2/go.mod h1:CROJUAu0tfjZLyYgSeBsBan2T7LUJGh0ucWwTCSSk7g=
+k8s.io/cli-runtime v0.35.2 h1:3DNctzpPNXavqyrm/FFiT60TLk4UjUxuUMYbKOE970E=
+k8s.io/cli-runtime v0.35.2/go.mod h1:G2Ieu0JidLm5m1z9b0OkFhnykvJ1w+vjbz1tR5OFKL0=
+k8s.io/client-go v0.35.2 h1:YUfPefdGJA4aljDdayAXkc98DnPkIetMl4PrKX97W9o=
+k8s.io/client-go v0.35.2/go.mod h1:4QqEwh4oQpeK8AaefZ0jwTFJw/9kIjdQi0jpKeYvz7g=
+k8s.io/component-base v0.35.2 h1:btgR+qNrpWuRSuvWSnQYsZy88yf5gVwemvz0yw79pGc=
+k8s.io/component-base v0.35.2/go.mod h1:B1iBJjooe6xIJYUucAxb26RwhAjzx0gHnqO9htWIX+0=
+k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
+k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
+k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE=
+k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ=
+k8s.io/kubectl v0.35.2 h1:aSmqhSOfsoG9NR5oR8OD5eMKpLN9x8oncxfqLHbJJII=
+k8s.io/kubectl v0.35.2/go.mod h1:+OJC779UsDJGxNPbHxCwvb4e4w9Eh62v/DNYU2TlsyM=
+k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck=
+k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
+oras.land/oras-go/v2 v2.6.0 h1:X4ELRsiGkrbeox69+9tzTu492FMUu7zJQW6eJU+I2oc=
+oras.land/oras-go/v2 v2.6.0/go.mod h1:magiQDfG6H1O9APp+rOsvCPcW1GD2MM7vgnKY0Y+u1o=
+sigs.k8s.io/controller-runtime v0.23.3 h1:VjB/vhoPoA9l1kEKZHBMnQF33tdCLQKJtydy4iqwZ80=
+sigs.k8s.io/controller-runtime v0.23.3/go.mod h1:B6COOxKptp+YaUT5q4l6LqUJTRpizbgf9KSRNdQGns0=
+sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=
+sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
+sigs.k8s.io/kustomize/api v0.20.1 h1:iWP1Ydh3/lmldBnH/S5RXgT98vWYMaTUL1ADcr+Sv7I=
+sigs.k8s.io/kustomize/api v0.20.1/go.mod h1:t6hUFxO+Ph0VxIk1sKp1WS0dOjbPCtLJ4p8aADLwqjM=
+sigs.k8s.io/kustomize/kyaml v0.20.1 h1:PCMnA2mrVbRP3NIB6v9kYCAc38uvFLVs8j/CD567A78=
+sigs.k8s.io/kustomize/kyaml v0.20.1/go.mod h1:0EmkQHRUsJxY8Ug9Niig1pUMSCGHxQ5RklbpV/Ri6po=
+sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
+sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
+sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 h1:2WOzJpHUBVrrkDjU4KBT8n5LDcj824eX0I5UKcgeRUs=
+sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
+sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
+sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
diff --git a/hack/boilerplate.go.txt b/hack/boilerplate.go.txt
index 439ccd868..f57d7e7fe 100644
--- a/hack/boilerplate.go.txt
+++ b/hack/boilerplate.go.txt
@@ -1,5 +1,5 @@
/*
-Copyright 2021 The Flux authors
+Copyright 2025 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
diff --git a/internal/acl/acl.go b/internal/acl/acl.go
new file mode 100644
index 000000000..b27b263ec
--- /dev/null
+++ b/internal/acl/acl.go
@@ -0,0 +1,43 @@
+/*
+Copyright 2023 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package acl
+
+import (
+ "fmt"
+
+ "k8s.io/apimachinery/pkg/types"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+
+ "github.com/fluxcd/pkg/runtime/acl"
+)
+
+var (
+ // AllowCrossNamespaceRef is a global flag that can be used to allow
+ // cross-namespace references.
+ AllowCrossNamespaceRef = false
+)
+
+// AllowsAccessTo returns an error if the object does not allow access to the
+// given reference.
+func AllowsAccessTo(obj client.Object, kind string, ref types.NamespacedName) error {
+ if !AllowCrossNamespaceRef && obj.GetNamespace() != ref.Namespace {
+ return acl.AccessDeniedError(fmt.Sprintf("cross-namespace references are not allowed: cannot access %s %s",
+ kind, ref.String(),
+ ))
+ }
+ return nil
+}
diff --git a/internal/acl/acl_test.go b/internal/acl/acl_test.go
new file mode 100644
index 000000000..3a0ab7c33
--- /dev/null
+++ b/internal/acl/acl_test.go
@@ -0,0 +1,95 @@
+/*
+Copyright 2023 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package acl
+
+import (
+ "testing"
+
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/types"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+
+ v2 "github.com/fluxcd/helm-controller/api/v2"
+)
+
+func TestAllowsAccessTo(t *testing.T) {
+ tests := []struct {
+ name string
+ allow bool
+ obj client.Object
+ ref types.NamespacedName
+ wantErr bool
+ }{
+ {
+ name: "allow cross-namespace reference",
+ allow: true,
+ obj: &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "some-name",
+ Namespace: "some-namespace",
+ },
+ },
+ ref: types.NamespacedName{
+ Name: "some-name",
+ Namespace: "some-other-namespace",
+ },
+ wantErr: false,
+ },
+ {
+ name: "disallow cross-namespace reference",
+ allow: false,
+ obj: &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "some-name",
+ Namespace: "some-namespace",
+ },
+ },
+ ref: types.NamespacedName{
+ Name: "some-name",
+ Namespace: "some-other-namespace",
+ },
+ wantErr: true,
+ },
+ {
+ name: "allow same-namespace reference",
+ allow: false,
+ obj: &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "some-name",
+ Namespace: "some-namespace",
+ },
+ },
+ ref: types.NamespacedName{
+ Name: "some-name",
+ Namespace: "some-namespace",
+ },
+ wantErr: false,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ curAllow := AllowCrossNamespaceRef
+ AllowCrossNamespaceRef = tt.allow
+ t.Cleanup(func() { AllowCrossNamespaceRef = curAllow })
+
+ if err := AllowsAccessTo(tt.obj, "mock", tt.ref); (err != nil) != tt.wantErr {
+ t.Errorf("AllowsAccessTo() error = %v, wantErr %v", err, tt.wantErr)
+ }
+ })
+ }
+}
diff --git a/internal/action/client.go b/internal/action/client.go
new file mode 100644
index 000000000..98b61fa9a
--- /dev/null
+++ b/internal/action/client.go
@@ -0,0 +1,78 @@
+/*
+Copyright 2026 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package action
+
+import (
+ "context"
+
+ helmkube "helm.sh/helm/v4/pkg/kube"
+ "k8s.io/cli-runtime/pkg/genericclioptions"
+
+ "github.com/fluxcd/pkg/ssa"
+)
+
+// Client wraps a Helm kube Client to replace the kstatus implementation.
+type Client struct {
+ // We need to embed the struct and not helmkube.Interface
+ // as Helm adds more methods to the struct over time
+ // without adding them to the interface. This ensures
+ // we always embed all methods.
+ *helmkube.Client
+
+ newResourceManager func(sr ...NewStatusReaderFunc) *ssa.ResourceManager
+ waitContext context.Context
+}
+
+// Ensure Client implements helmkube.Interface.
+var _ helmkube.Interface = (*Client)(nil)
+
+// Ensure Client implements helmkube.InterfaceWaitOptions.
+var _ helmkube.InterfaceWaitOptions = (*Client)(nil)
+
+// NewClient returns a new Helm kube Client that uses kstatus for waits.
+func NewClient(getter genericclioptions.RESTClientGetter) *Client {
+ return &Client{Client: helmkube.New(getter)}
+}
+
+// GetWaiter implements helmkube.InterfaceWaitOptions by returning
+// a custom kstatus-based Waiter.
+func (c *Client) GetWaiter(strategy helmkube.WaitStrategy) (helmkube.Waiter, error) {
+ return c.newWaiter(strategy)
+}
+
+// GetWaiterWithOptions implements helmkube.InterfaceWaitOptions by
+// returning a custom kstatus-based Waiter.
+func (c *Client) GetWaiterWithOptions(strategy helmkube.WaitStrategy,
+ opts ...helmkube.WaitOption) (helmkube.Waiter, error) {
+ return c.newWaiter(strategy, opts...)
+}
+
+// newWaiter returns a new Waiter based on the provided strategy.
+func (c *Client) newWaiter(strategy helmkube.WaitStrategy,
+ opts ...helmkube.WaitOption) (helmkube.Waiter, error) {
+
+ if strategy == helmkube.LegacyStrategy || c.newResourceManager == nil {
+ return c.Client.GetWaiterWithOptions(strategy, opts...)
+ }
+
+ return &waiter{
+ c: c.Client,
+ strategy: strategy,
+ newResourceManager: c.newResourceManager,
+ waitContext: c.waitContext,
+ }, nil
+}
diff --git a/internal/action/config.go b/internal/action/config.go
new file mode 100644
index 000000000..61a42e201
--- /dev/null
+++ b/internal/action/config.go
@@ -0,0 +1,201 @@
+/*
+Copyright 2022 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package action
+
+import (
+ "context"
+ "fmt"
+ "log/slog"
+
+ helmaction "helm.sh/helm/v4/pkg/action"
+ helmstorage "helm.sh/helm/v4/pkg/storage"
+ helmdriver "helm.sh/helm/v4/pkg/storage/driver"
+ "k8s.io/cli-runtime/pkg/genericclioptions"
+
+ "github.com/fluxcd/pkg/ssa"
+
+ "github.com/fluxcd/helm-controller/internal/storage"
+)
+
+const (
+ // DefaultStorageDriver is the default Helm storage driver.
+ DefaultStorageDriver = helmdriver.SecretsDriverName
+)
+
+// ConfigFactory is a factory for the Helm action configuration of a (series
+// of) Helm action(s). It allows for sharing Kubernetes client(s) and the
+// Helm storage driver between actions, where possible.
+//
+// To get a Helm action.Configuration for an action, use the Build method on an
+// initialized factory.
+type ConfigFactory struct {
+ // Getter is the RESTClientGetter used to get the RESTClient for the
+ // Kubernetes API.
+ Getter genericclioptions.RESTClientGetter
+ // KubeClient is the (wrapped) Helm Kubernetes client, it is Helm-specific and
+ // contains a factory used for lazy-loading.
+ KubeClient *Client
+ // Driver to use for the Helm action.
+ Driver helmdriver.Driver
+ // StorageLog is the logger to use for the Helm storage driver.
+ StorageLog slog.Handler
+ // NewResourceManager is the resource manager used to evaluate custom health checks.
+ NewResourceManager func(sr ...NewStatusReaderFunc) *ssa.ResourceManager
+ // WaitContext is the context used for waiting operations in the Helm
+ // Kubernetes client.
+ WaitContext context.Context
+}
+
+// ConfigFactoryOption is a function that configures a ConfigFactory.
+type ConfigFactoryOption func(*ConfigFactory) error
+
+// NewConfigFactory returns a new ConfigFactory configured with the provided
+// options.
+func NewConfigFactory(getter genericclioptions.RESTClientGetter, opts ...ConfigFactoryOption) (*ConfigFactory, error) {
+ kubeClient := NewClient(getter)
+ factory := &ConfigFactory{
+ Getter: getter,
+ KubeClient: kubeClient,
+ }
+ for _, opt := range opts {
+ if err := opt(factory); err != nil {
+ return nil, err
+ }
+ }
+ if err := factory.Valid(); err != nil {
+ return nil, err
+ }
+ return factory, nil
+}
+
+// WithStorage configures the ConfigFactory.Driver by constructing a new Helm
+// driver.Driver using the provided driver name and namespace.
+// It supports driver.ConfigMapsDriverName, driver.SecretsDriverName and
+// driver.MemoryDriverName.
+// It returns an error when the driver name is not supported, or the client
+// configuration for the storage fails.
+func WithStorage(driver, namespace string) ConfigFactoryOption {
+ if driver == "" {
+ driver = DefaultStorageDriver
+ }
+
+ return func(f *ConfigFactory) error {
+ if namespace == "" {
+ return fmt.Errorf("no namespace provided for '%s' storage driver", driver)
+ }
+
+ switch driver {
+ case helmdriver.SecretsDriverName, helmdriver.ConfigMapsDriverName, "":
+ clientSet, err := f.KubeClient.Factory.KubernetesClientSet()
+ if err != nil {
+ return fmt.Errorf("could not get client set for '%s' storage driver: %w", driver, err)
+ }
+ if driver == helmdriver.ConfigMapsDriverName {
+ f.Driver = helmdriver.NewConfigMaps(clientSet.CoreV1().ConfigMaps(namespace))
+ }
+ if driver == helmdriver.SecretsDriverName {
+ f.Driver = helmdriver.NewSecrets(clientSet.CoreV1().Secrets(namespace))
+ }
+ case helmdriver.MemoryDriverName:
+ driver := helmdriver.NewMemory()
+ driver.SetNamespace(namespace)
+ f.Driver = driver
+ default:
+ return fmt.Errorf("unsupported Helm storage driver '%s'", driver)
+ }
+ return nil
+ }
+}
+
+// WithDriver sets the ConfigFactory.Driver.
+func WithDriver(driver helmdriver.Driver) ConfigFactoryOption {
+ return func(f *ConfigFactory) error {
+ f.Driver = driver
+ return nil
+ }
+}
+
+// WithStorageLog sets the ConfigFactory.StorageLog.
+func WithStorageLog(log slog.Handler) ConfigFactoryOption {
+ return func(f *ConfigFactory) error {
+ f.StorageLog = log
+ return nil
+ }
+}
+
+// WithResourceManager sets the ConfigFactory.ResourceManager.
+func WithResourceManager(mgr func(sr ...NewStatusReaderFunc) *ssa.ResourceManager) ConfigFactoryOption {
+ return func(f *ConfigFactory) error {
+ f.NewResourceManager = mgr
+ return nil
+ }
+}
+
+// WithWaitContext sets the context used for waiting operations in the Helm
+// Kubernetes client.
+func WithWaitContext(ctx context.Context) ConfigFactoryOption {
+ return func(f *ConfigFactory) error {
+ f.WaitContext = ctx
+ return nil
+ }
+}
+
+// NewStorage returns a new Helm storage.Storage configured with any
+// observer(s) and the Driver configured on the ConfigFactory.
+func (c *ConfigFactory) NewStorage(observers ...storage.ObserveFunc) *helmstorage.Storage {
+ driver := c.Driver
+ if len(observers) > 0 {
+ driver = storage.NewObserver(driver, observers...)
+ }
+ s := helmstorage.Init(driver)
+ s.SetLogger(c.StorageLog)
+ return s
+}
+
+// Build returns a new Helm action.Configuration configured with the receiver
+// values, and the provided logger and observer(s).
+func (c *ConfigFactory) Build(log slog.Handler, observers ...storage.ObserveFunc) *helmaction.Configuration {
+ client := NewClient(c.Getter)
+ client.newResourceManager = c.NewResourceManager
+ client.waitContext = c.WaitContext
+
+ var opts []helmaction.ConfigurationOption
+ if log != nil {
+ client.SetLogger(log)
+ opts = append(opts, helmaction.ConfigurationSetLogger(log))
+ }
+
+ conf := helmaction.NewConfiguration(opts...)
+ conf.RESTClientGetter = c.Getter
+ conf.Releases = c.NewStorage(observers...)
+ conf.KubeClient = client
+ return conf
+}
+
+// Valid returns an error if the ConfigFactory is missing configuration
+// required to run a Helm action.
+func (c *ConfigFactory) Valid() error {
+ switch {
+ case c == nil:
+ return fmt.Errorf("ConfigFactory is nil")
+ case c.Driver == nil:
+ return fmt.Errorf("no Helm storage driver configured")
+ case c.KubeClient == nil, c.Getter == nil:
+ return fmt.Errorf("no Kubernetes client and/or getter configured")
+ }
+ return nil
+}
diff --git a/internal/action/config_test.go b/internal/action/config_test.go
new file mode 100644
index 000000000..1c82b6cbf
--- /dev/null
+++ b/internal/action/config_test.go
@@ -0,0 +1,319 @@
+/*
+Copyright 2022 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package action
+
+import (
+ "errors"
+ "log/slog"
+ "testing"
+
+ . "github.com/onsi/gomega"
+ helmkube "helm.sh/helm/v4/pkg/kube"
+ helmrelease "helm.sh/helm/v4/pkg/release"
+ helmdriver "helm.sh/helm/v4/pkg/storage/driver"
+ "k8s.io/cli-runtime/pkg/genericclioptions"
+ cmdtest "k8s.io/kubectl/pkg/cmd/testing"
+
+ "github.com/fluxcd/helm-controller/internal/kube"
+ "github.com/fluxcd/helm-controller/internal/storage"
+ "github.com/fluxcd/helm-controller/internal/testutil"
+)
+
+func TestNewConfigFactory(t *testing.T) {
+ tests := []struct {
+ name string
+ getter genericclioptions.RESTClientGetter
+ opts []ConfigFactoryOption
+ wantErr error
+ }{
+ {
+ name: "constructs config factory",
+ getter: &kube.MemoryRESTClientGetter{},
+ opts: []ConfigFactoryOption{
+ WithStorage(helmdriver.MemoryDriverName, "default"),
+ },
+ wantErr: nil,
+ },
+ {
+ name: "invalid config",
+ getter: &kube.MemoryRESTClientGetter{},
+ wantErr: errors.New("no Helm storage driver configured"),
+ },
+ {
+ name: "multiple options",
+ getter: &kube.MemoryRESTClientGetter{},
+ opts: []ConfigFactoryOption{
+ WithDriver(helmdriver.NewMemory()),
+ WithStorageLog(slog.DiscardHandler),
+ },
+ wantErr: nil,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ g := NewWithT(t)
+
+ factory, err := NewConfigFactory(tt.getter, tt.opts...)
+ if tt.wantErr != nil {
+ g.Expect(err).To(HaveOccurred())
+ g.Expect(factory).To(BeNil())
+ return
+ }
+
+ g.Expect(err).ToNot(HaveOccurred())
+ g.Expect(factory).ToNot(BeNil())
+ })
+ }
+}
+
+func TestWithStorage(t *testing.T) {
+ tests := []struct {
+ name string
+ factory ConfigFactory
+ driverName string
+ namespace string
+ wantErr error
+ wantDriver string
+ }{
+ {
+ name: "default_" + DefaultStorageDriver,
+ namespace: "default",
+ factory: ConfigFactory{
+ KubeClient: &Client{Client: helmkube.New(cmdtest.NewTestFactory())},
+ },
+ wantDriver: helmdriver.SecretsDriverName,
+ },
+ {
+ name: helmdriver.SecretsDriverName,
+ driverName: helmdriver.SecretsDriverName,
+ namespace: "default",
+ factory: ConfigFactory{
+ KubeClient: &Client{Client: helmkube.New(cmdtest.NewTestFactory())},
+ },
+ wantDriver: helmdriver.SecretsDriverName,
+ },
+ {
+ name: helmdriver.ConfigMapsDriverName,
+ driverName: helmdriver.ConfigMapsDriverName,
+ namespace: "default",
+ factory: ConfigFactory{
+ KubeClient: &Client{Client: helmkube.New(cmdtest.NewTestFactory())},
+ },
+ wantDriver: helmdriver.ConfigMapsDriverName,
+ },
+ {
+ name: helmdriver.MemoryDriverName,
+ driverName: helmdriver.MemoryDriverName,
+ namespace: "default",
+ factory: ConfigFactory{},
+ wantDriver: helmdriver.MemoryDriverName,
+ },
+ {
+ name: "invalid namespace",
+ driverName: helmdriver.SecretsDriverName,
+ namespace: "",
+ factory: ConfigFactory{},
+ wantErr: errors.New("no namespace provided for Helm storage driver 'secrets'"),
+ },
+ {
+ name: "invalid driver",
+ driverName: "invalid",
+ namespace: "default",
+ factory: ConfigFactory{},
+ wantErr: errors.New("unsupported Helm storage driver 'invalid'"),
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ g := NewWithT(t)
+
+ factory := tt.factory
+ err := WithStorage(tt.driverName, tt.namespace)(&factory)
+ if tt.wantErr != nil {
+ g.Expect(err).To(HaveOccurred())
+ g.Expect(factory.Driver).To(BeNil())
+ return
+ }
+
+ g.Expect(err).ToNot(HaveOccurred())
+ g.Expect(factory.Driver).ToNot(BeNil())
+ g.Expect(factory.Driver.Name()).To(Equal(tt.wantDriver))
+ })
+ }
+}
+
+func TestWithDriver(t *testing.T) {
+ g := NewWithT(t)
+
+ factory := &ConfigFactory{}
+ driver := helmdriver.NewMemory()
+ g.Expect(WithDriver(driver)(factory)).NotTo(HaveOccurred())
+ g.Expect(factory.Driver).To(Equal(driver))
+}
+
+func TestStorageLog(t *testing.T) {
+ g := NewWithT(t)
+
+ factory := &ConfigFactory{}
+ g.Expect(WithStorageLog(slog.DiscardHandler)(factory)).NotTo(HaveOccurred())
+ g.Expect(factory.StorageLog).ToNot(BeNil())
+}
+
+func TestConfigFactory_NewStorage(t *testing.T) {
+ t.Run("without observers", func(t *testing.T) {
+ g := NewWithT(t)
+
+ factory := &ConfigFactory{
+ Driver: helmdriver.NewMemory(),
+ }
+
+ s := factory.NewStorage()
+ g.Expect(s).ToNot(BeNil())
+ g.Expect(s.Driver).To(BeAssignableToTypeOf(factory.Driver))
+ })
+
+ t.Run("with observers", func(t *testing.T) {
+ g := NewWithT(t)
+
+ factory := &ConfigFactory{
+ Driver: helmdriver.NewMemory(),
+ }
+
+ obsFunc := func(rel helmrelease.Releaser) {}
+ s := factory.NewStorage(obsFunc)
+ g.Expect(s).ToNot(BeNil())
+ g.Expect(s.Driver).To(BeAssignableToTypeOf(&storage.Observer{}))
+ })
+
+ t.Run("with storage log", func(t *testing.T) {
+ g := NewWithT(t)
+
+ log := &testutil.MockSLogHandler{}
+
+ factory := &ConfigFactory{
+ Driver: helmdriver.NewMemory(),
+ StorageLog: log,
+ }
+
+ s := factory.NewStorage()
+ g.Expect(s).ToNot(BeNil())
+ s.Logger().Info("test log")
+ g.Expect(log.Called).To(BeTrue())
+ })
+}
+
+func TestConfigFactory_Build(t *testing.T) {
+ t.Run("build", func(t *testing.T) {
+ g := NewWithT(t)
+
+ getter := &kube.MemoryRESTClientGetter{}
+ factory := &ConfigFactory{
+ Getter: getter,
+ KubeClient: &Client{Client: helmkube.New(getter)},
+ }
+
+ cfg := factory.Build(nil)
+ g.Expect(cfg).ToNot(BeNil())
+ g.Expect(cfg.KubeClient).To(BeAssignableToTypeOf(&Client{}))
+ g.Expect(cfg.RESTClientGetter).To(Equal(factory.Getter))
+ })
+
+ t.Run("with log", func(t *testing.T) {
+ g := NewWithT(t)
+
+ log := &testutil.MockSLogHandler{}
+ cfg := (&ConfigFactory{}).Build(log)
+
+ g.Expect(cfg).ToNot(BeNil())
+ cfg.Logger().Info("test log")
+ g.Expect(log.Called).To(BeTrue())
+ })
+
+ t.Run("with observe func", func(t *testing.T) {
+ g := NewWithT(t)
+
+ factory := &ConfigFactory{
+ Driver: helmdriver.NewMemory(),
+ }
+
+ obsFunc := func(rel helmrelease.Releaser) {}
+ cfg := factory.Build(nil, obsFunc)
+
+ g.Expect(cfg).To(Not(BeNil()))
+ g.Expect(cfg.Releases).ToNot(BeNil())
+ g.Expect(cfg.Releases.Driver).To(BeAssignableToTypeOf(&storage.Observer{}))
+ })
+}
+
+func TestConfigFactory_Valid(t *testing.T) {
+ tests := []struct {
+ name string
+ factory *ConfigFactory
+ wantErr error
+ }{
+ {
+ name: "valid",
+ factory: &ConfigFactory{
+ Driver: helmdriver.NewMemory(),
+ Getter: &kube.MemoryRESTClientGetter{},
+ KubeClient: &Client{Client: helmkube.New(&kube.MemoryRESTClientGetter{})},
+ },
+ wantErr: nil,
+ },
+ {
+ name: "no Kubernetes client",
+ factory: &ConfigFactory{
+ Driver: helmdriver.NewMemory(),
+ Getter: &kube.MemoryRESTClientGetter{},
+ },
+ wantErr: errors.New("no Kubernetes client and/or getter configured"),
+ },
+ {
+ name: "no Kubernetes getter",
+ factory: &ConfigFactory{
+ Driver: helmdriver.NewMemory(),
+ KubeClient: &Client{Client: helmkube.New(&kube.MemoryRESTClientGetter{})},
+ },
+ wantErr: errors.New("no Kubernetes client and/or getter configured"),
+ },
+ {
+ name: "no driver",
+ factory: &ConfigFactory{
+ KubeClient: &Client{Client: helmkube.New(&kube.MemoryRESTClientGetter{})},
+ Getter: &kube.MemoryRESTClientGetter{},
+ },
+ wantErr: errors.New("no Helm storage driver configured"),
+ },
+ {
+ name: "nil factory",
+ factory: nil,
+ wantErr: errors.New("ConfigFactory is nil"),
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ g := NewWithT(t)
+
+ err := tt.factory.Valid()
+ if tt.wantErr == nil {
+ g.Expect(err).To(BeNil())
+ return
+ }
+ g.Expect(tt.factory.Valid()).To(Equal(tt.wantErr))
+ })
+ }
+}
diff --git a/internal/action/crds.go b/internal/action/crds.go
new file mode 100644
index 000000000..6d90d37b0
--- /dev/null
+++ b/internal/action/crds.go
@@ -0,0 +1,302 @@
+/*
+Copyright 2022 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package action
+
+import (
+ "bytes"
+ "context"
+ "fmt"
+ "time"
+
+ helmaction "helm.sh/helm/v4/pkg/action"
+ helmchartcommon "helm.sh/helm/v4/pkg/chart/common"
+ helmchart "helm.sh/helm/v4/pkg/chart/v2"
+ helmchartutil "helm.sh/helm/v4/pkg/chart/v2/util"
+ "helm.sh/helm/v4/pkg/kube"
+ helmkube "helm.sh/helm/v4/pkg/kube"
+ apiextension "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
+ apierrors "k8s.io/apimachinery/pkg/api/errors"
+ apimeta "k8s.io/apimachinery/pkg/api/meta"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ apiruntime "k8s.io/apimachinery/pkg/runtime"
+ "k8s.io/apimachinery/pkg/runtime/schema"
+ "k8s.io/cli-runtime/pkg/resource"
+
+ v2 "github.com/fluxcd/helm-controller/api/v2"
+)
+
+const (
+ // DefaultCRDPolicy is the default CRD policy.
+ DefaultCRDPolicy = v2.Create
+)
+
+var accessor = apimeta.NewAccessor()
+
+// crdPolicy returns the CRD policy for the given CRD.
+func crdPolicyOrDefault(policy v2.CRDsPolicy) (v2.CRDsPolicy, error) {
+ switch policy {
+ case "":
+ policy = DefaultCRDPolicy
+ case v2.Skip, v2.Create, v2.CreateReplace:
+ break
+ default:
+ return policy, fmt.Errorf("invalid CRD upgrade policy '%s', valid values are '%s', '%s' or '%s'",
+ policy, v2.Skip, v2.Create, v2.CreateReplace,
+ )
+ }
+ return policy, nil
+}
+
+type rootScoped struct{}
+
+func (*rootScoped) Name() apimeta.RESTScopeName {
+ return apimeta.RESTScopeNameRoot
+}
+
+func applyCRDs(cfg *helmaction.Configuration, policy v2.CRDsPolicy, chrt *helmchart.Chart,
+ vals helmchartcommon.Values, serverSideApply bool, waitStrategy helmkube.WaitStrategy,
+ waitOptions []helmkube.WaitOption, visitorFunc ...resource.VisitorFunc) error {
+
+ if len(chrt.CRDObjects()) == 0 {
+ return nil
+ }
+
+ l := cfg.Logger()
+
+ if policy == v2.Skip {
+ l.Info(fmt.Sprintf("skipping CustomResourceDefinition apply: policy is set to %s", policy))
+ return nil
+ }
+
+ if err := helmchartutil.ProcessDependencies(chrt, vals); err != nil {
+ return fmt.Errorf("failed to process chart dependencies: %w", err)
+ }
+
+ // We always force conflicts on server-side apply.
+ forceConflicts := serverSideApply
+
+ // Collect all CRDs from all files in `crds` directory.
+ allCRDs := make(helmkube.ResourceList, 0)
+ for _, obj := range chrt.CRDObjects() {
+ // Read in the resources
+ res, err := cfg.KubeClient.Build(bytes.NewBuffer(obj.File.Data), false)
+ if err != nil {
+ err = fmt.Errorf("failed to parse CustomResourceDefinitions from %s: %w", obj.Name, err)
+ l.Error(err.Error())
+ return err
+ }
+ allCRDs = append(allCRDs, res...)
+ }
+
+ // Visit CRDs with any provided visitor functions.
+ for _, visitor := range visitorFunc {
+ if err := allCRDs.Visit(visitor); err != nil {
+ return err
+ }
+ }
+
+ l.Info(fmt.Sprintf("applying CustomResourceDefinition(s) with policy %s", policy))
+ var totalItems []*resource.Info
+ switch policy {
+ case v2.Create:
+ for i := range allCRDs {
+ if rr, err := cfg.KubeClient.Create(allCRDs[i:i+1],
+ helmkube.ClientCreateOptionServerSideApply(serverSideApply, forceConflicts)); err != nil {
+ crdName := allCRDs[i].Name
+ // If the CustomResourceDefinition already exists, we skip it.
+ if apierrors.IsAlreadyExists(err) {
+ l.Info(fmt.Sprintf("CustomResourceDefinition %s is already present. Skipping.", crdName))
+ if rr != nil && rr.Created != nil {
+ totalItems = append(totalItems, rr.Created...)
+ }
+ continue
+ }
+ err = fmt.Errorf("failed to create CustomResourceDefinition %s: %w", crdName, err)
+ l.Error(err.Error())
+ return err
+ } else {
+ if rr != nil && rr.Created != nil {
+ totalItems = append(totalItems, rr.Created...)
+ }
+ }
+ }
+ case v2.CreateReplace:
+ config, err := cfg.RESTClientGetter.ToRESTConfig()
+ if err != nil {
+ err = fmt.Errorf("could not create Kubernetes client REST config: %w", err)
+ l.Error(err.Error())
+ return err
+ }
+ clientSet, err := apiextension.NewForConfig(config)
+ if err != nil {
+ err = fmt.Errorf("could not create Kubernetes client set for API extensions: %w", err)
+ l.Error(err.Error())
+ return err
+ }
+ client := clientSet.ApiextensionsV1().CustomResourceDefinitions()
+
+ // Note, we build the originals from the current set of Custom Resource
+ // Definitions, and therefore this upgrade will never delete CRDs that
+ // existed in the former release but no longer exist in the current
+ // release.
+ original := make(helmkube.ResourceList, 0)
+ for _, r := range allCRDs {
+ if o, err := client.Get(context.TODO(), r.Name, metav1.GetOptions{}); err == nil && o != nil {
+ o.GetResourceVersion()
+ original = append(original, &resource.Info{
+ Client: clientSet.ApiextensionsV1().RESTClient(),
+ Mapping: &apimeta.RESTMapping{
+ Resource: schema.GroupVersionResource{
+ Group: "apiextensions.k8s.io",
+ Version: r.Mapping.GroupVersionKind.Version,
+ Resource: "customresourcedefinition",
+ },
+ GroupVersionKind: schema.GroupVersionKind{
+ Kind: "CustomResourceDefinition",
+ Group: "apiextensions.k8s.io",
+ Version: r.Mapping.GroupVersionKind.Version,
+ },
+ Scope: &rootScoped{},
+ },
+ Namespace: o.ObjectMeta.Namespace,
+ Name: o.ObjectMeta.Name,
+ Object: o,
+ ResourceVersion: o.ObjectMeta.ResourceVersion,
+ })
+ } else if !apierrors.IsNotFound(err) {
+ err = fmt.Errorf("failed to get CustomResourceDefinition %s: %w", r.Name, err)
+ l.Error(err.Error())
+ return err
+ }
+ }
+
+ // We cannot pass both SSA=true and ForceReplace=true to the Helm client, but
+ // we always need to pass ClientUpdateOptionServerSideApply in order to disable
+ // SSA if serverSideApply is false (since the Helm SDK defaults to SSA=true).
+ opts := []helmkube.ClientUpdateOption{
+ kube.ClientUpdateOptionServerSideApply(serverSideApply, forceConflicts),
+ }
+ if !serverSideApply { // Pass ForceReplace only when SSA is disabled.
+ opts = append(opts, kube.ClientUpdateOptionForceReplace(true))
+ }
+
+ // Send them to Kubernetes...
+ if rr, err := cfg.KubeClient.Update(original, allCRDs, opts...); err != nil {
+ err = fmt.Errorf("failed to update CustomResourceDefinition(s): %w", err)
+ return err
+ } else {
+ if rr != nil {
+ if rr.Created != nil {
+ totalItems = append(totalItems, rr.Created...)
+ }
+ if rr.Updated != nil {
+ totalItems = append(totalItems, rr.Updated...)
+ }
+ if rr.Deleted != nil {
+ totalItems = append(totalItems, rr.Deleted...)
+ }
+ }
+ }
+ default:
+ err := fmt.Errorf("unexpected policy %s", policy)
+ l.Error(err.Error())
+ return err
+ }
+
+ if len(totalItems) > 0 {
+ // Give time for the CRD to be recognized.
+ var waiter kube.Waiter
+ var err error
+ if c, supportsOptions := cfg.KubeClient.(helmkube.InterfaceWaitOptions); supportsOptions {
+ waiter, err = c.GetWaiterWithOptions(waitStrategy, waitOptions...)
+ } else {
+ waiter, err = cfg.KubeClient.GetWaiter(waitStrategy)
+ }
+ if err != nil {
+ err = fmt.Errorf("failed to create CustomResourceDefinition waiter: %w", err)
+ l.Error(err.Error())
+ return err
+ }
+ if err := waiter.Wait(totalItems, 60*time.Second); err != nil {
+ err = fmt.Errorf("failed to wait for CustomResourceDefinition(s): %w", err)
+ l.Error(err.Error())
+ return err
+ }
+ l.Info(fmt.Sprintf("successfully applied %d CustomResourceDefinition(s)", len(totalItems)))
+
+ // Clear the RESTMapper cache, since it will not have the new CRDs.
+ // Helm does further invalidation of the client at a later stage
+ // when it gathers the server capabilities.
+ if m, err := cfg.RESTClientGetter.ToRESTMapper(); err == nil {
+ if rm, ok := m.(apimeta.ResettableRESTMapper); ok {
+ l.Info("clearing REST mapper cache")
+ rm.Reset()
+ }
+ }
+ }
+
+ return nil
+}
+
+func setOriginVisitor(group, namespace, name string) resource.VisitorFunc {
+ return func(info *resource.Info, err error) error {
+ if err != nil {
+ return err
+ }
+ if err = mergeLabels(info.Object, originLabels(group, namespace, name)); err != nil {
+ return fmt.Errorf(
+ "%s origin labels could not be updated: %s",
+ resourceString(info), err,
+ )
+ }
+ return nil
+ }
+}
+
+func originLabels(group, namespace, name string) map[string]string {
+ return map[string]string{
+ fmt.Sprintf("%s/name", group): name,
+ fmt.Sprintf("%s/namespace", group): namespace,
+ }
+}
+
+func mergeLabels(obj apiruntime.Object, labels map[string]string) error {
+ current, err := accessor.Labels(obj)
+ if err != nil {
+ return err
+ }
+ return accessor.SetLabels(obj, mergeStrStrMaps(current, labels))
+}
+
+func resourceString(info *resource.Info) string {
+ _, k := info.Mapping.GroupVersionKind.ToAPIVersionAndKind()
+ return fmt.Sprintf(
+ "%s %q in namespace %q",
+ k, info.Name, info.Namespace,
+ )
+}
+
+func mergeStrStrMaps(current, desired map[string]string) map[string]string {
+ result := make(map[string]string)
+ for k, v := range current {
+ result[k] = v
+ }
+ for k, desiredVal := range desired {
+ result[k] = desiredVal
+ }
+ return result
+}
diff --git a/internal/action/defaults.go b/internal/action/defaults.go
new file mode 100644
index 000000000..e6385fffb
--- /dev/null
+++ b/internal/action/defaults.go
@@ -0,0 +1,20 @@
+/*
+Copyright 2026 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package action
+
+// UseHelm3Defaults must be set from the feature gate of same name in main.go.
+var UseHelm3Defaults bool
diff --git a/internal/action/diff.go b/internal/action/diff.go
new file mode 100644
index 000000000..0f6d8bdc5
--- /dev/null
+++ b/internal/action/diff.go
@@ -0,0 +1,335 @@
+/*
+Copyright 2023 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package action
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "sort"
+ "strings"
+
+ helmaction "helm.sh/helm/v4/pkg/action"
+ helmrelease "helm.sh/helm/v4/pkg/release/v1"
+ apierrors "k8s.io/apimachinery/pkg/api/errors"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+ "k8s.io/apimachinery/pkg/types"
+ apierrutil "k8s.io/apimachinery/pkg/util/errors"
+ "k8s.io/utils/ptr"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+ "sigs.k8s.io/controller-runtime/pkg/client/apiutil"
+
+ "github.com/fluxcd/cli-utils/pkg/object"
+ "github.com/fluxcd/pkg/ssa"
+ "github.com/fluxcd/pkg/ssa/jsondiff"
+ ssanormalize "github.com/fluxcd/pkg/ssa/normalize"
+ ssautil "github.com/fluxcd/pkg/ssa/utils"
+
+ v2 "github.com/fluxcd/helm-controller/api/v2"
+
+ "github.com/fluxcd/helm-controller/internal/diff"
+)
+
+// Diff returns a jsondiff.DiffSet of the changes between the state of the
+// cluster and the Helm release.Release manifest.
+func Diff(ctx context.Context, config *helmaction.Configuration, rls *helmrelease.Release, fieldOwner string, disallowedFieldManagers []string, ignore ...v2.IgnoreRule) (jsondiff.DiffSet, error) {
+ // Create a dry-run only client to use solely for diffing.
+ cfg, err := config.RESTClientGetter.ToRESTConfig()
+ if err != nil {
+ return nil, err
+ }
+ c, err := client.New(cfg, client.Options{DryRun: ptr.To(true)})
+ if err != nil {
+ return nil, err
+ }
+
+ // Read the release manifest and normalize the objects.
+ objects, err := ssautil.ReadObjects(strings.NewReader(rls.Manifest))
+ if err != nil {
+ return nil, fmt.Errorf("failed to read objects from release manifest: %w", err)
+ }
+ if err = ssanormalize.UnstructuredListWithScheme(objects, c.Scheme()); err != nil {
+ return nil, fmt.Errorf("failed to normalize release objects: %w", err)
+ }
+
+ var (
+ isNamespacedGVK = map[string]bool{}
+ errs []error
+ )
+ for _, obj := range objects {
+ // Set the Helm metadata on the object which is normally set by Helm
+ // during object creation.
+ setHelmMetadata(obj, rls)
+
+ // Set the namespace of the object if it is not set.
+ if obj.GetNamespace() == "" {
+ // Manifest does not contain the namespace of the release.
+ // Figure out if the object is namespaced if the namespace is not
+ // explicitly set, and configure the namespace accordingly.
+ objGVK := obj.GetObjectKind().GroupVersionKind().String()
+ if _, ok := isNamespacedGVK[objGVK]; !ok {
+ namespaced, err := apiutil.IsObjectNamespaced(obj, c.Scheme(), c.RESTMapper())
+ if err != nil {
+ errs = append(errs, fmt.Errorf("failed to determine if %s is namespace scoped: %w",
+ obj.GetObjectKind().GroupVersionKind().Kind, err))
+ continue
+ }
+ // Cache the result, so we don't have to do this for every object
+ isNamespacedGVK[objGVK] = namespaced
+ }
+ if isNamespacedGVK[objGVK] {
+ obj.SetNamespace(rls.Namespace)
+ }
+ }
+ }
+
+ if len(disallowedFieldManagers) > 0 {
+ c, err := client.New(cfg, client.Options{})
+ if err != nil {
+ return nil, err
+ }
+ for _, obj := range objects {
+ err = replaceDisallowedFieldManagers(ctx, c, fieldOwner, disallowedFieldManagers, obj)
+ if err != nil {
+ return nil, fmt.Errorf("failed to clean-up disallowed field managers: %w", err)
+ }
+ }
+ }
+
+ // Base configuration for the diffing of the object.
+ diffOpts := []jsondiff.ListOption{
+ jsondiff.FieldOwner(fieldOwner),
+ jsondiff.ExclusionSelector{v2.DriftDetectionMetadataKey: v2.DriftDetectionDisabledValue},
+ jsondiff.Rationalize(true),
+ jsondiff.Graceful(true),
+ }
+
+ // Add ignore rules to the diffing configuration.
+ var ignoreRules jsondiff.IgnoreRules
+ for _, rule := range ignore {
+ r := jsondiff.IgnoreRule{
+ Paths: rule.Paths,
+ }
+ if rule.Target != nil {
+ r.Selector = &jsondiff.Selector{
+ Group: rule.Target.Group,
+ Version: rule.Target.Version,
+ Kind: rule.Target.Kind,
+ Name: rule.Target.Name,
+ Namespace: rule.Target.Namespace,
+ AnnotationSelector: rule.Target.AnnotationSelector,
+ LabelSelector: rule.Target.LabelSelector,
+ }
+ }
+ ignoreRules = append(ignoreRules, r)
+ }
+ if len(ignoreRules) > 0 {
+ diffOpts = append(diffOpts, ignoreRules)
+ }
+
+ // Actually diff the objects.
+ set, err := jsondiff.UnstructuredList(ctx, c, objects, diffOpts...)
+ if err != nil {
+ errs = append(errs, err)
+ }
+ return set, apierrutil.Reduce(apierrutil.Flatten(apierrutil.NewAggregate(errs)))
+}
+
+// ApplyDiff applies the changes described in the provided jsondiff.DiffSet to
+// the Kubernetes cluster.
+func ApplyDiff(ctx context.Context, config *helmaction.Configuration, diffSet jsondiff.DiffSet, fieldOwner string) (*ssa.ChangeSet, error) {
+ cfg, err := config.RESTClientGetter.ToRESTConfig()
+ if err != nil {
+ return nil, err
+ }
+ c, err := client.New(cfg, client.Options{})
+ if err != nil {
+ return nil, err
+ }
+
+ var toCreate, toPatch sortableDiffs
+ for _, d := range diffSet {
+ switch d.Type {
+ case jsondiff.DiffTypeCreate:
+ toCreate = append(toCreate, d)
+ case jsondiff.DiffTypeUpdate:
+ toPatch = append(toPatch, d)
+ }
+ }
+
+ var (
+ changeSet = ssa.NewChangeSet()
+ errs []error
+ )
+
+ sort.Sort(toCreate)
+ for _, d := range toCreate {
+ obj := d.DesiredObject.DeepCopyObject().(client.Object)
+ if err := c.Create(ctx, obj, client.FieldOwner(fieldOwner)); err != nil {
+ errs = append(errs, fmt.Errorf("%s creation failure: %w", diff.ResourceName(obj), err))
+ continue
+ }
+ changeSet.Add(objectToChangeSetEntry(obj, ssa.CreatedAction))
+ }
+
+ sort.Sort(toPatch)
+ for _, d := range toPatch {
+ data, err := json.Marshal(d.Patch)
+ if err != nil {
+ errs = append(errs, fmt.Errorf("%s patch failure: %w", diff.ResourceName(d.DesiredObject), err))
+ continue
+ }
+
+ obj := d.DesiredObject.DeepCopyObject().(client.Object)
+ patch := client.RawPatch(types.JSONPatchType, data)
+ if err := c.Patch(ctx, obj, patch, client.FieldOwner(fieldOwner)); err != nil {
+ if obj.GetObjectKind().GroupVersionKind().Kind == "Secret" {
+ err = maskSensitiveErrData(err)
+ }
+ errs = append(errs, fmt.Errorf("%s patch failure: %w", diff.ResourceName(obj), err))
+ continue
+ }
+ changeSet.Add(objectToChangeSetEntry(obj, ssa.ConfiguredAction))
+ }
+
+ return changeSet, apierrutil.NewAggregate(errs)
+}
+
+const (
+ appManagedByLabel = "app.kubernetes.io/managed-by"
+ appManagedByHelm = "Helm"
+ helmReleaseNameAnnotation = "meta.helm.sh/release-name"
+ helmReleaseNamespaceAnnotation = "meta.helm.sh/release-namespace"
+)
+
+// setHelmMetadata sets the metadata on the given object to indicate that it is
+// managed by Helm. This is safe to do, because we apply it to objects that
+// originate from the Helm release itself.
+// xref: https://github.com/helm/helm/blob/v3.13.2/pkg/action/validate.go
+// xref: https://github.com/helm/helm/blob/v3.13.2/pkg/action/rollback.go#L186-L191
+func setHelmMetadata(obj client.Object, rls *helmrelease.Release) {
+ labels := obj.GetLabels()
+ if labels == nil {
+ labels = make(map[string]string, 1)
+ }
+ labels[appManagedByLabel] = appManagedByHelm
+ obj.SetLabels(labels)
+
+ annotations := obj.GetAnnotations()
+ if annotations == nil {
+ annotations = make(map[string]string, 2)
+ }
+ annotations[helmReleaseNameAnnotation] = rls.Name
+ annotations[helmReleaseNamespaceAnnotation] = rls.Namespace
+ obj.SetAnnotations(annotations)
+}
+
+// objectToChangeSetEntry returns a ssa.ChangeSetEntry for the given object and
+// action.
+func objectToChangeSetEntry(obj client.Object, action ssa.Action) ssa.ChangeSetEntry {
+ return ssa.ChangeSetEntry{
+ ObjMetadata: object.ObjMetadata{
+ GroupKind: obj.GetObjectKind().GroupVersionKind().GroupKind(),
+ Name: obj.GetName(),
+ Namespace: obj.GetNamespace(),
+ },
+ GroupVersion: obj.GetObjectKind().GroupVersionKind().Version,
+ Subject: diff.ResourceName(obj),
+ Action: action,
+ }
+}
+
+// maskSensitiveErrData masks potentially sensitive data from the error message
+// returned by the Kubernetes API server.
+// This avoids leaking any sensitive data in logs or other output when a patch
+// operation fails.
+func maskSensitiveErrData(err error) error {
+ if apierrors.IsInvalid(err) {
+ // The last part of the error message is the reason for the error.
+ if i := strings.LastIndex(err.Error(), `:`); i != -1 {
+ err = errors.New(strings.TrimSpace(err.Error()[i+1:]))
+ }
+ }
+ return err
+}
+
+// sortableDiffs is a sortable slice of jsondiff.Diffs.
+type sortableDiffs []*jsondiff.Diff
+
+// Len returns the length of the slice.
+func (s sortableDiffs) Len() int { return len(s) }
+
+// Swap swaps the elements with indexes i and j.
+func (s sortableDiffs) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
+
+// Less returns true if the element with index i should sort before the element
+// with index j.
+// The elements are sorted by GroupKind, Namespace and Name.
+func (s sortableDiffs) Less(i, j int) bool {
+ iDiff, jDiff := s[i], s[j]
+
+ if !ssa.Equals(iDiff.GroupVersionKind().GroupKind(), jDiff.GroupVersionKind().GroupKind()) {
+ return ssa.IsLessThan(iDiff.GroupVersionKind().GroupKind(), jDiff.GroupVersionKind().GroupKind())
+ }
+
+ if iDiff.GetNamespace() != jDiff.GetNamespace() {
+ return iDiff.GetNamespace() < jDiff.GetNamespace()
+ }
+
+ return iDiff.GetName() < jDiff.GetName()
+}
+
+func replaceDisallowedFieldManagers(ctx context.Context, c client.Client, fieldOwner string, disallowedFieldManagers []string, obj *unstructured.Unstructured) error {
+ existingObj := &unstructured.Unstructured{}
+ existingObj.SetGroupVersionKind(obj.GroupVersionKind())
+ if err := c.Get(ctx, client.ObjectKeyFromObject(obj), existingObj); err != nil {
+ if apierrors.IsNotFound(err) {
+ return nil
+ }
+ return err
+ }
+
+ fieldManagers := []ssa.FieldManager{}
+ for _, fieldManager := range disallowedFieldManagers {
+ fieldManagers = append(fieldManagers, ssa.FieldManager{
+ Name: fieldManager,
+ OperationType: metav1.ManagedFieldsOperationApply,
+ })
+ fieldManagers = append(fieldManagers, ssa.FieldManager{
+ Name: fieldManager,
+ OperationType: metav1.ManagedFieldsOperationUpdate,
+ })
+ }
+
+ patches, err := ssa.PatchReplaceFieldsManagers(existingObj, fieldManagers, fieldOwner)
+ if err != nil {
+ return err
+ }
+ if len(patches) > 0 {
+ rawPatch, err := json.Marshal(patches)
+ if err != nil {
+ return err
+ }
+ err = c.Patch(ctx, existingObj, client.RawPatch(types.JSONPatchType, rawPatch), client.FieldOwner(fieldOwner))
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+}
diff --git a/internal/action/diff_test.go b/internal/action/diff_test.go
new file mode 100644
index 000000000..9fb19cd72
--- /dev/null
+++ b/internal/action/diff_test.go
@@ -0,0 +1,946 @@
+/*
+Copyright 2023 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package action
+
+import (
+ "context"
+ "encoding/base64"
+ "fmt"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/google/go-cmp/cmp"
+ "github.com/google/go-cmp/cmp/cmpopts"
+ . "github.com/onsi/gomega"
+ extjsondiff "github.com/wI2L/jsondiff"
+ helmaction "helm.sh/helm/v4/pkg/action"
+ helmrelease "helm.sh/helm/v4/pkg/release/v1"
+ corev1 "k8s.io/api/core/v1"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+ "k8s.io/apimachinery/pkg/types"
+ apierrutil "k8s.io/apimachinery/pkg/util/errors"
+ "k8s.io/apimachinery/pkg/util/rand"
+ "k8s.io/client-go/rest"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+ "sigs.k8s.io/controller-runtime/pkg/envtest"
+
+ "github.com/fluxcd/pkg/apis/kustomize"
+ "github.com/fluxcd/pkg/ssa"
+ "github.com/fluxcd/pkg/ssa/jsondiff"
+ ssanormalize "github.com/fluxcd/pkg/ssa/normalize"
+ ssautil "github.com/fluxcd/pkg/ssa/utils"
+
+ v2 "github.com/fluxcd/helm-controller/api/v2"
+ "github.com/fluxcd/helm-controller/internal/kube"
+)
+
+func TestDiff(t *testing.T) {
+ // Normally, we would create e.g. a `suite_test.go` file with a `TestMain`
+ // function. As this is one of the few tests in this package which needs a
+ // test cluster, we create it here instead.
+ config, cleanup := newTestCluster(t)
+ t.Cleanup(func() {
+ t.Log("Stopping the test environment")
+ if err := cleanup(); err != nil {
+ t.Logf("Failed to stop the test environment: %v", err)
+ }
+ })
+
+ // Construct a REST client getter for Helm's action configuration.
+ getter := kube.NewMemoryRESTClientGetter(config)
+
+ // Construct a client for to be able to mutate the cluster.
+ c, err := client.New(config, client.Options{})
+ if err != nil {
+ t.Fatalf("Failed to create client for test environment: %v", err)
+ }
+
+ const testOwner = "helm-controller"
+ const ownerToOverride = "kubectl"
+ const ownerToKeep = "kube-controller-manager"
+
+ tests := []struct {
+ name string
+ manifest string
+ ignoreRules []v2.IgnoreRule
+ mutateCluster func(objs []*unstructured.Unstructured, namespace string) ([]*unstructured.Unstructured, error)
+ updateCluster func(objs []*unstructured.Unstructured, namespace string) ([]*unstructured.Unstructured, client.FieldOwner, error)
+ want func(namespace string, desired, cluster []*unstructured.Unstructured) jsondiff.DiffSet
+ wantErr bool
+ }{
+ {
+ name: "detects drift",
+ manifest: `---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: changed
+data:
+ key: value
+---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: deleted
+data:
+ key: value
+---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: unchanged
+data:
+ key: value`,
+ mutateCluster: func(objs []*unstructured.Unstructured, namespace string) ([]*unstructured.Unstructured, error) {
+ var clusterObjs []*unstructured.Unstructured
+ for _, obj := range objs {
+ if obj.GetName() == "deleted" {
+ continue
+ }
+
+ obj := obj.DeepCopy()
+ obj.SetNamespace(namespace)
+
+ if obj.GetName() == "changed" {
+ if err := unstructured.SetNestedField(obj.Object, "changed", "data", "key"); err != nil {
+ return nil, fmt.Errorf("failed to set nested field: %w", err)
+ }
+ }
+
+ clusterObjs = append(clusterObjs, obj)
+ }
+ return clusterObjs, nil
+ },
+ want: func(namespace string, desired, cluster []*unstructured.Unstructured) jsondiff.DiffSet {
+ return jsondiff.DiffSet{
+ {
+ Type: jsondiff.DiffTypeUpdate,
+ DesiredObject: namespacedUnstructured(desired[0], namespace),
+ ClusterObject: cluster[0],
+ Patch: extjsondiff.Patch{
+ {
+ Type: extjsondiff.OperationReplace,
+ OldValue: "changed",
+ Value: "value",
+ Path: "/data/key",
+ },
+ },
+ },
+ {
+ Type: jsondiff.DiffTypeCreate,
+ DesiredObject: namespacedUnstructured(desired[1], namespace),
+ },
+ {
+ Type: jsondiff.DiffTypeNone,
+ DesiredObject: namespacedUnstructured(desired[2], namespace),
+ ClusterObject: cluster[1],
+ },
+ }
+ },
+ },
+ {
+ name: "detects drift after kubectl edit",
+ manifest: `---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: changed
+data:
+ key: value`,
+ mutateCluster: func(objs []*unstructured.Unstructured, namespace string) ([]*unstructured.Unstructured, error) {
+ var clusterObjs []*unstructured.Unstructured
+ for _, obj := range objs {
+ obj := obj.DeepCopy()
+ obj.SetNamespace(namespace)
+ clusterObjs = append(clusterObjs, obj)
+ }
+ return clusterObjs, nil
+ },
+ updateCluster: func(objs []*unstructured.Unstructured, namespace string) ([]*unstructured.Unstructured, client.FieldOwner, error) {
+ var clusterObjs []*unstructured.Unstructured
+ for _, obj := range objs {
+ obj := obj.DeepCopy()
+ if err := unstructured.SetNestedField(obj.Object, "changed", "data", "anotherKey"); err != nil {
+ return nil, "", fmt.Errorf("failed to set nested field: %w", err)
+ }
+ clusterObjs = append(clusterObjs, obj)
+ }
+ return clusterObjs, ownerToOverride, nil
+ },
+ want: func(namespace string, desired, cluster []*unstructured.Unstructured) jsondiff.DiffSet {
+ return jsondiff.DiffSet{
+ {
+ Type: jsondiff.DiffTypeUpdate,
+ DesiredObject: namespacedUnstructured(desired[0], namespace),
+ ClusterObject: cluster[0],
+ Patch: extjsondiff.Patch{
+ {
+ Type: extjsondiff.OperationRemove,
+ Path: "/data/anotherKey",
+ OldValue: "changed",
+ },
+ },
+ },
+ }
+ },
+ },
+ {
+ name: "detect no drift if edited by kube-controller-manager",
+ manifest: `---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: changed
+data:
+ key: value`,
+ mutateCluster: func(objs []*unstructured.Unstructured, namespace string) ([]*unstructured.Unstructured, error) {
+ var clusterObjs []*unstructured.Unstructured
+ for _, obj := range objs {
+ obj := obj.DeepCopy()
+ obj.SetNamespace(namespace)
+ clusterObjs = append(clusterObjs, obj)
+ }
+ return clusterObjs, nil
+ },
+ updateCluster: func(objs []*unstructured.Unstructured, namespace string) ([]*unstructured.Unstructured, client.FieldOwner, error) {
+ var clusterObjs []*unstructured.Unstructured
+ for _, obj := range objs {
+ obj := obj.DeepCopy()
+ if err := unstructured.SetNestedField(obj.Object, "changed", "data", "anotherKey"); err != nil {
+ return nil, "", fmt.Errorf("failed to set nested field: %w", err)
+ }
+ clusterObjs = append(clusterObjs, obj)
+ }
+ return clusterObjs, ownerToKeep, nil
+ },
+ want: func(namespace string, desired, cluster []*unstructured.Unstructured) jsondiff.DiffSet {
+ return jsondiff.DiffSet{
+ {
+ Type: jsondiff.DiffTypeNone,
+ DesiredObject: namespacedUnstructured(desired[0], namespace),
+ ClusterObject: cluster[0],
+ },
+ }
+ },
+ },
+ {
+ name: "empty release manifest",
+ manifest: "",
+ },
+ {
+ name: "manifest with disabled annotation",
+ manifest: fmt.Sprintf(`---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: disabled
+ annotations:
+ %[1]s: %[2]s
+data:
+ key: value`, v2.DriftDetectionMetadataKey, v2.DriftDetectionDisabledValue),
+ mutateCluster: func(objs []*unstructured.Unstructured, namespace string) ([]*unstructured.Unstructured, error) {
+ var clusterObjs []*unstructured.Unstructured
+ for _, obj := range objs {
+ obj := obj.DeepCopy()
+ obj.SetNamespace(namespace)
+ if err := unstructured.SetNestedField(obj.Object, "changed", "data", "key"); err != nil {
+ return nil, fmt.Errorf("failed to set nested field: %w", err)
+ }
+ clusterObjs = append(clusterObjs, obj)
+ }
+ return clusterObjs, nil
+ },
+ want: func(namespace string, desired, cluster []*unstructured.Unstructured) jsondiff.DiffSet {
+ return jsondiff.DiffSet{
+ {
+ Type: jsondiff.DiffTypeExclude,
+ DesiredObject: namespacedUnstructured(desired[0], namespace),
+ },
+ }
+ },
+ },
+ {
+ name: "adheres to ignore rules",
+ manifest: `---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: fully-ignored
+data:
+ key: value
+---
+apiVersion: v1
+kind: Secret
+metadata:
+ name: partially-ignored
+stringData:
+ key: value
+ otherKey: otherValue
+---
+apiVersion: v1
+kind: Secret
+metadata:
+ name: globally-ignored
+stringData:
+ globalKey: globalValue
+---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: not-ignored
+data:
+ key: value`,
+ mutateCluster: func(objs []*unstructured.Unstructured, namespace string) ([]*unstructured.Unstructured, error) {
+ var clusterObjs []*unstructured.Unstructured
+ for _, obj := range objs {
+ obj := obj.DeepCopy()
+ obj.SetNamespace(namespace)
+
+ switch obj.GetName() {
+ case "fully-ignored", "not-ignored":
+ if err := unstructured.SetNestedField(obj.Object, "changed", "data", "key"); err != nil {
+ return nil, fmt.Errorf("failed to set nested field: %w", err)
+ }
+ case "partially-ignored":
+ if err := unstructured.SetNestedField(obj.Object, "changed", "stringData", "key"); err != nil {
+ return nil, fmt.Errorf("failed to set nested field: %w", err)
+ }
+ if err := unstructured.SetNestedField(obj.Object, "changed", "stringData", "otherKey"); err != nil {
+ return nil, fmt.Errorf("failed to set nested field: %w", err)
+ }
+ case "globally-ignored":
+ if err := unstructured.SetNestedField(obj.Object, "changed", "stringData", "globalKey"); err != nil {
+ return nil, fmt.Errorf("failed to set nested field: %w", err)
+ }
+ }
+ clusterObjs = append(clusterObjs, obj)
+ }
+ return clusterObjs, nil
+ },
+ ignoreRules: []v2.IgnoreRule{
+ {Target: &kustomize.Selector{Name: "fully-ignored"}, Paths: []string{""}},
+ {Target: &kustomize.Selector{Name: "partially-ignored"}, Paths: []string{"/data/key"}},
+ {Paths: []string{"/data/globalKey"}},
+ },
+ want: func(namespace string, desired, cluster []*unstructured.Unstructured) jsondiff.DiffSet {
+ return jsondiff.DiffSet{
+ {
+ Type: jsondiff.DiffTypeExclude,
+ DesiredObject: namespacedUnstructured(desired[0], namespace),
+ },
+ {
+ Type: jsondiff.DiffTypeUpdate,
+ DesiredObject: namespacedUnstructured(desired[1], namespace),
+ ClusterObject: cluster[1],
+ Patch: extjsondiff.Patch{
+ {
+ Type: extjsondiff.OperationReplace,
+ Path: "/data/otherKey",
+ OldValue: base64.StdEncoding.EncodeToString([]byte("changed")),
+ Value: base64.StdEncoding.EncodeToString([]byte("otherValue")),
+ },
+ },
+ },
+ {
+ Type: jsondiff.DiffTypeNone,
+ DesiredObject: namespacedUnstructured(desired[2], namespace),
+ ClusterObject: cluster[2],
+ },
+ {
+ Type: jsondiff.DiffTypeUpdate,
+ DesiredObject: namespacedUnstructured(desired[3], namespace),
+ ClusterObject: cluster[3],
+ Patch: extjsondiff.Patch{
+ {
+ Type: extjsondiff.OperationReplace,
+ Path: "/data/key",
+ OldValue: "changed",
+ Value: "value",
+ },
+ },
+ },
+ }
+ },
+ },
+ {
+ name: "configures namespace",
+ manifest: `---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: without-namespace
+data:
+ key: value
+---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: with-namespace
+ namespace: diff-fixed-ns
+data:
+ key: value`,
+ mutateCluster: func(objs []*unstructured.Unstructured, namespace string) ([]*unstructured.Unstructured, error) {
+ var clusterObjs []*unstructured.Unstructured
+
+ otherNS := unstructured.Unstructured{Object: map[string]any{
+ "apiVersion": "v1",
+ "kind": "Namespace",
+ "metadata": map[string]any{
+ "name": "diff-fixed-ns",
+ },
+ }}
+ clusterObjs = append(clusterObjs, &otherNS)
+
+ for _, obj := range objs {
+ obj := obj.DeepCopy()
+ if obj.GetNamespace() == "" {
+ obj.SetNamespace(namespace)
+ }
+ clusterObjs = append(clusterObjs, obj)
+ }
+ return clusterObjs, nil
+ },
+ want: func(namespace string, desired, cluster []*unstructured.Unstructured) jsondiff.DiffSet {
+ return jsondiff.DiffSet{
+ {
+ Type: jsondiff.DiffTypeNone,
+ DesiredObject: namespacedUnstructured(desired[0], namespace),
+ ClusterObject: cluster[1],
+ },
+ {
+ Type: jsondiff.DiffTypeNone,
+ DesiredObject: namespacedUnstructured(desired[1], desired[1].GetNamespace()),
+ ClusterObject: cluster[2],
+ },
+ }
+ },
+ },
+ {
+ name: "configures Helm metadata",
+ manifest: `---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: without-helm-metadata
+data:
+ key: value`,
+ mutateCluster: func(objs []*unstructured.Unstructured, namespace string) ([]*unstructured.Unstructured, error) {
+ var clusterObjs []*unstructured.Unstructured
+ for _, obj := range objs {
+ obj := obj.DeepCopy()
+ if obj.GetNamespace() == "" {
+ obj.SetNamespace(namespace)
+ }
+ obj.SetAnnotations(nil)
+ obj.SetLabels(nil)
+ clusterObjs = append(clusterObjs, obj)
+ }
+ return clusterObjs, nil
+ },
+ want: func(namespace string, desired, cluster []*unstructured.Unstructured) jsondiff.DiffSet {
+ return jsondiff.DiffSet{
+ {
+ Type: jsondiff.DiffTypeUpdate,
+ DesiredObject: namespacedUnstructured(desired[0], namespace),
+ ClusterObject: cluster[0],
+ Patch: extjsondiff.Patch{
+ {
+ Type: extjsondiff.OperationAdd,
+ Path: "/metadata/annotations",
+ Value: map[string]any{
+ helmReleaseNameAnnotation: "configures Helm metadata",
+ helmReleaseNamespaceAnnotation: namespace,
+ },
+ },
+ {
+ Type: extjsondiff.OperationAdd,
+ Path: "/metadata/labels",
+ Value: map[string]any{
+ appManagedByLabel: appManagedByHelm,
+ },
+ },
+ },
+ },
+ }
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+ t.Cleanup(cancel)
+
+ ns, err := generateNamespace(ctx, c, "diff-action")
+ if err != nil {
+ t.Fatalf("Failed to generate namespace: %v", err)
+ }
+ t.Cleanup(func() {
+ if err := c.Delete(context.Background(), ns); client.IgnoreNotFound(err) != nil {
+ t.Logf("Failed to delete generated namespace: %v", err)
+ }
+ })
+
+ rls := &helmrelease.Release{Name: tt.name, Namespace: ns.Name, Manifest: tt.manifest}
+
+ objs, err := ssautil.ReadObjects(strings.NewReader(tt.manifest))
+ if err != nil {
+ t.Fatalf("Failed to read release objects: %v", err)
+ }
+ for _, obj := range objs {
+ setHelmMetadata(obj, rls)
+ }
+
+ clusterObjs := objs
+ if tt.mutateCluster != nil {
+ if clusterObjs, err = tt.mutateCluster(objs, ns.Name); err != nil {
+ t.Fatalf("Failed to modify cluster resource: %v", err)
+ }
+ }
+
+ t.Cleanup(func() {
+ for _, obj := range clusterObjs {
+ if err := c.Delete(context.Background(), obj); client.IgnoreNotFound(err) != nil {
+ t.Logf("Failed to delete object: %v", err)
+ }
+ }
+ })
+
+ for _, obj := range clusterObjs {
+ if err = ssanormalize.Unstructured(obj); err != nil {
+ t.Fatalf("Failed to normalize cluster manifest: %v", err)
+ }
+ if err := c.Create(ctx, obj, client.FieldOwner(testOwner)); err != nil {
+ t.Fatalf("Failed to create object: %v", err)
+ }
+ }
+
+ if tt.updateCluster != nil {
+ // tt.updateCluster emulates out-of-band modifications like `kubectl edit`
+ var (
+ fieldOwner client.FieldOwner
+ )
+ if clusterObjs, fieldOwner, err = tt.updateCluster(clusterObjs, ns.Name); err != nil {
+ t.Fatalf("Failed to modify cluster resource: %v", err)
+ }
+ for _, obj := range clusterObjs {
+ if err := c.Update(ctx, obj, fieldOwner); err != nil {
+ t.Fatalf("Failed to update object: %v", err)
+ }
+ }
+ }
+
+ got, err := Diff(ctx, &helmaction.Configuration{RESTClientGetter: getter}, rls, testOwner, []string{ownerToOverride}, tt.ignoreRules...)
+ if (err != nil) != tt.wantErr {
+ t.Errorf("Diff() error = %v, wantErr %v", err, tt.wantErr)
+ return
+ }
+
+ // Refresh all objects since Diff might do mutations and this would change resourceVersion
+ for _, obj := range clusterObjs {
+ if err := c.Get(ctx, client.ObjectKeyFromObject(obj), obj); err != nil {
+ t.Fatalf("Failed to create object: %v", err)
+ }
+ }
+
+ var want jsondiff.DiffSet
+ if tt.want != nil {
+ want = tt.want(ns.Name, objs, clusterObjs)
+ }
+ if diff := cmp.Diff(want, got, cmpopts.IgnoreUnexported(extjsondiff.Operation{})); diff != "" {
+ t.Errorf("Diff() mismatch (-want +got):\n%s", diff)
+ }
+ })
+ }
+}
+
+func TestApplyDiff(t *testing.T) {
+ // Normally, we would create e.g. a `suite_test.go` file with a `TestMain`
+ // function. As this is one of the few tests in this package which needs a
+ // test cluster, we create it here instead.
+ config, cleanup := newTestCluster(t)
+ t.Cleanup(func() {
+ t.Log("Stopping the test environment")
+ if err := cleanup(); err != nil {
+ t.Logf("Failed to stop the test environment: %v", err)
+ }
+ })
+
+ // Construct a REST client getter for Helm's action configuration.
+ getter := kube.NewMemoryRESTClientGetter(config)
+
+ // Construct a client for to be able to mutate the cluster.
+ c, err := client.New(config, client.Options{})
+ if err != nil {
+ t.Fatalf("Failed to create client for test environment: %v", err)
+ }
+
+ const testOwner = "helm-controller"
+
+ tests := []struct {
+ name string
+ diffSet func(namespace string) jsondiff.DiffSet
+ expect func(g *GomegaWithT, namespace string, got *ssa.ChangeSet, err error)
+ }{
+ {
+ name: "creates and updates resources",
+ diffSet: func(namespace string) jsondiff.DiffSet {
+ return jsondiff.DiffSet{
+ {
+ Type: jsondiff.DiffTypeCreate,
+ DesiredObject: &unstructured.Unstructured{
+ Object: map[string]any{
+ "apiVersion": "v1",
+ "kind": "Secret",
+ "metadata": map[string]any{
+ "name": "test-secret",
+ "namespace": namespace,
+ },
+ "stringData": map[string]any{
+ "key": "value",
+ },
+ },
+ },
+ },
+ {
+ Type: jsondiff.DiffTypeUpdate,
+ DesiredObject: &unstructured.Unstructured{
+ Object: map[string]any{
+ "apiVersion": "v1",
+ "kind": "ConfigMap",
+ "metadata": map[string]any{
+ "name": "test-cm",
+ "namespace": namespace,
+ },
+ "data": map[string]any{
+ "key": "value",
+ },
+ },
+ },
+ ClusterObject: &unstructured.Unstructured{
+ Object: map[string]any{
+ "apiVersion": "v1",
+ "kind": "ConfigMap",
+ "metadata": map[string]any{
+ "name": "test-cm",
+ "namespace": namespace,
+ },
+ "data": map[string]any{
+ "key": "changed",
+ },
+ },
+ },
+ Patch: extjsondiff.Patch{
+ {
+ Type: extjsondiff.OperationReplace,
+ Path: "/data/key",
+ Value: "value",
+ },
+ },
+ },
+ }
+ },
+ expect: func(g *GomegaWithT, namespace string, got *ssa.ChangeSet, err error) {
+ g.THelper()
+
+ g.Expect(err).NotTo(HaveOccurred())
+
+ g.Expect(got).NotTo(BeNil())
+ g.Expect(got.Entries).To(HaveLen(2))
+
+ g.Expect(got.Entries[0].Subject).To(Equal("Secret/" + namespace + "/test-secret"))
+ g.Expect(got.Entries[0].Action).To(Equal(ssa.CreatedAction))
+ g.Expect(c.Get(context.TODO(), types.NamespacedName{
+ Namespace: namespace,
+ Name: "test-secret",
+ }, &corev1.Secret{})).To(Succeed())
+
+ g.Expect(got.Entries[1].Subject).To(Equal("ConfigMap/" + namespace + "/test-cm"))
+ g.Expect(got.Entries[1].Action).To(Equal(ssa.ConfiguredAction))
+ cm := &corev1.ConfigMap{}
+ g.Expect(c.Get(context.TODO(), types.NamespacedName{
+ Namespace: namespace,
+ Name: "test-cm",
+ }, cm)).To(Succeed())
+ g.Expect(cm.Data).To(HaveKeyWithValue("key", "value"))
+ },
+ },
+ {
+ name: "continues on error",
+ diffSet: func(namespace string) jsondiff.DiffSet {
+ return jsondiff.DiffSet{
+ {
+ Type: jsondiff.DiffTypeCreate,
+ DesiredObject: &unstructured.Unstructured{
+ Object: map[string]any{
+ "apiVersion": "v1",
+ "kind": "Secret",
+ "metadata": map[string]any{
+ "name": "invalid-test-secret",
+ "namespace": namespace,
+ },
+ "data": map[string]any{
+ // Illegal base64 encoded data.
+ "key": "secret value",
+ },
+ },
+ },
+ },
+ {
+ Type: jsondiff.DiffTypeCreate,
+ DesiredObject: &unstructured.Unstructured{
+ Object: map[string]any{
+ "apiVersion": "v1",
+ "kind": "ConfigMap",
+ "metadata": map[string]any{
+ "name": "test-cm",
+ "namespace": namespace,
+ },
+ },
+ },
+ },
+ {
+ Type: jsondiff.DiffTypeUpdate,
+ DesiredObject: &unstructured.Unstructured{
+ Object: map[string]any{
+ "apiVersion": "v1",
+ "kind": "Secret",
+ "metadata": map[string]any{
+ "name": "invalid-test-secret-update",
+ "namespace": namespace,
+ },
+ "data": map[string]any{
+ // Illegal base64 encoded data.
+ "key": "secret value2",
+ },
+ },
+ },
+ ClusterObject: &unstructured.Unstructured{
+ Object: map[string]any{
+ "apiVersion": "v1",
+ "kind": "Secret",
+ "metadata": map[string]any{
+ "name": "invalid-test-secret-update",
+ "namespace": namespace,
+ },
+ "stringData": map[string]any{
+ "key": "value",
+ },
+ },
+ },
+ Patch: extjsondiff.Patch{
+ {
+ Type: extjsondiff.OperationReplace,
+ Path: "/data/key",
+ // Illegal base64 encoded data.
+ Value: "value",
+ },
+ },
+ },
+ {
+ Type: jsondiff.DiffTypeUpdate,
+ DesiredObject: &unstructured.Unstructured{
+ Object: map[string]any{
+ "apiVersion": "v1",
+ "kind": "ConfigMap",
+ "metadata": map[string]any{
+ "name": "test-cm-2",
+ "namespace": namespace,
+ },
+ "data": map[string]any{
+ "key": "value",
+ },
+ },
+ },
+ ClusterObject: &unstructured.Unstructured{
+ Object: map[string]any{
+ "apiVersion": "v1",
+ "kind": "ConfigMap",
+ "metadata": map[string]any{
+ "name": "test-cm-2",
+ "namespace": namespace,
+ },
+ "data": map[string]any{
+ "key": "changed",
+ },
+ },
+ },
+ Patch: extjsondiff.Patch{
+ {
+ Type: extjsondiff.OperationReplace,
+ Path: "/data/key",
+ Value: "value",
+ },
+ },
+ },
+ }
+ },
+ expect: func(g *GomegaWithT, namespace string, got *ssa.ChangeSet, err error) {
+ g.THelper()
+
+ g.Expect(err).To(HaveOccurred())
+ g.Expect(err.(apierrutil.Aggregate).Errors()).To(HaveLen(2))
+ g.Expect(err.Error()).To(ContainSubstring("invalid-test-secret creation failure"))
+ g.Expect(err.Error()).To(ContainSubstring("invalid-test-secret-update patch failure"))
+
+ // Verify that the error message does not contain the secret data.
+ g.Expect(err.Error()).ToNot(ContainSubstring("secret value"))
+ g.Expect(err.Error()).ToNot(ContainSubstring("secret value2"))
+
+ g.Expect(got).NotTo(BeNil())
+ g.Expect(got.Entries).To(HaveLen(2))
+
+ g.Expect(got.Entries[0].Subject).To(Equal("ConfigMap/" + namespace + "/test-cm"))
+ g.Expect(got.Entries[0].Action).To(Equal(ssa.CreatedAction))
+ g.Expect(c.Get(context.TODO(), types.NamespacedName{
+ Namespace: namespace,
+ Name: "test-cm",
+ }, &corev1.ConfigMap{})).To(Succeed())
+
+ g.Expect(got.Entries[1].Subject).To(Equal("ConfigMap/" + namespace + "/test-cm-2"))
+ g.Expect(got.Entries[1].Action).To(Equal(ssa.ConfiguredAction))
+
+ cm2 := &corev1.ConfigMap{}
+ g.Expect(c.Get(context.TODO(), types.NamespacedName{
+ Namespace: namespace,
+ Name: "test-cm-2",
+ }, cm2)).To(Succeed())
+ g.Expect(cm2.Data).To(HaveKeyWithValue("key", "value"))
+ },
+ },
+ {
+ name: "creates namespace before dependent resources",
+ diffSet: func(namespace string) jsondiff.DiffSet {
+ otherNS := generateName("test-ns")
+
+ return jsondiff.DiffSet{
+ {
+ Type: jsondiff.DiffTypeCreate,
+ DesiredObject: &unstructured.Unstructured{
+ Object: map[string]any{
+ "apiVersion": "v1",
+ "kind": "ConfigMap",
+ "metadata": map[string]any{
+ "name": "test-cm",
+ "namespace": otherNS,
+ },
+ },
+ },
+ },
+ {
+ Type: jsondiff.DiffTypeCreate,
+ DesiredObject: &unstructured.Unstructured{
+ Object: map[string]any{
+ "apiVersion": "v1",
+ "kind": "Namespace",
+ "metadata": map[string]any{
+ "name": otherNS,
+ },
+ },
+ },
+ },
+ }
+ },
+ expect: func(g *GomegaWithT, namespace string, got *ssa.ChangeSet, err error) {
+ g.THelper()
+
+ g.Expect(err).NotTo(HaveOccurred())
+
+ g.Expect(got).NotTo(BeNil())
+ g.Expect(got.Entries).To(HaveLen(2))
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ g := NewWithT(t)
+
+ ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+ t.Cleanup(cancel)
+
+ ns, err := generateNamespace(ctx, c, "diff-action")
+ if err != nil {
+ t.Fatalf("Failed to generate namespace: %v", err)
+ }
+ t.Cleanup(func() {
+ if err := c.Delete(context.Background(), ns); client.IgnoreNotFound(err) != nil {
+ t.Logf("Failed to delete generated namespace: %v", err)
+ }
+ })
+
+ diff := tt.diffSet(ns.Name)
+
+ for _, d := range diff {
+ if d.ClusterObject != nil {
+ if err := c.Create(ctx, d.ClusterObject, client.FieldOwner(testOwner)); err != nil {
+ t.Fatalf("Failed to create cluster object: %v", err)
+ }
+ t.Cleanup(func() {
+ if err := c.Delete(ctx, d.ClusterObject); client.IgnoreNotFound(err) != nil {
+ t.Logf("Failed to delete cluster object: %v", err)
+ }
+ })
+ }
+ }
+
+ got, err := ApplyDiff(context.Background(), &helmaction.Configuration{RESTClientGetter: getter}, diff, testOwner)
+ tt.expect(g, ns.Name, got, err)
+ })
+ }
+}
+
+// newTestCluster creates a new test cluster and returns a rest.Config and a
+// function to stop the test cluster.
+func newTestCluster(t *testing.T) (*rest.Config, func() error) {
+ t.Helper()
+
+ testEnv := &envtest.Environment{}
+
+ t.Log("Starting the test environment")
+ if _, err := testEnv.Start(); err != nil {
+ t.Fatalf("Failed to start the test environment: %v", err)
+ }
+
+ return testEnv.Config, testEnv.Stop
+}
+
+// generateNamespace creates a new namespace with the given generateName and
+// returns the namespace object.
+func generateNamespace(ctx context.Context, c client.Client, generateName string) (*corev1.Namespace, error) {
+ ns := &corev1.Namespace{
+ ObjectMeta: metav1.ObjectMeta{
+ GenerateName: fmt.Sprintf("%s-", generateName),
+ },
+ }
+ if err := c.Create(ctx, ns); err != nil {
+ return nil, err
+ }
+ return ns, nil
+}
+
+// generateName generates a name with the given name and a random suffix.
+func generateName(name string) string {
+ return fmt.Sprintf("%s-%s", name, rand.String(5))
+}
+
+func namespacedUnstructured(obj *unstructured.Unstructured, namespace string) *unstructured.Unstructured {
+ obj = obj.DeepCopy()
+ obj.SetNamespace(namespace)
+ _ = ssanormalize.Unstructured(obj)
+ return obj
+}
diff --git a/internal/action/install.go b/internal/action/install.go
new file mode 100644
index 000000000..6bfba8f65
--- /dev/null
+++ b/internal/action/install.go
@@ -0,0 +1,117 @@
+/*
+Copyright 2022 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package action
+
+import (
+ "context"
+ "fmt"
+
+ helmaction "helm.sh/helm/v4/pkg/action"
+ helmchartutil "helm.sh/helm/v4/pkg/chart/common"
+ helmchart "helm.sh/helm/v4/pkg/chart/v2"
+ helmrelease "helm.sh/helm/v4/pkg/release/v1"
+
+ v2 "github.com/fluxcd/helm-controller/api/v2"
+ "github.com/fluxcd/helm-controller/internal/features"
+ "github.com/fluxcd/helm-controller/internal/postrender"
+ "github.com/fluxcd/helm-controller/internal/release"
+)
+
+// InstallOption can be used to modify Helm's action.Install after the instructions
+// from the v2.HelmRelease have been applied. This is for example useful to
+// enable the dry-run setting as a CLI.
+type InstallOption func(action *helmaction.Install)
+
+// Install runs the Helm install action with the provided config, using the
+// v2.HelmReleaseSpec of the given object to determine the target release
+// and rollback configuration.
+//
+// It performs the installation according to the spec, which includes installing
+// the CRDs according to the defined policy.
+//
+// It does not determine if there is a desire to perform the action, this is
+// expected to be done by the caller. In addition, it does not take note of the
+// action result. The caller is expected to listen to this using a
+// storage.ObserveFunc, which provides superior access to Helm storage writes.
+func Install(ctx context.Context, config *helmaction.Configuration, obj *v2.HelmRelease,
+ chrt *helmchart.Chart, vals helmchartutil.Values, opts ...InstallOption) (*helmrelease.Release, error) {
+ install := newInstall(config, obj, opts)
+ install.ForceConflicts = install.ServerSideApply // We always force conflicts on server-side apply.
+
+ policy, err := crdPolicyOrDefault(obj.GetInstall().CRDs)
+ if err != nil {
+ return nil, err
+ }
+ if err := applyCRDs(config, policy, chrt, vals, install.ServerSideApply,
+ install.WaitStrategy, install.WaitOptions,
+ setOriginVisitor(v2.GroupVersion.Group, obj.Namespace, obj.Name)); err != nil {
+ return nil, fmt.Errorf("failed to apply CustomResourceDefinitions: %w", err)
+ }
+
+ rlsr, err := install.RunWithContext(ctx, chrt, vals.AsMap())
+ if err != nil {
+ return nil, err
+ }
+ rlsrTyped, ok := rlsr.(*helmrelease.Release)
+ if !ok {
+ return nil, fmt.Errorf("only the Chart API v2 is supported")
+ }
+ return rlsrTyped, err
+}
+
+func newInstall(config *helmaction.Configuration, obj *v2.HelmRelease, opts []InstallOption) *helmaction.Install {
+ install := helmaction.NewInstall(config)
+ switch {
+ case UseHelm3Defaults:
+ install.ServerSideApply = false
+ default:
+ install.ServerSideApply = true
+ }
+ if ssa := obj.GetInstall().ServerSideApply; ssa != nil {
+ install.ServerSideApply = *ssa
+ }
+
+ install.ReleaseName = release.ShortenName(obj.GetReleaseName())
+ install.Namespace = obj.GetReleaseNamespace()
+ install.Timeout = obj.GetInstall().GetTimeout(obj.GetTimeout()).Duration
+ install.TakeOwnership = !obj.GetInstall().DisableTakeOwnership
+ install.WaitStrategy = getWaitStrategy(obj.GetWaitStrategy(), obj.GetInstall())
+ install.WaitForJobs = !obj.GetInstall().DisableWaitForJobs
+ install.DisableHooks = obj.GetInstall().DisableHooks
+ install.DisableOpenAPIValidation = obj.GetInstall().DisableOpenAPIValidation
+ install.SkipSchemaValidation = obj.GetInstall().DisableSchemaValidation
+ install.Replace = obj.GetInstall().Replace
+ install.Devel = true
+ install.SkipCRDs = true
+
+ if obj.Spec.TargetNamespace != "" {
+ install.CreateNamespace = obj.GetInstall().CreateNamespace
+ }
+
+ // If the user opted-in to allow DNS lookups, enable it.
+ if allowDNS, _ := features.Enabled(features.AllowDNSLookups); allowDNS {
+ install.EnableDNS = allowDNS
+ }
+
+ install.PostRenderer = postrender.BuildPostRenderers(obj)
+
+ for _, opt := range opts {
+ opt(install)
+ }
+
+ return install
+}
diff --git a/internal/action/install_test.go b/internal/action/install_test.go
new file mode 100644
index 000000000..492c02de6
--- /dev/null
+++ b/internal/action/install_test.go
@@ -0,0 +1,211 @@
+/*
+Copyright 2022 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package action
+
+import (
+ "testing"
+ "time"
+
+ . "github.com/onsi/gomega"
+ helmaction "helm.sh/helm/v4/pkg/action"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+ v2 "github.com/fluxcd/helm-controller/api/v2"
+)
+
+func Test_newInstall(t *testing.T) {
+ t.Run("new install", func(t *testing.T) {
+ g := NewWithT(t)
+
+ obj := &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "install",
+ Namespace: "install-ns",
+ },
+ Spec: v2.HelmReleaseSpec{
+ Timeout: &metav1.Duration{Duration: time.Minute},
+ Install: &v2.Install{
+ Timeout: &metav1.Duration{Duration: 10 * time.Second},
+ Replace: true,
+ },
+ },
+ }
+
+ got := newInstall(&helmaction.Configuration{}, obj, nil)
+ g.Expect(got).ToNot(BeNil())
+ g.Expect(got.Namespace).To(Equal(obj.Namespace))
+ g.Expect(got.Timeout).To(Equal(obj.Spec.Install.Timeout.Duration))
+ g.Expect(got.Replace).To(Equal(obj.Spec.Install.Replace))
+ })
+
+ t.Run("timeout fallback", func(t *testing.T) {
+ g := NewWithT(t)
+
+ obj := &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "install",
+ Namespace: "install-ns",
+ },
+ Spec: v2.HelmReleaseSpec{
+ Timeout: &metav1.Duration{Duration: time.Minute},
+ },
+ }
+
+ got := newInstall(&helmaction.Configuration{}, obj, nil)
+ g.Expect(got).ToNot(BeNil())
+ g.Expect(got.Namespace).To(Equal(obj.Namespace))
+ g.Expect(got.Timeout).To(Equal(obj.Spec.Timeout.Duration))
+ })
+
+ t.Run("applies options", func(t *testing.T) {
+ g := NewWithT(t)
+
+ obj := &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "install",
+ Namespace: "install-ns",
+ },
+ Spec: v2.HelmReleaseSpec{},
+ }
+
+ got := newInstall(&helmaction.Configuration{}, obj, []InstallOption{
+ func(install *helmaction.Install) {
+ install.DisableHooks = true
+ },
+ func(install *helmaction.Install) {
+ install.DryRunStrategy = helmaction.DryRunClient
+ },
+ })
+ g.Expect(got).ToNot(BeNil())
+ g.Expect(got.DisableHooks).To(BeTrue())
+ g.Expect(got.DryRunStrategy).To(Equal(helmaction.DryRunClient))
+ })
+
+ t.Run("disable take ownership", func(t *testing.T) {
+ g := NewWithT(t)
+
+ obj := &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "install",
+ Namespace: "install-ns",
+ },
+ Spec: v2.HelmReleaseSpec{
+ Install: &v2.Install{
+ DisableTakeOwnership: true,
+ },
+ },
+ }
+
+ got := newInstall(&helmaction.Configuration{}, obj, nil)
+ g.Expect(got).ToNot(BeNil())
+ g.Expect(got.TakeOwnership).To(BeFalse())
+ })
+
+ t.Run("server side apply defaults to true with Helm4 defaults", func(t *testing.T) {
+ g := NewWithT(t)
+
+ // Save and restore UseHelm3Defaults
+ oldUseHelm3Defaults := UseHelm3Defaults
+ t.Cleanup(func() { UseHelm3Defaults = oldUseHelm3Defaults })
+ UseHelm3Defaults = false
+
+ obj := &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "install",
+ Namespace: "install-ns",
+ },
+ Spec: v2.HelmReleaseSpec{},
+ }
+
+ got := newInstall(&helmaction.Configuration{}, obj, nil)
+ g.Expect(got).ToNot(BeNil())
+ g.Expect(got.ServerSideApply).To(BeTrue())
+ })
+
+ t.Run("server side apply defaults to false with UseHelm3Defaults", func(t *testing.T) {
+ g := NewWithT(t)
+
+ // Save and restore UseHelm3Defaults
+ oldUseHelm3Defaults := UseHelm3Defaults
+ t.Cleanup(func() { UseHelm3Defaults = oldUseHelm3Defaults })
+ UseHelm3Defaults = true
+
+ obj := &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "install",
+ Namespace: "install-ns",
+ },
+ Spec: v2.HelmReleaseSpec{},
+ }
+
+ got := newInstall(&helmaction.Configuration{}, obj, nil)
+ g.Expect(got).ToNot(BeNil())
+ g.Expect(got.ServerSideApply).To(BeFalse())
+ })
+
+ t.Run("server side apply user specified true", func(t *testing.T) {
+ g := NewWithT(t)
+
+ // Save and restore UseHelm3Defaults
+ oldUseHelm3Defaults := UseHelm3Defaults
+ t.Cleanup(func() { UseHelm3Defaults = oldUseHelm3Defaults })
+ UseHelm3Defaults = true // default would be false
+
+ ssa := true
+ obj := &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "install",
+ Namespace: "install-ns",
+ },
+ Spec: v2.HelmReleaseSpec{
+ Install: &v2.Install{
+ ServerSideApply: &ssa,
+ },
+ },
+ }
+
+ got := newInstall(&helmaction.Configuration{}, obj, nil)
+ g.Expect(got).ToNot(BeNil())
+ g.Expect(got.ServerSideApply).To(BeTrue())
+ })
+
+ t.Run("server side apply user specified false", func(t *testing.T) {
+ g := NewWithT(t)
+
+ // Save and restore UseHelm3Defaults
+ oldUseHelm3Defaults := UseHelm3Defaults
+ t.Cleanup(func() { UseHelm3Defaults = oldUseHelm3Defaults })
+ UseHelm3Defaults = false // default would be true
+
+ ssa := false
+ obj := &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "install",
+ Namespace: "install-ns",
+ },
+ Spec: v2.HelmReleaseSpec{
+ Install: &v2.Install{
+ ServerSideApply: &ssa,
+ },
+ },
+ }
+
+ got := newInstall(&helmaction.Configuration{}, obj, nil)
+ g.Expect(got).ToNot(BeNil())
+ g.Expect(got.ServerSideApply).To(BeFalse())
+ })
+}
diff --git a/internal/action/log.go b/internal/action/log.go
new file mode 100644
index 000000000..f3b364485
--- /dev/null
+++ b/internal/action/log.go
@@ -0,0 +1,304 @@
+/*
+Copyright 2022 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package action
+
+import (
+ "container/ring"
+ "context"
+ "encoding/json"
+ "fmt"
+ "log/slog"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/go-logr/logr"
+ "sigs.k8s.io/controller-runtime/pkg/log"
+
+ "github.com/fluxcd/pkg/runtime/logger"
+)
+
+// nowTS can be used to stub out time.Now() in tests.
+var nowTS = time.Now
+
+// NewTraceLogger returns an slog.Handler that logs to the logger from the context at Trace level.
+func NewTraceLogger(ctx context.Context) slog.Handler {
+ return newLogBuffer(ctx, logger.TraceLevel)
+}
+
+// NewDebugLogBuffer returns an slog.Handler that logs to the logger from the context at Debug level,
+// and also keeps a ring buffer of log messages.
+func NewDebugLogBuffer(ctx context.Context) *LogBuffer {
+ l := newLogBuffer(ctx, logger.DebugLevel)
+ l.buf = newLogRingBuffer(ctx)
+ return l
+}
+
+// LogBuffer implements slog.Handler by logging to a
+// logr.Logger calling log.Info, and to a ring buffer
+// if level is Debug.
+type LogBuffer struct {
+ attrs []groupedAttr
+ group []string
+
+ // destinations
+ log logr.Logger
+ buf *logRingBuffer
+}
+
+// groupedAttr is an slog.Attr belonging to a group.
+type groupedAttr struct {
+ group []string
+ attr slog.Attr
+}
+
+// newLogBuffer creates a new LogBuffer.
+func newLogBuffer(ctx context.Context, level int) *LogBuffer {
+ return &LogBuffer{log: log.FromContext(ctx).V(level)}
+}
+
+// Appendf adds the log message to the ring buffer.
+func (l *LogBuffer) Appendf(format string, v ...any) {
+ if l != nil {
+ l.buf.Appendf(format, v...)
+ }
+}
+
+// Empty returns true if the buffer is empty.
+func (l *LogBuffer) Empty() bool {
+ return l == nil || l.buf.Empty()
+}
+
+// String returns the contents of the buffer as a string.
+func (l *LogBuffer) String() string {
+ if l == nil {
+ return ""
+ }
+ return l.buf.String()
+}
+
+// Enabled implements slog.Handler.
+func (l *LogBuffer) Enabled(context.Context, slog.Level) bool {
+ // We handle the level on the logr.Logger side.
+ return true
+}
+
+// Handle implements slog.Handler.
+func (l *LogBuffer) Handle(_ context.Context, r slog.Record) error {
+ // Prepare message based on the record level.
+ var msg string
+ switch r.Level {
+ case slog.LevelError:
+ msg = fmt.Sprintf("error: %s", r.Message)
+ case slog.LevelWarn:
+ msg = fmt.Sprintf("warning: %s", r.Message)
+ default:
+ msg = r.Message
+ }
+
+ // Collect record attributes.
+ slogAttrs := make([]slog.Attr, 0, r.NumAttrs())
+ r.Attrs(func(a slog.Attr) bool {
+ slogAttrs = append(slogAttrs, a)
+ return true
+ })
+ l = l.withAttrs(slogAttrs) // We intentionally update the method receiver here (it doesn't mutate the original).
+
+ // Build nested attribute map.
+ attrs := make(map[string]any)
+ for _, ga := range l.attrs {
+ target := attrs
+ for _, g := range ga.group {
+ next, ok := target[g].(map[string]any)
+ if !ok {
+ node := make(map[string]any)
+ target[g] = node
+ next = node
+ }
+ target = next
+ }
+ target[ga.attr.Key] = ga.attr.Value.Any()
+ }
+
+ // Sink to logger.
+ keysAndValues := make([]any, 0, len(attrs)*2)
+ for k, v := range attrs {
+ keysAndValues = append(keysAndValues, k, v)
+ }
+ l.log.Info(msg, keysAndValues...)
+
+ // Sink to buffer.
+ b, err := json.Marshal(attrs)
+ if err != nil {
+ l.buf.Appendf("%s", msg)
+ return err
+ }
+ l.buf.Appendf("%s: %s", msg, string(b))
+ return nil
+}
+
+// WithAttrs implements slog.Handler.
+func (l *LogBuffer) WithAttrs(attrs []slog.Attr) slog.Handler {
+ return l.withAttrs(attrs)
+}
+func (l *LogBuffer) withAttrs(attrs []slog.Attr) *LogBuffer {
+ nl := *l
+ nl.attrs = make([]groupedAttr, 0, len(l.attrs)+len(attrs))
+ nl.attrs = append(nl.attrs, l.attrs...)
+ for _, attr := range attrs {
+ nl.attrs = append(nl.attrs, groupedAttr{
+ group: l.group,
+ attr: attr,
+ })
+ }
+ return &nl
+}
+
+// WithGroup implements slog.Handler.
+func (l *LogBuffer) WithGroup(name string) slog.Handler {
+ if name == "" {
+ return l
+ }
+ nl := *l
+ nl.group = make([]string, 0, len(l.group)+1)
+ nl.group = append(nl.group, l.group...)
+ nl.group = append(nl.group, name)
+ return &nl
+}
+
+// logLine is a log message with a timestamp.
+type logLine struct {
+ ts time.Time
+ lastTS time.Time
+ msg string
+ count int64
+}
+
+// String returns the log line as a string, in the format of:
+// ': '. But only if the message is not empty.
+func (l *logLine) String() string {
+ if l == nil || l.msg == "" {
+ return ""
+ }
+
+ msg := fmt.Sprintf("%s: %s", l.ts.Format(time.RFC3339Nano), l.msg)
+ if c := l.count; c > 0 {
+ msg += fmt.Sprintf("\n%s: %s", l.lastTS.Format(time.RFC3339Nano), l.msg)
+ }
+ if c := l.count - 1; c > 0 {
+ var dup = "line"
+ if c > 1 {
+ dup += "s"
+ }
+ msg += fmt.Sprintf(" (%d duplicate %s omitted)", c, dup)
+ }
+ return msg
+}
+
+// logRingBuffer is a ring buffer for logLine entries.
+type logRingBuffer struct {
+ buf *ring.Ring
+ mu sync.RWMutex
+}
+
+// ringBufferSizeContextKey is the context key for the ring buffer size.
+// Used only for testing logRingBuffer.
+type ringBufferSizeContextKey struct{}
+
+// newLogRingBuffer creates a new logRingBuffer that logs to the logger from the context at Debug level.
+func newLogRingBuffer(ctx context.Context) *logRingBuffer {
+ size := 10
+ if v := ctx.Value(ringBufferSizeContextKey{}); v != nil {
+ size = v.(int)
+ }
+ return &logRingBuffer{buf: ring.New(size)}
+}
+
+// Appendf adds the log message to the ring buffer before calling the actual log
+// function. It is safe to call this function from multiple goroutines.
+func (l *logRingBuffer) Appendf(format string, v ...any) {
+ if l == nil {
+ return
+ }
+
+ l.mu.Lock()
+
+ // Filter out duplicate log lines, this happens for example when
+ // Helm is waiting on workloads to become ready.
+ msg := fmt.Sprintf(format, v...)
+ prev, ok := l.buf.Prev().Value.(*logLine)
+ if ok && prev.msg == msg {
+ prev.count++
+ prev.lastTS = nowTS().UTC()
+ l.buf.Prev().Value = prev
+ }
+ if !ok || prev.msg != msg {
+ l.buf.Value = &logLine{
+ ts: nowTS().UTC(),
+ msg: msg,
+ }
+ l.buf = l.buf.Next()
+ }
+
+ l.mu.Unlock()
+}
+
+// Empty returns true if the buffer is empty.
+func (l *logRingBuffer) Empty() bool {
+ if l == nil {
+ return true
+ }
+
+ var count int
+ l.mu.RLock()
+ l.buf.Do(func(s any) {
+ if s == nil {
+ return
+ }
+ ll, ok := s.(*logLine)
+ if !ok || ll.String() == "" {
+ return
+ }
+ count++
+ })
+ l.mu.RUnlock()
+ return count == 0
+}
+
+// String returns the contents of the buffer as a string.
+func (l *logRingBuffer) String() string {
+ if l == nil {
+ return ""
+ }
+
+ var str string
+ l.mu.RLock()
+ l.buf.Do(func(s any) {
+ if s == nil {
+ return
+ }
+ ll, ok := s.(*logLine)
+ if !ok {
+ return
+ }
+ if msg := ll.String(); msg != "" {
+ str += msg + "\n"
+ }
+ })
+ l.mu.RUnlock()
+ return strings.TrimSpace(str)
+}
diff --git a/internal/action/log_test.go b/internal/action/log_test.go
new file mode 100644
index 000000000..c60bfd316
--- /dev/null
+++ b/internal/action/log_test.go
@@ -0,0 +1,467 @@
+/*
+Copyright 2022 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package action
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "log/slog"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/go-logr/logr"
+ . "github.com/onsi/gomega"
+ "sigs.k8s.io/controller-runtime/pkg/log"
+)
+
+// stubNowTS returns a fixed time for testing purposes.
+func stubNowTS() time.Time {
+ return time.Date(2016, 2, 18, 12, 24, 5, 12345600, time.UTC)
+}
+
+// stubNowTS2 returns a different fixed time for testing duplicate log line timestamps.
+func stubNowTS2() time.Time {
+ return time.Date(2016, 2, 18, 12, 24, 6, 12345600, time.UTC)
+}
+
+func Test_logLine_String(t *testing.T) {
+ ts := stubNowTS()
+ ts2 := stubNowTS2()
+
+ for _, tt := range []struct {
+ name string
+ line *logLine
+ want string
+ }{
+ {
+ name: "nil logLine",
+ line: nil,
+ want: "",
+ },
+ {
+ name: "empty message",
+ line: &logLine{ts: ts, msg: ""},
+ want: "",
+ },
+ {
+ name: "simple message",
+ line: &logLine{ts: ts, msg: "test message"},
+ want: fmt.Sprintf("%s: test message", ts.Format(time.RFC3339Nano)),
+ },
+ {
+ name: "message with one duplicate",
+ line: &logLine{ts: ts, lastTS: ts2, msg: "duplicate message", count: 1},
+ want: fmt.Sprintf("%s: duplicate message\n%s: duplicate message", ts.Format(time.RFC3339Nano), ts2.Format(time.RFC3339Nano)),
+ },
+ {
+ name: "message with two duplicates",
+ line: &logLine{ts: ts, lastTS: ts2, msg: "duplicate message", count: 2},
+ want: fmt.Sprintf("%s: duplicate message\n%s: duplicate message (1 duplicate line omitted)", ts.Format(time.RFC3339Nano), ts2.Format(time.RFC3339Nano)),
+ },
+ {
+ name: "message with three duplicates",
+ line: &logLine{ts: ts, lastTS: ts2, msg: "duplicate message", count: 3},
+ want: fmt.Sprintf("%s: duplicate message\n%s: duplicate message (2 duplicate lines omitted)", ts.Format(time.RFC3339Nano), ts2.Format(time.RFC3339Nano)),
+ },
+ } {
+ t.Run(tt.name, func(t *testing.T) {
+ g := NewWithT(t)
+ g.Expect(tt.line.String()).To(Equal(tt.want))
+ })
+ }
+}
+
+func Test_logRingBuffer_Appendf(t *testing.T) {
+ origNowTS := nowTS
+ defer func() { nowTS = origNowTS }()
+ nowTS = stubNowTS
+
+ t.Run("nil buffer is safe to call", func(t *testing.T) {
+ g := NewWithT(t)
+ var l *logRingBuffer
+ g.Expect(func() { l.Appendf("test") }).NotTo(Panic())
+ })
+
+ t.Run("appends messages to buffer", func(t *testing.T) {
+ g := NewWithT(t)
+ ctx := context.WithValue(context.Background(), ringBufferSizeContextKey{}, 3)
+ l := newLogRingBuffer(ctx)
+
+ l.Appendf("message %d", 1)
+ l.Appendf("message %d", 2)
+
+ want := fmt.Sprintf("%[1]s: message 1\n%[1]s: message 2", stubNowTS().Format(time.RFC3339Nano))
+ g.Expect(l.String()).To(Equal(want))
+ })
+
+ t.Run("handles duplicate messages", func(t *testing.T) {
+ g := NewWithT(t)
+ ctx := context.WithValue(context.Background(), ringBufferSizeContextKey{}, 5)
+ l := newLogRingBuffer(ctx)
+
+ l.Appendf("same message")
+ l.Appendf("same message")
+ l.Appendf("same message")
+
+ want := fmt.Sprintf("%[1]s: same message\n%[1]s: same message (1 duplicate line omitted)", stubNowTS().Format(time.RFC3339Nano))
+ g.Expect(l.String()).To(Equal(want))
+ })
+
+ t.Run("ring buffer wraps around", func(t *testing.T) {
+ g := NewWithT(t)
+ ctx := context.WithValue(context.Background(), ringBufferSizeContextKey{}, 2)
+ l := newLogRingBuffer(ctx)
+
+ l.Appendf("a")
+ l.Appendf("b")
+ l.Appendf("c")
+
+ want := fmt.Sprintf("%[1]s: b\n%[1]s: c", stubNowTS().Format(time.RFC3339Nano))
+ g.Expect(l.String()).To(Equal(want))
+ })
+}
+
+func Test_logRingBuffer_Empty(t *testing.T) {
+ t.Run("nil buffer is empty", func(t *testing.T) {
+ g := NewWithT(t)
+ var l *logRingBuffer
+ g.Expect(l.Empty()).To(BeTrue())
+ })
+
+ t.Run("new buffer is empty", func(t *testing.T) {
+ g := NewWithT(t)
+ ctx := context.WithValue(context.Background(), ringBufferSizeContextKey{}, 5)
+ l := newLogRingBuffer(ctx)
+ g.Expect(l.Empty()).To(BeTrue())
+ })
+
+ t.Run("buffer with entries is not empty", func(t *testing.T) {
+ g := NewWithT(t)
+ ctx := context.WithValue(context.Background(), ringBufferSizeContextKey{}, 5)
+ l := newLogRingBuffer(ctx)
+ l.Appendf("test message")
+ g.Expect(l.Empty()).To(BeFalse())
+ })
+}
+
+func Test_logRingBuffer_String(t *testing.T) {
+ origNowTS := nowTS
+ defer func() { nowTS = origNowTS }()
+ nowTS = stubNowTS
+
+ t.Run("nil buffer returns empty string", func(t *testing.T) {
+ g := NewWithT(t)
+ var l *logRingBuffer
+ g.Expect(l.String()).To(Equal(""))
+ })
+
+ t.Run("empty buffer returns empty string", func(t *testing.T) {
+ g := NewWithT(t)
+ ctx := context.WithValue(context.Background(), ringBufferSizeContextKey{}, 5)
+ l := newLogRingBuffer(ctx)
+ g.Expect(l.String()).To(Equal(""))
+ })
+
+ t.Run("returns all messages joined by newlines", func(t *testing.T) {
+ g := NewWithT(t)
+ ctx := context.WithValue(context.Background(), ringBufferSizeContextKey{}, 5)
+ l := newLogRingBuffer(ctx)
+
+ l.Appendf("first")
+ l.Appendf("second")
+ l.Appendf("third")
+
+ want := fmt.Sprintf("%[1]s: first\n%[1]s: second\n%[1]s: third", stubNowTS().Format(time.RFC3339Nano))
+ g.Expect(l.String()).To(Equal(want))
+ })
+
+ t.Run("handles mixed duplicates and unique messages", func(t *testing.T) {
+ g := NewWithT(t)
+ ctx := context.WithValue(context.Background(), ringBufferSizeContextKey{}, 10)
+ l := newLogRingBuffer(ctx)
+
+ l.Appendf("a")
+ l.Appendf("b")
+ l.Appendf("b")
+ l.Appendf("b")
+ l.Appendf("c")
+ l.Appendf("c")
+
+ want := fmt.Sprintf("%[1]s: a\n%[1]s: b\n%[1]s: b (1 duplicate line omitted)\n%[1]s: c\n%[1]s: c", stubNowTS().Format(time.RFC3339Nano))
+ g.Expect(l.String()).To(Equal(want))
+ })
+}
+
+func TestLogBuffer_Enabled(t *testing.T) {
+ g := NewWithT(t)
+
+ ctx := log.IntoContext(context.Background(), logr.Discard())
+ l := newLogBuffer(ctx, 0)
+
+ g.Expect(l.Enabled(ctx, slog.LevelDebug)).To(BeTrue())
+ g.Expect(l.Enabled(ctx, slog.LevelInfo)).To(BeTrue())
+ g.Expect(l.Enabled(ctx, slog.LevelWarn)).To(BeTrue())
+ g.Expect(l.Enabled(ctx, slog.LevelError)).To(BeTrue())
+}
+
+func TestLogBuffer_Handle(t *testing.T) {
+ origNowTS := nowTS
+ defer func() { nowTS = origNowTS }()
+ nowTS = stubNowTS
+
+ t.Run("handles info level message", func(t *testing.T) {
+ g := NewWithT(t)
+
+ ctx := log.IntoContext(context.Background(), logr.Discard())
+ l := NewDebugLogBuffer(ctx)
+
+ record := slog.NewRecord(time.Now(), slog.LevelInfo, "info message", 0)
+ err := l.Handle(ctx, record)
+
+ g.Expect(err).ToNot(HaveOccurred())
+ g.Expect(l.String()).To(ContainSubstring("info message"))
+ })
+
+ t.Run("handles error level message with prefix", func(t *testing.T) {
+ g := NewWithT(t)
+
+ ctx := log.IntoContext(context.Background(), logr.Discard())
+ l := NewDebugLogBuffer(ctx)
+
+ record := slog.NewRecord(time.Now(), slog.LevelError, "error occurred", 0)
+ err := l.Handle(ctx, record)
+
+ g.Expect(err).ToNot(HaveOccurred())
+ g.Expect(l.String()).To(ContainSubstring("error: error occurred"))
+ })
+
+ t.Run("handles warning level message with prefix", func(t *testing.T) {
+ g := NewWithT(t)
+
+ ctx := log.IntoContext(context.Background(), logr.Discard())
+ l := NewDebugLogBuffer(ctx)
+
+ record := slog.NewRecord(time.Now(), slog.LevelWarn, "warning issued", 0)
+ err := l.Handle(ctx, record)
+
+ g.Expect(err).ToNot(HaveOccurred())
+ g.Expect(l.String()).To(ContainSubstring("warning: warning issued"))
+ })
+
+ t.Run("handles message with attributes", func(t *testing.T) {
+ g := NewWithT(t)
+
+ ctx := log.IntoContext(context.Background(), logr.Discard())
+ l := NewDebugLogBuffer(ctx)
+
+ record := slog.NewRecord(time.Now(), slog.LevelInfo, "test message", 0)
+ record.AddAttrs(slog.String("key", "value"))
+ err := l.Handle(ctx, record)
+
+ g.Expect(err).ToNot(HaveOccurred())
+ g.Expect(l.String()).To(ContainSubstring(`"key":"value"`))
+ })
+}
+
+func TestLogBuffer_WithAttrs(t *testing.T) {
+ g := NewWithT(t)
+
+ ctx := log.IntoContext(context.Background(), logr.Discard())
+ l := NewDebugLogBuffer(ctx)
+
+ handler := l.WithAttrs([]slog.Attr{slog.String("attr1", "val1")})
+ g.Expect(handler).ToNot(BeNil())
+
+ lb, ok := handler.(*LogBuffer)
+ g.Expect(ok).To(BeTrue())
+ g.Expect(lb.attrs).To(HaveLen(1))
+ g.Expect(lb.attrs[0].attr.Key).To(Equal("attr1"))
+ g.Expect(lb.attrs[0].attr.Value.String()).To(Equal("val1"))
+}
+
+func TestLogBuffer_WithGroup(t *testing.T) {
+ t.Run("returns same handler for empty group name", func(t *testing.T) {
+ g := NewWithT(t)
+
+ ctx := log.IntoContext(context.Background(), logr.Discard())
+ l := NewDebugLogBuffer(ctx)
+
+ handler := l.WithGroup("")
+ g.Expect(handler).To(Equal(l))
+ })
+
+ t.Run("creates new handler with group", func(t *testing.T) {
+ g := NewWithT(t)
+
+ ctx := log.IntoContext(context.Background(), logr.Discard())
+ l := NewDebugLogBuffer(ctx)
+
+ handler := l.WithGroup("mygroup")
+ g.Expect(handler).ToNot(Equal(l))
+
+ lb, ok := handler.(*LogBuffer)
+ g.Expect(ok).To(BeTrue())
+ g.Expect(lb.group).To(Equal([]string{"mygroup"}))
+ })
+
+ t.Run("supports nested groups", func(t *testing.T) {
+ g := NewWithT(t)
+
+ ctx := log.IntoContext(context.Background(), logr.Discard())
+ l := NewDebugLogBuffer(ctx)
+
+ handler := l.WithGroup("outer").WithGroup("inner")
+ lb, ok := handler.(*LogBuffer)
+ g.Expect(ok).To(BeTrue())
+ g.Expect(lb.group).To(Equal([]string{"outer", "inner"}))
+ })
+}
+
+func TestLogBuffer_HandleWithMixedAttrsAndGroups(t *testing.T) {
+ origNowTS := nowTS
+ defer func() { nowTS = origNowTS }()
+ nowTS = stubNowTS
+
+ t.Run("attrs before group remain ungrouped", func(t *testing.T) {
+ g := NewWithT(t)
+
+ ctx := log.IntoContext(context.Background(), logr.Discard())
+ l := NewDebugLogBuffer(ctx)
+
+ // Add attr first, then group with its own attr
+ handler := l.WithAttrs([]slog.Attr{slog.String("root", "val1")}).
+ WithGroup("nested").
+ WithAttrs([]slog.Attr{slog.String("inner", "val2")})
+
+ record := slog.NewRecord(time.Now(), slog.LevelInfo, "mixed test", 0)
+ lb, ok := handler.(*LogBuffer)
+ g.Expect(ok).To(BeTrue())
+
+ err := lb.Handle(ctx, record)
+ g.Expect(err).ToNot(HaveOccurred())
+
+ output := lb.String()
+ g.Expect(output).To(ContainSubstring("mixed test"))
+
+ // Extract and parse JSON from output
+ jsonStart := strings.Index(output, "{")
+ g.Expect(jsonStart).To(BeNumerically(">=", 0), "expected JSON in output")
+ var attrs map[string]any
+ err = json.Unmarshal([]byte(output[jsonStart:]), &attrs)
+ g.Expect(err).ToNot(HaveOccurred())
+
+ // root attr should be at top level
+ g.Expect(attrs).To(HaveKeyWithValue("root", "val1"))
+ // inner attr should be nested under "nested" group
+ g.Expect(attrs).To(HaveKey("nested"))
+ nested, ok := attrs["nested"].(map[string]any)
+ g.Expect(ok).To(BeTrue(), "nested should be a map")
+ g.Expect(nested).To(HaveKeyWithValue("inner", "val2"))
+ })
+
+ t.Run("alternating attrs and groups", func(t *testing.T) {
+ g := NewWithT(t)
+
+ ctx := log.IntoContext(context.Background(), logr.Discard())
+ l := NewDebugLogBuffer(ctx)
+
+ // Create a chain: attr -> group -> attr -> group -> attr
+ handler := l.
+ WithAttrs([]slog.Attr{slog.String("level0", "a")}).
+ WithGroup("g1").
+ WithAttrs([]slog.Attr{slog.String("level1", "b")}).
+ WithGroup("g2").
+ WithAttrs([]slog.Attr{slog.String("level2", "c")})
+
+ record := slog.NewRecord(time.Now(), slog.LevelInfo, "alternating test", 0)
+ lb, ok := handler.(*LogBuffer)
+ g.Expect(ok).To(BeTrue())
+
+ err := lb.Handle(ctx, record)
+ g.Expect(err).ToNot(HaveOccurred())
+
+ output := lb.String()
+ g.Expect(output).To(ContainSubstring("alternating test"))
+
+ // Extract and parse JSON from output
+ jsonStart := strings.Index(output, "{")
+ g.Expect(jsonStart).To(BeNumerically(">=", 0), "expected JSON in output")
+ var attrs map[string]any
+ err = json.Unmarshal([]byte(output[jsonStart:]), &attrs)
+ g.Expect(err).ToNot(HaveOccurred())
+
+ // level0 should be at root
+ g.Expect(attrs).To(HaveKeyWithValue("level0", "a"))
+
+ // level1 should be under g1
+ g.Expect(attrs).To(HaveKey("g1"))
+ g1, ok := attrs["g1"].(map[string]any)
+ g.Expect(ok).To(BeTrue(), "g1 should be a map")
+ g.Expect(g1).To(HaveKeyWithValue("level1", "b"))
+
+ // level2 should be under g1.g2
+ g.Expect(g1).To(HaveKey("g2"))
+ g2, ok := g1["g2"].(map[string]any)
+ g.Expect(ok).To(BeTrue(), "g2 should be a map")
+ g.Expect(g2).To(HaveKeyWithValue("level2", "c"))
+ })
+}
+
+func TestNewTraceLogger(t *testing.T) {
+ g := NewWithT(t)
+
+ ctx := log.IntoContext(context.Background(), logr.Discard())
+ handler := NewTraceLogger(ctx)
+
+ g.Expect(handler).ToNot(BeNil())
+
+ lb, ok := handler.(*LogBuffer)
+ g.Expect(ok).To(BeTrue())
+ g.Expect(lb.buf).To(BeNil())
+}
+
+func TestNewDebugLogBuffer(t *testing.T) {
+ g := NewWithT(t)
+
+ ctx := log.IntoContext(context.Background(), logr.Discard())
+ handler := NewDebugLogBuffer(ctx)
+
+ g.Expect(handler).ToNot(BeNil())
+ g.Expect(handler.buf).ToNot(BeNil())
+}
+
+func Test_newLogRingBuffer_defaultSize(t *testing.T) {
+ g := NewWithT(t)
+
+ ctx := context.Background()
+ l := newLogRingBuffer(ctx)
+
+ g.Expect(l).ToNot(BeNil())
+ g.Expect(l.buf.Len()).To(Equal(10))
+}
+
+func Test_newLogRingBuffer_customSize(t *testing.T) {
+ g := NewWithT(t)
+
+ ctx := context.WithValue(context.Background(), ringBufferSizeContextKey{}, 20)
+ l := newLogRingBuffer(ctx)
+
+ g.Expect(l).ToNot(BeNil())
+ g.Expect(l.buf.Len()).To(Equal(20))
+}
diff --git a/internal/action/pod_status_reader.go b/internal/action/pod_status_reader.go
new file mode 100644
index 000000000..b8e48feae
--- /dev/null
+++ b/internal/action/pod_status_reader.go
@@ -0,0 +1,123 @@
+/*
+Copyright 2026 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Copied from:
+// https://github.com/helm/helm/blob/75880fa498b511cdc4d31dd8752b4937cfb15cf9/internal/statusreaders/pod_status_reader.go
+
+/*
+Copyright The Helm Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package action
+
+import (
+ "context"
+ "fmt"
+
+ corev1 "k8s.io/api/core/v1"
+ "k8s.io/apimachinery/pkg/api/meta"
+ "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+ "k8s.io/apimachinery/pkg/runtime/schema"
+
+ "github.com/fluxcd/cli-utils/pkg/kstatus/polling/engine"
+ "github.com/fluxcd/cli-utils/pkg/kstatus/polling/event"
+ "github.com/fluxcd/cli-utils/pkg/kstatus/polling/statusreaders"
+ "github.com/fluxcd/cli-utils/pkg/kstatus/status"
+ "github.com/fluxcd/cli-utils/pkg/object"
+)
+
+type customPodStatusReader struct {
+ genericStatusReader engine.StatusReader
+}
+
+func NewCustomPodStatusReader(mapper meta.RESTMapper) engine.StatusReader {
+ genericStatusReader := statusreaders.NewGenericStatusReader(mapper, podConditions)
+ return &customPodStatusReader{
+ genericStatusReader: genericStatusReader,
+ }
+}
+
+func (j *customPodStatusReader) Supports(gk schema.GroupKind) bool {
+ return gk == corev1.SchemeGroupVersion.WithKind("Pod").GroupKind()
+}
+
+func (j *customPodStatusReader) ReadStatus(ctx context.Context, reader engine.ClusterReader, resource object.ObjMetadata) (*event.ResourceStatus, error) {
+ return j.genericStatusReader.ReadStatus(ctx, reader, resource)
+}
+
+func (j *customPodStatusReader) ReadStatusForObject(ctx context.Context, reader engine.ClusterReader, resource *unstructured.Unstructured) (*event.ResourceStatus, error) {
+ return j.genericStatusReader.ReadStatusForObject(ctx, reader, resource)
+}
+
+func podConditions(u *unstructured.Unstructured) (*status.Result, error) {
+ obj := u.UnstructuredContent()
+ phase := status.GetStringField(obj, ".status.phase", "")
+ switch corev1.PodPhase(phase) {
+ case corev1.PodSucceeded:
+ message := fmt.Sprintf("pod %s succeeded", u.GetName())
+ return &status.Result{
+ Status: status.CurrentStatus,
+ Message: message,
+ Conditions: []status.Condition{
+ {
+ Type: status.ConditionStalled,
+ Status: corev1.ConditionTrue,
+ Message: message,
+ },
+ },
+ }, nil
+ case corev1.PodFailed:
+ message := fmt.Sprintf("pod %s failed", u.GetName())
+ return &status.Result{
+ Status: status.FailedStatus,
+ Message: message,
+ Conditions: []status.Condition{
+ {
+ Type: status.ConditionStalled,
+ Status: corev1.ConditionTrue,
+ Reason: "PodFailed",
+ Message: message,
+ },
+ },
+ }, nil
+ default:
+ message := "Pod in progress"
+ return &status.Result{
+ Status: status.InProgressStatus,
+ Message: message,
+ Conditions: []status.Condition{
+ {
+ Type: status.ConditionReconciling,
+ Status: corev1.ConditionTrue,
+ Reason: "PodInProgress",
+ Message: message,
+ },
+ },
+ }, nil
+ }
+}
diff --git a/internal/action/reset.go b/internal/action/reset.go
new file mode 100644
index 000000000..5bbbe0fca
--- /dev/null
+++ b/internal/action/reset.go
@@ -0,0 +1,70 @@
+/*
+Copyright 2023 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package action
+
+import (
+ "github.com/opencontainers/go-digest"
+ "helm.sh/helm/v4/pkg/chart/common"
+ chart "helm.sh/helm/v4/pkg/chart/v2"
+
+ intchartutil "github.com/fluxcd/pkg/chartutil"
+
+ v2 "github.com/fluxcd/helm-controller/api/v2"
+)
+
+const (
+ differentGenerationReason = "generation differs from last attempt"
+ differentRevisionReason = "chart version differs from last attempt"
+ differentValuesReason = "values differ from last attempt"
+ resetRequestedReason = "reset requested through annotation"
+)
+
+// MustResetFailures returns a reason and true if the HelmRelease's status
+// indicates that the HelmRelease failure counters must be reset.
+// This is the case if the data used to make the last (failed) attempt has
+// changed in a way that indicates that a new attempt should be made.
+// For example, a change in generation, chart version, or values.
+// If no change is detected, an empty string is returned along with false.
+func MustResetFailures(obj *v2.HelmRelease, chart *chart.Metadata, values common.Values) (string, bool) {
+ // Always check if a reset is requested.
+ // This is done first, so that the HelmReleaseStatus.LastHandledResetAt
+ // field is updated even if the reset request is not handled due to other
+ // diverging data.
+ resetRequested := v2.ShouldHandleResetRequest(obj)
+
+ switch {
+ case obj.Status.LastAttemptedGeneration != obj.Generation:
+ return differentGenerationReason, true
+ case obj.Status.GetLastAttemptedRevision() != chart.Version:
+ return differentRevisionReason, true
+ case obj.Status.LastAttemptedConfigDigest != "" || obj.Status.LastAttemptedValuesChecksum != "":
+ d := obj.Status.LastAttemptedConfigDigest
+ if d == "" {
+ // TODO: remove this when the deprecated field is removed.
+ d = "sha1:" + obj.Status.LastAttemptedValuesChecksum
+ }
+ if ok := intchartutil.VerifyValues(digest.Digest(d), values); !ok {
+ return differentValuesReason, true
+ }
+ }
+
+ if resetRequested {
+ return resetRequestedReason, true
+ }
+
+ return "", false
+}
diff --git a/internal/action/reset_test.go b/internal/action/reset_test.go
new file mode 100644
index 000000000..4b22392e0
--- /dev/null
+++ b/internal/action/reset_test.go
@@ -0,0 +1,168 @@
+/*
+Copyright 2023 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package action
+
+import (
+ "testing"
+
+ . "github.com/onsi/gomega"
+ "helm.sh/helm/v4/pkg/chart/common"
+ chart "helm.sh/helm/v4/pkg/chart/v2"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+ "github.com/fluxcd/pkg/apis/meta"
+
+ v2 "github.com/fluxcd/helm-controller/api/v2"
+)
+
+func TestMustResetFailures(t *testing.T) {
+ tests := []struct {
+ name string
+ obj *v2.HelmRelease
+ chart *chart.Metadata
+ values common.Values
+ want bool
+ wantReason string
+ }{
+ {
+ name: "on generation change",
+ obj: &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Generation: 1,
+ },
+ Status: v2.HelmReleaseStatus{
+ LastAttemptedGeneration: 2,
+ },
+ },
+ want: true,
+ wantReason: differentGenerationReason,
+ },
+ {
+ name: "on revision change",
+ obj: &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Generation: 1,
+ },
+ Status: v2.HelmReleaseStatus{
+ LastAttemptedGeneration: 1,
+ LastAttemptedRevision: "1.0.0",
+ },
+ },
+ chart: &chart.Metadata{
+ Version: "1.1.0",
+ },
+ want: true,
+ wantReason: differentRevisionReason,
+ },
+ {
+ name: "on config digest change",
+ obj: &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Generation: 1,
+ },
+ Status: v2.HelmReleaseStatus{
+ LastAttemptedGeneration: 1,
+ LastAttemptedRevision: "1.0.0",
+ LastAttemptedConfigDigest: "sha256:9933f58f8bf459eb199d59ebc8a05683f3944e1242d9f5467d99aa2cf08a5370",
+ },
+ },
+ chart: &chart.Metadata{
+ Version: "1.0.0",
+ },
+ values: common.Values{
+ "foo": "bar",
+ },
+ want: true,
+ wantReason: differentValuesReason,
+ },
+ {
+ name: "on (deprecated) values checksum change",
+ obj: &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Generation: 1,
+ },
+ Status: v2.HelmReleaseStatus{
+ LastAttemptedGeneration: 1,
+ LastAttemptedRevision: "1.0.0",
+ LastAttemptedValuesChecksum: "a856118d270c0db44a9019d51e2bba4fc3e6bac7",
+ },
+ },
+ chart: &chart.Metadata{
+ Version: "1.0.0",
+ },
+ values: common.Values{
+ "foo": "bar",
+ },
+ want: true,
+ wantReason: differentValuesReason,
+ },
+ {
+ name: "on reset request through annotation",
+ obj: &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Generation: 1,
+ Annotations: map[string]string{
+ meta.ReconcileRequestAnnotation: "a",
+ v2.ResetRequestAnnotation: "a",
+ },
+ },
+ Status: v2.HelmReleaseStatus{
+ LastAttemptedGeneration: 1,
+ LastAttemptedRevision: "1.0.0",
+ LastAttemptedConfigDigest: "sha256:1dabc4e3cbbd6a0818bd460f3a6c9855bfe95d506c74726bc0f2edb0aecb1f4e",
+ },
+ },
+ chart: &chart.Metadata{
+ Version: "1.0.0",
+ },
+ values: common.Values{
+ "foo": "bar",
+ },
+ want: true,
+ wantReason: resetRequestedReason,
+ },
+ {
+ name: "without change no reset",
+ obj: &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Generation: 1,
+ },
+ Status: v2.HelmReleaseStatus{
+ LastAttemptedGeneration: 1,
+ LastAttemptedRevision: "1.0.0",
+ LastAttemptedConfigDigest: "sha256:1dabc4e3cbbd6a0818bd460f3a6c9855bfe95d506c74726bc0f2edb0aecb1f4e",
+ },
+ },
+ chart: &chart.Metadata{
+ Version: "1.0.0",
+ },
+ values: common.Values{
+ "foo": "bar",
+ },
+ want: false,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ g := NewWithT(t)
+
+ reason, got := MustResetFailures(tt.obj, tt.chart, tt.values)
+ g.Expect(got).To(Equal(tt.want))
+ g.Expect(reason).To(Equal(tt.wantReason))
+ })
+ }
+}
diff --git a/internal/action/rollback.go b/internal/action/rollback.go
new file mode 100644
index 000000000..1bca4b075
--- /dev/null
+++ b/internal/action/rollback.go
@@ -0,0 +1,121 @@
+/*
+Copyright 2022 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package action
+
+import (
+ "fmt"
+
+ helmaction "helm.sh/helm/v4/pkg/action"
+ helmrelease "helm.sh/helm/v4/pkg/release/v1"
+
+ v2 "github.com/fluxcd/helm-controller/api/v2"
+)
+
+// RollbackOption can be used to modify Helm's action.Rollback after the
+// instructions from the v2.HelmRelease have been applied. This is for
+// example useful to enable the dry-run setting as a CLI.
+type RollbackOption func(*helmaction.Rollback)
+
+// Rollback runs the Helm rollback action with the provided config. Targeting
+// a specific release or enabling dry-run is possible by providing
+// RollbackToVersion as option.
+//
+// It does not determine if there is a desire to perform the action, this is
+// expected to be done by the caller. In addition, it does not take note of the
+// action result. The caller is expected to listen to this using a
+// storage.ObserveFunc, which provides superior access to Helm storage writes.
+func Rollback(config *helmaction.Configuration, obj *v2.HelmRelease,
+ releaseName string, version int, opts ...RollbackOption) error {
+
+ rollback := newRollback(config, obj, version, opts)
+
+ // Resolve "auto" server-side apply setting.
+ // We need to copy this code from Helm because we need to set ForceConflicts
+ // based on the resolved value, since we always force conflicts on server-side apply
+ // (Helm does not).
+ serverSideApply := rollback.ServerSideApply == "true"
+ if rollback.ServerSideApply == "auto" {
+ currentRelease, err := config.Releases.Last(releaseName)
+ if err != nil {
+ return err
+ }
+ currentReleaseTyped, ok := currentRelease.(*helmrelease.Release)
+ if !ok {
+ return fmt.Errorf("only the Chart API v2 is supported")
+ }
+ previousVersion := rollback.Version
+ if rollback.Version == 0 {
+ previousVersion = currentReleaseTyped.Version - 1
+ }
+ historyReleases, err := config.Releases.History(releaseName)
+ if err != nil {
+ return err
+ }
+ previousVersionExist := false
+ for _, rlsr := range historyReleases {
+ rlsrTyped, ok := rlsr.(*helmrelease.Release)
+ if !ok {
+ return fmt.Errorf("only the Chart API v2 is supported")
+ }
+ if previousVersion == rlsrTyped.Version {
+ previousVersionExist = true
+ break
+ }
+ }
+ if !previousVersionExist {
+ return fmt.Errorf("release has no %d version", previousVersion)
+ }
+ previousRelease, err := config.Releases.Get(releaseName, previousVersion)
+ if err != nil {
+ return err
+ }
+ previousReleaseTyped, ok := previousRelease.(*helmrelease.Release)
+ if !ok {
+ return fmt.Errorf("only the Chart API v2 is supported")
+ }
+ serverSideApply = previousReleaseTyped.ApplyMethod == "ssa"
+ rollback.ServerSideApply = fmt.Sprint(serverSideApply)
+ }
+ rollback.ForceConflicts = serverSideApply // We always force conflicts on server-side apply.
+
+ return rollback.Run(releaseName)
+}
+
+func newRollback(config *helmaction.Configuration, obj *v2.HelmRelease,
+ version int, opts []RollbackOption) *helmaction.Rollback {
+
+ rollback := helmaction.NewRollback(config)
+ rollback.ServerSideApply = "auto" // This must be the rollback default regardless of UseHelm3Defaults.
+ if ssa := obj.GetRollback().ServerSideApply; ssa != "" {
+ rollback.ServerSideApply = toHelmSSAValue(ssa)
+ }
+
+ rollback.Version = version
+ rollback.Timeout = obj.GetRollback().GetTimeout(obj.GetTimeout()).Duration
+ rollback.WaitStrategy = getWaitStrategy(obj.GetWaitStrategy(), obj.GetRollback())
+ rollback.WaitForJobs = !obj.GetRollback().DisableWaitForJobs
+ rollback.DisableHooks = obj.GetRollback().DisableHooks
+ rollback.ForceReplace = obj.GetRollback().Force
+ rollback.CleanupOnFail = obj.GetRollback().CleanupOnFail
+ rollback.MaxHistory = obj.GetMaxHistory()
+
+ for _, opt := range opts {
+ opt(rollback)
+ }
+
+ return rollback
+}
diff --git a/internal/action/rollback_test.go b/internal/action/rollback_test.go
new file mode 100644
index 000000000..b24fca301
--- /dev/null
+++ b/internal/action/rollback_test.go
@@ -0,0 +1,165 @@
+/*
+Copyright 2022 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package action
+
+import (
+ "testing"
+ "time"
+
+ . "github.com/onsi/gomega"
+ helmaction "helm.sh/helm/v4/pkg/action"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+ v2 "github.com/fluxcd/helm-controller/api/v2"
+)
+
+func Test_newRollback(t *testing.T) {
+ t.Run("new rollback", func(t *testing.T) {
+ g := NewWithT(t)
+
+ obj := &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "rollback",
+ Namespace: "rollback-ns",
+ },
+ Spec: v2.HelmReleaseSpec{
+ Timeout: &metav1.Duration{Duration: time.Minute},
+ Rollback: &v2.Rollback{
+ Timeout: &metav1.Duration{Duration: 10 * time.Second},
+ Force: true,
+ },
+ },
+ }
+
+ got := newRollback(&helmaction.Configuration{}, obj, 0, nil)
+ g.Expect(got).ToNot(BeNil())
+ g.Expect(got.Timeout).To(Equal(obj.Spec.Rollback.Timeout.Duration))
+ g.Expect(got.ForceReplace).To(Equal(obj.Spec.Rollback.Force))
+ g.Expect(got.MaxHistory).To(Equal(obj.GetMaxHistory()))
+ })
+
+ t.Run("rollback to version", func(t *testing.T) {
+ g := NewWithT(t)
+
+ obj := &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "rollback",
+ Namespace: "rollback-ns",
+ },
+ }
+
+ toVersion := 3
+ got := newRollback(&helmaction.Configuration{}, obj, toVersion, nil)
+ g.Expect(got).ToNot(BeNil())
+ g.Expect(got.Version).To(Equal(toVersion))
+ })
+
+ t.Run("timeout fallback", func(t *testing.T) {
+ g := NewWithT(t)
+
+ obj := &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "rollback",
+ Namespace: "rollback-ns",
+ },
+ Spec: v2.HelmReleaseSpec{
+ Timeout: &metav1.Duration{Duration: time.Minute},
+ },
+ }
+
+ got := newRollback(&helmaction.Configuration{}, obj, 0, nil)
+ g.Expect(got).ToNot(BeNil())
+ g.Expect(got.Timeout).To(Equal(obj.Spec.Timeout.Duration))
+ })
+
+ t.Run("applies options", func(t *testing.T) {
+ g := NewWithT(t)
+
+ obj := &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "rollback",
+ Namespace: "rollback-ns",
+ },
+ Spec: v2.HelmReleaseSpec{},
+ }
+
+ got := newRollback(&helmaction.Configuration{}, obj, 0, []RollbackOption{
+ func(rollback *helmaction.Rollback) {
+ rollback.CleanupOnFail = true
+ },
+ func(rollback *helmaction.Rollback) {
+ rollback.DryRunStrategy = helmaction.DryRunClient
+ },
+ })
+ g.Expect(got).ToNot(BeNil())
+ g.Expect(got.CleanupOnFail).To(BeTrue())
+ g.Expect(got.DryRunStrategy).To(Equal(helmaction.DryRunClient))
+ })
+
+ t.Run("server side apply is auto regardless of UseHelm3Defaults", func(t *testing.T) {
+ g := NewWithT(t)
+
+ obj := &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "rollback",
+ Namespace: "rollback-ns",
+ },
+ Spec: v2.HelmReleaseSpec{},
+ }
+
+ // Save and restore UseHelm3Defaults
+ oldUseHelm3Defaults := UseHelm3Defaults
+ t.Cleanup(func() { UseHelm3Defaults = oldUseHelm3Defaults })
+
+ // Test with UseHelm3Defaults = false
+ UseHelm3Defaults = false
+ got := newRollback(&helmaction.Configuration{}, obj, 0, nil)
+ g.Expect(got).ToNot(BeNil())
+ g.Expect(got.ServerSideApply).To(Equal("auto"))
+
+ // Test with UseHelm3Defaults = true
+ UseHelm3Defaults = true
+ got = newRollback(&helmaction.Configuration{}, obj, 0, nil)
+ g.Expect(got).ToNot(BeNil())
+ g.Expect(got.ServerSideApply).To(Equal("auto"))
+ })
+
+ t.Run("server side apply user specified", func(t *testing.T) {
+ g := NewWithT(t)
+
+ obj := &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "rollback",
+ Namespace: "rollback-ns",
+ },
+ Spec: v2.HelmReleaseSpec{
+ Rollback: &v2.Rollback{
+ ServerSideApply: v2.ServerSideApplyEnabled,
+ },
+ },
+ }
+
+ got := newRollback(&helmaction.Configuration{}, obj, 0, nil)
+ g.Expect(got).ToNot(BeNil())
+ g.Expect(got.ServerSideApply).To(Equal("true"))
+
+ obj.Spec.Rollback.ServerSideApply = v2.ServerSideApplyDisabled
+ got = newRollback(&helmaction.Configuration{}, obj, 0, nil)
+ g.Expect(got).ToNot(BeNil())
+ g.Expect(got.ServerSideApply).To(Equal("false"))
+ })
+}
diff --git a/internal/action/ssa.go b/internal/action/ssa.go
new file mode 100644
index 000000000..f413a2393
--- /dev/null
+++ b/internal/action/ssa.go
@@ -0,0 +1,35 @@
+/*
+Copyright 2026 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package action
+
+import (
+ v2 "github.com/fluxcd/helm-controller/api/v2"
+)
+
+// toHelmSSAValue converts the API ServerSideApplyMode to the Helm SDK value.
+// The API uses "enabled"/"disabled"/"auto" to avoid YAML boolean auto-conversion,
+// while the Helm SDK expects "true"/"false"/"auto".
+func toHelmSSAValue(mode v2.ServerSideApplyMode) string {
+ switch mode {
+ case v2.ServerSideApplyEnabled:
+ return "true"
+ case v2.ServerSideApplyDisabled:
+ return "false"
+ default:
+ return string(mode)
+ }
+}
diff --git a/internal/action/test.go b/internal/action/test.go
new file mode 100644
index 000000000..ab08d664b
--- /dev/null
+++ b/internal/action/test.go
@@ -0,0 +1,81 @@
+/*
+Copyright 2022 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package action
+
+import (
+ "context"
+ "fmt"
+
+ helmaction "helm.sh/helm/v4/pkg/action"
+ helmrelease "helm.sh/helm/v4/pkg/release/v1"
+
+ v2 "github.com/fluxcd/helm-controller/api/v2"
+)
+
+// TestOption can be used to modify Helm's action.ReleaseTesting after the
+// instructions from the v2.HelmRelease have been applied. This is for
+// example useful to enable the dry-run setting as a CLI.
+type TestOption func(action *helmaction.ReleaseTesting)
+
+// Test runs the Helm test action with the provided config, using the
+// v2.HelmReleaseSpec of the given object to determine the target release
+// and test configuration.
+//
+// It does not determine if there is a desire to perform the action, this is
+// expected to be done by the caller. In addition, it does not take note of the
+// action result. The caller is expected to listen to this using a
+// storage.ObserveFunc, which provides superior access to Helm storage writes.
+func Test(_ context.Context, config *helmaction.Configuration, obj *v2.HelmRelease, opts ...TestOption) (*helmrelease.Release, error) {
+ test := newTest(config, obj, opts)
+ rlsr, shutdownFunc, err := test.Run(obj.GetReleaseName())
+ defer shutdownFunc() // A non-nil shutdownFunc is always returned.
+ if err != nil {
+ return nil, err
+ }
+ rlsrTyped, ok := rlsr.(*helmrelease.Release)
+ if !ok {
+ return nil, fmt.Errorf("only the Chart API v2 is supported")
+ }
+ return rlsrTyped, err
+}
+
+func newTest(config *helmaction.Configuration, obj *v2.HelmRelease, opts []TestOption) *helmaction.ReleaseTesting {
+ test := helmaction.NewReleaseTesting(config)
+
+ test.Namespace = obj.GetReleaseNamespace()
+ test.Timeout = obj.GetTest().GetTimeout(obj.GetTimeout()).Duration
+
+ filters := make(map[string][]string)
+
+ for _, f := range obj.GetTest().GetFilters() {
+ name := "name"
+
+ if f.Exclude {
+ name = "!" + name
+ }
+
+ filters[name] = append(filters[name], f.Name)
+ }
+
+ test.Filters = filters
+
+ for _, opt := range opts {
+ opt(test)
+ }
+
+ return test
+}
diff --git a/internal/action/test_test.go b/internal/action/test_test.go
new file mode 100644
index 000000000..37f13e01c
--- /dev/null
+++ b/internal/action/test_test.go
@@ -0,0 +1,108 @@
+/*
+Copyright 2022 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package action
+
+import (
+ "testing"
+ "time"
+
+ . "github.com/onsi/gomega"
+ helmaction "helm.sh/helm/v4/pkg/action"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+ v2 "github.com/fluxcd/helm-controller/api/v2"
+)
+
+func Test_newTest(t *testing.T) {
+ t.Run("new test", func(t *testing.T) {
+ g := NewWithT(t)
+
+ obj := &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test",
+ Namespace: "test-ns",
+ },
+ Spec: v2.HelmReleaseSpec{
+ Timeout: &metav1.Duration{Duration: time.Minute},
+ Test: &v2.Test{
+ Timeout: &metav1.Duration{Duration: 10 * time.Second},
+ Filters: &[]v2.Filter{
+ {
+ Name: "test",
+ },
+ {
+ Name: "test2",
+ Exclude: true,
+ },
+ },
+ },
+ },
+ }
+
+ got := newTest(&helmaction.Configuration{}, obj, nil)
+ g.Expect(got).ToNot(BeNil())
+ g.Expect(got.Namespace).To(Equal(obj.Namespace))
+ g.Expect(got.Timeout).To(Equal(obj.Spec.Test.Timeout.Duration))
+ g.Expect(got.Filters).To(HaveLen(2))
+ g.Expect(got.Filters).To(HaveKeyWithValue(Equal("name"), ContainElement("test")))
+ g.Expect(got.Filters).To(HaveKeyWithValue(Equal("!name"), ContainElement("test2")))
+ })
+
+ t.Run("timeout fallback", func(t *testing.T) {
+ g := NewWithT(t)
+
+ obj := &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test",
+ Namespace: "test-ns",
+ },
+ Spec: v2.HelmReleaseSpec{
+ Timeout: &metav1.Duration{Duration: time.Minute},
+ },
+ }
+
+ got := newTest(&helmaction.Configuration{}, obj, nil)
+ g.Expect(got).ToNot(BeNil())
+ g.Expect(got.Namespace).To(Equal(obj.Namespace))
+ g.Expect(got.Timeout).To(Equal(obj.Spec.Timeout.Duration))
+ })
+
+ t.Run("applies options", func(t *testing.T) {
+ g := NewWithT(t)
+
+ obj := &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test",
+ Namespace: "test-ns",
+ },
+ Spec: v2.HelmReleaseSpec{},
+ }
+
+ got := newTest(&helmaction.Configuration{}, obj, []TestOption{
+ func(test *helmaction.ReleaseTesting) {
+ test.Filters = map[string][]string{
+ "test": {"test"},
+ }
+ },
+ func(test *helmaction.ReleaseTesting) {
+ test.Filters["test2"] = []string{"test2"}
+ },
+ })
+ g.Expect(got).ToNot(BeNil())
+ g.Expect(got.Filters).To(HaveLen(2))
+ })
+}
diff --git a/internal/action/uninstall.go b/internal/action/uninstall.go
new file mode 100644
index 000000000..6b37fdc8c
--- /dev/null
+++ b/internal/action/uninstall.go
@@ -0,0 +1,60 @@
+/*
+Copyright 2022 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package action
+
+import (
+ "context"
+
+ helmaction "helm.sh/helm/v4/pkg/action"
+ helmrelease "helm.sh/helm/v4/pkg/release"
+
+ v2 "github.com/fluxcd/helm-controller/api/v2"
+)
+
+// UninstallOption can be used to modify Helm's action.Uninstall after the
+// instructions from the v2.HelmRelease have been applied. This is for
+// example useful to enable the dry-run setting as a CLI.
+type UninstallOption func(cfg *helmaction.Uninstall)
+
+// Uninstall runs the Helm uninstall action with the provided config, using the
+// v2.HelmReleaseSpec of the given object to determine the target release
+// and uninstall configuration.
+//
+// It does not determine if there is a desire to perform the action, this is
+// expected to be done by the caller. In addition, it does not take note of the
+// action result. The caller is expected to listen to this using a
+// storage.ObserveFunc, which provides superior access to Helm storage writes.
+func Uninstall(_ context.Context, config *helmaction.Configuration, obj *v2.HelmRelease, releaseName string, opts ...UninstallOption) (*helmrelease.UninstallReleaseResponse, error) {
+ uninstall := newUninstall(config, obj, opts)
+ return uninstall.Run(releaseName)
+}
+
+func newUninstall(config *helmaction.Configuration, obj *v2.HelmRelease, opts []UninstallOption) *helmaction.Uninstall {
+ uninstall := helmaction.NewUninstall(config)
+
+ uninstall.Timeout = obj.GetUninstall().GetTimeout(obj.GetTimeout()).Duration
+ uninstall.DisableHooks = obj.GetUninstall().DisableHooks
+ uninstall.KeepHistory = obj.GetUninstall().KeepHistory
+ uninstall.WaitStrategy = getWaitStrategy(obj.GetWaitStrategy(), obj.GetUninstall())
+ uninstall.DeletionPropagation = obj.GetUninstall().GetDeletionPropagation()
+
+ for _, opt := range opts {
+ opt(uninstall)
+ }
+
+ return uninstall
+}
diff --git a/internal/action/uninstall_test.go b/internal/action/uninstall_test.go
new file mode 100644
index 000000000..6f890e05c
--- /dev/null
+++ b/internal/action/uninstall_test.go
@@ -0,0 +1,96 @@
+/*
+Copyright 2022 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package action
+
+import (
+ "testing"
+ "time"
+
+ . "github.com/onsi/gomega"
+ helmaction "helm.sh/helm/v4/pkg/action"
+ helmkube "helm.sh/helm/v4/pkg/kube"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+ v2 "github.com/fluxcd/helm-controller/api/v2"
+)
+
+func Test_newUninstall(t *testing.T) {
+ t.Run("new uninstall", func(t *testing.T) {
+ g := NewWithT(t)
+
+ obj := &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "uninstall",
+ Namespace: "uninstall-ns",
+ },
+ Spec: v2.HelmReleaseSpec{
+ Timeout: &metav1.Duration{Duration: time.Minute},
+ Uninstall: &v2.Uninstall{
+ Timeout: &metav1.Duration{Duration: 10 * time.Second},
+ KeepHistory: true,
+ },
+ },
+ }
+
+ got := newUninstall(&helmaction.Configuration{}, obj, nil)
+ g.Expect(got).ToNot(BeNil())
+ g.Expect(got.Timeout).To(Equal(obj.Spec.Uninstall.Timeout.Duration))
+ g.Expect(got.KeepHistory).To(Equal(obj.Spec.Uninstall.KeepHistory))
+ })
+
+ t.Run("timeout fallback", func(t *testing.T) {
+ g := NewWithT(t)
+
+ obj := &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "uninstall",
+ Namespace: "uninstall-ns",
+ },
+ Spec: v2.HelmReleaseSpec{
+ Timeout: &metav1.Duration{Duration: time.Minute},
+ },
+ }
+
+ got := newUninstall(&helmaction.Configuration{}, obj, nil)
+ g.Expect(got).ToNot(BeNil())
+ g.Expect(got.Timeout).To(Equal(obj.Spec.Timeout.Duration))
+ })
+
+ t.Run("applies options", func(t *testing.T) {
+ g := NewWithT(t)
+
+ obj := &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "uninstall",
+ Namespace: "uninstall-ns",
+ },
+ Spec: v2.HelmReleaseSpec{},
+ }
+
+ got := newUninstall(&helmaction.Configuration{}, obj, []UninstallOption{
+ func(uninstall *helmaction.Uninstall) {
+ uninstall.WaitStrategy = helmkube.LegacyStrategy
+ },
+ func(uninstall *helmaction.Uninstall) {
+ uninstall.DisableHooks = true
+ },
+ })
+ g.Expect(got).ToNot(BeNil())
+ g.Expect(got.WaitStrategy).To(Equal(helmkube.LegacyStrategy))
+ g.Expect(got.DisableHooks).To(BeTrue())
+ })
+}
diff --git a/internal/action/upgrade.go b/internal/action/upgrade.go
new file mode 100644
index 000000000..101cf9302
--- /dev/null
+++ b/internal/action/upgrade.go
@@ -0,0 +1,135 @@
+/*
+Copyright 2022 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package action
+
+import (
+ "context"
+ "errors"
+ "fmt"
+
+ helmaction "helm.sh/helm/v4/pkg/action"
+ helmchartutil "helm.sh/helm/v4/pkg/chart/common"
+ helmchart "helm.sh/helm/v4/pkg/chart/v2"
+ helmrelease "helm.sh/helm/v4/pkg/release/v1"
+ helmdriver "helm.sh/helm/v4/pkg/storage/driver"
+
+ v2 "github.com/fluxcd/helm-controller/api/v2"
+ "github.com/fluxcd/helm-controller/internal/features"
+ "github.com/fluxcd/helm-controller/internal/postrender"
+ "github.com/fluxcd/helm-controller/internal/release"
+)
+
+// UpgradeOption can be used to modify Helm's action.Upgrade after the instructions
+// from the v2.HelmRelease have been applied. This is for example useful to
+// enable the dry-run setting as a CLI.
+type UpgradeOption func(upgrade *helmaction.Upgrade)
+
+// Upgrade runs the Helm upgrade action with the provided config, using the
+// v2.HelmReleaseSpec of the given object to determine the target release
+// and upgrade configuration.
+//
+// It performs the upgrade according to the spec, which includes upgrading the
+// CRDs according to the defined policy.
+//
+// It does not determine if there is a desire to perform the action, this is
+// expected to be done by the caller. In addition, it does not take note of the
+// action result. The caller is expected to listen to this using a
+// storage.ObserveFunc, which provides superior access to Helm storage writes.
+func Upgrade(ctx context.Context, config *helmaction.Configuration, obj *v2.HelmRelease, chrt *helmchart.Chart,
+ vals helmchartutil.Values, opts ...UpgradeOption) (*helmrelease.Release, error) {
+ upgrade := newUpgrade(config, obj, opts)
+
+ // Resolve "auto" server-side apply setting.
+ // We need to copy this code from Helm because we need to set ForceConflicts
+ // based on the resolved value, since we always force conflicts on server-side apply
+ // (Helm does not).
+ releaseName := release.ShortenName(obj.GetReleaseName())
+ serverSideApply := upgrade.ServerSideApply == "true"
+ if upgrade.ServerSideApply == "auto" {
+ lastRelease, err := config.Releases.Last(releaseName)
+ if err != nil {
+ if errors.Is(err, helmdriver.ErrReleaseNotFound) {
+ return nil, helmdriver.NewErrNoDeployedReleases(releaseName)
+ }
+ return nil, err
+ }
+ lastReleaseTyped, ok := lastRelease.(*helmrelease.Release)
+ if !ok {
+ return nil, fmt.Errorf("only the Chart API v2 is supported")
+ }
+ serverSideApply = lastReleaseTyped.ApplyMethod == "ssa"
+ upgrade.ServerSideApply = fmt.Sprint(serverSideApply)
+ }
+ upgrade.ForceConflicts = serverSideApply // We always force conflicts on server-side apply.
+
+ policy, err := crdPolicyOrDefault(obj.GetUpgrade().CRDs)
+ if err != nil {
+ return nil, err
+ }
+
+ if err := applyCRDs(config, policy, chrt, vals, serverSideApply,
+ upgrade.WaitStrategy, upgrade.WaitOptions,
+ setOriginVisitor(v2.GroupVersion.Group, obj.Namespace, obj.Name)); err != nil {
+ return nil, fmt.Errorf("failed to apply CustomResourceDefinitions: %w", err)
+ }
+
+ rlsr, err := upgrade.RunWithContext(ctx, releaseName, chrt, vals.AsMap())
+ if err != nil {
+ return nil, err
+ }
+ rlsrTyped, ok := rlsr.(*helmrelease.Release)
+ if !ok {
+ return nil, fmt.Errorf("only the Chart API v2 is supported")
+ }
+ return rlsrTyped, err
+}
+
+func newUpgrade(config *helmaction.Configuration, obj *v2.HelmRelease, opts []UpgradeOption) *helmaction.Upgrade {
+ upgrade := helmaction.NewUpgrade(config)
+ upgrade.ServerSideApply = "auto" // This must be the upgrade default regardless of UseHelm3Defaults.
+ if ssa := obj.GetUpgrade().ServerSideApply; ssa != "" {
+ upgrade.ServerSideApply = toHelmSSAValue(ssa)
+ }
+
+ upgrade.Namespace = obj.GetReleaseNamespace()
+ upgrade.ResetValues = !obj.GetUpgrade().PreserveValues
+ upgrade.ReuseValues = obj.GetUpgrade().PreserveValues
+ upgrade.MaxHistory = obj.GetMaxHistory()
+ upgrade.Timeout = obj.GetUpgrade().GetTimeout(obj.GetTimeout()).Duration
+ upgrade.TakeOwnership = !obj.GetUpgrade().DisableTakeOwnership
+ upgrade.WaitStrategy = getWaitStrategy(obj.GetWaitStrategy(), obj.GetUpgrade())
+ upgrade.WaitForJobs = !obj.GetUpgrade().DisableWaitForJobs
+ upgrade.DisableHooks = obj.GetUpgrade().DisableHooks
+ upgrade.DisableOpenAPIValidation = obj.GetUpgrade().DisableOpenAPIValidation
+ upgrade.SkipSchemaValidation = obj.GetUpgrade().DisableSchemaValidation
+ upgrade.ForceReplace = obj.GetUpgrade().Force
+ upgrade.CleanupOnFail = obj.GetUpgrade().CleanupOnFail
+ upgrade.Devel = true
+
+ // If the user opted-in to allow DNS lookups, enable it.
+ if allowDNS, _ := features.Enabled(features.AllowDNSLookups); allowDNS {
+ upgrade.EnableDNS = allowDNS
+ }
+
+ upgrade.PostRenderer = postrender.BuildPostRenderers(obj)
+
+ for _, opt := range opts {
+ opt(upgrade)
+ }
+
+ return upgrade
+}
diff --git a/internal/action/upgrade_test.go b/internal/action/upgrade_test.go
new file mode 100644
index 000000000..7b1f09c2e
--- /dev/null
+++ b/internal/action/upgrade_test.go
@@ -0,0 +1,170 @@
+/*
+Copyright 2022 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package action
+
+import (
+ "testing"
+ "time"
+
+ . "github.com/onsi/gomega"
+ helmaction "helm.sh/helm/v4/pkg/action"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+ v2 "github.com/fluxcd/helm-controller/api/v2"
+)
+
+func Test_newUpgrade(t *testing.T) {
+ t.Run("new upgrade", func(t *testing.T) {
+ g := NewWithT(t)
+
+ obj := &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "upgrade",
+ Namespace: "upgrade-ns",
+ },
+ Spec: v2.HelmReleaseSpec{
+ Timeout: &metav1.Duration{Duration: time.Minute},
+ Upgrade: &v2.Upgrade{
+ Timeout: &metav1.Duration{Duration: 10 * time.Second},
+ Force: true,
+ },
+ },
+ }
+
+ got := newUpgrade(&helmaction.Configuration{}, obj, nil)
+ g.Expect(got).ToNot(BeNil())
+ g.Expect(got.Namespace).To(Equal(obj.Namespace))
+ g.Expect(got.Timeout).To(Equal(obj.Spec.Upgrade.Timeout.Duration))
+ g.Expect(got.ForceReplace).To(Equal(obj.Spec.Upgrade.Force))
+ })
+
+ t.Run("timeout fallback", func(t *testing.T) {
+ g := NewWithT(t)
+
+ obj := &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "upgrade",
+ Namespace: "upgrade-ns",
+ },
+ Spec: v2.HelmReleaseSpec{
+ Timeout: &metav1.Duration{Duration: time.Minute},
+ },
+ }
+
+ got := newUpgrade(&helmaction.Configuration{}, obj, nil)
+ g.Expect(got).ToNot(BeNil())
+ g.Expect(got.Namespace).To(Equal(obj.Namespace))
+ g.Expect(got.Timeout).To(Equal(obj.Spec.Timeout.Duration))
+ })
+
+ t.Run("applies options", func(t *testing.T) {
+ g := NewWithT(t)
+
+ obj := &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "upgrade",
+ Namespace: "upgrade-ns",
+ },
+ Spec: v2.HelmReleaseSpec{},
+ }
+
+ got := newUpgrade(&helmaction.Configuration{}, obj, []UpgradeOption{
+ func(upgrade *helmaction.Upgrade) {
+ upgrade.Install = true
+ },
+ func(upgrade *helmaction.Upgrade) {
+ upgrade.DryRunStrategy = helmaction.DryRunClient
+ },
+ })
+ g.Expect(got).ToNot(BeNil())
+ g.Expect(got.Install).To(BeTrue())
+ g.Expect(got.DryRunStrategy).To(Equal(helmaction.DryRunClient))
+ })
+
+ t.Run("disable take ownership", func(t *testing.T) {
+ g := NewWithT(t)
+
+ obj := &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "upgrade",
+ Namespace: "upgrade-ns",
+ },
+ Spec: v2.HelmReleaseSpec{
+ Upgrade: &v2.Upgrade{
+ DisableTakeOwnership: true,
+ },
+ },
+ }
+
+ got := newUpgrade(&helmaction.Configuration{}, obj, nil)
+ g.Expect(got).ToNot(BeNil())
+ g.Expect(got.TakeOwnership).To(BeFalse())
+ })
+
+ t.Run("server side apply is auto regardless of UseHelm3Defaults", func(t *testing.T) {
+ g := NewWithT(t)
+
+ obj := &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "upgrade",
+ Namespace: "upgrade-ns",
+ },
+ Spec: v2.HelmReleaseSpec{},
+ }
+
+ // Save and restore UseHelm3Defaults
+ oldUseHelm3Defaults := UseHelm3Defaults
+ t.Cleanup(func() { UseHelm3Defaults = oldUseHelm3Defaults })
+
+ // Test with UseHelm3Defaults = false
+ UseHelm3Defaults = false
+ got := newUpgrade(&helmaction.Configuration{}, obj, nil)
+ g.Expect(got).ToNot(BeNil())
+ g.Expect(got.ServerSideApply).To(Equal("auto"))
+
+ // Test with UseHelm3Defaults = true
+ UseHelm3Defaults = true
+ got = newUpgrade(&helmaction.Configuration{}, obj, nil)
+ g.Expect(got).ToNot(BeNil())
+ g.Expect(got.ServerSideApply).To(Equal("auto"))
+ })
+
+ t.Run("server side apply user specified", func(t *testing.T) {
+ g := NewWithT(t)
+
+ obj := &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "upgrade",
+ Namespace: "upgrade-ns",
+ },
+ Spec: v2.HelmReleaseSpec{
+ Upgrade: &v2.Upgrade{
+ ServerSideApply: v2.ServerSideApplyEnabled,
+ },
+ },
+ }
+
+ got := newUpgrade(&helmaction.Configuration{}, obj, nil)
+ g.Expect(got).ToNot(BeNil())
+ g.Expect(got.ServerSideApply).To(Equal("true"))
+
+ obj.Spec.Upgrade.ServerSideApply = v2.ServerSideApplyDisabled
+ got = newUpgrade(&helmaction.Configuration{}, obj, nil)
+ g.Expect(got).ToNot(BeNil())
+ g.Expect(got.ServerSideApply).To(Equal("false"))
+ })
+}
diff --git a/internal/action/verify.go b/internal/action/verify.go
new file mode 100644
index 000000000..3407db143
--- /dev/null
+++ b/internal/action/verify.go
@@ -0,0 +1,182 @@
+/*
+Copyright 2022 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package action
+
+import (
+ "errors"
+ "fmt"
+
+ "github.com/opencontainers/go-digest"
+ helmaction "helm.sh/helm/v4/pkg/action"
+ helmchartutil "helm.sh/helm/v4/pkg/chart/common"
+ helmchart "helm.sh/helm/v4/pkg/chart/v2"
+ helmrelease "helm.sh/helm/v4/pkg/release/v1"
+
+ helmdriver "helm.sh/helm/v4/pkg/storage/driver"
+
+ v2 "github.com/fluxcd/helm-controller/api/v2"
+ "github.com/fluxcd/helm-controller/internal/release"
+ "github.com/fluxcd/pkg/chartutil"
+)
+
+var (
+ ErrReleaseDisappeared = errors.New("release disappeared from storage")
+ ErrReleaseNotFound = errors.New("no release found")
+ ErrReleaseNotObserved = errors.New("release not observed to be made for object")
+ ErrReleaseDigest = errors.New("release digest verification error")
+ ErrChartChanged = errors.New("release chart changed")
+ ErrConfigDigest = errors.New("release config values changed")
+)
+
+const (
+ targetStorageNamespace = "storage namespace"
+ targetReleaseNamespace = "release namespace"
+ targetReleaseName = "release name"
+ targetChartName = "chart name"
+)
+
+// ReleaseTargetChanged returns a reason and true if the given release and/or
+// chart name have been mutated in such a way that it no longer has the same
+// release target as recorded in the Status.History of the object, by comparing
+// the (storage) namespace, and release and chart names.
+// This can be used to e.g. trigger a garbage collection of the old release
+// before installing the new one.
+// If no change is detected, an empty string is returned along with false.
+func ReleaseTargetChanged(obj *v2.HelmRelease, chartName string) (string, bool) {
+ cur := obj.Status.History.Latest()
+ switch {
+ case obj.Status.StorageNamespace == "", cur == nil:
+ return "", false
+ case obj.GetStorageNamespace() != obj.Status.StorageNamespace:
+ return targetStorageNamespace, true
+ case obj.GetReleaseNamespace() != cur.Namespace:
+ return targetReleaseNamespace, true
+ case release.ShortenName(obj.GetReleaseName()) != cur.Name:
+ return targetReleaseName, true
+ case chartName != cur.ChartName:
+ return targetChartName, true
+ default:
+ return "", false
+ }
+}
+
+// LastRelease returns the last release object in the Helm storage with the
+// given name.
+// It returns an error of type ErrReleaseNotFound if there is no
+// release with the given name.
+// When the release name is too long, it will be shortened to the maximum
+// allowed length using the release.ShortenName function.
+func LastRelease(config *helmaction.Configuration, releaseName string) (*helmrelease.Release, error) {
+ rls, err := config.Releases.Last(release.ShortenName(releaseName))
+ if err != nil {
+ if errors.Is(err, helmdriver.ErrReleaseNotFound) {
+ return nil, ErrReleaseNotFound
+ }
+ return nil, err
+ }
+ rlsTyped, ok := rls.(*helmrelease.Release)
+ if !ok {
+ return nil, fmt.Errorf("only the Chart API v2 is supported")
+ }
+ return rlsTyped, nil
+}
+
+// VerifySnapshot verifies the data of the given v2.Snapshot
+// matches the release object in the Helm storage. It returns the verified
+// release, or an error of type ErrReleaseNotFound, ErrReleaseDisappeared,
+// ErrReleaseDigest or ErrReleaseNotObserved indicating the reason for the
+// verification failure.
+func VerifySnapshot(config *helmaction.Configuration, snapshot *v2.Snapshot) (rls *helmrelease.Release, err error) {
+ if snapshot == nil {
+ return nil, ErrReleaseNotFound
+ }
+
+ rlsr, err := config.Releases.Get(snapshot.Name, snapshot.Version)
+ if err != nil {
+ if errors.Is(err, helmdriver.ErrReleaseNotFound) {
+ return nil, ErrReleaseDisappeared
+ }
+ return nil, err
+ }
+ rls, ok := rlsr.(*helmrelease.Release)
+ if !ok {
+ return nil, fmt.Errorf("only the Chart API v2 is supported")
+ }
+
+ if err = VerifyReleaseObject(snapshot, rls); err != nil {
+ return nil, err
+ }
+ return rls, nil
+}
+
+// VerifyReleaseObject verifies the data of the given v2.Snapshot
+// matches the given Helm release object. It returns an error of type
+// ErrReleaseDigest or ErrReleaseNotObserved indicating the reason for the
+// verification failure, or nil.
+//
+// For legacy snapshots (those with an APIVersion missing or not matching the
+// current version), digest verification is skipped to allow graceful migration
+// from older helm-controller versions.
+func VerifyReleaseObject(snapshot *v2.Snapshot, rls *helmrelease.Release) error {
+ // Skip digest verification for legacy snapshots to allow migration.
+ // The release ownership is still verified by matching the release
+ // name, namespace, and version in the caller.
+ if snapshot.APIVersion != v2.CurrentSnapshotAPIVersion {
+ return nil
+ }
+
+ relDig, err := digest.Parse(snapshot.Digest)
+ if err != nil {
+ return ErrReleaseDigest
+ }
+ verifier := relDig.Verifier()
+
+ obs := release.ObserveRelease(rls)
+
+ // unfortunately we have to pass in the OciDigest as is, because helmrelease.Release
+ // does not have a field for it.
+ obs.OCIDigest = snapshot.OCIDigest
+
+ if err = obs.Encode(verifier); err != nil {
+ // We are expected to be able to encode valid JSON, error out without a
+ // typed error assuming malfunction to signal to e.g. retry.
+ return err
+ }
+ if !verifier.Verified() {
+ return ErrReleaseNotObserved
+ }
+ return nil
+}
+
+// VerifyRelease verifies that the data of the given release matches the given
+// chart metadata, and the provided values match the Snapshot.ConfigDigest.
+// It returns either an error of type ErrReleaseNotFound, ErrChartChanged or
+// ErrConfigDigest, or nil.
+func VerifyRelease(rls *helmrelease.Release, snapshot *v2.Snapshot, chrt *helmchart.Metadata, vals helmchartutil.Values) error {
+ if rls == nil {
+ return ErrReleaseNotFound
+ }
+
+ if chrt != nil && (rls.Chart.Metadata.Name != chrt.Name || rls.Chart.Metadata.Version != chrt.Version) {
+ return ErrChartChanged
+ }
+
+ if snapshot == nil || !chartutil.VerifyValues(digest.Digest(snapshot.ConfigDigest), vals) {
+ return ErrConfigDigest
+ }
+ return nil
+}
diff --git a/internal/action/verify_test.go b/internal/action/verify_test.go
new file mode 100644
index 000000000..c26b2b57f
--- /dev/null
+++ b/internal/action/verify_test.go
@@ -0,0 +1,487 @@
+/*
+Copyright 2022 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package action
+
+import (
+ "errors"
+ "testing"
+
+ . "github.com/onsi/gomega"
+ helmaction "helm.sh/helm/v4/pkg/action"
+ "helm.sh/helm/v4/pkg/chart/common"
+ helmchart "helm.sh/helm/v4/pkg/chart/v2"
+ helmreleasecommon "helm.sh/helm/v4/pkg/release/common"
+ helmrelease "helm.sh/helm/v4/pkg/release/v1"
+ helmstorage "helm.sh/helm/v4/pkg/storage"
+ "helm.sh/helm/v4/pkg/storage/driver"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+ v2 "github.com/fluxcd/helm-controller/api/v2"
+ "github.com/fluxcd/helm-controller/internal/release"
+ "github.com/fluxcd/helm-controller/internal/storage"
+ "github.com/fluxcd/helm-controller/internal/testutil"
+)
+
+func TestReleaseTargetChanged(t *testing.T) {
+ const (
+ defaultNamespace = "default-ns"
+ defaultName = "default-name"
+ defaultChartName = "default-chart"
+ defaultReleaseName = "default-release"
+ defaultTargetNamespace = "default-target-ns"
+ defaultStorageNamespace = "default-storage-ns"
+ )
+
+ tests := []struct {
+ name string
+ chartName string
+ spec v2.HelmReleaseSpec
+ status v2.HelmReleaseStatus
+ wantReason string
+ want bool
+ }{
+ {
+ name: "no change",
+ chartName: defaultChartName,
+ spec: v2.HelmReleaseSpec{},
+ status: v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ {
+ Name: defaultName,
+ Namespace: defaultNamespace,
+ ChartName: defaultChartName,
+ },
+ },
+ StorageNamespace: defaultNamespace,
+ },
+ want: false,
+ },
+ {
+ name: "no storage namespace",
+ chartName: defaultChartName,
+ spec: v2.HelmReleaseSpec{
+ ReleaseName: defaultReleaseName,
+ },
+ status: v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ {
+ Name: defaultReleaseName,
+ Namespace: defaultNamespace,
+ ChartName: defaultChartName,
+ },
+ },
+ },
+ want: false,
+ },
+ {
+ name: "no current",
+ spec: v2.HelmReleaseSpec{},
+ status: v2.HelmReleaseStatus{
+ StorageNamespace: defaultNamespace,
+ History: nil,
+ },
+ want: false,
+ },
+ {
+ name: "different storage namespace",
+ chartName: defaultChartName,
+ spec: v2.HelmReleaseSpec{
+ StorageNamespace: defaultStorageNamespace,
+ },
+ status: v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ {
+ Name: defaultName,
+ Namespace: defaultNamespace,
+ ChartName: defaultChartName,
+ },
+ },
+ StorageNamespace: defaultNamespace,
+ },
+ wantReason: targetStorageNamespace,
+ want: true,
+ },
+ {
+ name: "different release namespace",
+ chartName: defaultChartName,
+ spec: v2.HelmReleaseSpec{
+ TargetNamespace: defaultTargetNamespace,
+ },
+ status: v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ {
+ Name: defaultName,
+ Namespace: defaultNamespace,
+ ChartName: defaultChartName,
+ },
+ },
+ StorageNamespace: defaultNamespace,
+ },
+ wantReason: targetReleaseNamespace,
+ want: true,
+ },
+ {
+ name: "different release name",
+ chartName: defaultChartName,
+ spec: v2.HelmReleaseSpec{
+ ReleaseName: defaultReleaseName,
+ },
+ status: v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ {
+ Name: defaultName,
+ Namespace: defaultNamespace,
+ ChartName: defaultChartName,
+ },
+ },
+ StorageNamespace: defaultNamespace,
+ },
+ wantReason: targetReleaseName,
+ want: true,
+ },
+ {
+ name: "different chart name",
+ chartName: "other-chart",
+ spec: v2.HelmReleaseSpec{},
+ status: v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ {
+ Name: defaultName,
+ Namespace: defaultNamespace,
+ ChartName: defaultChartName,
+ },
+ },
+ StorageNamespace: defaultNamespace,
+ },
+ wantReason: targetChartName,
+ want: true,
+ },
+ {
+ name: "matching shortened release name",
+ chartName: defaultChartName,
+ spec: v2.HelmReleaseSpec{
+ TargetNamespace: "target-namespace-exceeding-max-characters",
+ },
+ status: v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ {
+ Name: "target-namespace-exceeding-max-character-eceb26601388",
+ Namespace: "target-namespace-exceeding-max-characters",
+ ChartName: defaultChartName,
+ },
+ },
+ StorageNamespace: defaultNamespace,
+ },
+ want: false,
+ },
+ {
+ name: "different shortened release name",
+ chartName: defaultChartName,
+ spec: v2.HelmReleaseSpec{
+ TargetNamespace: "target-namespace-exceeding-max-characters",
+ },
+ status: v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ {
+ Name: defaultName,
+ Namespace: "target-namespace-exceeding-max-characters",
+ ChartName: defaultChartName,
+ },
+ },
+ StorageNamespace: defaultNamespace,
+ },
+ wantReason: targetReleaseName,
+ want: true,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ g := NewWithT(t)
+
+ reason, changed := ReleaseTargetChanged(&v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Namespace: defaultNamespace,
+ Name: defaultName,
+ },
+ Spec: tt.spec,
+ Status: tt.status,
+ }, tt.chartName)
+ g.Expect(changed).To(Equal(tt.want))
+ g.Expect(reason).To(Equal(tt.wantReason))
+ })
+ }
+}
+
+func TestVerifySnapshot(t *testing.T) {
+ mock := testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: "release",
+ Version: 1,
+ Status: helmreleasecommon.StatusDeployed,
+ Namespace: "default",
+ })
+ otherMock := testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: "release",
+ Version: 1,
+ Status: helmreleasecommon.StatusSuperseded,
+ Namespace: "default",
+ })
+ mockInfo := release.ObservedToSnapshot(release.ObserveRelease(mock))
+ mockGetErr := errors.New("mock get error")
+
+ tests := []struct {
+ name string
+ snapshot *v2.Snapshot
+ release *helmrelease.Release
+ getError error
+ want *helmrelease.Release
+ wantErr error
+ }{
+ {
+ name: "valid release",
+ snapshot: mockInfo,
+ release: mock,
+ want: mock,
+ },
+ {
+ name: "invalid release",
+ snapshot: mockInfo,
+ release: otherMock,
+ wantErr: ErrReleaseNotObserved,
+ },
+ {
+ name: "release not found",
+ snapshot: mockInfo,
+ release: nil,
+ wantErr: ErrReleaseDisappeared,
+ },
+ {
+ name: "no release snapshot",
+ snapshot: nil,
+ release: nil,
+ wantErr: ErrReleaseNotFound,
+ },
+ {
+ name: "driver get error",
+ snapshot: mockInfo,
+ getError: mockGetErr,
+ wantErr: mockGetErr,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ g := NewWithT(t)
+
+ s := helmstorage.Init(driver.NewMemory())
+ if tt.release != nil {
+ g.Expect(s.Create(tt.release)).To(Succeed())
+ }
+
+ s.Driver = &storage.Failing{
+ Driver: s.Driver,
+ GetErr: tt.getError,
+ }
+
+ rls, err := VerifySnapshot(&helmaction.Configuration{Releases: s}, tt.snapshot)
+ if tt.wantErr != nil {
+ g.Expect(err).To(HaveOccurred())
+ g.Expect(err).To(Equal(tt.wantErr))
+ g.Expect(rls).To(BeNil())
+ return
+ }
+
+ g.Expect(err).ToNot(HaveOccurred())
+ g.Expect(rls).To(Equal(tt.want))
+ })
+ }
+}
+
+func TestVerifyReleaseObject(t *testing.T) {
+ mockRls := testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: "release",
+ Version: 1,
+ Status: helmreleasecommon.StatusSuperseded,
+ Namespace: "default",
+ })
+ mockSnapshot := release.ObservedToSnapshot(release.ObserveRelease(mockRls))
+ mockSnapshotIllegal := mockSnapshot.DeepCopy()
+ mockSnapshotIllegal.Digest = "illegal"
+
+ // Current snapshot with wrong digest (simulates tampered release)
+ mockSnapshotCurrentWrongDigest := mockSnapshot.DeepCopy()
+ mockSnapshotCurrentWrongDigest.Digest = "sha256:0000000000000000000000000000000000000000000000000000000000000000"
+
+ // Legacy snapshot without APIVersion (simulates pre-Helm v4 snapshot)
+ mockSnapshotLegacyEmpty := mockSnapshot.DeepCopy()
+ mockSnapshotLegacyEmpty.APIVersion = ""
+ mockSnapshotLegacyEmpty.Digest = "sha256:0000000000000000000000000000000000000000000000000000000000000000"
+
+ // Legacy snapshot with old APIVersion (simulates future version change)
+ mockSnapshotLegacyOld := mockSnapshot.DeepCopy()
+ mockSnapshotLegacyOld.APIVersion = "v1"
+ mockSnapshotLegacyOld.Digest = "sha256:0000000000000000000000000000000000000000000000000000000000000000"
+
+ tests := []struct {
+ name string
+ snapshot *v2.Snapshot
+ rls *helmrelease.Release
+ wantErr error
+ }{
+ {
+ name: "valid digest",
+ snapshot: mockSnapshot,
+ rls: mockRls,
+ },
+ {
+ name: "illegal digest",
+ snapshot: mockSnapshotIllegal,
+ wantErr: ErrReleaseDigest,
+ },
+ {
+ name: "invalid digest",
+ snapshot: mockSnapshot,
+ rls: testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: "release",
+ Version: 1,
+ Status: helmreleasecommon.StatusDeployed,
+ Namespace: "default",
+ }),
+ wantErr: ErrReleaseNotObserved,
+ },
+ {
+ name: "current APIVersion with wrong digest fails verification",
+ snapshot: mockSnapshotCurrentWrongDigest,
+ rls: mockRls,
+ wantErr: ErrReleaseNotObserved,
+ },
+ {
+ name: "legacy snapshot without APIVersion skips verification",
+ snapshot: mockSnapshotLegacyEmpty,
+ rls: testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: "release",
+ Version: 1,
+ Status: helmreleasecommon.StatusDeployed,
+ Namespace: "default",
+ }),
+ wantErr: nil,
+ },
+ {
+ name: "legacy snapshot with old APIVersion skips verification",
+ snapshot: mockSnapshotLegacyOld,
+ rls: testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: "release",
+ Version: 1,
+ Status: helmreleasecommon.StatusDeployed,
+ Namespace: "default",
+ }),
+ wantErr: nil,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ g := NewWithT(t)
+
+ got := VerifyReleaseObject(tt.snapshot, tt.rls)
+
+ if tt.wantErr != nil {
+ g.Expect(got).To(HaveOccurred())
+ g.Expect(got).To(Equal(tt.wantErr))
+ return
+ }
+
+ g.Expect(got).NotTo(HaveOccurred())
+ })
+ }
+}
+
+func TestVerifyRelease(t *testing.T) {
+ mockRls := testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: "release",
+ Version: 1,
+ Status: helmreleasecommon.StatusSuperseded,
+ Namespace: "default",
+ })
+ mockSnapshot := release.ObservedToSnapshot(release.ObserveRelease(mockRls))
+
+ tests := []struct {
+ name string
+ rls *helmrelease.Release
+ snapshot *v2.Snapshot
+ chrt *helmchart.Metadata
+ vals common.Values
+ wantErr error
+ }{
+ {
+ name: "equal",
+ rls: mockRls,
+ snapshot: mockSnapshot,
+ chrt: mockRls.Chart.Metadata,
+ vals: mockRls.Config,
+ },
+ {
+ name: "no release",
+ rls: nil,
+ snapshot: mockSnapshot,
+ chrt: mockRls.Chart.Metadata,
+ vals: mockRls.Config,
+ wantErr: ErrReleaseNotFound,
+ },
+ {
+ name: "no release snapshot",
+ rls: mockRls,
+ snapshot: nil,
+ chrt: mockRls.Chart.Metadata,
+ vals: mockRls.Config,
+ wantErr: ErrConfigDigest,
+ },
+ {
+ name: "chart meta diff",
+ rls: mockRls,
+ snapshot: mockSnapshot,
+ chrt: &helmchart.Metadata{
+ Name: "some-other-chart",
+ Version: "1.0.0",
+ },
+ vals: mockRls.Config,
+ wantErr: ErrChartChanged,
+ },
+ {
+ name: "chart values diff",
+ rls: mockRls,
+ snapshot: mockSnapshot,
+ chrt: mockRls.Chart.Metadata,
+ vals: common.Values{
+ "some": "other",
+ },
+ wantErr: ErrConfigDigest,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ g := NewWithT(t)
+
+ got := VerifyRelease(tt.rls, tt.snapshot, tt.chrt, tt.vals)
+
+ if tt.wantErr != nil {
+ g.Expect(got).To(HaveOccurred())
+ g.Expect(got).To(Equal(tt.wantErr))
+ return
+ }
+
+ g.Expect(got).ToNot(HaveOccurred())
+ })
+ }
+}
diff --git a/internal/action/wait.go b/internal/action/wait.go
new file mode 100644
index 000000000..ebab3cc6e
--- /dev/null
+++ b/internal/action/wait.go
@@ -0,0 +1,204 @@
+/*
+Copyright 2026 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package action
+
+import (
+ "context"
+ "time"
+
+ "github.com/fluxcd/cli-utils/pkg/kstatus/polling/engine"
+ "github.com/fluxcd/cli-utils/pkg/kstatus/polling/statusreaders"
+ "github.com/fluxcd/cli-utils/pkg/kstatus/status"
+ "github.com/fluxcd/cli-utils/pkg/object"
+ helmkube "helm.sh/helm/v4/pkg/kube"
+ appsv1 "k8s.io/api/apps/v1"
+ batchv1 "k8s.io/api/batch/v1"
+ apimeta "k8s.io/apimachinery/pkg/api/meta"
+ "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+ "k8s.io/apimachinery/pkg/runtime"
+
+ "github.com/fluxcd/pkg/runtime/controller"
+ runtimestatusreaders "github.com/fluxcd/pkg/runtime/statusreaders"
+ "github.com/fluxcd/pkg/ssa"
+ ssautils "github.com/fluxcd/pkg/ssa/utils"
+
+ v2 "github.com/fluxcd/helm-controller/api/v2"
+)
+
+// actionThatWaits is implemented by HelmRelease action specs that
+// support wait strategies.
+type actionThatWaits interface {
+ GetDisableWait() bool
+}
+
+// getWaitStrategy returns the wait strategy for the given action spec.
+func getWaitStrategy(strategy v2.WaitStrategyName, spec actionThatWaits) helmkube.WaitStrategy {
+ switch {
+ case spec.GetDisableWait():
+ return helmkube.HookOnlyStrategy
+ case strategy == v2.WaitStrategyPoller: // We hijack the Watcher name to mean Poller.
+ return helmkube.StatusWatcherStrategy
+ case strategy != "":
+ return helmkube.WaitStrategy(strategy)
+ case UseHelm3Defaults:
+ return helmkube.LegacyStrategy
+ default:
+ return helmkube.StatusWatcherStrategy
+ }
+}
+
+// NewStatusReaderFunc is a function type that returns a kstatus StatusReader.
+type NewStatusReaderFunc = func(apimeta.RESTMapper) engine.StatusReader
+
+// waiter waits for resources using kstatus polling.
+type waiter struct {
+ c *helmkube.Client
+ strategy helmkube.WaitStrategy
+ newResourceManager func(sr ...NewStatusReaderFunc) *ssa.ResourceManager
+ waitContext context.Context
+}
+
+// waitCtx returns the wait context, defaulting to context.Background()
+// when no context was configured via WithWaitContext.
+func (w *waiter) waitCtx() context.Context {
+ if w.waitContext != nil {
+ return w.waitContext
+ }
+ return context.Background()
+}
+
+// WatchUntilReady implements kube.Waiter.
+// Helm uses this method for hooks.
+func (w *waiter) WatchUntilReady(resources helmkube.ResourceList, timeout time.Duration) error {
+ const failFast = false // We never want to fail fast for hooks as they could be database migrations etc.
+ return w.wait(w.waitCtx(), resources, timeout, failFast,
+ // Helm docs say that Jobs and Pods are waited on during hooks,
+ // but everything else is always considered ready. Except for
+ // custom status readers built into w.newResourceManager, which
+ // take precedence (the Helm `watcher` strategy does the same).
+ runtimestatusreaders.NewCustomJobStatusReader,
+ NewCustomPodStatusReader,
+ alwaysReady)
+}
+
+// alwaysReady is copied from Helm for waiting on hooks.
+func alwaysReady(restMapper apimeta.RESTMapper) engine.StatusReader {
+ return statusreaders.NewGenericStatusReader(restMapper, func(_ *unstructured.Unstructured) (*status.Result, error) {
+ return &status.Result{
+ Status: status.CurrentStatus,
+ Message: "Resource is current",
+ }, nil
+ })
+}
+
+// Wait implements kube.Waiter. Not used for hooks.
+func (w *waiter) Wait(resources helmkube.ResourceList, timeout time.Duration) error {
+ if w.strategy == helmkube.HookOnlyStrategy {
+ return nil
+ }
+ ctx := controller.GetInterruptContext(w.waitCtx())
+ const failFast = true
+ return w.wait(ctx, resources, timeout, failFast)
+}
+
+// WaitWithJobs implements kube.Waiter. Not used for hooks.
+func (w *waiter) WaitWithJobs(resources helmkube.ResourceList, timeout time.Duration) error {
+ if w.strategy == helmkube.HookOnlyStrategy {
+ return nil
+ }
+ ctx := controller.GetInterruptContext(w.waitCtx())
+ const failFast = true
+ return w.wait(ctx, resources, timeout, failFast,
+ runtimestatusreaders.NewCustomJobStatusReader)
+}
+
+// WaitForDelete implements kube.Waiter. Not used for hooks, used only for deletion.
+func (w *waiter) WaitForDelete(resources helmkube.ResourceList, timeout time.Duration) error {
+ if w.strategy == helmkube.HookOnlyStrategy {
+ return nil
+ }
+
+ // WaitForTermination expects a list of Unstructured.
+ objs := make([]*unstructured.Unstructured, 0, len(resources))
+ for _, r := range resources {
+ gvk := r.Object.GetObjectKind().GroupVersionKind()
+ var o unstructured.Unstructured
+ o.SetGroupVersionKind(gvk)
+ o.SetNamespace(r.Namespace)
+ o.SetName(r.Name)
+ objs = append(objs, &o)
+ }
+
+ return w.newResourceManager().WaitForTermination(objs, ssa.WaitOptions{
+ Interval: 2 * time.Second, // Copied from kustomize-controller.
+ Timeout: timeout,
+ })
+}
+
+// wait is a helper method to wait for resources using the given status readers.
+func (w *waiter) wait(ctx context.Context, resources helmkube.ResourceList,
+ timeout time.Duration, failFast bool, sr ...NewStatusReaderFunc) error {
+
+ // WaitForSetWithContext expects a list of ObjMetadata.
+ var objs object.ObjMetadataSet
+ var jobs []*unstructured.Unstructured
+ for _, res := range resources {
+ gvk := res.Object.GetObjectKind().GroupVersionKind()
+
+ // Skip paused apps/v1/Deployment (copied from Helm).
+ if gvk == deploymentGVK {
+ uns, err := runtime.DefaultUnstructuredConverter.ToUnstructured(res.Object)
+ if err != nil {
+ return err
+ }
+ paused, ok, _ := unstructured.NestedBool(uns, "spec", "paused")
+ if ok && paused {
+ continue
+ }
+ }
+
+ // Collect Jobs with TTL for special handling.
+ if gvk == jobGVK {
+ uns, err := runtime.DefaultUnstructuredConverter.ToUnstructured(res.Object)
+ if err != nil {
+ return err
+ }
+ jobs = append(jobs, &unstructured.Unstructured{Object: uns})
+ }
+
+ // Convert to ObjMetadata.
+ obj, err := object.RuntimeToObjMeta(res.Object)
+ if err != nil {
+ return err
+ }
+ objs = append(objs, obj)
+ }
+
+ return w.newResourceManager(sr...).WaitForSetWithContext(ctx, objs, ssa.WaitOptions{
+ JobsWithTTL: ssautils.ExtractJobsWithTTL(jobs),
+ Interval: 5 * time.Second, // Copied from kustomize-controller.
+ Timeout: timeout,
+ // The kustomize-controller has an opt-in feature gate that disables
+ // fail fast here: DisableFailFastBehavior.
+ FailFast: failFast,
+ })
+}
+
+var (
+ deploymentGVK = appsv1.SchemeGroupVersion.WithKind("Deployment")
+ jobGVK = batchv1.SchemeGroupVersion.WithKind("Job")
+)
diff --git a/internal/action/wait_test.go b/internal/action/wait_test.go
new file mode 100644
index 000000000..a39482a6a
--- /dev/null
+++ b/internal/action/wait_test.go
@@ -0,0 +1,102 @@
+/*
+Copyright 2026 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package action
+
+import (
+ "testing"
+
+ . "github.com/onsi/gomega"
+ helmkube "helm.sh/helm/v4/pkg/kube"
+
+ v2 "github.com/fluxcd/helm-controller/api/v2"
+)
+
+type mockActionThatWaits struct {
+ disableWait bool
+}
+
+func (m *mockActionThatWaits) GetDisableWait() bool {
+ return m.disableWait
+}
+
+func TestGetWaitStrategy(t *testing.T) {
+ for _, tt := range []struct {
+ name string
+ useHelm3Defaults bool
+ strategy v2.WaitStrategyName
+ actionSpec actionThatWaits
+ expectedWait helmkube.WaitStrategy
+ }{
+ {
+ name: "wait disabled",
+ useHelm3Defaults: false,
+ actionSpec: &mockActionThatWaits{disableWait: true},
+ expectedWait: helmkube.HookOnlyStrategy,
+ },
+ {
+ name: "wait disabled with UseHelm3Defaults",
+ useHelm3Defaults: true,
+ actionSpec: &mockActionThatWaits{disableWait: true},
+ expectedWait: helmkube.HookOnlyStrategy,
+ },
+ {
+ name: "wait enabled with UseHelm3Defaults",
+ useHelm3Defaults: true,
+ actionSpec: &mockActionThatWaits{disableWait: false},
+ expectedWait: helmkube.LegacyStrategy,
+ },
+ {
+ name: "wait enabled with Helm4 defaults",
+ useHelm3Defaults: false,
+ actionSpec: &mockActionThatWaits{disableWait: false},
+ expectedWait: helmkube.StatusWatcherStrategy,
+ },
+ {
+ name: "user specified poller strategy",
+ useHelm3Defaults: true, // default would be legacy
+ strategy: v2.WaitStrategyPoller,
+ actionSpec: &mockActionThatWaits{disableWait: false},
+ expectedWait: helmkube.StatusWatcherStrategy,
+ },
+ {
+ name: "user specified legacy strategy",
+ useHelm3Defaults: false, // default would be poller
+ strategy: v2.WaitStrategyLegacy,
+ actionSpec: &mockActionThatWaits{disableWait: false},
+ expectedWait: helmkube.LegacyStrategy,
+ },
+ {
+ name: "wait disabled takes precedence over user specified strategy",
+ useHelm3Defaults: false,
+ strategy: v2.WaitStrategyPoller,
+ actionSpec: &mockActionThatWaits{disableWait: true},
+ expectedWait: helmkube.HookOnlyStrategy,
+ },
+ } {
+ t.Run(tt.name, func(t *testing.T) {
+ g := NewWithT(t)
+
+ // Save and restore UseHelm3Defaults
+ oldUseHelm3Defaults := UseHelm3Defaults
+ t.Cleanup(func() { UseHelm3Defaults = oldUseHelm3Defaults })
+ UseHelm3Defaults = tt.useHelm3Defaults
+
+ waitStrategy := getWaitStrategy(tt.strategy, tt.actionSpec)
+ g.Expect(waitStrategy).To(Equal(tt.expectedWait))
+ })
+ }
+}
diff --git a/internal/cmp/simple_unstructured.go b/internal/cmp/simple_unstructured.go
index edae2a46d..84a6c1407 100644
--- a/internal/cmp/simple_unstructured.go
+++ b/internal/cmp/simple_unstructured.go
@@ -106,5 +106,4 @@ func writePathString(path cmp.Path, sb *strings.Builder) {
sb.WriteString(fmt.Sprintf("%v", t.String()))
}
}
- return
}
diff --git a/internal/cmp/simple_unstructured_test.go b/internal/cmp/simple_unstructured_test.go
index 6cba9fa11..26ecc821b 100644
--- a/internal/cmp/simple_unstructured_test.go
+++ b/internal/cmp/simple_unstructured_test.go
@@ -277,7 +277,7 @@ c`},
}
func yamlToUnstructured(str string) (*unstructured.Unstructured, error) {
- var obj map[string]interface{}
+ var obj map[string]any
if err := yaml.Unmarshal([]byte(str), &obj); err != nil {
return nil, err
}
diff --git a/internal/controller/helmrelease_controller.go b/internal/controller/helmrelease_controller.go
index fa96e5cac..d772ee6a2 100644
--- a/internal/controller/helmrelease_controller.go
+++ b/internal/controller/helmrelease_controller.go
@@ -1,5 +1,5 @@
/*
-Copyright 2020 The Flux authors
+Copyright 2023 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -23,48 +23,55 @@ import (
"strings"
"time"
- "github.com/hashicorp/go-retryablehttp"
- "helm.sh/helm/v3/pkg/chart"
- "helm.sh/helm/v3/pkg/chartutil"
- "helm.sh/helm/v3/pkg/storage/driver"
- "helm.sh/helm/v3/pkg/strvals"
+ "github.com/Masterminds/semver/v3"
+ "github.com/fluxcd/cli-utils/pkg/kstatus/polling"
+ "github.com/fluxcd/cli-utils/pkg/kstatus/polling/clusterreader"
+ "github.com/fluxcd/cli-utils/pkg/kstatus/polling/engine"
+ celtypes "github.com/google/cel-go/common/types"
+ chart "helm.sh/helm/v4/pkg/chart/v2"
corev1 "k8s.io/api/core/v1"
+ apiequality "k8s.io/apimachinery/pkg/api/equality"
apierrors "k8s.io/apimachinery/pkg/api/errors"
apimeta "k8s.io/apimachinery/pkg/api/meta"
- metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
+ apierrutil "k8s.io/apimachinery/pkg/util/errors"
+ "k8s.io/apimachinery/pkg/util/wait"
"k8s.io/cli-runtime/pkg/genericclioptions"
"k8s.io/client-go/rest"
kuberecorder "k8s.io/client-go/tools/record"
- "sigs.k8s.io/cli-utils/pkg/kstatus/polling"
ctrl "sigs.k8s.io/controller-runtime"
- "sigs.k8s.io/controller-runtime/pkg/builder"
"sigs.k8s.io/controller-runtime/pkg/client"
- "sigs.k8s.io/controller-runtime/pkg/controller"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
- "sigs.k8s.io/controller-runtime/pkg/handler"
- "sigs.k8s.io/controller-runtime/pkg/predicate"
- "sigs.k8s.io/controller-runtime/pkg/ratelimiter"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
- apiacl "github.com/fluxcd/pkg/apis/acl"
- eventv1 "github.com/fluxcd/pkg/apis/event/v1beta1"
+ aclv1 "github.com/fluxcd/pkg/apis/acl"
"github.com/fluxcd/pkg/apis/meta"
+ "github.com/fluxcd/pkg/auth"
+ authutils "github.com/fluxcd/pkg/auth/utils"
+ "github.com/fluxcd/pkg/cache"
+ "github.com/fluxcd/pkg/chartutil"
"github.com/fluxcd/pkg/runtime/acl"
+ "github.com/fluxcd/pkg/runtime/cel"
runtimeClient "github.com/fluxcd/pkg/runtime/client"
+ "github.com/fluxcd/pkg/runtime/conditions"
helper "github.com/fluxcd/pkg/runtime/controller"
"github.com/fluxcd/pkg/runtime/jitter"
- "github.com/fluxcd/pkg/runtime/predicates"
- "github.com/fluxcd/pkg/runtime/transform"
- sourcev1 "github.com/fluxcd/source-controller/api/v1beta2"
-
- v2 "github.com/fluxcd/helm-controller/api/v2beta1"
- "github.com/fluxcd/helm-controller/internal/diff"
- "github.com/fluxcd/helm-controller/internal/features"
+ "github.com/fluxcd/pkg/runtime/logger"
+ "github.com/fluxcd/pkg/runtime/object"
+ "github.com/fluxcd/pkg/runtime/patch"
+ "github.com/fluxcd/pkg/ssa"
+
+ sourcev1 "github.com/fluxcd/source-controller/api/v1"
+
+ v2 "github.com/fluxcd/helm-controller/api/v2"
+ intacl "github.com/fluxcd/helm-controller/internal/acl"
+ "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/kube"
- "github.com/fluxcd/helm-controller/internal/runner"
- "github.com/fluxcd/helm-controller/internal/util"
+ "github.com/fluxcd/helm-controller/internal/loader"
+ intreconcile "github.com/fluxcd/helm-controller/internal/reconcile"
)
// +kubebuilder:rbac:groups=helm.toolkit.fluxcd.io,resources=helmreleases,verbs=get;list;watch;create;update;patch;delete
@@ -72,733 +79,999 @@ import (
// +kubebuilder:rbac:groups=helm.toolkit.fluxcd.io,resources=helmreleases/finalizers,verbs=get;create;update;patch;delete
// +kubebuilder:rbac:groups=source.toolkit.fluxcd.io,resources=helmcharts,verbs=get;list;watch
// +kubebuilder:rbac:groups=source.toolkit.fluxcd.io,resources=helmcharts/status,verbs=get
+// +kubebuilder:rbac:groups=source.toolkit.fluxcd.io,resources=ocirepositories,verbs=get;list;watch
+// +kubebuilder:rbac:groups=source.toolkit.fluxcd.io,resources=ocirepositories/status,verbs=get
// +kubebuilder:rbac:groups="",resources=events,verbs=create;patch
-// HelmReleaseReconciler reconciles a HelmRelease object
+// HelmReleaseReconciler reconciles a HelmRelease object.
type HelmReleaseReconciler struct {
client.Client
+ kuberecorder.EventRecorder
helper.Metrics
- Config *rest.Config
- Scheme *runtime.Scheme
- EventRecorder kuberecorder.EventRecorder
- DefaultServiceAccount string
- NoCrossNamespaceRef bool
- ClientOpts runtimeClient.Options
- KubeConfigOpts runtimeClient.KubeConfigOptions
- StatusPoller *polling.StatusPoller
- PollingOpts polling.Options
- ControllerName string
-
- httpClient *retryablehttp.Client
- requeueDependency time.Duration
-}
+ // Kubernetes configuration
-func (r *HelmReleaseReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager, opts HelmReleaseReconcilerOptions) error {
- // Index the HelmRelease by the HelmChart references they point at
- if err := mgr.GetFieldIndexer().IndexField(ctx, &v2.HelmRelease{}, v2.SourceIndexKey,
- func(o client.Object) []string {
- hr := o.(*v2.HelmRelease)
- return []string{
- fmt.Sprintf("%s/%s", hr.Spec.Chart.GetNamespace(hr.GetNamespace()), hr.GetHelmChartName()),
- }
- },
- ); err != nil {
- return err
- }
+ FieldManager string
+ DisallowedFieldManagers []string
+ DefaultServiceAccount string
+ GetClusterConfig func() (*rest.Config, error)
+ ClientOpts runtimeClient.Options
+ KubeConfigOpts runtimeClient.KubeConfigOptions
+ APIReader client.Reader
+ TokenCache *cache.TokenCache
- r.requeueDependency = opts.DependencyRequeueInterval
-
- // Configure the retryable http client used for fetching artifacts.
- // By default, it retries 10 times within a 3.5 minutes window.
- httpClient := retryablehttp.NewClient()
- httpClient.RetryWaitMin = 5 * time.Second
- httpClient.RetryWaitMax = 30 * time.Second
- httpClient.RetryMax = opts.HTTPRetry
- httpClient.Logger = nil
- r.httpClient = httpClient
-
- return ctrl.NewControllerManagedBy(mgr).
- For(&v2.HelmRelease{}, builder.WithPredicates(
- predicate.Or(predicate.GenerationChangedPredicate{}, predicates.ReconcileRequestedPredicate{}),
- )).
- Watches(
- &sourcev1.HelmChart{},
- handler.EnqueueRequestsFromMapFunc(r.requestsForHelmChartChange),
- builder.WithPredicates(SourceRevisionChangePredicate{}),
- ).
- WithOptions(controller.Options{
- RateLimiter: opts.RateLimiter,
- }).
- Complete(r)
-}
+ // Retry and requeue configuration
-// ConditionError represents an error with a status condition reason attached.
-type ConditionError struct {
- Reason string
- Err error
-}
+ DependencyRequeueInterval time.Duration
+ ArtifactFetchRetries int
+
+ // Feature gates
-func (c ConditionError) Error() string {
- return c.Err.Error()
+ AdditiveCELDependencyCheck bool
+ AllowExternalArtifact bool
+ DefaultToRetryOnFailure bool
+ DirectSourceFetch bool
+ DisableChartDigestTracking bool
}
-func (r *HelmReleaseReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
+const terminalErrorMessage = "Reconciliation failed terminally due to configuration error"
+
+var (
+ errWaitForDependency = errors.New("must wait for dependency")
+ errWaitForChart = errors.New("must wait for chart")
+)
+
+func (r *HelmReleaseReconciler) Reconcile(ctx context.Context, req ctrl.Request) (result ctrl.Result, retErr error) {
start := time.Now()
log := ctrl.LoggerFrom(ctx)
- var hr v2.HelmRelease
- if err := r.Get(ctx, req.NamespacedName, &hr); err != nil {
+ // Fetch the HelmRelease
+ obj := &v2.HelmRelease{}
+ if err := r.Get(ctx, req.NamespacedName, obj); err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}
+ if !isValidChartRef(obj) {
+ return ctrl.Result{}, reconcile.TerminalError(fmt.Errorf("invalid Chart reference"))
+ }
+
+ // Initialize the patch helper with the current version of the object.
+ patchHelper := patch.NewSerialPatcher(obj, r.Client)
+
+ // Always attempt to patch the object after each reconciliation.
defer func() {
- // Always record metrics.
- r.Metrics.RecordSuspend(ctx, &hr, hr.Spec.Suspend)
- r.Metrics.RecordReadiness(ctx, &hr)
- r.Metrics.RecordDuration(ctx, &hr, start)
- }()
+ if v, ok := meta.ReconcileAnnotationValue(obj.GetAnnotations()); ok {
+ obj.Status.SetLastHandledReconcileRequest(v)
+ }
- // Add our finalizer if it does not exist
- if !controllerutil.ContainsFinalizer(&hr, v2.HelmReleaseFinalizer) {
- patch := client.MergeFrom(hr.DeepCopy())
- controllerutil.AddFinalizer(&hr, v2.HelmReleaseFinalizer)
- if err := r.Patch(ctx, &hr, patch); err != nil {
- log.Error(err, "unable to register finalizer")
- return ctrl.Result{}, err
+ patchOpts := []patch.Option{
+ patch.WithFieldOwner(r.FieldManager),
+ patch.WithOwnedConditions{Conditions: intreconcile.OwnedConditions},
}
+
+ if errors.Is(retErr, reconcile.TerminalError(nil)) || (retErr == nil && (result.IsZero() || !result.Requeue)) {
+ patchOpts = append(patchOpts, patch.WithStatusObservedGeneration{})
+ }
+
+ // We do not want to return these errors, but rather wait for the
+ // designated RequeueAfter to expire and try again.
+ // However, not returning an error will cause the patch helper to
+ // patch the observed generation, which we do not want. So we ignore
+ // these errors here after patching.
+ retErr = interrors.Ignore(retErr, errWaitForDependency, errWaitForChart)
+
+ if err := patchHelper.Patch(ctx, obj, patchOpts...); err != nil {
+ if !obj.DeletionTimestamp.IsZero() {
+ err = apierrutil.FilterOut(err, func(e error) bool { return apierrors.IsNotFound(e) })
+ }
+ retErr = apierrutil.Reduce(apierrutil.NewAggregate([]error{retErr, err}))
+ }
+
+ // Wait for the object to have synced in-cache after patching.
+ // This is required to ensure that the next reconciliation will
+ // operate on the patched object when an immediate reconcile is
+ // requested.
+ if err := wait.PollUntilContextTimeout(context.Background(), 1*time.Second, 10*time.Second, true, r.waitForHistoryCacheSync(obj)); err != nil {
+ log.Error(err, "failed to wait for object to sync in-cache after patching")
+ }
+
+ // Record the duration of the reconciliation.
+ r.Metrics.RecordDuration(ctx, obj, start)
+ }()
+
+ // Examine if the object is under deletion.
+ if !obj.ObjectMeta.DeletionTimestamp.IsZero() {
+ return r.reconcileDelete(ctx, obj)
}
- // Examine if the object is under deletion
- if !hr.ObjectMeta.DeletionTimestamp.IsZero() {
- return r.reconcileDelete(ctx, &hr)
+ // Add finalizer first if not exist to avoid the race condition
+ // between init and delete.
+ // Note: Finalizers in general can only be added when the deletionTimestamp
+ // is not set.
+ if !controllerutil.ContainsFinalizer(obj, v2.HelmReleaseFinalizer) {
+ controllerutil.AddFinalizer(obj, v2.HelmReleaseFinalizer)
+ return ctrl.Result{Requeue: true}, nil
}
- // Return early if the HelmRelease is suspended.
- if hr.Spec.Suspend {
- log.Info("Reconciliation is suspended for this object")
+ // Return early if the object is suspended.
+ if obj.Spec.Suspend {
+ log.Info("reconciliation is suspended for this object")
return ctrl.Result{}, nil
}
- hr, result, err := r.reconcile(ctx, hr)
-
- // Update status after reconciliation.
- if updateStatusErr := r.patchStatus(ctx, &hr); updateStatusErr != nil {
- log.Error(updateStatusErr, "unable to update status after reconciliation")
- return ctrl.Result{Requeue: true}, updateStatusErr
+ // Configure custom health checks.
+ statusReader, err := cel.NewStatusReader(obj.Spec.HealthCheckExprs)
+ if err != nil {
+ errMsg := fmt.Sprintf("%s: %v", terminalErrorMessage, err)
+ conditions.MarkFalse(obj, meta.ReadyCondition, meta.InvalidCELExpressionReason, "%s", errMsg)
+ conditions.MarkStalled(obj, meta.InvalidCELExpressionReason, "%s", errMsg)
+ obj.Status.ObservedGeneration = obj.Generation
+ r.Eventf(obj, corev1.EventTypeWarning, meta.InvalidCELExpressionReason, err.Error())
+ return ctrl.Result{}, reconcile.TerminalError(err)
}
- // Log reconciliation duration
- durationMsg := fmt.Sprintf("reconciliation finished in %s", time.Now().Sub(start).String())
- if result.RequeueAfter > 0 {
- durationMsg = fmt.Sprintf("%s, next run in %s", durationMsg, result.RequeueAfter.String())
+ // Reconcile the HelmChart template.
+ if err := r.reconcileChartTemplate(ctx, obj); err != nil {
+ return ctrl.Result{}, err
}
- log.Info(durationMsg)
- return result, err
+ return r.reconcileRelease(ctx, patchHelper, obj, statusReader)
}
-func (r *HelmReleaseReconciler) reconcile(ctx context.Context, hr v2.HelmRelease) (v2.HelmRelease, ctrl.Result, error) {
+func (r *HelmReleaseReconciler) reconcileRelease(ctx context.Context,
+ patchHelper *patch.SerialPatcher, obj *v2.HelmRelease,
+ newStatusReader func(apimeta.RESTMapper) engine.StatusReader) (ctrl.Result, error) {
+
log := ctrl.LoggerFrom(ctx)
- // Record the value of the reconciliation request, if any
- if v, ok := meta.ReconcileAnnotationValue(hr.GetAnnotations()); ok {
- hr.Status.SetLastHandledReconcileRequest(v)
+
+ // Check deprecated fields.
+ if obj.GetRollback().Recreate {
+ log.Info("warning: the .spec.rollback.recreate field is deprecated and has no effect. " +
+ "for details, please see: https://github.com/fluxcd/helm-controller/issues/1300#issuecomment-3740272924")
}
- // Observe HelmRelease generation.
- if hr.Status.ObservedGeneration != hr.Generation {
- hr.Status.ObservedGeneration = hr.Generation
- hr = v2.HelmReleaseProgressing(hr)
- if updateStatusErr := r.patchStatus(ctx, &hr); updateStatusErr != nil {
- log.Error(updateStatusErr, "unable to update status after generation update")
- return hr, ctrl.Result{Requeue: true}, updateStatusErr
+ // Mark the resource as under reconciliation.
+ // We set Ready=Unknown down below after we assess the readiness of dependencies and the source.
+ conditions.MarkReconciling(obj, meta.ProgressingReason, "Fulfilling prerequisites")
+ if err := patchHelper.Patch(ctx, obj, patch.WithOwnedConditions{Conditions: intreconcile.OwnedConditions}, patch.WithFieldOwner(r.FieldManager)); err != nil {
+ return ctrl.Result{}, err
+ }
+
+ // Confirm dependencies are Ready before proceeding.
+ if c := len(obj.Spec.DependsOn); c > 0 {
+ log.Info(fmt.Sprintf("checking %d dependencies", c))
+
+ if err := r.checkDependencies(ctx, obj); err != nil {
+ // Check if this is a terminal error that should not trigger retries
+ if errors.Is(err, reconcile.TerminalError(nil)) {
+ errMsg := fmt.Sprintf("%s: %v", terminalErrorMessage, err)
+ conditions.MarkFalse(obj, meta.ReadyCondition, meta.InvalidCELExpressionReason, "%s", errMsg)
+ conditions.MarkStalled(obj, meta.InvalidCELExpressionReason, "%s", errMsg)
+ r.Eventf(obj, corev1.EventTypeWarning, meta.InvalidCELExpressionReason, err.Error())
+ return ctrl.Result{}, err
+ }
+
+ // Retry on transient errors.
+ msg := fmt.Sprintf("dependencies do not meet ready condition (%s): retrying in %s",
+ err.Error(), r.DependencyRequeueInterval.String())
+ conditions.MarkFalse(obj, meta.ReadyCondition, v2.DependencyNotReadyReason, "%s", err)
+ r.Eventf(obj, corev1.EventTypeNormal, v2.DependencyNotReadyReason, err.Error())
+ log.Info(msg)
+
+ // Exponential backoff would cause execution to be prolonged too much,
+ // instead we requeue on a fixed interval.
+ return ctrl.Result{RequeueAfter: r.DependencyRequeueInterval}, errWaitForDependency
}
+
+ log.Info("all dependencies are ready")
+ }
+ // Remove any stale corresponding Ready=False condition with Unknown.
+ if conditions.HasAnyReason(obj, meta.ReadyCondition, v2.DependencyNotReadyReason) {
+ conditions.MarkUnknown(obj, meta.ReadyCondition, meta.ProgressingReason, "reconciliation in progress")
}
- // Reconcile chart based on the HelmChartTemplate
- hc, reconcileErr := r.reconcileChart(ctx, &hr)
- if reconcileErr != nil {
- if acl.IsAccessDenied(reconcileErr) {
- log.Error(reconcileErr, "access denied to cross-namespace source")
- r.event(ctx, hr, hr.Status.LastAttemptedRevision, eventv1.EventSeverityError, reconcileErr.Error())
- return v2.HelmReleaseNotReady(hr, apiacl.AccessDeniedReason, reconcileErr.Error()),
- jitter.JitteredRequeueInterval(ctrl.Result{RequeueAfter: hr.GetRequeueAfter()}), nil
+ // Get the source object containing the HelmChart.
+ source, err := r.getSource(ctx, obj)
+ if err != nil {
+ if acl.IsAccessDenied(err) {
+ conditions.MarkStalled(obj, aclv1.AccessDeniedReason, "%s", err)
+ conditions.MarkFalse(obj, meta.ReadyCondition, aclv1.AccessDeniedReason, "%s", err)
+ conditions.Delete(obj, meta.ReconcilingCondition)
+ r.Eventf(obj, corev1.EventTypeWarning, aclv1.AccessDeniedReason, err.Error())
+
+ // Recovering from this is not possible without a restart of the
+ // controller or a change of spec, both triggering a new
+ // reconciliation.
+ return ctrl.Result{}, reconcile.TerminalError(err)
}
- msg := fmt.Sprintf("chart reconciliation failed: %s", reconcileErr.Error())
- r.event(ctx, hr, hr.Status.LastAttemptedRevision, eventv1.EventSeverityError, msg)
- return v2.HelmReleaseNotReady(hr, v2.ArtifactFailedReason, msg), ctrl.Result{Requeue: true}, reconcileErr
+ msg := fmt.Sprintf("could not get Source object: %s", err.Error())
+ conditions.MarkFalse(obj, meta.ReadyCondition, v2.ArtifactFailedReason, "%s", msg)
+ return ctrl.Result{}, err
+ }
+ // Remove any stale corresponding Ready=False condition with Unknown.
+ if conditions.HasAnyReason(obj, meta.ReadyCondition, aclv1.AccessDeniedReason, v2.ArtifactFailedReason) {
+ conditions.MarkUnknown(obj, meta.ReadyCondition, meta.ProgressingReason, "reconciliation in progress")
}
- // Check chart readiness
- if hc.Generation != hc.Status.ObservedGeneration || !apimeta.IsStatusConditionTrue(hc.Status.Conditions, meta.ReadyCondition) {
- msg := fmt.Sprintf("HelmChart '%s/%s' is not ready", hc.GetNamespace(), hc.GetName())
- r.event(ctx, hr, hr.Status.LastAttemptedRevision, eventv1.EventSeverityInfo, msg)
+ // Check if the source is ready.
+ if ready, msg := isSourceReady(source); !ready {
log.Info(msg)
+ conditions.MarkFalse(obj, meta.ReadyCondition, "SourceNotReady", "%s", msg)
// Do not requeue immediately, when the artifact is created
// the watcher should trigger a reconciliation.
- return v2.HelmReleaseNotReady(hr, v2.ArtifactFailedReason, msg), jitter.JitteredRequeueInterval(ctrl.Result{RequeueAfter: hr.GetRequeueAfter()}), nil
+ return jitter.JitteredRequeueInterval(ctrl.Result{RequeueAfter: r.DependencyRequeueInterval}), errWaitForChart
+ }
+ // Remove any stale corresponding Ready=False condition with Unknown.
+ if conditions.HasAnyReason(obj, meta.ReadyCondition, "SourceNotReady") {
+ conditions.MarkUnknown(obj, meta.ReadyCondition, meta.ProgressingReason, "reconciliation in progress")
}
- // Check dependencies
- if len(hr.Spec.DependsOn) > 0 {
- if err := r.checkDependencies(hr); err != nil {
- msg := fmt.Sprintf("dependencies do not meet ready condition (%s), retrying in %s",
- err.Error(), r.requeueDependency.String())
- r.event(ctx, hr, hc.GetArtifact().Revision, eventv1.EventSeverityInfo, msg)
- log.Info(msg)
+ // Compose values based from the spec and references.
+ values, err := chartutil.ChartValuesFromReferences(ctx,
+ log,
+ r.Client,
+ obj.Namespace,
+ obj.GetValues(),
+ obj.Spec.ValuesFrom...)
+ if err != nil {
+ conditions.MarkFalse(obj, meta.ReadyCondition, "ValuesError", "%s", err)
+ r.Eventf(obj, corev1.EventTypeWarning, "ValuesError", err.Error())
+ return ctrl.Result{}, err
+ }
+ // Remove any stale corresponding Ready=False condition with Unknown.
+ if conditions.HasAnyReason(obj, meta.ReadyCondition, "ValuesError") {
+ conditions.MarkUnknown(obj, meta.ReadyCondition, meta.ProgressingReason, "reconciliation in progress")
+ }
- // Exponential backoff would cause execution to be prolonged too much,
- // instead we requeue on a fixed interval.
- return v2.HelmReleaseNotReady(hr,
- v2.DependencyNotReadyReason, err.Error()), ctrl.Result{RequeueAfter: r.requeueDependency}, nil
+ // Load chart from artifact.
+ loadedChart, err := loader.SecureLoadChartFromURL(loader.NewRetryableHTTPClient(ctx, r.ArtifactFetchRetries), source.GetArtifact().URL, source.GetArtifact().Digest)
+ if err != nil {
+ if errors.Is(err, loader.ErrFileNotFound) {
+ msg := fmt.Sprintf("Source not ready: artifact not found. Retrying in %s", r.DependencyRequeueInterval.String())
+ conditions.MarkFalse(obj, meta.ReadyCondition, v2.ArtifactFailedReason, "%s", msg)
+ log.Info(msg)
+ return ctrl.Result{RequeueAfter: r.DependencyRequeueInterval}, errWaitForDependency
}
- log.Info("all dependencies are ready, proceeding with release")
+
+ conditions.MarkFalse(obj, meta.ReadyCondition, v2.ArtifactFailedReason, "Could not load chart: %s", err)
+ r.Eventf(obj, corev1.EventTypeWarning, v2.ArtifactFailedReason, err.Error())
+ return ctrl.Result{}, err
+ }
+ // Remove any stale corresponding Ready=False condition with Unknown.
+ if conditions.HasAnyReason(obj, meta.ReadyCondition, v2.ArtifactFailedReason) {
+ conditions.MarkUnknown(obj, meta.ReadyCondition, meta.ProgressingReason, "reconciliation in progress")
}
- // Compose values
- values, err := r.composeValues(ctx, hr)
+ ociDigest, err := r.mutateChartWithSourceRevision(loadedChart, source)
if err != nil {
- r.event(ctx, hr, hr.Status.LastAttemptedRevision, eventv1.EventSeverityError, err.Error())
- return v2.HelmReleaseNotReady(hr, v2.InitFailedReason, err.Error()), ctrl.Result{Requeue: true}, nil
+ conditions.MarkFalse(obj, meta.ReadyCondition, "ChartMutateError", "%s", err)
+ return ctrl.Result{}, err
}
- // Load chart from artifact
- chart, err := r.loadHelmChart(hc)
+ // Build the REST client getter.
+ getter, err := r.buildRESTClientGetter(ctx, obj)
if err != nil {
- r.event(ctx, hr, hr.Status.LastAttemptedRevision, eventv1.EventSeverityError, err.Error())
- return v2.HelmReleaseNotReady(hr, v2.ArtifactFailedReason, err.Error()), ctrl.Result{Requeue: true}, nil
+ conditions.MarkFalse(obj, meta.ReadyCondition, "RESTClientError", "%s", err)
+ return ctrl.Result{}, err
}
- // Reconcile Helm release
- reconciledHr, reconcileErr := r.reconcileRelease(ctx, *hr.DeepCopy(), chart, values)
- if reconcileErr != nil {
- r.event(ctx, hr, hc.GetArtifact().Revision, eventv1.EventSeverityError,
- fmt.Sprintf("reconciliation failed: %s", reconcileErr.Error()))
+ // Build resource manager for wait operations.
+ resourceManager, err := r.newResourceManager(getter, newStatusReader)
+ if err != nil {
+ conditions.MarkFalse(obj, meta.ReadyCondition, "ResourceManagerError", "%s", err)
+ return ctrl.Result{}, err
}
- return reconciledHr, jitter.JitteredRequeueInterval(ctrl.Result{RequeueAfter: hr.GetRequeueAfter()}), reconcileErr
-}
-type HelmReleaseReconcilerOptions struct {
- HTTPRetry int
- DependencyRequeueInterval time.Duration
- RateLimiter ratelimiter.RateLimiter
-}
+ // Remove any stale corresponding Ready=False condition with Unknown.
+ if conditions.HasAnyReason(obj, meta.ReadyCondition, "RESTClientError") {
+ conditions.MarkUnknown(obj, meta.ReadyCondition, meta.ProgressingReason, "reconciliation in progress")
+ }
-func (r *HelmReleaseReconciler) reconcileRelease(ctx context.Context,
- hr v2.HelmRelease, chart *chart.Chart, values chartutil.Values) (v2.HelmRelease, error) {
- log := ctrl.LoggerFrom(ctx)
+ // If the release target configuration has changed, we need to uninstall the
+ // previous release target first. If we did not do this, the installation would
+ // fail due to resources already existing.
+ if reason, changed := action.ReleaseTargetChanged(obj, loadedChart.Name()); changed {
+ log.Info(fmt.Sprintf("release target configuration changed (%s): running uninstall for current release", reason))
+ if err = r.reconcileUninstall(ctx, getter, obj); err != nil && !errors.Is(err, intreconcile.ErrNoLatest) {
+ return ctrl.Result{}, err
+ }
+ obj.Status.ClearHistory()
+ obj.Status.ClearFailures()
+ obj.Status.StorageNamespace = ""
+ return ctrl.Result{Requeue: true}, nil
+ }
- // Initialize Helm action runner
- getter, err := r.buildRESTClientGetter(ctx, hr)
- if err != nil {
- return v2.HelmReleaseNotReady(hr, v2.InitFailedReason, err.Error()), err
+ // Set current storage namespace.
+ obj.Status.StorageNamespace = obj.GetStorageNamespace()
+
+ // Reset the failure count if the chart or values have changed.
+ if reason, ok := action.MustResetFailures(obj, loadedChart.Metadata, values); ok {
+ log.V(logger.DebugLevel).Info(fmt.Sprintf("resetting failure count (%s)", reason))
+ obj.Status.ClearFailures()
}
- run, err := runner.NewRunner(getter, hr.GetStorageNamespace(), log)
- if err != nil {
- return v2.HelmReleaseNotReady(hr, v2.InitFailedReason, "failed to initialize Helm action runner"), err
- }
-
- // Determine last release revision.
- rel, observeLastReleaseErr := run.ObserveLastRelease(hr)
- if observeLastReleaseErr != nil {
- err = fmt.Errorf("failed to get last release revision: %w", observeLastReleaseErr)
- return v2.HelmReleaseNotReady(hr, v2.GetLastReleaseFailedReason, "failed to get last release revision"), err
- }
-
- // Detect divergence between release in storage and HelmRelease spec.
- revision := chart.Metadata.Version
- releaseRevision := util.ReleaseRevision(rel)
- // TODO: deprecate "unordered" checksum.
- valuesChecksum := util.OrderedValuesChecksum(values)
- hasNewState := v2.HelmReleaseChanged(hr, revision, releaseRevision, util.ValuesChecksum(values), valuesChecksum)
-
- // Register the current release attempt.
- v2.HelmReleaseRecordAttempt(&hr, revision, releaseRevision, valuesChecksum)
-
- // Run diff against current cluster state.
- if !hasNewState && rel != nil {
- if ok, _ := features.Enabled(features.DetectDrift); ok {
- differ := diff.NewDiffer(runtimeClient.NewImpersonator(
- r.Client,
- r.StatusPoller,
- r.PollingOpts,
- hr.Spec.KubeConfig,
- r.KubeConfigOpts,
- r.DefaultServiceAccount,
- hr.Spec.ServiceAccountName,
- hr.GetNamespace(),
- ), r.ControllerName)
-
- changeSet, drift, err := differ.Diff(ctx, rel)
- if err != nil {
- if changeSet == nil {
- msg := "failed to diff release against cluster resources"
- r.event(ctx, hr, rel.Chart.Metadata.Version, eventv1.EventSeverityError, err.Error())
- return v2.HelmReleaseNotReady(hr, "DiffFailed", fmt.Sprintf("%s: %s", msg, err.Error())), err
- }
- log.Error(err, "diff of release against cluster resources finished with error")
- }
- msg := "no diff in cluster resources compared to release"
- if drift {
- msg = "diff in cluster resources compared to release"
- hasNewState, _ = features.Enabled(features.CorrectDrift)
- }
- if changeSet != nil {
- msg = fmt.Sprintf("%s:\n\n%s", msg, changeSet.String())
- r.event(ctx, hr, rel.Chart.Metadata.Version, eventv1.EventSeverityInfo, msg)
- }
- log.Info(msg)
- }
+ // Set last attempt values.
+ obj.Status.LastAttemptedGeneration = obj.Generation
+ obj.Status.LastAttemptedRevision = loadedChart.Metadata.Version
+ obj.Status.LastAttemptedRevisionDigest = ociDigest
+ obj.Status.LastAttemptedConfigDigest = chartutil.DigestValues(digest.Canonical, values).String()
+ obj.Status.LastAttemptedValuesChecksum = ""
+ obj.Status.LastReleaseRevision = 0
+
+ // Construct config factory for any further Helm actions.
+ cfg, err := action.NewConfigFactory(getter,
+ action.WithStorage(action.DefaultStorageDriver, obj.Status.StorageNamespace),
+ action.WithStorageLog(action.NewTraceLogger(ctx)),
+ action.WithResourceManager(resourceManager),
+ action.WithWaitContext(ctx),
+ )
+ if err != nil {
+ conditions.MarkFalse(obj, meta.ReadyCondition, "FactoryError", "%s", err)
+ return ctrl.Result{}, err
+ }
+ // Remove any stale corresponding Ready=False condition with Unknown.
+ if conditions.HasAnyReason(obj, meta.ReadyCondition, "FactoryError") {
+ conditions.MarkUnknown(obj, meta.ReadyCondition, meta.ProgressingReason, "reconciliation in progress")
}
- if hasNewState {
- hr = v2.HelmReleaseProgressing(hr)
- if updateStatusErr := r.patchStatus(ctx, &hr); updateStatusErr != nil {
- log.Error(updateStatusErr, "unable to update status after state update")
- return hr, updateStatusErr
+ // Off we go!
+ if err = intreconcile.NewAtomicRelease(patchHelper, cfg, r.EventRecorder, r.FieldManager, r.DisallowedFieldManagers, r.DefaultToRetryOnFailure).Reconcile(ctx, &intreconcile.Request{
+ Object: obj,
+ Chart: loadedChart,
+ Values: values,
+ }); err != nil {
+ // Handle health check cancellation due to requeue.
+ // Check if a new reconciliation request has been enqueued and the interrupt context was canceled.
+ if enqueued, qes := helper.IsObjectEnqueued(ctx); enqueued {
+ conditions.MarkFalse(obj, meta.ReadyCondition, meta.HealthCheckCanceledReason,
+ "New reconciliation triggered by %s/%s/%s", qes.Kind, qes.Namespace, qes.Name)
+ log.Info("New reconciliation triggered, canceling health checks", "trigger", qes)
+ r.Eventf(obj, corev1.EventTypeNormal, meta.HealthCheckCanceledReason,
+ "Health checks canceled due to new reconciliation triggered by %s/%s/%s",
+ qes.Kind, qes.Namespace, qes.Name)
+ return ctrl.Result{Requeue: true}, nil
+ }
+ switch {
+ case errors.Is(err, intreconcile.ErrRetryAfterInterval):
+ return jitter.JitteredRequeueInterval(ctrl.Result{RequeueAfter: obj.GetActiveRetry(r.DefaultToRetryOnFailure).GetRetryInterval()}), nil
+ case errors.Is(err, intreconcile.ErrMustRequeue):
+ return ctrl.Result{Requeue: true}, nil
+ case interrors.IsOneOf(err, intreconcile.ErrExceededMaxRetries, intreconcile.ErrMissingRollbackTarget):
+ err = reconcile.TerminalError(err)
}
+ return ctrl.Result{}, err
}
+ return jitter.JitteredRequeueInterval(ctrl.Result{RequeueAfter: obj.GetRequeueAfter()}), nil
+}
- // Check status of any previous release attempt.
- released := apimeta.FindStatusCondition(hr.Status.Conditions, v2.ReleasedCondition)
- if released != nil {
- switch released.Status {
- // Succeed if the previous release attempt succeeded.
- case metav1.ConditionTrue:
- return v2.HelmReleaseReady(hr), nil
- case metav1.ConditionFalse:
- // Fail if the previous release attempt remediation failed.
- remediated := apimeta.FindStatusCondition(hr.Status.Conditions, v2.RemediatedCondition)
- if remediated != nil && remediated.Status == metav1.ConditionFalse {
- err = fmt.Errorf("previous release attempt remediation failed")
- return v2.HelmReleaseNotReady(hr, remediated.Reason, remediated.Message), err
- }
+// reconcileDelete deletes the v1.HelmChart of the v2.HelmRelease,
+// and uninstalls the Helm release if the resource has not been suspended.
+func (r *HelmReleaseReconciler) reconcileDelete(ctx context.Context, obj *v2.HelmRelease) (ctrl.Result, error) {
+ // Only uninstall the release and delete the HelmChart resource if the
+ // resource is not suspended.
+ if !obj.Spec.Suspend {
+ if err := r.reconcileReleaseDeletion(ctx, obj); err != nil {
+ return ctrl.Result{}, err
}
- // Fail if install retries are exhausted.
- if hr.Spec.GetInstall().GetRemediation().RetriesExhausted(hr) {
- err = fmt.Errorf("install retries exhausted")
- return v2.HelmReleaseNotReady(hr, released.Reason, err.Error()), err
+ if err := r.reconcileChartTemplate(ctx, obj); err != nil {
+ return ctrl.Result{}, err
}
+ }
- // Fail if there is a release and upgrade retries are exhausted.
- // This avoids failing after an upgrade uninstall remediation strategy.
- if rel != nil && hr.Spec.GetUpgrade().GetRemediation().RetriesExhausted(hr) {
- err = fmt.Errorf("upgrade retries exhausted")
- return v2.HelmReleaseNotReady(hr, released.Reason, err.Error()), err
- }
+ if !obj.DeletionTimestamp.IsZero() {
+ // Remove our finalizer from the list.
+ controllerutil.RemoveFinalizer(obj, v2.HelmReleaseFinalizer)
+
+ // Cleanup caches.
+ r.TokenCache.DeleteEventsForObject(v2.HelmReleaseKind,
+ obj.GetName(), obj.GetNamespace(), cache.OperationReconcile)
+
+ // Stop reconciliation as the object is being deleted.
+ return ctrl.Result{}, nil
}
- // Deploy the release.
- var deployAction v2.DeploymentAction
- if rel == nil {
- r.event(ctx, hr, revision, eventv1.EventSeverityInfo, "Helm install has started")
- deployAction = hr.Spec.GetInstall()
- rel, err = run.Install(ctx, hr, chart, values)
- err = r.handleHelmActionResult(ctx, &hr, revision, err, deployAction.GetDescription(),
- v2.ReleasedCondition, v2.InstallSucceededReason, v2.InstallFailedReason)
- } else {
- r.event(ctx, hr, revision, eventv1.EventSeverityInfo, "Helm upgrade has started")
- deployAction = hr.Spec.GetUpgrade()
- rel, err = run.Upgrade(ctx, hr, chart, values)
- err = r.handleHelmActionResult(ctx, &hr, revision, err, deployAction.GetDescription(),
- v2.ReleasedCondition, v2.UpgradeSucceededReason, v2.UpgradeFailedReason)
- }
- remediation := deployAction.GetRemediation()
-
- // If there is a new release revision...
- if util.ReleaseRevision(rel) > releaseRevision {
- // Ensure release is not marked remediated.
- apimeta.RemoveStatusCondition(&hr.Status.Conditions, v2.RemediatedCondition)
-
- // If new release revision is successful and tests are enabled, run them.
- if err == nil && hr.Spec.GetTest().Enable {
- _, testErr := run.Test(hr)
- testErr = r.handleHelmActionResult(ctx, &hr, revision, testErr, "test",
- v2.TestSuccessCondition, v2.TestSucceededReason, v2.TestFailedReason)
-
- // Propagate any test error if not marked ignored.
- if testErr != nil && !remediation.MustIgnoreTestFailures(hr.Spec.GetTest().IgnoreFailures) {
- testsPassing := apimeta.FindStatusCondition(hr.Status.Conditions, v2.TestSuccessCondition)
- newCondition := metav1.Condition{
- Type: v2.ReleasedCondition,
- Status: metav1.ConditionFalse,
- Reason: testsPassing.Reason,
- Message: testsPassing.Message,
- }
- apimeta.SetStatusCondition(hr.GetStatusConditions(), newCondition)
- err = testErr
- }
- }
+ return ctrl.Result{Requeue: true}, nil
+}
+
+// reconcileReleaseDeletion handles the deletion of a HelmRelease resource.
+//
+// Before uninstalling the release, it will check if the current configuration
+// allows for uninstallation. If this is not the case, for example because a
+// Secret reference is missing, it will skip the uninstallation gracefully.
+//
+// If the release is uninstalled successfully, the HelmRelease resource will
+// be marked as ready and the current status will be cleared. If the release
+// cannot be uninstalled, the HelmRelease resource will be marked as not ready
+// and the error will be recorded in the status.
+//
+// Any returned error signals that the release could not be uninstalled, and
+// the reconciliation should be retried.
+func (r *HelmReleaseReconciler) reconcileReleaseDeletion(ctx context.Context, obj *v2.HelmRelease) error {
+ // If the release is not marked for deletion, we should not attempt to
+ // uninstall it.
+ if obj.DeletionTimestamp.IsZero() {
+ return fmt.Errorf("refusing to uninstall Helm release: deletion timestamp is not set")
}
+ // If the release has not been installed yet, we can skip the uninstallation.
+ if obj.Status.StorageNamespace == "" {
+ ctrl.LoggerFrom(ctx).Info("skipping Helm release uninstallation: no storage namespace configured")
+ return nil
+ }
+
+ // Build client getter.
+ getter, err := r.buildRESTClientGetter(ctx, obj)
if err != nil {
- // Increment failure count for deployment action.
- remediation.IncrementFailureCount(&hr)
- // Remediate deployment failure if necessary.
- if !remediation.RetriesExhausted(hr) || remediation.MustRemediateLastFailure() {
- if util.ReleaseRevision(rel) <= releaseRevision {
- log.Info(fmt.Sprintf("skipping remediation, no new release revision created"))
- } else {
- var remediationErr error
- switch remediation.GetStrategy() {
- case v2.RollbackRemediationStrategy:
- rollbackErr := run.Rollback(hr)
- remediationErr = r.handleHelmActionResult(ctx, &hr, revision, rollbackErr, "rollback",
- v2.RemediatedCondition, v2.RollbackSucceededReason, v2.RollbackFailedReason)
- case v2.UninstallRemediationStrategy:
- uninstallErr := run.Uninstall(hr)
- remediationErr = r.handleHelmActionResult(ctx, &hr, revision, uninstallErr, "uninstall",
- v2.RemediatedCondition, v2.UninstallSucceededReason, v2.UninstallFailedReason)
- }
- if remediationErr != nil {
- err = remediationErr
- }
+ if apierrors.IsNotFound(err) {
+ // Without a Secret reference, we cannot get a REST client
+ // to uninstall the release.
+ ctrl.LoggerFrom(ctx).Error(err, "skipping Helm release uninstallation")
+ return nil
+ }
+
+ conditions.MarkFalse(obj, meta.ReadyCondition, v2.UninstallFailedReason,
+ "failed to build REST client getter to uninstall release: %s", err)
+ return err
+ }
+
+ // Confirm any ServiceAccount used for impersonation exists before
+ // attempting to uninstall.
+ // If the ServiceAccount does not exist, for example, because the
+ // namespace is being terminated, we should not attempt to uninstall the
+ // release.
+ if obj.Spec.KubeConfig == nil {
+ cfg, err := getter.ToRESTConfig()
+ if err != nil {
+ // This should never happen.
+ return err
+ }
+
+ if serviceAccount := cfg.Impersonate.UserName; serviceAccount != "" {
+ i := strings.LastIndex(serviceAccount, ":")
+ if i != -1 {
+ serviceAccount = serviceAccount[i+1:]
}
- // Determine release after remediation.
- rel, observeLastReleaseErr = run.ObserveLastRelease(hr)
- if observeLastReleaseErr != nil {
- err = &ConditionError{
- Reason: v2.GetLastReleaseFailedReason,
- Err: errors.New("failed to get last release revision after remediation"),
+ if err = r.Client.Get(ctx, types.NamespacedName{
+ Namespace: obj.GetNamespace(),
+ Name: serviceAccount,
+ }, &corev1.ServiceAccount{}); err != nil {
+ if client.IgnoreNotFound(err) == nil {
+ // Without a ServiceAccount reference, we cannot confirm
+ // the ServiceAccount exists.
+ ctrl.LoggerFrom(ctx).Error(err, "skipping Helm release uninstallation")
+ return nil
}
+
+ conditions.MarkFalse(obj, meta.ReadyCondition, v2.UninstallFailedReason,
+ "failed to confirm ServiceAccount '%s' can be used to uninstall release: %s", serviceAccount, err)
+ return err
}
}
}
- hr.Status.LastReleaseRevision = util.ReleaseRevision(rel)
- if updateStatusErr := r.patchStatus(ctx, &hr); updateStatusErr != nil {
- log.Error(updateStatusErr, "unable to update status after state update")
- return hr, updateStatusErr
+ // Attempt to uninstall the release.
+ if err = r.reconcileUninstall(ctx, getter, obj); err != nil && !errors.Is(err, intreconcile.ErrNoLatest) {
+ return err
}
+ if err == nil {
+ ctrl.LoggerFrom(ctx).Info("uninstalled Helm release for deleted resource")
+ }
+
+ // Truncate the current release details in the status.
+ obj.Status.ClearHistory()
+ obj.Status.StorageNamespace = ""
+ return nil
+}
+
+// reconcileChartTemplate reconciles the HelmChart template from the HelmRelease.
+// Effectively, this means that the HelmChart resource is created, updated or
+// deleted based on the state of the HelmRelease.
+func (r *HelmReleaseReconciler) reconcileChartTemplate(ctx context.Context, obj *v2.HelmRelease) error {
+ return intreconcile.NewHelmChartTemplate(r.Client, r.EventRecorder, r.FieldManager).Reconcile(ctx, &intreconcile.Request{
+ Object: obj,
+ })
+}
+
+func (r *HelmReleaseReconciler) reconcileUninstall(ctx context.Context, getter genericclioptions.RESTClientGetter, obj *v2.HelmRelease) error {
+ // Construct config factory for current release first to validate
+ // storage configuration before building the resource manager.
+ cfg, err := action.NewConfigFactory(getter,
+ action.WithStorage(action.DefaultStorageDriver, obj.Status.StorageNamespace),
+ action.WithStorageLog(action.NewTraceLogger(ctx)),
+ action.WithWaitContext(ctx),
+ )
if err != nil {
- reason := v2.ReconciliationFailedReason
- if condErr := (*ConditionError)(nil); errors.As(err, &condErr) {
- reason = condErr.Reason
- }
- return v2.HelmReleaseNotReady(hr, reason, err.Error()), err
+ conditions.MarkFalse(obj, meta.ReadyCondition, "ConfigFactoryErr", "%s", err)
+ return err
+ }
+
+ // Build resource manager for wait operations during uninstall.
+ resourceManager, err := r.newResourceManager(getter, nil)
+ if err != nil {
+ conditions.MarkFalse(obj, meta.ReadyCondition, v2.UninstallFailedReason,
+ "failed to build resource manager to uninstall release: %s", err)
+ return err
}
- return v2.HelmReleaseReady(hr), nil
+ cfg.NewResourceManager = resourceManager
+
+ // Run uninstall.
+ return intreconcile.NewUninstall(cfg, r.EventRecorder).Reconcile(ctx, &intreconcile.Request{Object: obj})
}
-func (r *HelmReleaseReconciler) checkDependencies(hr v2.HelmRelease) error {
- for _, d := range hr.Spec.DependsOn {
- if d.Namespace == "" {
- d.Namespace = hr.GetNamespace()
+// checkDependencies checks if the dependencies of the current HelmRelease are ready.
+// To be considered ready, a dependencies must meet the following criteria:
+// - The dependency exists in the API server.
+// - The CEL expression (if provided) must evaluate to true.
+// - The dependency observed generation must match the current generation.
+// - The dependency Ready condition must be true.
+func (r *HelmReleaseReconciler) checkDependencies(ctx context.Context, obj *v2.HelmRelease) error {
+ // Convert the HelmRelease object to Unstructured for CEL evaluation.
+ objMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj)
+ if err != nil {
+ return fmt.Errorf("failed to convert HelmRelease to unstructured: %w", err)
+ }
+
+ for _, depRef := range obj.Spec.DependsOn {
+ depName := types.NamespacedName{
+ Namespace: depRef.Namespace,
+ Name: depRef.Name,
}
- dName := types.NamespacedName{
- Namespace: d.Namespace,
- Name: d.Name,
+ if depName.Namespace == "" {
+ depName.Namespace = obj.GetNamespace()
}
- var dHr v2.HelmRelease
- err := r.Get(context.Background(), dName, &dHr)
- if err != nil {
- return fmt.Errorf("unable to get '%s' dependency: %w", dName, err)
+
+ // Check if the dependency exists by querying
+ // the API server bypassing the cache.
+ dep := &v2.HelmRelease{}
+ if err := r.APIReader.Get(ctx, depName, dep); err != nil {
+ return fmt.Errorf("unable to get '%s' dependency: %w", depName, err)
+ }
+
+ // Evaluate the CEL expression (if specified) to determine if the dependency is ready.
+ if depRef.ReadyExpr != "" {
+ ready, err := r.evalReadyExpr(ctx, depRef.ReadyExpr, objMap, dep)
+ if err != nil {
+ return err
+ }
+ if !ready {
+ return fmt.Errorf("dependency '%s' is not ready according to readyExpr eval", depName)
+ }
}
- if len(dHr.Status.Conditions) == 0 || dHr.Generation != dHr.Status.ObservedGeneration {
- return fmt.Errorf("dependency '%s' is not ready", dName)
+ // Skip the built-in readiness check if the CEL expression is provided
+ // and the AdditiveCELDependencyCheck feature gate is not enabled.
+ if depRef.ReadyExpr != "" && !r.AdditiveCELDependencyCheck {
+ continue
}
- if !apimeta.IsStatusConditionTrue(dHr.Status.Conditions, meta.ReadyCondition) {
- return fmt.Errorf("dependency '%s' is not ready", dName)
+ // Check if the dependency observed generation is up to date
+ // and if the dependency is in a ready state.
+ if dep.Generation != dep.Status.ObservedGeneration || !conditions.IsTrue(dep, meta.ReadyCondition) {
+ return fmt.Errorf("dependency '%s' is not ready", depName)
}
}
return nil
}
-func (r *HelmReleaseReconciler) buildRESTClientGetter(ctx context.Context, hr v2.HelmRelease) (genericclioptions.RESTClientGetter, error) {
+// evalReadyExpr evaluates the CEL expression for the dependency readiness check.
+func (r *HelmReleaseReconciler) evalReadyExpr(
+ ctx context.Context,
+ expr string,
+ selfMap map[string]any,
+ dep *v2.HelmRelease,
+) (bool, error) {
+ const (
+ selfName = "self"
+ depName = "dep"
+ )
+
+ celExpr, err := cel.NewExpression(expr,
+ cel.WithCompile(),
+ cel.WithOutputType(celtypes.BoolType),
+ cel.WithStructVariables(selfName, depName))
+ if err != nil {
+ return false, reconcile.TerminalError(fmt.Errorf("failed to evaluate dependency %s: %w", dep.Name, err))
+ }
+
+ depMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(dep)
+ if err != nil {
+ return false, fmt.Errorf("failed to convert %s object to map: %w", depName, err)
+ }
+
+ vars := map[string]any{
+ selfName: selfMap,
+ depName: depMap,
+ }
+
+ return celExpr.EvaluateBoolean(ctx, vars)
+}
+
+func (r *HelmReleaseReconciler) buildRESTClientGetter(ctx context.Context, obj *v2.HelmRelease) (genericclioptions.RESTClientGetter, error) {
opts := []kube.Option{
- kube.WithNamespace(hr.GetReleaseNamespace()),
+ kube.WithNamespace(obj.GetReleaseNamespace()),
kube.WithClientOptions(r.ClientOpts),
- // When ServiceAccountName is empty, it will fall back to the configured default.
- // If this is not configured either, this option will result in a no-op.
- kube.WithImpersonate(hr.Spec.ServiceAccountName, hr.GetNamespace()),
- kube.WithPersistent(hr.UsePersistentClient()),
+ // When ServiceAccountName is empty, it will fall back to the configured
+ // default. If this is not configured either, this option will result in
+ // a no-op.
+ kube.WithImpersonate(obj.Spec.ServiceAccountName, obj.GetNamespace()),
+ kube.WithPersistent(obj.UsePersistentClient()),
}
- if hr.Spec.KubeConfig != nil {
+
+ // No kubeconfig specified, use in-cluster config.
+ kc := obj.Spec.KubeConfig
+ if kc == nil {
+ cfg, err := r.GetClusterConfig()
+ if err != nil {
+ return nil, fmt.Errorf("could not get in-cluster REST config: %w", err)
+ }
+ return kube.NewMemoryRESTClientGetter(cfg, opts...), nil
+ }
+
+ // If a kubeconfig is specified, we need to create a REST client getter
+ // based on the kubeconfig.
+ var restConfig *rest.Config
+ var err error
+ switch {
+ case kc.ConfigMapRef != nil && kc.SecretRef == nil:
+ var opts []auth.Option
+ if r.TokenCache != nil {
+ involvedObject := cache.InvolvedObject{
+ Kind: v2.HelmReleaseKind,
+ Name: obj.GetName(),
+ Namespace: obj.GetNamespace(),
+ Operation: cache.OperationReconcile,
+ }
+ opts = append(opts, auth.WithCache(*r.TokenCache, involvedObject))
+ }
+ restConfig, err = authutils.GetRESTConfig(ctx, *kc, obj.GetNamespace(), r.Client, opts...)
+ case kc.SecretRef != nil && kc.ConfigMapRef == nil:
secretName := types.NamespacedName{
- Namespace: hr.GetNamespace(),
- Name: hr.Spec.KubeConfig.SecretRef.Name,
+ Namespace: obj.GetNamespace(),
+ Name: obj.Spec.KubeConfig.SecretRef.Name,
}
var secret corev1.Secret
if err := r.Get(ctx, secretName, &secret); err != nil {
- return nil, fmt.Errorf("could not find KubeConfig secret '%s': %w", secretName, err)
- }
- kubeConfig, err := kube.ConfigFromSecret(&secret, hr.Spec.KubeConfig.SecretRef.Key, r.KubeConfigOpts)
- if err != nil {
- return nil, err
+ return nil, fmt.Errorf("could not get KubeConfig secret '%s': %w", secretName, err)
}
- return kube.NewMemoryRESTClientGetter(kubeConfig, opts...), nil
+ restConfig, err = kube.ConfigFromSecret(ctx, &secret, obj.Spec.KubeConfig.SecretRef.Key, r.KubeConfigOpts)
+ default:
+ return nil, errors.New("exactly one of .spec.kubeConfig.configMapRef or spec.kubeConfig.secretRef must be set")
+ }
+ if err != nil {
+ return nil, err
}
- return kube.NewInClusterMemoryRESTClientGetter(opts...)
+ return kube.NewMemoryRESTClientGetter(restConfig, opts...), nil
}
-// composeValues attempts to resolve all v2beta1.ValuesReference resources
-// and merges them as defined. Referenced resources are only retrieved once
-// to ensure a single version is taken into account during the merge.
-func (r *HelmReleaseReconciler) composeValues(ctx context.Context, hr v2.HelmRelease) (chartutil.Values, error) {
- result := chartutil.Values{}
-
- configMaps := make(map[string]*corev1.ConfigMap)
- secrets := make(map[string]*corev1.Secret)
-
- for _, v := range hr.Spec.ValuesFrom {
- namespacedName := types.NamespacedName{Namespace: hr.Namespace, Name: v.Name}
- var valuesData []byte
-
- switch v.Kind {
- case "ConfigMap":
- resource, ok := configMaps[namespacedName.String()]
- if !ok {
- // The resource may not exist, but we want to act on a single version
- // of the resource in case the values reference is marked as optional.
- configMaps[namespacedName.String()] = nil
-
- resource = &corev1.ConfigMap{}
- if err := r.Get(ctx, namespacedName, resource); err != nil {
- if apierrors.IsNotFound(err) {
- if v.Optional {
- (ctrl.LoggerFrom(ctx)).
- Info(fmt.Sprintf("could not find optional %s '%s'", v.Kind, namespacedName))
- continue
- }
- return nil, fmt.Errorf("could not find %s '%s'", v.Kind, namespacedName)
- }
- return nil, err
- }
- configMaps[namespacedName.String()] = resource
- }
- if resource == nil {
- if v.Optional {
- (ctrl.LoggerFrom(ctx)).Info(fmt.Sprintf("could not find optional %s '%s'", v.Kind, namespacedName))
- continue
- }
- return nil, fmt.Errorf("could not find %s '%s'", v.Kind, namespacedName)
- }
- if data, ok := resource.Data[v.GetValuesKey()]; !ok {
- return nil, fmt.Errorf("missing key '%s' in %s '%s'", v.GetValuesKey(), v.Kind, namespacedName)
- } else {
- valuesData = []byte(data)
- }
- case "Secret":
- resource, ok := secrets[namespacedName.String()]
- if !ok {
- // The resource may not exist, but we want to act on a single version
- // of the resource in case the values reference is marked as optional.
- secrets[namespacedName.String()] = nil
-
- resource = &corev1.Secret{}
- if err := r.Get(ctx, namespacedName, resource); err != nil {
- if apierrors.IsNotFound(err) {
- if v.Optional {
- (ctrl.LoggerFrom(ctx)).
- Info(fmt.Sprintf("could not find optional %s '%s'", v.Kind, namespacedName))
- continue
- }
- return nil, fmt.Errorf("could not find %s '%s'", v.Kind, namespacedName)
- }
- return nil, err
- }
- secrets[namespacedName.String()] = resource
- }
- if resource == nil {
- if v.Optional {
- (ctrl.LoggerFrom(ctx)).Info(fmt.Sprintf("could not find optional %s '%s'", v.Kind, namespacedName))
- continue
- }
- return nil, fmt.Errorf("could not find %s '%s'", v.Kind, namespacedName)
- }
- if data, ok := resource.Data[v.GetValuesKey()]; !ok {
- return nil, fmt.Errorf("missing key '%s' in %s '%s'", v.GetValuesKey(), v.Kind, namespacedName)
- } else {
- valuesData = data
- }
- default:
- return nil, fmt.Errorf("unsupported ValuesReference kind '%s'", v.Kind)
+// getSourceClient returns the client.Reader to use for fetching source objects.
+// When DirectSourceFetch is enabled, it returns the APIReader to fetch directly
+// from the API server, otherwise it returns the cached Client.
+func (r *HelmReleaseReconciler) getSourceClient() client.Reader {
+ if r.DirectSourceFetch {
+ return r.APIReader
+ }
+ return r.Client
+}
+
+// getSource returns the source object containing the HelmChart, either by
+// using the chartRef in the spec, or by looking up the HelmChart
+// referenced in the status object.
+// It returns the source object or an error.
+func (r *HelmReleaseReconciler) getSource(ctx context.Context, obj *v2.HelmRelease) (sourcev1.Source, error) {
+ var name, namespace string
+ if obj.HasChartRef() {
+ if obj.Spec.ChartRef.Kind == sourcev1.OCIRepositoryKind {
+ return r.getSourceFromOCIRef(ctx, obj)
}
- switch v.TargetPath {
- case "":
- values, err := chartutil.ReadValues(valuesData)
- if err != nil {
- return nil, fmt.Errorf("unable to read values from key '%s' in %s '%s': %w", v.GetValuesKey(), v.Kind, namespacedName, err)
- }
- result = transform.MergeMaps(result, values)
- default:
- // TODO(hidde): this is a bit of hack, as it mimics the way the option string is passed
- // to Helm from a CLI perspective. Given the parser is however not publicly accessible
- // while it contains all logic around parsing the target path, it is a fair trade-off.
- stringValuesData := string(valuesData)
- const singleQuote = "'"
- const doubleQuote = "\""
- var err error
- if (strings.HasPrefix(stringValuesData, singleQuote) && strings.HasSuffix(stringValuesData, singleQuote)) || (strings.HasPrefix(stringValuesData, doubleQuote) && strings.HasSuffix(stringValuesData, doubleQuote)) {
- stringValuesData = strings.Trim(stringValuesData, singleQuote+doubleQuote)
- singleValue := v.TargetPath + "=" + stringValuesData
- err = strvals.ParseIntoString(singleValue, result)
- } else {
- singleValue := v.TargetPath + "=" + stringValuesData
- err = strvals.ParseInto(singleValue, result)
- }
- if err != nil {
- return nil, fmt.Errorf("unable to merge value from key '%s' in %s '%s' into target path '%s': %w", v.GetValuesKey(), v.Kind, namespacedName, v.TargetPath, err)
- }
+ if obj.Spec.ChartRef.Kind == sourcev1.ExternalArtifactKind {
+ return r.getSourceFromExternalArtifact(ctx, obj)
}
+ name, namespace = obj.Spec.ChartRef.Name, obj.Spec.ChartRef.Namespace
+ if namespace == "" {
+ namespace = obj.GetNamespace()
+ }
+ } else {
+ namespace, name = obj.Status.GetHelmChart()
}
- return transform.MergeMaps(result, hr.GetValues()), nil
+
+ chartRef := types.NamespacedName{Namespace: namespace, Name: name}
+
+ if err := intacl.AllowsAccessTo(obj, sourcev1.HelmChartKind, chartRef); err != nil {
+ return nil, err
+ }
+
+ hc := sourcev1.HelmChart{}
+ if err := r.getSourceClient().Get(ctx, chartRef, &hc); err != nil {
+ return nil, err
+ }
+ return &hc, nil
}
-// reconcileDelete deletes the v1beta2.HelmChart of the v2beta1.HelmRelease,
-// and uninstalls the Helm release if the resource has not been suspended.
-// It only performs a Helm uninstall if the ServiceAccount to be impersonated
-// exists.
-func (r *HelmReleaseReconciler) reconcileDelete(ctx context.Context, hr *v2.HelmRelease) (ctrl.Result, error) {
- log := ctrl.LoggerFrom(ctx)
+func (r *HelmReleaseReconciler) getSourceFromOCIRef(ctx context.Context, obj *v2.HelmRelease) (sourcev1.Source, error) {
+ name, namespace := obj.Spec.ChartRef.Name, obj.Spec.ChartRef.Namespace
+ if namespace == "" {
+ namespace = obj.GetNamespace()
+ }
+ ociRepoRef := types.NamespacedName{Namespace: namespace, Name: name}
- // Delete the HelmChart that belongs to this resource.
- if err := r.deleteHelmChart(ctx, hr); err != nil {
- return ctrl.Result{}, err
+ if err := intacl.AllowsAccessTo(obj, sourcev1.OCIRepositoryKind, ociRepoRef); err != nil {
+ return nil, err
}
- // Only uninstall the Helm Release if the resource is not suspended.
- if !hr.Spec.Suspend {
- impersonator := runtimeClient.NewImpersonator(
- r.Client,
- r.StatusPoller,
- r.PollingOpts,
- hr.Spec.KubeConfig,
- r.KubeConfigOpts,
- kube.DefaultServiceAccountName,
- hr.Spec.ServiceAccountName,
- hr.GetNamespace(),
- )
-
- if impersonator.CanImpersonate(ctx) {
- getter, err := r.buildRESTClientGetter(ctx, *hr)
- if err != nil {
- return ctrl.Result{}, err
- }
- run, err := runner.NewRunner(getter, hr.GetStorageNamespace(), ctrl.LoggerFrom(ctx))
- if err != nil {
- return ctrl.Result{}, err
- }
- if err := run.Uninstall(*hr); err != nil && !errors.Is(err, driver.ErrReleaseNotFound) {
- return ctrl.Result{}, err
+ or := sourcev1.OCIRepository{}
+ if err := r.getSourceClient().Get(ctx, ociRepoRef, &or); err != nil {
+ return nil, err
+ }
+ return &or, nil
+}
+
+func (r *HelmReleaseReconciler) getSourceFromExternalArtifact(ctx context.Context, obj *v2.HelmRelease) (sourcev1.Source, error) {
+ name, namespace := obj.Spec.ChartRef.Name, obj.Spec.ChartRef.Namespace
+ if namespace == "" {
+ namespace = obj.GetNamespace()
+ }
+ sourceRef := types.NamespacedName{Namespace: namespace, Name: name}
+
+ if err := intacl.AllowsAccessTo(obj, sourcev1.ExternalArtifactKind, sourceRef); err != nil {
+ return nil, err
+ }
+
+ // Check if ExternalArtifact kind is allowed.
+ if obj.Spec.ChartRef.Kind == sourcev1.ExternalArtifactKind && !r.AllowExternalArtifact {
+ return nil, acl.AccessDeniedError(
+ fmt.Sprintf("can't access '%s/%s/%s', %s feature gate is disabled",
+ obj.Spec.ChartRef.Kind, namespace, name, helper.FeatureGateExternalArtifact))
+ }
+
+ or := sourcev1.ExternalArtifact{}
+ if err := r.getSourceClient().Get(ctx, sourceRef, &or); err != nil {
+ return nil, err
+ }
+ return &or, nil
+}
+
+// waitForHistoryCacheSync returns a function that can be used to wait for the
+// cache backing the Kubernetes client to be in sync with the current state of
+// the v2.HelmRelease.
+// This is a trade-off between not caching at all, and introducing a slight
+// delay to ensure we always have the latest history state.
+func (r *HelmReleaseReconciler) waitForHistoryCacheSync(obj *v2.HelmRelease) wait.ConditionWithContextFunc {
+ newObj := &v2.HelmRelease{}
+ return func(ctx context.Context) (bool, error) {
+ if err := r.Get(ctx, client.ObjectKeyFromObject(obj), newObj); err != nil {
+ if apierrors.IsNotFound(err) {
+ return true, nil
}
- log.Info("uninstalled Helm release for deleted resource")
- } else {
- err := fmt.Errorf("failed to find service account to impersonate")
- msg := "skipping Helm uninstall"
- log.Error(err, msg)
- r.event(ctx, *hr, hr.Status.LastAppliedRevision, eventv1.EventSeverityError, fmt.Sprintf("%s: %s", msg, err.Error()))
+ return false, err
}
- } else {
- ctrl.LoggerFrom(ctx).Info("skipping Helm uninstall for suspended resource")
+ return apiequality.Semantic.DeepEqual(obj.Status.History, newObj.Status.History), nil
}
+}
- // Remove our finalizer from the list and update it.
- controllerutil.RemoveFinalizer(hr, v2.HelmReleaseFinalizer)
- if err := r.Update(ctx, hr); err != nil {
- return ctrl.Result{}, err
+func isSourceReady(obj sourcev1.Source) (bool, string) {
+ if o, ok := obj.(*sourcev1.ExternalArtifact); ok {
+ if obj.GetArtifact() == nil {
+ return false, fmt.Sprintf("ExternalArtifact '%s/%s' is not ready: does not have an artifact",
+ o.GetNamespace(), o.GetName())
+ }
+ return true, ""
+ }
+ if o, ok := obj.(conditions.Getter); ok {
+ return isReady(o, obj.GetArtifact())
+ }
+ return false, fmt.Sprintf("unknown sourcev1 type: %T", obj)
+}
+
+func isReady(obj conditions.Getter, artifact *meta.Artifact) (bool, string) {
+ observedGen, err := object.GetStatusObservedGeneration(obj)
+ if err != nil {
+ return false, err.Error()
}
- return ctrl.Result{}, nil
+ kind := obj.GetObjectKind().GroupVersionKind().Kind
+
+ switch {
+ case obj.GetGeneration() != observedGen:
+ msg := "latest generation of object has not been reconciled"
+
+ if conditions.IsFalse(obj, meta.ReadyCondition) {
+ msg = conditions.GetMessage(obj, meta.ReadyCondition)
+ }
+ return false, fmt.Sprintf("%s '%s/%s' is not ready: %s",
+ kind, obj.GetNamespace(), obj.GetName(), msg)
+ case conditions.IsStalled(obj):
+ return false, fmt.Sprintf("%s '%s/%s' is not ready: %s",
+ kind, obj.GetNamespace(), obj.GetName(), conditions.GetMessage(obj, meta.StalledCondition))
+ case artifact == nil:
+ return false, fmt.Sprintf("%s '%s/%s' is not ready: %s",
+ kind, obj.GetNamespace(), obj.GetName(), "does not have an artifact")
+ default:
+ return true, ""
+ }
+}
+
+func isValidChartRef(obj *v2.HelmRelease) bool {
+ return (obj.HasChartRef() && !obj.HasChartTemplate()) ||
+ (!obj.HasChartRef() && obj.HasChartTemplate())
}
-func (r *HelmReleaseReconciler) handleHelmActionResult(ctx context.Context,
- hr *v2.HelmRelease, revision string, err error, action string, condition string, succeededReason string, failedReason string) error {
+// mutateChartWithSourceRevision mutates the chart version by appending the
+// digest part of the source revision to the chart version metadata.
+// It returns the digest that was appended, or an error if the mutation failed.
+func (r *HelmReleaseReconciler) mutateChartWithSourceRevision(chart *chart.Chart, source sourcev1.Source) (string, error) {
+ if !isSourceWithRevisionDigest(source) {
+ // Clear any previously set digest in status
+ // if the source revision does not contain a digest.
+ return "", nil
+ }
+ ver, err := semver.NewVersion(chart.Metadata.Version)
if err != nil {
- err = fmt.Errorf("Helm %s failed: %w", action, err)
- msg := err.Error()
- if actionErr := (*runner.ActionError)(nil); errors.As(err, &actionErr) {
- msg = strings.TrimSpace(msg) + "\n\nLast Helm logs:\n\n" + actionErr.CapturedLogs
- }
- newCondition := metav1.Condition{
- Type: condition,
- Status: metav1.ConditionFalse,
- Reason: failedReason,
- Message: msg,
- }
- apimeta.SetStatusCondition(hr.GetStatusConditions(), newCondition)
- r.event(ctx, *hr, revision, eventv1.EventSeverityError, msg)
- return &ConditionError{Reason: failedReason, Err: err}
- } else {
- msg := fmt.Sprintf("Helm %s succeeded", action)
- newCondition := metav1.Condition{
- Type: condition,
- Status: metav1.ConditionTrue,
- Reason: succeededReason,
- Message: msg,
- }
- apimeta.SetStatusCondition(hr.GetStatusConditions(), newCondition)
- r.event(ctx, *hr, revision, eventv1.EventSeverityInfo, msg)
- return nil
+ return "", fmt.Errorf("invalid chart version %s", chart.Metadata.Version)
+ }
+
+ var revDigest string
+ revision := source.GetArtifact().Revision
+ tagVer, isSemverTag := parseSemverFromTag(revision)
+ switch {
+ case isSemverTag:
+ tagDigestPair := strings.Split(revision, "@")
+ if len(tagDigestPair) != 2 || !tagVer.Equal(ver) {
+ return "", fmt.Errorf("artifact revision %s does not match chart version %s", tagDigestPair[0], chart.Metadata.Version)
+ }
+ // The digest should be in format :
+ sha, err := extractDigestSubString(tagDigestPair[1])
+ if err != nil {
+ return "", err
+ }
+ // add the digest to the chart version to make sure mutable tags are detected
+ *ver, err = ver.SetMetadata(sha)
+ if err != nil {
+ return "", err
+ }
+ revDigest = tagDigestPair[1]
+ default:
+ // default to the digest
+ sha, err := extractDigestSubString(revision)
+ if err != nil {
+ return "", err
+ }
+ *ver, err = ver.SetMetadata(sha)
+ if err != nil {
+ return "", err
+ }
+ revDigest = revision
+ }
+
+ if !r.DisableChartDigestTracking {
+ chart.Metadata.Version = ver.String()
}
+
+ return revDigest, nil
}
-func (r *HelmReleaseReconciler) patchStatus(ctx context.Context, hr *v2.HelmRelease) error {
- latest := &v2.HelmRelease{}
- if err := r.Client.Get(ctx, client.ObjectKeyFromObject(hr), latest); err != nil {
- return err
+// isSourceWithRevisionDigest returns true if the source
+// is of a type that supports immutable revisions,
+// such as OCIRepository or ExternalArtifact.
+func isSourceWithRevisionDigest(source sourcev1.Source) bool {
+ if _, ok := source.(*sourcev1.OCIRepository); ok {
+ return true
+ }
+ if _, ok := source.(*sourcev1.ExternalArtifact); ok {
+ // ExternalArtifact revision can be a semantic version or a digest.
+ // We only want to mutate the chart version if the revision contains a digest.
+ if source.GetArtifact() != nil &&
+ strings.Contains(source.GetArtifact().Revision, ":") {
+ return true
+ }
}
- patch := client.MergeFrom(latest.DeepCopy())
- latest.Status = hr.Status
- return r.Client.Status().Patch(ctx, latest, patch, client.FieldOwner(r.ControllerName))
+ return false
}
-func (r *HelmReleaseReconciler) requestsForHelmChartChange(ctx context.Context, o client.Object) []reconcile.Request {
- hc, ok := o.(*sourcev1.HelmChart)
- if !ok {
- err := fmt.Errorf("expected a HelmChart, got %T", o)
- ctrl.LoggerFrom(ctx).Error(err, "failed to get requests for HelmChart change")
- return nil
+// parseSemverFromTag attempts to parse a semver version from a tag
+// in the @: or format.
+// It returns the parsed semver version, or nil if the tag does not
+// contain a valid semver version. The boolean return value indicates
+// whether the parsing was successful.
+func parseSemverFromTag(tag string) (*semver.Version, bool) {
+ semverCandidate := tag
+ if strings.Contains(tag, "@") {
+ tagDigestPair := strings.Split(tag, "@")
+ // Replace '+' with '_' for OCI tag semver compatibility
+ // per https://github.com/helm/helm/blob/v3.14.4/pkg/registry/client.go#L45-L50
+ semverCandidate = strings.ReplaceAll(tagDigestPair[0], "_", "+")
}
- // If we do not have an artifact, we have no requests to make
- if hc.GetArtifact() == nil {
- return nil
+ ver, err := semver.NewVersion(semverCandidate)
+ if err != nil {
+ return nil, false
}
+ return ver, true
+}
- var list v2.HelmReleaseList
- if err := r.List(ctx, &list, client.MatchingFields{
- v2.SourceIndexKey: client.ObjectKeyFromObject(hc).String(),
- }); err != nil {
- ctrl.LoggerFrom(ctx).Error(err, "failed to list HelmReleases for HelmChart change")
- return nil
+func extractDigestSubString(revision string) (string, error) {
+ var sha string
+ // expects a revision in the : format
+ if pair := strings.Split(revision, ":"); len(pair) != 2 {
+ return "", fmt.Errorf("invalid artifact revision %s", revision)
+ } else {
+ sha = pair[1]
+ }
+ if len(sha) < 12 {
+ return "", fmt.Errorf("invalid artifact revision %s", revision)
}
+ return sha[0:12], nil
+}
- var reqs []reconcile.Request
- for _, i := range list.Items {
- // If the revision of the artifact equals to the last attempted revision,
- // we should not make a request for this HelmRelease
- if hc.GetArtifact().HasRevision(i.Status.LastAttemptedRevision) {
- continue
+func extractDigest(revision string) string {
+ if strings.Contains(revision, "@") {
+ // expects a revision in the @: format
+ tagDigestPair := strings.Split(revision, "@")
+ if len(tagDigestPair) != 2 {
+ return ""
}
- reqs = append(reqs, reconcile.Request{NamespacedName: client.ObjectKeyFromObject(&i)})
+ return tagDigestPair[1]
+ } else {
+ // revision in the : format
+ return revision
}
- return reqs
}
-// event emits a Kubernetes event and forwards the event to notification controller if configured.
-func (r *HelmReleaseReconciler) event(_ context.Context, hr v2.HelmRelease, revision, severity, msg string) {
- var eventMeta map[string]string
+// newResourceManager creates a constructor for a new SSA ResourceManager with
+// custom status readers for wait operations taking additional status readers
+// as parameters. The additional status readers can for example be used to
+// conditionally append built-in status readers based on API fields, such as
+// DisableWaitForJobs (should append the custom built-in Job status reader when
+// this field is false). This argument is left for lazy injection because
+// different Helm actions may have different field values in the HelmRelease
+// spec. Which Helm action will use the ResourceManager is not known at the time
+// when this function is called.
+func (r *HelmReleaseReconciler) newResourceManager(
+ getter genericclioptions.RESTClientGetter,
+ newStatusReader func(apimeta.RESTMapper) engine.StatusReader,
+) (func(sr ...action.NewStatusReaderFunc) *ssa.ResourceManager, error) {
+
+ // Build controller-runtime client.
+ restConfig, err := getter.ToRESTConfig()
+ if err != nil {
+ return nil, err
+ }
+ c, err := client.New(restConfig, client.Options{})
+ if err != nil {
+ return nil, err
+ }
+
+ // Get REST mapper for later use.
+ restMapper, err := getter.ToRESTMapper()
+ if err != nil {
+ return nil, err
+ }
- if revision != "" || hr.Status.LastAttemptedValuesChecksum != "" {
- eventMeta = make(map[string]string)
- if revision != "" {
- eventMeta[v2.GroupVersion.Group+"/"+eventv1.MetaRevisionKey] = revision
+ // Return constructor function.
+ return func(sr ...action.NewStatusReaderFunc) *ssa.ResourceManager {
+ var statusReaders []engine.StatusReader
+
+ // Spec-defined status readers have precedence.
+ if newStatusReader != nil {
+ statusReaders = append(statusReaders, newStatusReader(restMapper))
}
- if hr.Status.LastAttemptedValuesChecksum != "" {
- eventMeta[v2.GroupVersion.Group+"/"+eventv1.MetaTokenKey] = hr.Status.LastAttemptedValuesChecksum
+
+ // Additional built-in status readers come last.
+ for _, f := range sr {
+ statusReaders = append(statusReaders, f(restMapper))
}
- }
- eventType := corev1.EventTypeNormal
- if severity == eventv1.EventSeverityError {
- eventType = corev1.EventTypeWarning
- }
- r.EventRecorder.AnnotatedEventf(&hr, eventMeta, eventType, severity, msg)
+ // Build poller.
+ poller := polling.NewStatusPoller(c, restMapper, polling.Options{
+ CustomStatusReaders: statusReaders,
+ // The kustomize-controller has an opt-out feature gate that enables
+ // this direct cluster reader: DisableStatusPollerCache.
+ ClusterReaderFactory: engine.ClusterReaderFactoryFunc(clusterreader.NewDirectClusterReader),
+ })
+
+ // Build resource manager.
+ return ssa.NewResourceManager(c, poller, ssa.Owner{})
+ }, nil
}
diff --git a/internal/controller/helmrelease_controller_chart.go b/internal/controller/helmrelease_controller_chart.go
deleted file mode 100644
index 4ab351fa6..000000000
--- a/internal/controller/helmrelease_controller_chart.go
+++ /dev/null
@@ -1,278 +0,0 @@
-/*
-Copyright 2020 The Flux authors
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package controller
-
-import (
- "context"
- _ "crypto/sha256"
- _ "crypto/sha512"
- "fmt"
- "io"
- "net/http"
- "net/url"
- "os"
- "reflect"
- "strings"
-
- "github.com/fluxcd/pkg/runtime/acl"
- "github.com/hashicorp/go-retryablehttp"
- "github.com/opencontainers/go-digest"
- _ "github.com/opencontainers/go-digest/blake3"
- "helm.sh/helm/v3/pkg/chart"
- "helm.sh/helm/v3/pkg/chart/loader"
- apiequality "k8s.io/apimachinery/pkg/api/equality"
- apierrors "k8s.io/apimachinery/pkg/api/errors"
- metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
- "k8s.io/apimachinery/pkg/types"
- ctrl "sigs.k8s.io/controller-runtime"
-
- sourcev1 "github.com/fluxcd/source-controller/api/v1"
- sourcev1b2 "github.com/fluxcd/source-controller/api/v1beta2"
-
- v2 "github.com/fluxcd/helm-controller/api/v2beta1"
-)
-
-func (r *HelmReleaseReconciler) reconcileChart(ctx context.Context, hr *v2.HelmRelease) (*sourcev1b2.HelmChart, error) {
- chartName := types.NamespacedName{
- Namespace: hr.Spec.Chart.GetNamespace(hr.Namespace),
- Name: hr.GetHelmChartName(),
- }
-
- if r.NoCrossNamespaceRef && chartName.Namespace != hr.Namespace {
- return nil, acl.AccessDeniedError(fmt.Sprintf("can't access '%s/%s', cross-namespace references have been blocked",
- hr.Spec.Chart.Spec.SourceRef.Kind, types.NamespacedName{
- Namespace: hr.Spec.Chart.Spec.SourceRef.Namespace,
- Name: hr.Spec.Chart.Spec.SourceRef.Name,
- }))
- }
-
- // Garbage collect the previous HelmChart if the namespace named changed.
- if hr.Status.HelmChart != "" && hr.Status.HelmChart != chartName.String() {
- if err := r.deleteHelmChart(ctx, hr); err != nil {
- return nil, err
- }
- }
-
- // Continue with the reconciliation of the current template.
- var helmChart sourcev1b2.HelmChart
- err := r.Client.Get(ctx, chartName, &helmChart)
- if err != nil && !apierrors.IsNotFound(err) {
- return nil, err
- }
- hc := buildHelmChartFromTemplate(hr)
- switch {
- case apierrors.IsNotFound(err):
- if err = r.Client.Create(ctx, hc); err != nil {
- return nil, err
- }
- hr.Status.HelmChart = chartName.String()
- return hc, nil
- case helmChartRequiresUpdate(hr, &helmChart):
- ctrl.LoggerFrom(ctx).Info("chart diverged from template", strings.ToLower(sourcev1b2.HelmChartKind), chartName.String())
- helmChart.Spec = hc.Spec
- helmChart.Labels = hc.Labels
- helmChart.Annotations = hc.Annotations
-
- if err = r.Client.Update(ctx, &helmChart); err != nil {
- return nil, err
- }
- hr.Status.HelmChart = chartName.String()
- }
- return &helmChart, nil
-}
-
-// getHelmChart retrieves the v1beta2.HelmChart for the given
-// v2beta1.HelmRelease using the name that is advertised in the status
-// object. It returns the v1beta2.HelmChart, or an error.
-func (r *HelmReleaseReconciler) getHelmChart(ctx context.Context, hr *v2.HelmRelease) (*sourcev1b2.HelmChart, error) {
- namespace, name := hr.Status.GetHelmChart()
- hc := &sourcev1b2.HelmChart{}
- if err := r.Client.Get(ctx, types.NamespacedName{Namespace: namespace, Name: name}, hc); err != nil {
- return nil, err
- }
- return hc, nil
-}
-
-// loadHelmChart attempts to download the artifact from the provided source,
-// loads it into a chart.Chart, and removes the downloaded artifact.
-// It returns the loaded chart.Chart on success, or an error.
-func (r *HelmReleaseReconciler) loadHelmChart(source *sourcev1b2.HelmChart) (*chart.Chart, error) {
- artifact := source.GetArtifact()
- if artifact == nil {
- return nil, fmt.Errorf("cannot load chart: HelmChart '%s/%s' has no artifact", source.GetNamespace(), source.GetName())
- }
-
- f, err := os.CreateTemp("", fmt.Sprintf("%s-%s-*.tgz", source.GetNamespace(), source.GetName()))
- if err != nil {
- return nil, err
- }
- defer f.Close()
- defer os.Remove(f.Name())
-
- artifactURL := artifact.URL
- if hostname := os.Getenv("SOURCE_CONTROLLER_LOCALHOST"); hostname != "" {
- u, err := url.Parse(artifactURL)
- if err != nil {
- return nil, err
- }
- u.Host = hostname
- artifactURL = u.String()
- }
-
- req, err := retryablehttp.NewRequest(http.MethodGet, artifactURL, nil)
- if err != nil {
- return nil, fmt.Errorf("failed to create a new request: %w", err)
- }
-
- resp, err := r.httpClient.Do(req)
- if err != nil {
- return nil, fmt.Errorf("failed to download artifact, error: %w", err)
- }
- defer resp.Body.Close()
-
- if resp.StatusCode != http.StatusOK {
- return nil, fmt.Errorf("artifact '%s' download failed (status code: %s)", source.GetArtifact().URL, resp.Status)
- }
-
- // verify checksum matches origin
- if err := r.copyAndVerifyArtifact(source.GetArtifact(), resp.Body, f); err != nil {
- return nil, err
- }
-
- return loader.Load(f.Name())
-}
-
-func (r *HelmReleaseReconciler) copyAndVerifyArtifact(artifact *sourcev1.Artifact, reader io.Reader, writer io.Writer) error {
- dig, err := digest.Parse(artifact.Digest)
- if err != nil {
- return fmt.Errorf("failed to verify artifact: %w", err)
- }
-
- // Verify the downloaded artifact against the advertised digest.
- verifier := dig.Verifier()
- mw := io.MultiWriter(verifier, writer)
- if _, err := io.Copy(mw, reader); err != nil {
- return err
- }
-
- if !verifier.Verified() {
- return fmt.Errorf("failed to verify artifact: computed digest doesn't match advertised '%s'", dig)
- }
- return nil
-}
-
-// deleteHelmChart deletes the v1beta2.HelmChart of the v2beta1.HelmRelease.
-func (r *HelmReleaseReconciler) deleteHelmChart(ctx context.Context, hr *v2.HelmRelease) error {
- if hr.Status.HelmChart == "" {
- return nil
- }
- var hc sourcev1b2.HelmChart
- chartNS, chartName := hr.Status.GetHelmChart()
- err := r.Client.Get(ctx, types.NamespacedName{Namespace: chartNS, Name: chartName}, &hc)
- if err != nil {
- if apierrors.IsNotFound(err) {
- hr.Status.HelmChart = ""
- return nil
- }
- err = fmt.Errorf("failed to delete HelmChart '%s': %w", hr.Status.HelmChart, err)
- return err
- }
- if err = r.Client.Delete(ctx, &hc); err != nil {
- err = fmt.Errorf("failed to delete HelmChart '%s': %w", hr.Status.HelmChart, err)
- return err
- }
- // Truncate the chart reference in the status object.
- hr.Status.HelmChart = ""
- return nil
-}
-
-// buildHelmChartFromTemplate builds a v1beta2.HelmChart from the
-// v2beta1.HelmChartTemplate of the given v2beta1.HelmRelease.
-func buildHelmChartFromTemplate(hr *v2.HelmRelease) *sourcev1b2.HelmChart {
- template := hr.Spec.Chart
- result := &sourcev1b2.HelmChart{
- ObjectMeta: metav1.ObjectMeta{
- Name: hr.GetHelmChartName(),
- Namespace: hr.Spec.Chart.GetNamespace(hr.Namespace),
- },
- Spec: sourcev1b2.HelmChartSpec{
- Chart: template.Spec.Chart,
- Version: template.Spec.Version,
- SourceRef: sourcev1b2.LocalHelmChartSourceReference{
- Name: template.Spec.SourceRef.Name,
- Kind: template.Spec.SourceRef.Kind,
- },
- Interval: template.GetInterval(hr.Spec.Interval),
- ReconcileStrategy: template.Spec.ReconcileStrategy,
- ValuesFiles: template.Spec.ValuesFiles,
- ValuesFile: template.Spec.ValuesFile,
- Verify: templateVerificationToSourceVerification(template.Spec.Verify),
- },
- }
- if hr.Spec.Chart.ObjectMeta != nil {
- result.ObjectMeta.Labels = hr.Spec.Chart.ObjectMeta.Labels
- result.ObjectMeta.Annotations = hr.Spec.Chart.ObjectMeta.Annotations
- }
- return result
-}
-
-// helmChartRequiresUpdate compares the v2beta1.HelmChartTemplate of the
-// v2beta1.HelmRelease to the given v1beta2.HelmChart to determine if an
-// update is required.
-func helmChartRequiresUpdate(hr *v2.HelmRelease, chart *sourcev1b2.HelmChart) bool {
- template := hr.Spec.Chart
- switch {
- case template.Spec.Chart != chart.Spec.Chart:
- return true
- // TODO(hidde): remove emptiness checks on next MINOR version
- case template.Spec.Version == "" && chart.Spec.Version != "*",
- template.Spec.Version != "" && template.Spec.Version != chart.Spec.Version:
- return true
- case template.Spec.SourceRef.Name != chart.Spec.SourceRef.Name:
- return true
- case template.Spec.SourceRef.Kind != chart.Spec.SourceRef.Kind:
- return true
- case template.GetInterval(hr.Spec.Interval) != chart.Spec.Interval:
- return true
- case template.Spec.ReconcileStrategy != chart.Spec.ReconcileStrategy:
- return true
- case !reflect.DeepEqual(template.Spec.ValuesFiles, chart.Spec.ValuesFiles):
- return true
- case template.Spec.ValuesFile != chart.Spec.ValuesFile:
- return true
- case template.ObjectMeta != nil && !apiequality.Semantic.DeepEqual(template.ObjectMeta.Annotations, chart.Annotations):
- return true
- case template.ObjectMeta != nil && !apiequality.Semantic.DeepEqual(template.ObjectMeta.Labels, chart.Labels):
- return true
- case !reflect.DeepEqual(templateVerificationToSourceVerification(template.Spec.Verify), chart.Spec.Verify):
- return true
- default:
- return false
- }
-}
-
-// templateVerificationToSourceVerification converts the HelmChartTemplateVerification to the OCIRepositoryVerification.
-func templateVerificationToSourceVerification(template *v2.HelmChartTemplateVerification) *sourcev1b2.OCIRepositoryVerification {
- if template == nil {
- return nil
- }
-
- return &sourcev1b2.OCIRepositoryVerification{
- Provider: template.Provider,
- SecretRef: template.SecretRef,
- }
-}
diff --git a/internal/controller/helmrelease_controller_chart_test.go b/internal/controller/helmrelease_controller_chart_test.go
deleted file mode 100644
index 3511679bc..000000000
--- a/internal/controller/helmrelease_controller_chart_test.go
+++ /dev/null
@@ -1,547 +0,0 @@
-/*
-Copyright 2020 The Flux authors
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package controller
-
-import (
- "context"
- "fmt"
- "testing"
- "time"
-
- "github.com/fluxcd/pkg/apis/meta"
- sourcev1 "github.com/fluxcd/source-controller/api/v1beta2"
- "github.com/go-logr/logr"
- . "github.com/onsi/gomega"
- apierrors "k8s.io/apimachinery/pkg/api/errors"
- metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
- "k8s.io/client-go/kubernetes/scheme"
- "sigs.k8s.io/controller-runtime/pkg/client"
- "sigs.k8s.io/controller-runtime/pkg/client/fake"
-
- v2 "github.com/fluxcd/helm-controller/api/v2beta1"
-)
-
-func TestHelmReleaseReconciler_reconcileChart(t *testing.T) {
- tests := []struct {
- name string
- hr *v2.HelmRelease
- hc *sourcev1.HelmChart
- expectHelmChartStatus string
- expectGC bool
- expectErr bool
- noCrossNamspaceRef bool
- }{
- {
- name: "new HelmChart",
- hr: &v2.HelmRelease{
- ObjectMeta: metav1.ObjectMeta{
- Name: "test-release",
- Namespace: "default",
- },
- Spec: v2.HelmReleaseSpec{
- Interval: metav1.Duration{Duration: time.Minute},
- Chart: v2.HelmChartTemplate{
- Spec: v2.HelmChartTemplateSpec{
- Chart: "chart",
- SourceRef: v2.CrossNamespaceObjectReference{
- Name: "test-repository",
- Kind: "HelmRepository",
- },
- },
- },
- },
- },
- hc: nil,
- expectHelmChartStatus: "default/default-test-release",
- },
- {
- name: "existing HelmChart",
- hr: &v2.HelmRelease{
- ObjectMeta: metav1.ObjectMeta{
- Name: "test-release",
- Namespace: "default",
- },
- Spec: v2.HelmReleaseSpec{
- Interval: metav1.Duration{Duration: time.Minute},
- Chart: v2.HelmChartTemplate{
- Spec: v2.HelmChartTemplateSpec{
- Chart: "chart",
- SourceRef: v2.CrossNamespaceObjectReference{
- Name: "test-repository",
- Kind: "HelmRepository",
- },
- },
- },
- },
- },
- hc: &sourcev1.HelmChart{
- ObjectMeta: metav1.ObjectMeta{
- Name: "default-test-release",
- Namespace: "default",
- },
- Spec: sourcev1.HelmChartSpec{
- Chart: "chart",
- SourceRef: sourcev1.LocalHelmChartSourceReference{
- Name: "test-repository",
- Kind: "HelmRepository",
- },
- },
- },
- expectHelmChartStatus: "default/default-test-release",
- },
- {
- name: "modified HelmChart",
- hr: &v2.HelmRelease{
- ObjectMeta: metav1.ObjectMeta{
- Name: "test-release",
- Namespace: "default",
- },
- Spec: v2.HelmReleaseSpec{
- Interval: metav1.Duration{Duration: time.Minute},
- Chart: v2.HelmChartTemplate{
- Spec: v2.HelmChartTemplateSpec{
- Chart: "chart",
- SourceRef: v2.CrossNamespaceObjectReference{
- Name: "test-repository",
- Kind: "HelmRepository",
- Namespace: "cross",
- },
- },
- },
- },
- Status: v2.HelmReleaseStatus{
- HelmChart: "default/default-test-release",
- },
- },
- hc: &sourcev1.HelmChart{
- ObjectMeta: metav1.ObjectMeta{
- Name: "default-test-release",
- Namespace: "default",
- },
- Spec: sourcev1.HelmChartSpec{
- Chart: "chart",
- SourceRef: sourcev1.LocalHelmChartSourceReference{
- Name: "test-repository",
- Kind: "HelmRepository",
- },
- },
- },
- expectHelmChartStatus: "cross/default-test-release",
- expectGC: true,
- },
- {
- name: "block cross namespace access when flag is set",
- hr: &v2.HelmRelease{
- ObjectMeta: metav1.ObjectMeta{
- Name: "test-release",
- Namespace: "default",
- },
- Spec: v2.HelmReleaseSpec{
- Interval: metav1.Duration{Duration: time.Minute},
- Chart: v2.HelmChartTemplate{
- Spec: v2.HelmChartTemplateSpec{
- Chart: "chart",
- SourceRef: v2.CrossNamespaceObjectReference{
- Name: "test-repository",
- Kind: "HelmRepository",
- Namespace: "cross",
- },
- },
- },
- },
- Status: v2.HelmReleaseStatus{
- HelmChart: "",
- },
- },
- noCrossNamspaceRef: true,
- expectErr: true,
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- g := NewWithT(t)
-
- g.Expect(v2.AddToScheme(scheme.Scheme)).To(Succeed())
- g.Expect(sourcev1.AddToScheme(scheme.Scheme)).To(Succeed())
-
- var c client.Client
- if tt.hc != nil {
- c = fake.NewFakeClientWithScheme(scheme.Scheme, tt.hc)
- } else {
- c = fake.NewFakeClientWithScheme(scheme.Scheme)
- }
-
- r := &HelmReleaseReconciler{
- Client: c,
- NoCrossNamespaceRef: tt.noCrossNamspaceRef,
- }
-
- hc, err := r.reconcileChart(logr.NewContext(context.TODO(), logr.Discard()), tt.hr)
- if tt.expectErr {
- g.Expect(err).To(HaveOccurred())
- g.Expect(hc).To(BeNil())
- } else {
- g.Expect(err).NotTo(HaveOccurred())
- g.Expect(hc).NotTo(BeNil())
- }
-
- g.Expect(tt.hr.Status.HelmChart).To(Equal(tt.expectHelmChartStatus))
-
- if tt.expectGC {
- objKey := client.ObjectKeyFromObject(tt.hc)
- err = c.Get(context.TODO(), objKey, tt.hc.DeepCopy())
- g.Expect(apierrors.IsNotFound(err)).To(BeTrue())
- }
- })
- }
-}
-
-func TestHelmReleaseReconciler_deleteHelmChart(t *testing.T) {
- tests := []struct {
- name string
- hc *sourcev1.HelmChart
- hr *v2.HelmRelease
- expectHelmChartStatus string
- expectErr bool
- }{
- {
- name: "delete existing HelmChart",
- hc: &sourcev1.HelmChart{
- ObjectMeta: metav1.ObjectMeta{
- Name: "test-chart",
- Namespace: "default",
- },
- },
- hr: &v2.HelmRelease{
- ObjectMeta: metav1.ObjectMeta{
- Name: "test-release",
- },
- Status: v2.HelmReleaseStatus{
- HelmChart: "default/test-chart",
- },
- },
- expectHelmChartStatus: "",
- expectErr: false,
- },
- {
- name: "delete already removed HelmChart",
- hc: nil,
- hr: &v2.HelmRelease{
- ObjectMeta: metav1.ObjectMeta{
- Name: "test-release",
- },
- Status: v2.HelmReleaseStatus{
- HelmChart: "default/test-chart",
- },
- },
- expectHelmChartStatus: "",
- expectErr: false,
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- g := NewWithT(t)
-
- g.Expect(v2.AddToScheme(scheme.Scheme)).To(Succeed())
- g.Expect(sourcev1.AddToScheme(scheme.Scheme)).To(Succeed())
-
- var c client.Client
- if tt.hc != nil {
- c = fake.NewFakeClientWithScheme(scheme.Scheme, tt.hc)
- } else {
- c = fake.NewFakeClientWithScheme(scheme.Scheme)
- }
-
- r := &HelmReleaseReconciler{
- Client: c,
- }
-
- err := r.deleteHelmChart(context.TODO(), tt.hr)
- if tt.expectErr {
- g.Expect(err).To(HaveOccurred())
- } else {
- g.Expect(err).NotTo(HaveOccurred())
- }
- g.Expect(tt.hr.Status.HelmChart).To(Equal(tt.expectHelmChartStatus))
- })
- }
-}
-
-func Test_buildHelmChartFromTemplate(t *testing.T) {
- hrWithChartTemplate := v2.HelmRelease{
- ObjectMeta: metav1.ObjectMeta{
- Name: "test-release",
- Namespace: "default",
- },
- Spec: v2.HelmReleaseSpec{
- Interval: metav1.Duration{Duration: time.Minute},
- Chart: v2.HelmChartTemplate{
- Spec: v2.HelmChartTemplateSpec{
- Chart: "chart",
- Version: "1.0.0",
- SourceRef: v2.CrossNamespaceObjectReference{
- Name: "test-repository",
- Kind: "HelmRepository",
- },
- Interval: &metav1.Duration{Duration: 2 * time.Minute},
- ValuesFiles: []string{"values.yaml"},
- },
- },
- },
- }
-
- tests := []struct {
- name string
- modify func(release *v2.HelmRelease)
- want *sourcev1.HelmChart
- }{
- {
- name: "builds HelmChart from HelmChartTemplate",
- modify: func(*v2.HelmRelease) {},
- want: &sourcev1.HelmChart{
- ObjectMeta: metav1.ObjectMeta{
- Name: "default-test-release",
- Namespace: "default",
- },
- Spec: sourcev1.HelmChartSpec{
- Chart: "chart",
- Version: "1.0.0",
- SourceRef: sourcev1.LocalHelmChartSourceReference{
- Name: "test-repository",
- Kind: "HelmRepository",
- },
- Interval: metav1.Duration{Duration: 2 * time.Minute},
- ValuesFiles: []string{"values.yaml"},
- },
- },
- },
- {
- name: "takes SourceRef namespace into account",
- modify: func(hr *v2.HelmRelease) {
- hr.Spec.Chart.Spec.SourceRef.Namespace = "cross"
- },
- want: &sourcev1.HelmChart{
- ObjectMeta: metav1.ObjectMeta{
- Name: "default-test-release",
- Namespace: "cross",
- },
- Spec: sourcev1.HelmChartSpec{
- Chart: "chart",
- Version: "1.0.0",
- SourceRef: sourcev1.LocalHelmChartSourceReference{
- Name: "test-repository",
- Kind: "HelmRepository",
- },
- Interval: metav1.Duration{Duration: 2 * time.Minute},
- ValuesFiles: []string{"values.yaml"},
- },
- },
- },
- {
- name: "falls back to HelmRelease interval",
- modify: func(hr *v2.HelmRelease) {
- hr.Spec.Chart.Spec.Interval = nil
- },
- want: &sourcev1.HelmChart{
- ObjectMeta: metav1.ObjectMeta{
- Name: "default-test-release",
- Namespace: "default",
- },
- Spec: sourcev1.HelmChartSpec{
- Chart: "chart",
- Version: "1.0.0",
- SourceRef: sourcev1.LocalHelmChartSourceReference{
- Name: "test-repository",
- Kind: "HelmRepository",
- },
- Interval: metav1.Duration{Duration: time.Minute},
- ValuesFiles: []string{"values.yaml"},
- },
- },
- },
- {
- name: "take cosign verification into account",
- modify: func(hr *v2.HelmRelease) {
- hr.Spec.Chart.Spec.Verify = &v2.HelmChartTemplateVerification{
- Provider: "cosign",
- SecretRef: &meta.LocalObjectReference{
- Name: "cosign-key",
- },
- }
- },
- want: &sourcev1.HelmChart{
- ObjectMeta: metav1.ObjectMeta{
- Name: "default-test-release",
- Namespace: "default",
- },
- Spec: sourcev1.HelmChartSpec{
- Chart: "chart",
- Version: "1.0.0",
- SourceRef: sourcev1.LocalHelmChartSourceReference{
- Name: "test-repository",
- Kind: "HelmRepository",
- },
- Interval: metav1.Duration{Duration: 2 * time.Minute},
- ValuesFiles: []string{"values.yaml"},
- Verify: &sourcev1.OCIRepositoryVerification{
- Provider: "cosign",
- SecretRef: &meta.LocalObjectReference{
- Name: "cosign-key",
- },
- },
- },
- },
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- g := NewWithT(t)
- hr := hrWithChartTemplate.DeepCopy()
- tt.modify(hr)
- g.Expect(buildHelmChartFromTemplate(hr)).To(Equal(tt.want))
- })
- }
-}
-
-func Test_helmChartRequiresUpdate(t *testing.T) {
- hrWithChartTemplate := v2.HelmRelease{
- ObjectMeta: metav1.ObjectMeta{
- Name: "test-release",
- },
- Spec: v2.HelmReleaseSpec{
- Interval: metav1.Duration{Duration: time.Minute},
- Chart: v2.HelmChartTemplate{
- Spec: v2.HelmChartTemplateSpec{
- Chart: "chart",
- Version: "1.0.0",
- SourceRef: v2.CrossNamespaceObjectReference{
- Name: "test-repository",
- Kind: "HelmRepository",
- },
- Interval: &metav1.Duration{Duration: 2 * time.Minute},
- Verify: &v2.HelmChartTemplateVerification{
- Provider: "cosign",
- },
- },
- },
- },
- }
-
- tests := []struct {
- name string
- modify func(*v2.HelmRelease, *sourcev1.HelmChart)
- want bool
- }{
- {
- name: "detects no change",
- modify: func(*v2.HelmRelease, *sourcev1.HelmChart) {},
- want: false,
- },
- {
- name: "detects chart change",
- modify: func(hr *v2.HelmRelease, hc *sourcev1.HelmChart) {
- hr.Spec.Chart.Spec.Chart = "new"
- },
- want: true,
- },
- {
- name: "detects version change",
- modify: func(hr *v2.HelmRelease, hc *sourcev1.HelmChart) {
- hr.Spec.Chart.Spec.Version = "2.0.0"
- },
- want: true,
- },
- {
- name: "detects chart source name change",
- modify: func(hr *v2.HelmRelease, hc *sourcev1.HelmChart) {
- hr.Spec.Chart.Spec.SourceRef.Name = "new"
- },
- want: true,
- },
- {
- name: "detects chart source kind change",
- modify: func(hr *v2.HelmRelease, hc *sourcev1.HelmChart) {
- hr.Spec.Chart.Spec.SourceRef.Kind = "GitRepository"
- },
- want: true,
- },
- {
- name: "detects interval change",
- modify: func(hr *v2.HelmRelease, hc *sourcev1.HelmChart) {
- hr.Spec.Chart.Spec.Interval = nil
- },
- want: true,
- },
- {
- name: "detects reconcile strategy change",
- modify: func(hr *v2.HelmRelease, hc *sourcev1.HelmChart) {
- hr.Spec.Chart.Spec.ReconcileStrategy = "Revision"
- },
- want: true,
- },
- {
- name: "detects values files change",
- modify: func(hr *v2.HelmRelease, hc *sourcev1.HelmChart) {
- hr.Spec.Chart.Spec.ValuesFiles = []string{"values-prod.yaml"}
- },
- want: true,
- },
- {
- name: "detects values file change",
- modify: func(hr *v2.HelmRelease, hc *sourcev1.HelmChart) {
- hr.Spec.Chart.Spec.ValuesFile = "values-prod.yaml"
- },
- want: true,
- },
- {
- name: "detects verify change",
- modify: func(hr *v2.HelmRelease, hc *sourcev1.HelmChart) {
- hr.Spec.Chart.Spec.Verify.Provider = "foo-bar"
- },
- want: true,
- },
- {
- name: "detects labels change",
- modify: func(hr *v2.HelmRelease, hc *sourcev1.HelmChart) {
- hr.Spec.Chart.ObjectMeta = &v2.HelmChartTemplateObjectMeta{Labels: map[string]string{"foo": "bar"}}
- },
- want: true,
- },
- {
- name: "detects annotations change",
- modify: func(hr *v2.HelmRelease, hc *sourcev1.HelmChart) {
- hr.Spec.Chart.ObjectMeta = &v2.HelmChartTemplateObjectMeta{Annotations: map[string]string{"foo": "bar"}}
- },
- want: true,
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- g := NewWithT(t)
-
- hr := hrWithChartTemplate.DeepCopy()
- hc := buildHelmChartFromTemplate(hr)
- // second copy to avoid modifying the original
- hr = hrWithChartTemplate.DeepCopy()
- g.Expect(helmChartRequiresUpdate(hr, hc)).To(Equal(false))
-
- tt.modify(hr, hc)
- fmt.Println("verify", hr.Spec.Chart.Spec.Verify.Provider, hc.Spec.Verify.Provider)
- g.Expect(helmChartRequiresUpdate(hr, hc)).To(Equal(tt.want))
- })
- }
-}
diff --git a/internal/controller/helmrelease_controller_fuzz_test.go b/internal/controller/helmrelease_controller_fuzz_test.go
index b431c0714..b772f381e 100644
--- a/internal/controller/helmrelease_controller_fuzz_test.go
+++ b/internal/controller/helmrelease_controller_fuzz_test.go
@@ -28,151 +28,15 @@ import (
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
+ "sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
"sigs.k8s.io/yaml"
- v2 "github.com/fluxcd/helm-controller/api/v2beta1"
- sourcev1 "github.com/fluxcd/source-controller/api/v1beta2"
-)
-
-func FuzzHelmReleaseReconciler_composeValues(f *testing.F) {
- scheme := testScheme()
+ "github.com/fluxcd/pkg/runtime/patch"
+ sourcev1 "github.com/fluxcd/source-controller/api/v1"
- tests := []struct {
- targetPath string
- valuesKey string
- hrValues string
- createObject bool
- secretData []byte
- configData string
- }{
- {
- targetPath: "flat",
- valuesKey: "custom-values.yaml",
- secretData: []byte(`flat:
- nested: value
-nested: value
-`),
- configData: `flat: value
-nested:
- configuration: value
-`,
- hrValues: `
-other: values
-`,
- createObject: true,
- },
- {
- targetPath: "'flat'",
- valuesKey: "custom-values.yaml",
- secretData: []byte(`flat:
- nested: value
-nested: value
-`),
- configData: `flat: value
-nested:
- configuration: value
-`,
- hrValues: `
-other: values
-`,
- createObject: true,
- },
- {
- targetPath: "flat[0]",
- secretData: []byte(``),
- configData: `flat: value`,
- hrValues: `
-other: values
-`,
- createObject: true,
- },
- {
- secretData: []byte(`flat:
- nested: value
-nested: value
-`),
- configData: `flat: value
-nested:
- configuration: value
-`,
- hrValues: `
-other: values
-`,
- createObject: true,
- },
- {
- targetPath: "some-value",
- hrValues: `
-other: values
-`,
- createObject: false,
- },
- }
-
- for _, tt := range tests {
- f.Add(tt.targetPath, tt.valuesKey, tt.hrValues, tt.createObject, tt.secretData, tt.configData)
- }
-
- f.Fuzz(func(t *testing.T,
- targetPath, valuesKey, hrValues string, createObject bool, secretData []byte, configData string) {
-
- // objectName represents a core Kubernetes name (Secret/ConfigMap) which is validated
- // upstream, and also validated by us in the OpenAPI-based validation set in
- // v2.ValuesReference. Therefore a static value here suffices, and instead we just
- // play with the objects presence/absence.
- objectName := "values"
- resources := []runtime.Object{}
-
- if createObject {
- resources = append(resources,
- valuesConfigMap(objectName, map[string]string{valuesKey: configData}),
- valuesSecret(objectName, map[string][]byte{valuesKey: secretData}),
- )
- }
-
- references := []v2.ValuesReference{
- {
- Kind: "ConfigMap",
- Name: objectName,
- ValuesKey: valuesKey,
- TargetPath: targetPath,
- },
- {
- Kind: "Secret",
- Name: objectName,
- ValuesKey: valuesKey,
- TargetPath: targetPath,
- },
- }
-
- c := fake.NewFakeClientWithScheme(scheme, resources...)
- r := &HelmReleaseReconciler{Client: c}
- var values *apiextensionsv1.JSON
- if hrValues != "" {
- v, _ := yaml.YAMLToJSON([]byte(hrValues))
- values = &apiextensionsv1.JSON{Raw: v}
- }
-
- hr := v2.HelmRelease{
- Spec: v2.HelmReleaseSpec{
- ValuesFrom: references,
- Values: values,
- },
- }
-
- // OpenAPI-based validation on schema is not verified here.
- // Therefore some false positives may be arise, as the apiserver
- // would not allow such values to make their way into the control plane.
- //
- // Testenv could be used so the fuzzing covers the entire E2E.
- // The downsize being the resource and time cost per test would be a lot higher.
- //
- // Another approach could be to add validation to reject invalid inputs before
- // the r.composeValues call.
- _, _ = r.composeValues(logr.NewContext(context.TODO(), logr.Discard()), hr)
- })
-}
+ v2 "github.com/fluxcd/helm-controller/api/v2"
+)
func FuzzHelmReleaseReconciler_reconcile(f *testing.F) {
scheme := testScheme()
@@ -221,19 +85,18 @@ other: values
hc.ObjectMeta.Name = hr.GetHelmChartName()
hc.ObjectMeta.Namespace = hr.Spec.Chart.GetNamespace(hr.Namespace)
- resources := []runtime.Object{
+ resources := []client.Object{
valuesConfigMap("values", map[string]string{valuesKey: configData}),
valuesSecret("values", map[string][]byte{valuesKey: secretData}),
&hc,
}
- c := fake.NewFakeClientWithScheme(scheme, resources...)
+ c := fake.NewClientBuilder().WithScheme(scheme).WithStatusSubresource(&v2.HelmRelease{}).WithObjects(resources...).Build()
r := &HelmReleaseReconciler{
Client: c,
EventRecorder: &DummyRecorder{},
}
-
- _, _, _ = r.reconcile(logr.NewContext(context.TODO(), logr.Discard()), hr)
+ _, _ = r.reconcileRelease(logr.NewContext(context.TODO(), logr.Discard()), patch.NewSerialPatcher(&hr, c), &hr)
})
}
@@ -265,9 +128,9 @@ type DummyRecorder struct{}
func (r *DummyRecorder) Event(object runtime.Object, eventtype, reason, message string) {
}
-func (r *DummyRecorder) Eventf(object runtime.Object, eventtype, reason, messageFmt string, args ...interface{}) {
+func (r *DummyRecorder) Eventf(object runtime.Object, eventtype, reason, messageFmt string, args ...any) {
}
func (r *DummyRecorder) AnnotatedEventf(object runtime.Object, annotations map[string]string,
- eventtype, reason string, messageFmt string, args ...interface{}) {
+ eventtype, reason string, messageFmt string, args ...any) {
}
diff --git a/internal/controller/helmrelease_controller_test.go b/internal/controller/helmrelease_controller_test.go
index 47f442b98..9e48fda8c 100644
--- a/internal/controller/helmrelease_controller_test.go
+++ b/internal/controller/helmrelease_controller_test.go
@@ -18,265 +18,3650 @@ package controller
import (
"context"
- "reflect"
+ "errors"
+ "fmt"
"strings"
"testing"
"time"
- "github.com/go-logr/logr"
- "helm.sh/helm/v3/pkg/chartutil"
+ . "github.com/onsi/gomega"
+ "github.com/opencontainers/go-digest"
+ chart "helm.sh/helm/v4/pkg/chart/v2"
+ helmreleasecommon "helm.sh/helm/v4/pkg/release/common"
+ helmrelease "helm.sh/helm/v4/pkg/release/v1"
+ helmstorage "helm.sh/helm/v4/pkg/storage"
+ helmdriver "helm.sh/helm/v4/pkg/storage/driver"
corev1 "k8s.io/api/core/v1"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
+ apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
- "k8s.io/apimachinery/pkg/runtime"
+ "k8s.io/apimachinery/pkg/types"
+ "k8s.io/cli-runtime/pkg/genericclioptions"
+ "k8s.io/client-go/rest"
+ "k8s.io/client-go/tools/clientcmd"
+ "k8s.io/client-go/tools/record"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
+ "sigs.k8s.io/controller-runtime/pkg/client/interceptor"
+ "sigs.k8s.io/controller-runtime/pkg/reconcile"
"sigs.k8s.io/yaml"
- v2 "github.com/fluxcd/helm-controller/api/v2beta1"
+ "github.com/fluxcd/pkg/apis/acl"
+ aclv1 "github.com/fluxcd/pkg/apis/acl"
+ "github.com/fluxcd/pkg/apis/kustomize"
+ "github.com/fluxcd/pkg/apis/meta"
+ "github.com/fluxcd/pkg/chartutil"
+ "github.com/fluxcd/pkg/runtime/conditions"
+ "github.com/fluxcd/pkg/runtime/patch"
+ sourcev1 "github.com/fluxcd/source-controller/api/v1"
+
+ v2 "github.com/fluxcd/helm-controller/api/v2"
+ intacl "github.com/fluxcd/helm-controller/internal/acl"
+ "github.com/fluxcd/helm-controller/internal/action"
+ "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"
)
-func TestHelmReleaseReconciler_composeValues(t *testing.T) {
- scheme := runtime.NewScheme()
- _ = corev1.AddToScheme(scheme)
- _ = v2.AddToScheme(scheme)
+func TestHelmReleaseReconciler_reconcileRelease(t *testing.T) {
+ t.Run("confirms dependencies are ready", func(t *testing.T) {
+ g := NewWithT(t)
+
+ dependency := &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "dependency",
+ Namespace: "mock",
+ Generation: 1,
+ },
+ Status: v2.HelmReleaseStatus{
+ Conditions: []metav1.Condition{
+ {
+ Type: meta.StalledCondition,
+ Status: metav1.ConditionTrue,
+ },
+ },
+ ObservedGeneration: 1,
+ },
+ }
+
+ obj := &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "dependant",
+ Namespace: "mock",
+ },
+ Spec: v2.HelmReleaseSpec{
+ DependsOn: []v2.DependencyReference{
+ {
+ Name: "dependency",
+ },
+ },
+ },
+ }
+
+ r := &HelmReleaseReconciler{
+ Client: fake.NewClientBuilder().
+ WithScheme(NewTestScheme()).
+ WithStatusSubresource(&v2.HelmRelease{}).
+ WithObjects(dependency, obj).
+ Build(),
+ EventRecorder: record.NewFakeRecorder(32),
+ DependencyRequeueInterval: 5 * time.Second,
+ }
+ r.APIReader = r.Client
+
+ res, err := r.reconcileRelease(context.TODO(), patch.NewSerialPatcher(obj, r.Client), obj, nil)
+ g.Expect(err).To(Equal(errWaitForDependency))
+ g.Expect(res.RequeueAfter).To(Equal(r.DependencyRequeueInterval))
+
+ g.Expect(obj.Status.Conditions).To(conditions.MatchConditions([]metav1.Condition{
+ *conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, ""),
+ *conditions.FalseCondition(meta.ReadyCondition, meta.DependencyNotReadyReason, "dependency 'mock/dependency' is not ready"),
+ }))
+ })
+
+ t.Run("handles HelmChart get failure", func(t *testing.T) {
+ g := NewWithT(t)
+
+ obj := &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "release",
+ Namespace: "mock",
+ },
+ Status: v2.HelmReleaseStatus{
+ HelmChart: "mock/chart",
+ },
+ }
+
+ r := &HelmReleaseReconciler{
+ Client: fake.NewClientBuilder().
+ WithScheme(NewTestScheme()).
+ WithStatusSubresource(&v2.HelmRelease{}).
+ WithObjects(obj).
+ Build(),
+ }
+ r.APIReader = r.Client
+
+ _, err := r.reconcileRelease(context.TODO(), patch.NewSerialPatcher(obj, r.Client), obj, nil)
+ g.Expect(err).To(HaveOccurred())
+
+ g.Expect(obj.Status.Conditions).To(conditions.MatchConditions([]metav1.Condition{
+ *conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "Fulfilling prerequisites"),
+ *conditions.FalseCondition(meta.ReadyCondition, v2.ArtifactFailedReason, "could not get Source object"),
+ }))
+ })
+
+ t.Run("handles ACL error for HelmChart", func(t *testing.T) {
+ g := NewWithT(t)
+
+ obj := &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "release",
+ Namespace: "mock",
+ },
+ Status: v2.HelmReleaseStatus{
+ HelmChart: "other/chart",
+ },
+ }
+
+ r := &HelmReleaseReconciler{
+ Client: fake.NewClientBuilder().
+ WithScheme(NewTestScheme()).
+ WithStatusSubresource(&v2.HelmRelease{}).
+ WithObjects(obj).
+ Build(),
+ EventRecorder: record.NewFakeRecorder(32),
+ }
+ r.APIReader = r.Client
+
+ res, err := r.reconcileRelease(context.TODO(), patch.NewSerialPatcher(obj, r.Client), obj, nil)
+ g.Expect(err).To(HaveOccurred())
+ g.Expect(errors.Is(err, reconcile.TerminalError(nil))).To(BeTrue())
+ g.Expect(res.IsZero()).To(BeTrue())
+
+ g.Expect(obj.Status.Conditions).To(conditions.MatchConditions([]metav1.Condition{
+ *conditions.TrueCondition(meta.StalledCondition, acl.AccessDeniedReason, "cross-namespace references are not allowed"),
+ *conditions.FalseCondition(meta.ReadyCondition, acl.AccessDeniedReason, "cross-namespace references are not allowed"),
+ }))
+ })
+
+ t.Run("waits for HelmChart to have an Artifact", func(t *testing.T) {
+ g := NewWithT(t)
+
+ chart := &sourcev1.HelmChart{
+ TypeMeta: metav1.TypeMeta{
+ APIVersion: sourcev1.GroupVersion.String(),
+ Kind: sourcev1.HelmChartKind,
+ },
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "chart",
+ Namespace: "mock",
+ Generation: 2,
+ },
+ Status: sourcev1.HelmChartStatus{
+ ObservedGeneration: 2,
+ Artifact: nil,
+ Conditions: []metav1.Condition{
+ {
+ Type: meta.ReadyCondition,
+ Status: metav1.ConditionTrue,
+ },
+ },
+ },
+ }
+
+ obj := &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "release",
+ Namespace: "mock",
+ },
+ Spec: v2.HelmReleaseSpec{
+ Interval: metav1.Duration{Duration: 1 * time.Second},
+ },
+ Status: v2.HelmReleaseStatus{
+ HelmChart: "mock/chart",
+ },
+ }
+
+ r := &HelmReleaseReconciler{
+ Client: fake.NewClientBuilder().
+ WithScheme(NewTestScheme()).
+ WithStatusSubresource(&v2.HelmRelease{}).
+ WithObjects(chart, obj).
+ Build(),
+ }
+
+ res, err := r.reconcileRelease(context.TODO(), patch.NewSerialPatcher(obj, r.Client), obj, nil)
+ g.Expect(err).To(Equal(errWaitForChart))
+ g.Expect(res.RequeueAfter).To(Equal(r.DependencyRequeueInterval))
+
+ g.Expect(obj.Status.Conditions).To(conditions.MatchConditions([]metav1.Condition{
+ *conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, ""),
+ *conditions.FalseCondition(meta.ReadyCondition, "SourceNotReady", "'mock/chart' is not ready"),
+ }))
+ })
+
+ t.Run("waits for HelmChart ObservedGeneration to equal Generation", func(t *testing.T) {
+ g := NewWithT(t)
+
+ chart := &sourcev1.HelmChart{
+ TypeMeta: metav1.TypeMeta{
+ APIVersion: sourcev1.GroupVersion.String(),
+ Kind: sourcev1.HelmChartKind,
+ },
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "chart",
+ Namespace: "mock",
+ Generation: 2,
+ },
+ Status: sourcev1.HelmChartStatus{
+ ObservedGeneration: 1,
+ Artifact: &meta.Artifact{},
+ Conditions: []metav1.Condition{
+ {
+ Type: meta.ReadyCondition,
+ Status: metav1.ConditionTrue,
+ },
+ },
+ },
+ }
+
+ obj := &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "release",
+ Namespace: "mock",
+ },
+ Spec: v2.HelmReleaseSpec{
+ Interval: metav1.Duration{Duration: 1 * time.Second},
+ },
+ Status: v2.HelmReleaseStatus{
+ HelmChart: "mock/chart",
+ },
+ }
+
+ r := &HelmReleaseReconciler{
+ Client: fake.NewClientBuilder().
+ WithScheme(NewTestScheme()).
+ WithStatusSubresource(&v2.HelmRelease{}).
+ WithObjects(chart, obj).
+ Build(),
+ }
+ r.APIReader = r.Client
+
+ res, err := r.reconcileRelease(context.TODO(), patch.NewSerialPatcher(obj, r.Client), obj, nil)
+ g.Expect(err).To(Equal(errWaitForChart))
+ g.Expect(res.RequeueAfter).To(Equal(r.DependencyRequeueInterval))
+
+ g.Expect(obj.Status.Conditions).To(conditions.MatchConditions([]metav1.Condition{
+ *conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, ""),
+ *conditions.FalseCondition(meta.ReadyCondition, "SourceNotReady", "'mock/chart' is not ready"),
+ }))
+ })
+
+ t.Run("reports values composition failure", func(t *testing.T) {
+ g := NewWithT(t)
+
+ chart := &sourcev1.HelmChart{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "chart",
+ Namespace: "mock",
+ Generation: 2,
+ },
+ Spec: sourcev1.HelmChartSpec{
+ Interval: metav1.Duration{Duration: 1 * time.Second},
+ },
+ Status: sourcev1.HelmChartStatus{
+ ObservedGeneration: 2,
+ Artifact: &meta.Artifact{},
+ Conditions: []metav1.Condition{
+ {
+ Type: meta.ReadyCondition,
+ Status: metav1.ConditionTrue,
+ },
+ },
+ },
+ }
+
+ obj := &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "release",
+ Namespace: "mock",
+ },
+ Spec: v2.HelmReleaseSpec{
+ ValuesFrom: []meta.ValuesReference{
+ {
+ Kind: "Secret",
+ Name: "missing",
+ },
+ },
+ },
+ Status: v2.HelmReleaseStatus{
+ HelmChart: "mock/chart",
+ },
+ }
+
+ r := &HelmReleaseReconciler{
+ Client: fake.NewClientBuilder().
+ WithScheme(NewTestScheme()).
+ WithStatusSubresource(&v2.HelmRelease{}).
+ WithObjects(chart, obj).
+ Build(),
+ EventRecorder: record.NewFakeRecorder(32),
+ }
+ r.APIReader = r.Client
+
+ _, err := r.reconcileRelease(context.TODO(), patch.NewSerialPatcher(obj, r.Client), obj, nil)
+ g.Expect(err).To(HaveOccurred())
+
+ g.Expect(obj.Status.Conditions).To(conditions.MatchConditions([]metav1.Condition{
+ *conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "Fulfilling prerequisites"),
+ *conditions.FalseCondition(meta.ReadyCondition, "ValuesError", "could not resolve Secret chart values reference 'mock/missing' with key 'values.yaml'"),
+ }))
+ })
+
+ t.Run("reports Helm chart load failure", func(t *testing.T) {
+ g := NewWithT(t)
+
+ chart := &sourcev1.HelmChart{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "chart",
+ Namespace: "mock",
+ Generation: 1,
+ },
+ Status: sourcev1.HelmChartStatus{
+ ObservedGeneration: 1,
+ Artifact: &meta.Artifact{
+ URL: testServer.URL() + "/does-not-exist",
+ },
+ Conditions: []metav1.Condition{
+ {
+ Type: meta.ReadyCondition,
+ Status: metav1.ConditionTrue,
+ },
+ },
+ },
+ }
+
+ obj := &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "release",
+ Namespace: "mock",
+ },
+ Status: v2.HelmReleaseStatus{
+ HelmChart: "mock/chart",
+ },
+ }
+
+ r := &HelmReleaseReconciler{
+ Client: fake.NewClientBuilder().
+ WithScheme(NewTestScheme()).
+ WithStatusSubresource(&v2.HelmRelease{}).
+ WithObjects(chart, obj).
+ Build(),
+ DependencyRequeueInterval: 10 * time.Second,
+ }
+ r.APIReader = r.Client
+
+ res, err := r.reconcileRelease(context.TODO(), patch.NewSerialPatcher(obj, r.Client), obj, nil)
+ g.Expect(err).To(Equal(errWaitForDependency))
+ g.Expect(res.RequeueAfter).To(Equal(r.DependencyRequeueInterval))
+
+ g.Expect(obj.Status.Conditions).To(conditions.MatchConditions([]metav1.Condition{
+ *conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, ""),
+ *conditions.FalseCondition(meta.ReadyCondition, v2.ArtifactFailedReason, "Source not ready"),
+ }))
+ })
+
+ t.Run("uninstalls HelmRelease if target has changed", func(t *testing.T) {
+ g := NewWithT(t)
+
+ chartMock := testutil.BuildChart()
+ chartArtifact, err := testutil.SaveChartAsArtifact(chartMock, digest.SHA256, testServer.URL(), testServer.Root())
+ g.Expect(err).ToNot(HaveOccurred())
+
+ chart := &sourcev1.HelmChart{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "chart",
+ Namespace: "mock",
+ Generation: 1,
+ },
+ Status: sourcev1.HelmChartStatus{
+ ObservedGeneration: 1,
+ Artifact: chartArtifact,
+ Conditions: []metav1.Condition{
+ {
+ Type: meta.ReadyCondition,
+ Status: metav1.ConditionTrue,
+ },
+ },
+ },
+ }
+
+ obj := &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "release",
+ Namespace: "mock",
+ },
+ Spec: v2.HelmReleaseSpec{
+ StorageNamespace: "other",
+ },
+ Status: v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ {
+ Name: "mock",
+ Namespace: "mock",
+ },
+ },
+ HelmChart: "mock/chart",
+ StorageNamespace: "mock",
+ },
+ }
+
+ c := fake.NewClientBuilder().
+ WithScheme(NewTestScheme()).
+ WithStatusSubresource(&v2.HelmRelease{}).
+ WithObjects(chart, obj).
+ Build()
+
+ r := &HelmReleaseReconciler{
+ Client: c,
+ APIReader: c,
+ GetClusterConfig: GetTestClusterConfig,
+ EventRecorder: record.NewFakeRecorder(32),
+ }
+
+ res, err := r.reconcileRelease(context.TODO(), patch.NewSerialPatcher(obj, c), obj, nil)
+ g.Expect(err).ToNot(HaveOccurred())
+ g.Expect(res.Requeue).To(BeTrue())
+
+ g.Expect(obj.Status.Conditions).To(conditions.MatchConditions([]metav1.Condition{
+ *conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, ""),
+ *conditions.FalseCondition(meta.ReadyCondition, v2.UninstallSucceededReason, "Release mock/mock.v0 was not found, assuming it is uninstalled"),
+ *conditions.FalseCondition(v2.ReleasedCondition, v2.UninstallSucceededReason, "Release mock/mock.v0 was not found, assuming it is uninstalled"),
+ }))
+
+ // Verify history and storage namespace are cleared.
+ g.Expect(obj.Status.History).To(BeNil())
+ g.Expect(obj.Status.StorageNamespace).To(BeEmpty())
+ })
+
+ t.Run("resets failure counts on configuration change", func(t *testing.T) {
+ g := NewWithT(t)
+
+ chartMock := testutil.BuildChart()
+ chartArtifact, err := testutil.SaveChartAsArtifact(chartMock, digest.SHA256, testServer.URL(), testServer.Root())
+ g.Expect(err).ToNot(HaveOccurred())
+
+ chart := &sourcev1.HelmChart{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "chart",
+ Namespace: "mock",
+ Generation: 1,
+ },
+ Status: sourcev1.HelmChartStatus{
+ ObservedGeneration: 1,
+ Artifact: chartArtifact,
+ Conditions: []metav1.Condition{
+ {
+ Type: meta.ReadyCondition,
+ Status: metav1.ConditionTrue,
+ },
+ },
+ },
+ }
+
+ obj := &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "release",
+ Namespace: "mock",
+ Generation: 2,
+ },
+ Spec: v2.HelmReleaseSpec{
+ // Trigger a failure by setting an invalid storage namespace,
+ // preventing the release from actually being installed.
+ // This allows us to just test the failure count reset, without
+ // having to facilitate a full install.
+ StorageNamespace: "not-exist",
+ },
+ Status: v2.HelmReleaseStatus{
+ HelmChart: "mock/chart",
+ InstallFailures: 2,
+ UpgradeFailures: 3,
+ Failures: 5,
+ // Trigger actual failure reset due to change in spec.
+ LastAttemptedGeneration: 1,
+ },
+ }
+
+ c := fake.NewClientBuilder().
+ WithScheme(NewTestScheme()).
+ WithStatusSubresource(&v2.HelmRelease{}).
+ WithObjects(chart, obj).
+ Build()
+
+ r := &HelmReleaseReconciler{
+ Client: c,
+ APIReader: c,
+ GetClusterConfig: GetTestClusterConfig,
+ EventRecorder: record.NewFakeRecorder(32),
+ }
+
+ _, err = r.reconcileRelease(context.TODO(), patch.NewSerialPatcher(obj, c), obj, nil)
+ g.Expect(err).To(HaveOccurred())
+ g.Expect(err.Error()).To(ContainSubstring("namespaces \"not-exist\" not found"))
+
+ // Verify failure counts are reset.
+ g.Expect(obj.Status.InstallFailures).To(Equal(int64(0)))
+ g.Expect(obj.Status.UpgradeFailures).To(Equal(int64(0)))
+ g.Expect(obj.Status.Failures).To(Equal(int64(1)))
+ })
+
+ t.Run("uses retry interval when the error ErrRetryAfterInterval is returned", func(t *testing.T) {
+ g := NewWithT(t)
+
+ chartMock := testutil.BuildChart()
+ chartArtifact, err := testutil.SaveChartAsArtifact(chartMock, digest.SHA256, testServer.URL(), testServer.Root())
+ g.Expect(err).ToNot(HaveOccurred())
+
+ chart := &sourcev1.HelmChart{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "chart",
+ Namespace: "mock",
+ Generation: 1,
+ },
+ Status: sourcev1.HelmChartStatus{
+ ObservedGeneration: 1,
+ Artifact: chartArtifact,
+ Conditions: []metav1.Condition{
+ {
+ Type: meta.ReadyCondition,
+ Status: metav1.ConditionTrue,
+ },
+ },
+ },
+ }
+
+ obj := &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "release",
+ Namespace: "mock",
+ Generation: 1,
+ },
+ Spec: v2.HelmReleaseSpec{
+ // Trigger a failure by setting an invalid storage namespace,
+ // preventing the release from actually being installed.
+ // This allows us to just test the RetryOnFailure strategy, without
+ // having to facilitate a full install.
+ StorageNamespace: "not-exist",
+ Install: &v2.Install{
+ Strategy: &v2.InstallStrategy{
+ Name: "RetryOnFailure",
+ RetryInterval: &metav1.Duration{Duration: time.Minute},
+ },
+ },
+ },
+ Status: v2.HelmReleaseStatus{
+ HelmChart: "mock/chart",
+ ObservedGeneration: 1,
+ },
+ }
+
+ c := fake.NewClientBuilder().
+ WithScheme(NewTestScheme()).
+ WithStatusSubresource(&v2.HelmRelease{}).
+ WithObjects(chart, obj).
+ Build()
+
+ r := &HelmReleaseReconciler{
+ Client: c,
+ APIReader: c,
+ GetClusterConfig: GetTestClusterConfig,
+ EventRecorder: record.NewFakeRecorder(32),
+ }
+
+ res, err := r.reconcileRelease(context.TODO(), patch.NewSerialPatcher(obj, c), obj, nil)
+ g.Expect(err).NotTo(HaveOccurred())
+ g.Expect(res.RequeueAfter).To(BeNumerically("==", time.Minute))
+
+ g.Expect(obj.Status.Conditions).To(conditions.MatchConditions([]metav1.Condition{
+ {
+ Type: "Reconciling",
+ Status: "True",
+ Reason: "ProgressingWithRetry",
+ Message: "retrying after 1m0s",
+ },
+ {
+ Type: "Ready",
+ Status: "False",
+ Reason: "InstallFailed",
+ Message: "Helm install failed for release mock/release with chart hello@0.1.0: create: failed to create: namespaces \"not-exist\" not found",
+ },
+ {
+ Type: "Released",
+ Status: "False",
+ Reason: "InstallFailed",
+ Message: "Helm install failed for release mock/release with chart hello@0.1.0: create: failed to create: namespaces \"not-exist\" not found",
+ },
+ }))
+ })
+
+ t.Run("sets last attempted values", func(t *testing.T) {
+ g := NewWithT(t)
+
+ chartMock := testutil.BuildChart()
+ chartArtifact, err := testutil.SaveChartAsArtifact(chartMock, digest.SHA256, testServer.URL(), testServer.Root())
+ g.Expect(err).ToNot(HaveOccurred())
+
+ chart := &sourcev1.HelmChart{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "chart",
+ Namespace: "mock",
+ Generation: 1,
+ },
+ Status: sourcev1.HelmChartStatus{
+ ObservedGeneration: 1,
+ Artifact: chartArtifact,
+ Conditions: []metav1.Condition{
+ {
+ Type: meta.ReadyCondition,
+ Status: metav1.ConditionTrue,
+ },
+ },
+ },
+ }
+
+ obj := &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "release",
+ Namespace: "mock",
+ Generation: 2,
+ },
+ Spec: v2.HelmReleaseSpec{
+ // Trigger a failure by setting an invalid storage namespace,
+ // preventing the release from actually being installed.
+ // This allows us to just test the values being set, without
+ // having to facilitate a full install.
+ StorageNamespace: "not-exist",
+ Values: &apiextensionsv1.JSON{
+ Raw: []byte(`{"foo":"bar"}`),
+ },
+ },
+ Status: v2.HelmReleaseStatus{
+ HelmChart: "mock/chart",
+ ObservedGeneration: 2,
+ // Confirm deprecated value is cleared.
+ LastAttemptedValuesChecksum: "b5cbcf5c23cfd945d2cdf0ffaab387a46f2d054f",
+ },
+ }
+
+ c := fake.NewClientBuilder().
+ WithScheme(NewTestScheme()).
+ WithStatusSubresource(&v2.HelmRelease{}).
+ WithObjects(chart, obj).
+ Build()
+
+ r := &HelmReleaseReconciler{
+ Client: c,
+ APIReader: c,
+ GetClusterConfig: GetTestClusterConfig,
+ EventRecorder: record.NewFakeRecorder(32),
+ }
+
+ _, err = r.reconcileRelease(context.TODO(), patch.NewSerialPatcher(obj, c), obj, nil)
+ g.Expect(err).To(HaveOccurred())
+ g.Expect(err.Error()).To(ContainSubstring("namespaces \"not-exist\" not found"))
+
+ // Verify attempted values are set.
+ g.Expect(obj.Status.LastAttemptedGeneration).To(Equal(obj.Generation))
+ g.Expect(obj.Status.LastAttemptedRevision).To(Equal(chartMock.Metadata.Version))
+ g.Expect(obj.Status.LastAttemptedConfigDigest).To(Equal("sha256:1dabc4e3cbbd6a0818bd460f3a6c9855bfe95d506c74726bc0f2edb0aecb1f4e"))
+ g.Expect(obj.Status.LastAttemptedValuesChecksum).To(BeEmpty())
+ })
+
+ t.Run("error recovery updates ready condition", func(t *testing.T) {
+ g := NewWithT(t)
+
+ // Create a test namespace for storing the Helm release mock.
+ ns, err := testEnv.CreateNamespace(context.TODO(), "error-recovery")
+ g.Expect(err).ToNot(HaveOccurred())
+ t.Cleanup(func() {
+ _ = testEnv.Delete(context.TODO(), ns)
+ })
+
+ // Create HelmChart mock.
+ chartMock := testutil.BuildChart()
+ chartArtifact, err := testutil.SaveChartAsArtifact(chartMock, digest.SHA256, testServer.URL(), testServer.Root())
+ g.Expect(err).ToNot(HaveOccurred())
+
+ chart := &sourcev1.HelmChart{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "error-recovery",
+ Namespace: ns.Name,
+ Generation: 1,
+ },
+ Spec: sourcev1.HelmChartSpec{
+ Chart: "testdata/test-helmrepo",
+ Version: "0.1.0",
+ SourceRef: sourcev1.LocalHelmChartSourceReference{
+ Kind: sourcev1.HelmRepositoryKind,
+ Name: "error-recovery",
+ },
+ },
+ 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: "error-recovery",
+ Namespace: ns.Name,
+ Version: 1,
+ Chart: chartMock,
+ Status: helmreleasecommon.StatusDeployed,
+ }, testutil.ReleaseWithConfig(nil))
+
+ obj := &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "error-recovery",
+ Namespace: ns.Name,
+ },
+ Spec: v2.HelmReleaseSpec{
+ StorageNamespace: ns.Name,
+ },
+ Status: v2.HelmReleaseStatus{
+ HelmChart: chart.Namespace + "/" + chart.Name,
+ History: v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(rls)),
+ },
+ },
+ }
+
+ c := fake.NewClientBuilder().
+ WithScheme(NewTestScheme()).
+ WithStatusSubresource(&v2.HelmRelease{}).
+ WithObjects(chart, obj).
+ Build()
+
+ r := &HelmReleaseReconciler{
+ Client: c,
+ APIReader: c,
+ GetClusterConfig: GetTestClusterConfig,
+ EventRecorder: record.NewFakeRecorder(32),
+ FieldManager: "test",
+ }
+
+ // 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.GetStorageNamespace()))
+ g.Expect(err).ToNot(HaveOccurred())
+
+ store := helmstorage.Init(cfg.Driver)
+ g.Expect(store.Create(rls)).To(Succeed())
+
+ sp := patch.NewSerialPatcher(obj, r.Client)
+
+ // List of failure reasons to test.
+ prereqFailures := []string{
+ v2.DependencyNotReadyReason,
+ aclv1.AccessDeniedReason,
+ v2.ArtifactFailedReason,
+ "SourceNotReady",
+ "ValuesError",
+ "RESTClientError",
+ "FactoryError",
+ }
+
+ // Update ready condition for each failure, reconcile and check if the
+ // stale failure condition gets updated.
+ for _, failReason := range prereqFailures {
+ conditions.MarkFalse(obj, meta.ReadyCondition, failReason, "foo")
+ err := sp.Patch(context.TODO(), obj,
+ patch.WithOwnedConditions{Conditions: intreconcile.OwnedConditions},
+ patch.WithFieldOwner(r.FieldManager),
+ )
+ g.Expect(err).ToNot(HaveOccurred())
+
+ _, err = r.reconcileRelease(context.TODO(), sp, obj, nil)
+ g.Expect(err).ToNot(HaveOccurred())
+
+ ready := conditions.Get(obj, meta.ReadyCondition)
+ g.Expect(ready.Status).To(Equal(metav1.ConditionUnknown))
+ g.Expect(ready.Reason).To(Equal(meta.ProgressingReason))
+ }
+ })
+}
+
+func TestHelmReleaseReconciler_reconcileReleaseFromHelmChartSource(t *testing.T) {
+ t.Run("handles chartRef and Chart definition failure", func(t *testing.T) {
+ g := NewWithT(t)
+
+ obj := &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "release",
+ Namespace: "mock",
+ },
+ Spec: v2.HelmReleaseSpec{
+ ChartRef: &v2.CrossNamespaceSourceReference{
+ Kind: sourcev1.HelmChartKind,
+ Name: "chart",
+ Namespace: "mock",
+ },
+ Chart: &v2.HelmChartTemplate{
+ Spec: v2.HelmChartTemplateSpec{
+ Chart: "mychart",
+ SourceRef: v2.CrossNamespaceObjectReference{
+ Name: "something",
+ },
+ },
+ },
+ },
+ }
+
+ r := &HelmReleaseReconciler{
+ Client: fake.NewClientBuilder().
+ WithScheme(NewTestScheme()).
+ WithStatusSubresource(&v2.HelmRelease{}).
+ WithObjects(obj).
+ Build(),
+ }
+
+ res, err := r.Reconcile(context.TODO(), reconcile.Request{
+ NamespacedName: types.NamespacedName{
+ Namespace: obj.GetNamespace(),
+ Name: obj.GetName(),
+ },
+ })
+
+ // only chartRef or Chart must be set
+ g.Expect(errors.Is(err, reconcile.TerminalError(fmt.Errorf("invalid Chart reference")))).To(BeTrue())
+ g.Expect(res.IsZero()).To(BeTrue())
+ })
+
+ t.Run("handles ChartRef get failure", func(t *testing.T) {
+ g := NewWithT(t)
+
+ obj := &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "release",
+ Namespace: "mock",
+ },
+ Spec: v2.HelmReleaseSpec{
+ ChartRef: &v2.CrossNamespaceSourceReference{
+ Kind: sourcev1.HelmChartKind,
+ Name: "chart",
+ Namespace: "mock",
+ },
+ },
+ }
+
+ r := &HelmReleaseReconciler{
+ Client: fake.NewClientBuilder().
+ WithScheme(NewTestScheme()).
+ WithStatusSubresource(&v2.HelmRelease{}).
+ WithObjects(obj).
+ Build(),
+ EventRecorder: record.NewFakeRecorder(32),
+ }
+
+ _, err := r.reconcileRelease(context.TODO(), patch.NewSerialPatcher(obj, r.Client), obj, nil)
+ g.Expect(err).To(HaveOccurred())
+
+ g.Expect(obj.Status.Conditions).To(conditions.MatchConditions([]metav1.Condition{
+ *conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "Fulfilling prerequisites"),
+ *conditions.FalseCondition(meta.ReadyCondition, v2.ArtifactFailedReason, "could not get Source object"),
+ }))
+ })
+
+ t.Run("handles ACL error for ChartRef", func(t *testing.T) {
+ g := NewWithT(t)
+
+ obj := &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "release",
+ Namespace: "mock",
+ },
+ Spec: v2.HelmReleaseSpec{
+ ChartRef: &v2.CrossNamespaceSourceReference{
+ Kind: sourcev1.HelmChartKind,
+ Name: "chart",
+ Namespace: "mock-other",
+ },
+ },
+ }
+
+ r := &HelmReleaseReconciler{
+ Client: fake.NewClientBuilder().
+ WithScheme(NewTestScheme()).
+ WithStatusSubresource(&v2.HelmRelease{}).
+ WithObjects(obj).
+ Build(),
+ EventRecorder: record.NewFakeRecorder(32),
+ }
+
+ res, err := r.reconcileRelease(context.TODO(), patch.NewSerialPatcher(obj, r.Client), obj, nil)
+ g.Expect(err).To(HaveOccurred())
+ g.Expect(errors.Is(err, reconcile.TerminalError(nil))).To(BeTrue())
+ g.Expect(res.IsZero()).To(BeTrue())
+
+ g.Expect(obj.Status.Conditions).To(conditions.MatchConditions([]metav1.Condition{
+ *conditions.TrueCondition(meta.StalledCondition, acl.AccessDeniedReason, "cross-namespace references are not allowed"),
+ *conditions.FalseCondition(meta.ReadyCondition, acl.AccessDeniedReason, "cross-namespace references are not allowed"),
+ }))
+ })
+
+ t.Run("waits for ChartRef to have an Artifact", func(t *testing.T) {
+ g := NewWithT(t)
+
+ chart := &sourcev1.HelmChart{
+ TypeMeta: metav1.TypeMeta{
+ APIVersion: sourcev1.GroupVersion.String(),
+ Kind: sourcev1.HelmChartKind,
+ },
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "chart",
+ Namespace: "mock",
+ Generation: 2,
+ },
+ Status: sourcev1.HelmChartStatus{
+ ObservedGeneration: 2,
+ Conditions: []metav1.Condition{
+ {
+ Type: meta.ReadyCondition,
+ Status: metav1.ConditionTrue,
+ },
+ },
+ },
+ }
+
+ obj := &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "release",
+ Namespace: "mock",
+ },
+ Spec: v2.HelmReleaseSpec{
+ ChartRef: &v2.CrossNamespaceSourceReference{
+ Kind: sourcev1.HelmChartKind,
+ Name: "chart",
+ Namespace: "mock",
+ },
+ Interval: metav1.Duration{Duration: 1 * time.Second},
+ },
+ }
+
+ r := &HelmReleaseReconciler{
+ Client: fake.NewClientBuilder().
+ WithScheme(NewTestScheme()).
+ WithStatusSubresource(&v2.HelmRelease{}).
+ WithObjects(chart, obj).
+ Build(),
+ }
+
+ res, err := r.reconcileRelease(context.TODO(), patch.NewSerialPatcher(obj, r.Client), obj, nil)
+ g.Expect(err).To(Equal(errWaitForChart))
+ g.Expect(res.RequeueAfter).To(Equal(r.DependencyRequeueInterval))
+
+ g.Expect(obj.Status.Conditions).To(conditions.MatchConditions([]metav1.Condition{
+ *conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, ""),
+ *conditions.FalseCondition(meta.ReadyCondition, "SourceNotReady", "'mock/chart' is not ready"),
+ }))
+ })
+
+ t.Run("reports Helm chart load failure", func(t *testing.T) {
+ g := NewWithT(t)
+
+ chart := &sourcev1.HelmChart{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "chart",
+ Namespace: "mock",
+ Generation: 2,
+ },
+ Spec: sourcev1.HelmChartSpec{
+ Interval: metav1.Duration{Duration: 1 * time.Second},
+ },
+ Status: sourcev1.HelmChartStatus{
+ ObservedGeneration: 2,
+ Artifact: &meta.Artifact{
+ URL: testServer.URL() + "/does-not-exist",
+ },
+ Conditions: []metav1.Condition{
+ {
+ Type: meta.ReadyCondition,
+ Status: metav1.ConditionTrue,
+ },
+ },
+ },
+ }
+
+ obj := &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "release",
+ Namespace: "mock",
+ },
+ Spec: v2.HelmReleaseSpec{
+ ChartRef: &v2.CrossNamespaceSourceReference{
+ Kind: sourcev1.HelmChartKind,
+ Name: "chart",
+ Namespace: "mock",
+ },
+ },
+ }
+
+ r := &HelmReleaseReconciler{
+ Client: fake.NewClientBuilder().
+ WithScheme(NewTestScheme()).
+ WithStatusSubresource(&v2.HelmRelease{}).
+ WithObjects(chart, obj).
+ Build(),
+ DependencyRequeueInterval: 10 * time.Second,
+ EventRecorder: record.NewFakeRecorder(32),
+ }
+
+ res, err := r.reconcileRelease(context.TODO(), patch.NewSerialPatcher(obj, r.Client), obj, nil)
+ g.Expect(err).To(Equal(errWaitForDependency))
+ g.Expect(res.RequeueAfter).To(Equal(r.DependencyRequeueInterval))
+
+ g.Expect(obj.Status.Conditions).To(conditions.MatchConditions([]metav1.Condition{
+ *conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, ""),
+ *conditions.FalseCondition(meta.ReadyCondition, v2.ArtifactFailedReason, "Source not ready"),
+ }))
+ })
+ t.Run("report helmChart load failure when switching from existing HelmChat to chartRef", func(t *testing.T) {
+ g := NewWithT(t)
+
+ chart := &sourcev1.HelmChart{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "chart",
+ Namespace: "mock",
+ Generation: 1,
+ },
+ Status: sourcev1.HelmChartStatus{
+ ObservedGeneration: 1,
+ Artifact: &meta.Artifact{},
+ Conditions: []metav1.Condition{
+ {
+ Type: meta.ReadyCondition,
+ Status: metav1.ConditionTrue,
+ },
+ },
+ },
+ }
+
+ sharedChart := &sourcev1.HelmChart{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "sharedChart",
+ Namespace: "mock",
+ Generation: 2,
+ },
+ Spec: sourcev1.HelmChartSpec{
+ Interval: metav1.Duration{Duration: 1 * time.Second},
+ },
+ Status: sourcev1.HelmChartStatus{
+ ObservedGeneration: 2,
+ Artifact: &meta.Artifact{
+ URL: testServer.URL() + "/does-not-exist",
+ },
+ Conditions: []metav1.Condition{
+ {
+ Type: meta.ReadyCondition,
+ Status: metav1.ConditionTrue,
+ },
+ },
+ },
+ }
+
+ obj := &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "release",
+ Namespace: "mock",
+ },
+ Spec: v2.HelmReleaseSpec{
+ ChartRef: &v2.CrossNamespaceSourceReference{
+ Kind: sourcev1.HelmChartKind,
+ Name: "sharedChart",
+ Namespace: "mock",
+ },
+ },
+ Status: v2.HelmReleaseStatus{
+ HelmChart: "mock/chart",
+ },
+ }
+
+ r := &HelmReleaseReconciler{
+ Client: fake.NewClientBuilder().
+ WithScheme(NewTestScheme()).
+ WithStatusSubresource(&v2.HelmRelease{}).
+ WithObjects(chart, sharedChart, obj).
+ Build(),
+ DependencyRequeueInterval: 10 * time.Second,
+ EventRecorder: record.NewFakeRecorder(32),
+ }
+
+ res, err := r.reconcileRelease(context.TODO(), patch.NewSerialPatcher(obj, r.Client), obj, nil)
+ g.Expect(err).To(Equal(errWaitForDependency))
+ g.Expect(res.RequeueAfter).To(Equal(r.DependencyRequeueInterval))
+
+ g.Expect(obj.Status.Conditions).To(conditions.MatchConditions([]metav1.Condition{
+ *conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, ""),
+ *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: helmreleasecommon.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, nil)
+ 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) {
+
+ t.Run("handles chartRef and Chart definition failure", func(t *testing.T) {
+ g := NewWithT(t)
+
+ obj := &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "release",
+ Namespace: "mock",
+ },
+ Spec: v2.HelmReleaseSpec{
+ ChartRef: &v2.CrossNamespaceSourceReference{
+ Kind: "OCIRepository",
+ Name: "ocirepo",
+ Namespace: "mock",
+ },
+ Chart: &v2.HelmChartTemplate{
+ Spec: v2.HelmChartTemplateSpec{
+ Chart: "mychart",
+ SourceRef: v2.CrossNamespaceObjectReference{
+ Name: "something",
+ },
+ },
+ },
+ },
+ }
+
+ r := &HelmReleaseReconciler{
+ Client: fake.NewClientBuilder().
+ WithScheme(NewTestScheme()).
+ WithStatusSubresource(&v2.HelmRelease{}).
+ WithObjects(obj).
+ Build(),
+ }
+
+ res, err := r.Reconcile(context.TODO(), reconcile.Request{
+ NamespacedName: types.NamespacedName{
+ Namespace: obj.GetNamespace(),
+ Name: obj.GetName(),
+ },
+ })
+
+ // only chartRef or Chart must be set
+ g.Expect(errors.Is(err, reconcile.TerminalError(fmt.Errorf("invalid Chart reference")))).To(BeTrue())
+ g.Expect(res.IsZero()).To(BeTrue())
+ })
+ t.Run("handles ChartRef get failure", func(t *testing.T) {
+ g := NewWithT(t)
+
+ obj := &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "release",
+ Namespace: "mock",
+ },
+ Spec: v2.HelmReleaseSpec{
+ ChartRef: &v2.CrossNamespaceSourceReference{
+ Kind: "OCIRepository",
+ Name: "ocirepo",
+ Namespace: "mock",
+ },
+ },
+ }
+
+ r := &HelmReleaseReconciler{
+ Client: fake.NewClientBuilder().
+ WithScheme(NewTestScheme()).
+ WithStatusSubresource(&v2.HelmRelease{}).
+ WithObjects(obj).
+ Build(),
+ EventRecorder: record.NewFakeRecorder(32),
+ }
+
+ _, err := r.reconcileRelease(context.TODO(), patch.NewSerialPatcher(obj, r.Client), obj, nil)
+ g.Expect(err).To(HaveOccurred())
+
+ g.Expect(obj.Status.Conditions).To(conditions.MatchConditions([]metav1.Condition{
+ *conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "Fulfilling prerequisites"),
+ *conditions.FalseCondition(meta.ReadyCondition, v2.ArtifactFailedReason, "could not get Source object"),
+ }))
+ })
+
+ t.Run("handles ACL error for ChartRef", func(t *testing.T) {
+ g := NewWithT(t)
+
+ obj := &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "release",
+ Namespace: "mock",
+ },
+ Spec: v2.HelmReleaseSpec{
+ ChartRef: &v2.CrossNamespaceSourceReference{
+ Kind: "OCIRepository",
+ Name: "ocirepo",
+ Namespace: "mock-other",
+ },
+ },
+ }
+
+ r := &HelmReleaseReconciler{
+ Client: fake.NewClientBuilder().
+ WithScheme(NewTestScheme()).
+ WithStatusSubresource(&v2.HelmRelease{}).
+ WithObjects(obj).
+ Build(),
+ EventRecorder: record.NewFakeRecorder(32),
+ }
+
+ res, err := r.reconcileRelease(context.TODO(), patch.NewSerialPatcher(obj, r.Client), obj, nil)
+ g.Expect(err).To(HaveOccurred())
+ g.Expect(errors.Is(err, reconcile.TerminalError(nil))).To(BeTrue())
+ g.Expect(res.IsZero()).To(BeTrue())
+
+ g.Expect(obj.Status.Conditions).To(conditions.MatchConditions([]metav1.Condition{
+ *conditions.TrueCondition(meta.StalledCondition, acl.AccessDeniedReason, "cross-namespace references are not allowed"),
+ *conditions.FalseCondition(meta.ReadyCondition, acl.AccessDeniedReason, "cross-namespace references are not allowed"),
+ }))
+ })
+
+ t.Run("waits for ChartRef to have an Artifact", func(t *testing.T) {
+ g := NewWithT(t)
+
+ ocirepo := &sourcev1.OCIRepository{
+ TypeMeta: metav1.TypeMeta{
+ APIVersion: sourcev1.GroupVersion.String(),
+ Kind: sourcev1.OCIRepositoryKind,
+ },
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "ocirepo",
+ Namespace: "mock",
+ Generation: 2,
+ },
+ Status: sourcev1.OCIRepositoryStatus{
+ ObservedGeneration: 2,
+ Conditions: []metav1.Condition{
+ {
+ Type: meta.ReadyCondition,
+ Status: metav1.ConditionTrue,
+ },
+ },
+ },
+ }
+
+ obj := &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "release",
+ Namespace: "mock",
+ },
+ Spec: v2.HelmReleaseSpec{
+ ChartRef: &v2.CrossNamespaceSourceReference{
+ Kind: "OCIRepository",
+ Name: "ocirepo",
+ Namespace: "mock",
+ },
+ Interval: metav1.Duration{Duration: 1 * time.Second},
+ },
+ }
+
+ r := &HelmReleaseReconciler{
+ Client: fake.NewClientBuilder().
+ WithScheme(NewTestScheme()).
+ WithStatusSubresource(&v2.HelmRelease{}).
+ WithObjects(ocirepo, obj).
+ Build(),
+ }
+
+ res, err := r.reconcileRelease(context.TODO(), patch.NewSerialPatcher(obj, r.Client), obj, nil)
+ g.Expect(err).To(Equal(errWaitForChart))
+ g.Expect(res.RequeueAfter).To(Equal(r.DependencyRequeueInterval))
+
+ g.Expect(obj.Status.Conditions).To(conditions.MatchConditions([]metav1.Condition{
+ *conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, ""),
+ *conditions.FalseCondition(meta.ReadyCondition, "SourceNotReady", "'mock/ocirepo' is not ready"),
+ }))
+ })
+
+ t.Run("reports values composition failure", func(t *testing.T) {
+ g := NewWithT(t)
+
+ ocirepo := &sourcev1.OCIRepository{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "ocirepo",
+ Namespace: "mock",
+ Generation: 2,
+ },
+ Spec: sourcev1.OCIRepositorySpec{
+ Interval: metav1.Duration{Duration: 1 * time.Second},
+ },
+ Status: sourcev1.OCIRepositoryStatus{
+ ObservedGeneration: 2,
+ Artifact: &meta.Artifact{},
+ Conditions: []metav1.Condition{
+ {
+ Type: meta.ReadyCondition,
+ Status: metav1.ConditionTrue,
+ },
+ },
+ },
+ }
+
+ obj := &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "release",
+ Namespace: "mock",
+ },
+ Spec: v2.HelmReleaseSpec{
+ ChartRef: &v2.CrossNamespaceSourceReference{
+ Kind: "OCIRepository",
+ Name: "ocirepo",
+ Namespace: "mock",
+ },
+ ValuesFrom: []meta.ValuesReference{
+ {
+ Kind: "Secret",
+ Name: "missing",
+ },
+ },
+ },
+ }
+
+ r := &HelmReleaseReconciler{
+ Client: fake.NewClientBuilder().
+ WithScheme(NewTestScheme()).
+ WithStatusSubresource(&v2.HelmRelease{}).
+ WithObjects(ocirepo, obj).
+ Build(),
+ EventRecorder: record.NewFakeRecorder(32),
+ }
+
+ _, err := r.reconcileRelease(context.TODO(), patch.NewSerialPatcher(obj, r.Client), obj, nil)
+ g.Expect(err).To(HaveOccurred())
+
+ g.Expect(obj.Status.Conditions).To(conditions.MatchConditions([]metav1.Condition{
+ *conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "Fulfilling prerequisites"),
+ *conditions.FalseCondition(meta.ReadyCondition, "ValuesError", "could not resolve Secret chart values reference 'mock/missing' with key 'values.yaml'"),
+ }))
+ })
+
+ t.Run("reports Helm chart load failure", func(t *testing.T) {
+ g := NewWithT(t)
+
+ ocirepo := &sourcev1.OCIRepository{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "ocirepo",
+ Namespace: "mock",
+ Generation: 2,
+ },
+ Spec: sourcev1.OCIRepositorySpec{
+ Interval: metav1.Duration{Duration: 1 * time.Second},
+ },
+ Status: sourcev1.OCIRepositoryStatus{
+ ObservedGeneration: 2,
+ Artifact: &meta.Artifact{
+ URL: testServer.URL() + "/does-not-exist",
+ },
+ Conditions: []metav1.Condition{
+ {
+ Type: meta.ReadyCondition,
+ Status: metav1.ConditionTrue,
+ },
+ },
+ },
+ }
+
+ obj := &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "release",
+ Namespace: "mock",
+ },
+ Spec: v2.HelmReleaseSpec{
+ ChartRef: &v2.CrossNamespaceSourceReference{
+ Kind: "OCIRepository",
+ Name: "ocirepo",
+ Namespace: "mock",
+ },
+ },
+ }
+
+ r := &HelmReleaseReconciler{
+ Client: fake.NewClientBuilder().
+ WithScheme(NewTestScheme()).
+ WithStatusSubresource(&v2.HelmRelease{}).
+ WithObjects(ocirepo, obj).
+ Build(),
+ DependencyRequeueInterval: 10 * time.Second,
+ EventRecorder: record.NewFakeRecorder(32),
+ }
+
+ res, err := r.reconcileRelease(context.TODO(), patch.NewSerialPatcher(obj, r.Client), obj, nil)
+ g.Expect(err).To(Equal(errWaitForDependency))
+ g.Expect(res.RequeueAfter).To(Equal(r.DependencyRequeueInterval))
+
+ g.Expect(obj.Status.Conditions).To(conditions.MatchConditions([]metav1.Condition{
+ *conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, ""),
+ *conditions.FalseCondition(meta.ReadyCondition, v2.ArtifactFailedReason, "Source not ready"),
+ }))
+ })
+ t.Run("report helmChart load failure when switching from existing HelmChat to chartRef", func(t *testing.T) {
+ g := NewWithT(t)
+
+ chart := &sourcev1.HelmChart{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "chart",
+ Namespace: "mock",
+ Generation: 1,
+ },
+ Status: sourcev1.HelmChartStatus{
+ ObservedGeneration: 1,
+ Artifact: &meta.Artifact{},
+ Conditions: []metav1.Condition{
+ {
+ Type: meta.ReadyCondition,
+ Status: metav1.ConditionTrue,
+ },
+ },
+ },
+ }
+
+ ocirepo := &sourcev1.OCIRepository{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "ocirepo",
+ Namespace: "mock",
+ Generation: 2,
+ },
+ Spec: sourcev1.OCIRepositorySpec{
+ Interval: metav1.Duration{Duration: 1 * time.Second},
+ },
+ Status: sourcev1.OCIRepositoryStatus{
+ ObservedGeneration: 2,
+ Artifact: &meta.Artifact{
+ URL: testServer.URL() + "/does-not-exist",
+ },
+ Conditions: []metav1.Condition{
+ {
+ Type: meta.ReadyCondition,
+ Status: metav1.ConditionTrue,
+ },
+ },
+ },
+ }
+
+ obj := &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "release",
+ Namespace: "mock",
+ },
+ Spec: v2.HelmReleaseSpec{
+ ChartRef: &v2.CrossNamespaceSourceReference{
+ Kind: "OCIRepository",
+ Name: "ocirepo",
+ Namespace: "mock",
+ },
+ },
+ Status: v2.HelmReleaseStatus{
+ HelmChart: "mock/chart",
+ },
+ }
+
+ r := &HelmReleaseReconciler{
+ Client: fake.NewClientBuilder().
+ WithScheme(NewTestScheme()).
+ WithStatusSubresource(&v2.HelmRelease{}).
+ WithObjects(chart, ocirepo, obj).
+ Build(),
+ DependencyRequeueInterval: 10 * time.Second,
+ EventRecorder: record.NewFakeRecorder(32),
+ }
+
+ res, err := r.reconcileRelease(context.TODO(), patch.NewSerialPatcher(obj, r.Client), obj, nil)
+ g.Expect(err).To(Equal(errWaitForDependency))
+ g.Expect(res.RequeueAfter).To(Equal(r.DependencyRequeueInterval))
+
+ g.Expect(obj.Status.Conditions).To(conditions.MatchConditions([]metav1.Condition{
+ *conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, ""),
+ *conditions.FalseCondition(meta.ReadyCondition, v2.ArtifactFailedReason, "Source not ready"),
+ }))
+ })
+
+ t.Run("handle chartRef mutable tag", func(t *testing.T) {
+ g := NewWithT(t)
+
+ // Create HelmChart mock.
+ chartMock := testutil.BuildChart()
+ chartArtifact, err := testutil.SaveChartAsArtifact(chartMock, digest.SHA256, testServer.URL(), testServer.Root())
+ g.Expect(err).ToNot(HaveOccurred())
+ chartArtifact.Revision += "@" + chartArtifact.Digest
+
+ ocirepo := &sourcev1.OCIRepository{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "ocirepo",
+ Namespace: "mock",
+ Generation: 1,
+ },
+ Spec: sourcev1.OCIRepositorySpec{
+ Interval: metav1.Duration{Duration: 1 * time.Second},
+ },
+ Status: sourcev1.OCIRepositoryStatus{
+ ObservedGeneration: 1,
+ Artifact: chartArtifact,
+ Conditions: []metav1.Condition{
+ {
+ Type: meta.ReadyCondition,
+ Status: metav1.ConditionTrue,
+ },
+ },
+ },
+ }
+
+ obj := &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "release",
+ Namespace: "mock",
+ },
+ Spec: v2.HelmReleaseSpec{
+ ChartRef: &v2.CrossNamespaceSourceReference{
+ Kind: "OCIRepository",
+ Name: "ocirepo",
+ Namespace: "mock",
+ },
+ },
+ }
+
+ r := &HelmReleaseReconciler{
+ Client: fake.NewClientBuilder().
+ WithScheme(NewTestScheme()).
+ WithStatusSubresource(&v2.HelmRelease{}).
+ WithObjects(ocirepo, obj).
+ Build(),
+ GetClusterConfig: GetTestClusterConfig,
+ EventRecorder: record.NewFakeRecorder(32),
+ }
+
+ _, err = r.reconcileRelease(context.TODO(), patch.NewSerialPatcher(obj, r.Client), obj, nil)
+ g.Expect(err).To(HaveOccurred())
+ g.Expect(err.Error()).To(ContainSubstring("namespaces \"mock\" not found"))
+
+ // Verify attempted values are set.
+ g.Expect(obj.Status.LastAttemptedGeneration).To(Equal(obj.Generation))
+ dig := strings.Split(chartArtifact.Revision, ":")[1][0:12]
+ g.Expect(obj.Status.LastAttemptedRevision).To(Equal(chartMock.Metadata.Version + "+" + dig))
+ g.Expect(obj.Status.LastAttemptedConfigDigest).To(Equal("sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"))
+ g.Expect(obj.Status.LastAttemptedValuesChecksum).To(BeEmpty())
+
+ // change the chart revision to simulate a new digest
+ chartArtifact.Revision = chartMock.Metadata.Version + "@" + "sha256:adebc5e3cbcd6a0918bd470f3a6c9855bfe95d506c74726bc0f2edb0aecb1f4e"
+ ocirepo.Status.Artifact = chartArtifact
+ r.Client.Update(context.Background(), ocirepo)
+
+ _, err = r.reconcileRelease(context.TODO(), patch.NewSerialPatcher(obj, r.Client), obj, nil)
+ g.Expect(err).To(HaveOccurred())
+ g.Expect(err.Error()).To(ContainSubstring("namespaces \"mock\" not found"))
+
+ // Verify attempted values are set.
+ g.Expect(obj.Status.LastAttemptedGeneration).To(Equal(obj.Generation))
+ dig = strings.Split(chartArtifact.Revision, ":")[1][0:12]
+ g.Expect(obj.Status.LastAttemptedRevision).To(Equal(chartMock.Metadata.Version + "+" + dig))
+ g.Expect(obj.Status.LastAttemptedConfigDigest).To(Equal("sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"))
+ g.Expect(obj.Status.LastAttemptedValuesChecksum).To(BeEmpty())
+ })
+
+ t.Run("convert '_' to '+' in OCIRepository tag for semver compatibility", func(t *testing.T) {
+ g := NewWithT(t)
+
+ // Create HelmChart mock.
+ chartMock := testutil.BuildChart(testutil.ChartWithVersion("0.1.0+20241101123015"))
+ chartArtifact, err := testutil.SaveChartAsArtifact(chartMock, digest.SHA256, testServer.URL(), testServer.Root())
+ g.Expect(err).ToNot(HaveOccurred())
+ // copy the artifact to mutate the revision to have an underscore (_) in the tag
+ ociArtifact := chartArtifact.DeepCopy()
+ ociArtifact.Revision = "0.1.0_20241101123015"
+ ociArtifact.Revision += "@" + chartArtifact.Digest
+
+ ns, err := testEnv.CreateNamespace(context.TODO(), "mock")
+ g.Expect(err).ToNot(HaveOccurred())
+ t.Cleanup(func() {
+ _ = testEnv.Delete(context.TODO(), ns)
+ })
+
+ // ocirepo is the chartRef object to switch to.
+ ocirepo := &sourcev1.OCIRepository{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "ocirepo",
+ Namespace: ns.Name,
+ Generation: 1,
+ },
+ Spec: sourcev1.OCIRepositorySpec{
+ URL: "oci://test-example.com",
+ Interval: metav1.Duration{Duration: 1 * time.Second},
+ },
+ Status: sourcev1.OCIRepositoryStatus{
+ ObservedGeneration: 1,
+ Artifact: ociArtifact,
+ Conditions: []metav1.Condition{
+ {
+ Type: meta.ReadyCondition,
+ Status: metav1.ConditionTrue,
+ },
+ },
+ },
+ }
+
+ obj := &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "release",
+ Namespace: ns.Name,
+ },
+ Spec: v2.HelmReleaseSpec{
+ ChartRef: &v2.CrossNamespaceSourceReference{
+ Kind: "OCIRepository",
+ Name: "ocirepo",
+ },
+ },
+ }
+
+ c := fake.NewClientBuilder().
+ WithScheme(NewTestScheme()).
+ WithStatusSubresource(&v2.HelmRelease{}).
+ WithObjects(ocirepo, obj).
+ Build()
+
+ r := &HelmReleaseReconciler{
+ Client: c,
+ GetClusterConfig: GetTestClusterConfig,
+ EventRecorder: record.NewFakeRecorder(32),
+ }
+
+ _, err = r.reconcileRelease(context.TODO(), patch.NewSerialPatcher(obj, r.Client), obj, nil)
+ g.Expect(err).ToNot(HaveOccurred())
+
+ // Verify attempted values are set.
+ g.Expect(obj.Status.LastAttemptedGeneration).To(Equal(obj.Generation))
+ dig := strings.Split(ociArtifact.Revision, ":")[1][0:12]
+ // Expect the revision with underscore converted to plus in the final result
+ // to only have the leading 12 chars digest as build metadata (initial tag metadata overwritten)
+ g.Expect(obj.Status.LastAttemptedRevision).To(Equal("0.1.0" + "+" + dig))
+ g.Expect(obj.Status.LastAttemptedConfigDigest).To(Equal("sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"))
+ g.Expect(obj.Status.LastAttemptedReleaseAction).To(Equal(v2.ReleaseActionInstall))
+ g.Expect(obj.Status.LastAttemptedReleaseActionDuration).ToNot(BeNil())
+ g.Expect(obj.Status.LastAttemptedValuesChecksum).To(BeEmpty())
+
+ // change the chart revision with a new version (build metadata) to simulate a new digest
+ chartArtifact.Revision = "0.1.0_20241102104025" + "@" + "sha256:adebc5e3cbcd6a0918bd470f3a6c9855bfe95d506c74726bc0f2edb0aecb1f4e"
+ ocirepo.Status.Artifact = chartArtifact
+ r.Client.Update(context.Background(), ocirepo)
+ r.reconcileRelease(context.TODO(), patch.NewSerialPatcher(obj, r.Client), obj, nil)
+
+ // Verify attempted values are set.
+ g.Expect(obj.Status.LastAttemptedGeneration).To(Equal(obj.Generation))
+ // Expect the revision with underscore converted to plus in the final result
+ // to only have the leading 12 chars digest as build metadata (initial tag metadata overwritten)
+ g.Expect(obj.Status.LastAttemptedRevision).To(Equal("0.1.0" + "+" + "adebc5e3cbcd"))
+ g.Expect(obj.Status.LastAttemptedConfigDigest).To(Equal("sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"))
+ g.Expect(obj.Status.LastAttemptedReleaseAction).To(Equal(v2.ReleaseActionUpgrade))
+ g.Expect(obj.Status.LastAttemptedReleaseActionDuration).ToNot(BeNil())
+ g.Expect(obj.Status.LastAttemptedValuesChecksum).To(BeEmpty())
+ })
+
+ t.Run("ignore 'v' prefix in OCIRepository tag", func(t *testing.T) {
+ g := NewWithT(t)
+
+ // 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 = "v" + ociArtifact.Revision
+ ociArtifact.Revision += "@" + chartArtifact.Digest
+
+ ns, err := testEnv.CreateNamespace(context.TODO(), "mock")
+ g.Expect(err).ToNot(HaveOccurred())
+ t.Cleanup(func() {
+ _ = testEnv.Delete(context.TODO(), ns)
+ })
+
+ // ocirepo is the chartRef object to switch to.
+ ocirepo := &sourcev1.OCIRepository{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "ocirepo",
+ Namespace: ns.Name,
+ Generation: 1,
+ },
+ Spec: sourcev1.OCIRepositorySpec{
+ URL: "oci://test-example.com",
+ Interval: metav1.Duration{Duration: 1 * time.Second},
+ },
+ Status: sourcev1.OCIRepositoryStatus{
+ ObservedGeneration: 1,
+ Artifact: ociArtifact,
+ Conditions: []metav1.Condition{
+ {
+ Type: meta.ReadyCondition,
+ Status: metav1.ConditionTrue,
+ },
+ },
+ },
+ }
+
+ obj := &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "release",
+ Namespace: ns.Name,
+ },
+ Spec: v2.HelmReleaseSpec{
+ ChartRef: &v2.CrossNamespaceSourceReference{
+ Kind: "OCIRepository",
+ Name: "ocirepo",
+ },
+ },
+ }
+
+ c := fake.NewClientBuilder().
+ WithScheme(NewTestScheme()).
+ WithStatusSubresource(&v2.HelmRelease{}).
+ WithObjects(ocirepo, obj).
+ Build()
+
+ r := &HelmReleaseReconciler{
+ Client: c,
+ GetClusterConfig: GetTestClusterConfig,
+ EventRecorder: record.NewFakeRecorder(32),
+ }
+
+ _, err = r.reconcileRelease(context.TODO(), patch.NewSerialPatcher(obj, r.Client), obj, nil)
+ g.Expect(err).ToNot(HaveOccurred())
+
+ // Verify attempted values are set.
+ g.Expect(obj.Status.LastAttemptedGeneration).To(Equal(obj.Generation))
+ dig := strings.Split(ociArtifact.Revision, ":")[1][0:12]
+ g.Expect(obj.Status.LastAttemptedRevision).To(Equal(chartMock.Metadata.Version + "+" + dig))
+ g.Expect(obj.Status.LastAttemptedConfigDigest).To(Equal("sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"))
+ g.Expect(obj.Status.LastAttemptedValuesChecksum).To(BeEmpty())
+ })
+
+ t.Run("ignore 'v' prefix in chart version", func(t *testing.T) {
+ g := NewWithT(t)
+
+ // Create HelmChart mock.
+ chartMock := testutil.BuildChart(testutil.ChartWithVersion("v0.1.0"))
+ 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 = "0.1.0"
+ ociArtifact.Revision += "@" + chartArtifact.Digest
+
+ ns, err := testEnv.CreateNamespace(context.TODO(), "mock")
+ g.Expect(err).ToNot(HaveOccurred())
+ t.Cleanup(func() {
+ _ = testEnv.Delete(context.TODO(), ns)
+ })
+
+ // ocirepo is the chartRef object to switch to.
+ ocirepo := &sourcev1.OCIRepository{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "ocirepo",
+ Namespace: ns.Name,
+ Generation: 1,
+ },
+ Spec: sourcev1.OCIRepositorySpec{
+ URL: "oci://test-example.com",
+ Interval: metav1.Duration{Duration: 1 * time.Second},
+ },
+ Status: sourcev1.OCIRepositoryStatus{
+ ObservedGeneration: 1,
+ Artifact: ociArtifact,
+ Conditions: []metav1.Condition{
+ {
+ Type: meta.ReadyCondition,
+ Status: metav1.ConditionTrue,
+ },
+ },
+ },
+ }
+
+ obj := &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "release",
+ Namespace: ns.Name,
+ },
+ Spec: v2.HelmReleaseSpec{
+ ChartRef: &v2.CrossNamespaceSourceReference{
+ Kind: "OCIRepository",
+ Name: "ocirepo",
+ },
+ },
+ }
+
+ c := fake.NewClientBuilder().
+ WithScheme(NewTestScheme()).
+ WithStatusSubresource(&v2.HelmRelease{}).
+ WithObjects(ocirepo, obj).
+ Build()
+
+ r := &HelmReleaseReconciler{
+ Client: c,
+ GetClusterConfig: GetTestClusterConfig,
+ EventRecorder: record.NewFakeRecorder(32),
+ }
+
+ _, err = r.reconcileRelease(context.TODO(), patch.NewSerialPatcher(obj, r.Client), obj, nil)
+ g.Expect(err).ToNot(HaveOccurred())
+
+ // Verify attempted values are set.
+ g.Expect(obj.Status.LastAttemptedGeneration).To(Equal(obj.Generation))
+ dig := strings.Split(ociArtifact.Revision, ":")[1][0:12]
+ g.Expect(obj.Status.LastAttemptedRevision).To(Equal(strings.Split(ociArtifact.Revision, "@")[0] + "+" + dig))
+ g.Expect(obj.Status.LastAttemptedConfigDigest).To(Equal("sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"))
+ g.Expect(obj.Status.LastAttemptedValuesChecksum).To(BeEmpty())
+ })
+
+ t.Run("upgrade by switching from existing HelmChat to chartRef", func(t *testing.T) {
+ g := NewWithT(t)
+
+ // 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 is the HelmChart object created by the HelmRelease object.
+ 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,
+ },
+ },
+ },
+ }
+
+ // ocirepo is the chartRef object to switch to.
+ ocirepo := &sourcev1.OCIRepository{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "ocirepo",
+ Namespace: ns.Name,
+ Generation: 1,
+ },
+ Spec: sourcev1.OCIRepositorySpec{
+ URL: "oci://test-example.com",
+ Interval: metav1.Duration{Duration: 1 * time.Second},
+ },
+ Status: sourcev1.OCIRepositoryStatus{
+ ObservedGeneration: 1,
+ Artifact: ociArtifact,
+ 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: helmreleasecommon.StatusDeployed,
+ })
+
+ obj := &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "release",
+ Namespace: ns.Name,
+ },
+ Spec: v2.HelmReleaseSpec{
+ ChartRef: &v2.CrossNamespaceSourceReference{
+ Kind: "OCIRepository",
+ Name: "ocirepo",
+ },
+ },
+ Status: v2.HelmReleaseStatus{
+ StorageNamespace: ns.Name,
+ History: v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(rls)),
+ },
+ HelmChart: hc.Namespace + "/" + hc.Name,
+ },
+ }
+
+ c := fake.NewClientBuilder().
+ WithScheme(NewTestScheme()).
+ WithStatusSubresource(&v2.HelmRelease{}).
+ WithObjects(hc, ocirepo, 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())
+
+ _, err = r.reconcileRelease(context.TODO(), patch.NewSerialPatcher(obj, r.Client), obj, nil)
+ g.Expect(err).ToNot(HaveOccurred())
+
+ // Verify attempted values are set.
+ g.Expect(obj.Status.LastAttemptedGeneration).To(Equal(obj.Generation))
+ dig := strings.Split(ociArtifact.Revision, ":")[1][0:12]
+ g.Expect(obj.Status.LastAttemptedRevision).To(Equal(chartMock.Metadata.Version + "+" + dig))
+ g.Expect(obj.Status.LastAttemptedConfigDigest).To(Equal("sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"))
+ g.Expect(obj.Status.LastAttemptedValuesChecksum).To(BeEmpty())
+
+ // 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(),
+ fmt.Sprintf("%s+%s", chartMock.Metadata.Version, dig))),
+ *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(),
+ fmt.Sprintf("%s+%s", chartMock.Metadata.Version, dig))),
+ }))
+ })
+}
+
+func TestHelmReleaseReconciler_reconcileDelete(t *testing.T) {
+ t.Run("uninstalls Helm release and removes chart", func(t *testing.T) {
+ g := NewWithT(t)
+
+ // Create a test namespace for storing the Helm release mock.
+ ns, err := testEnv.CreateNamespace(context.TODO(), "reconcile-delete")
+ g.Expect(err).ToNot(HaveOccurred())
+ t.Cleanup(func() {
+ _ = testEnv.Delete(context.TODO(), ns)
+ })
+
+ // Create HelmChart mock.
+ hc := &sourcev1.HelmChart{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "reconcile-delete",
+ Namespace: ns.Name,
+ },
+ Spec: sourcev1.HelmChartSpec{
+ Chart: "testdata/test-helmrepo",
+ Version: "0.1.0",
+ SourceRef: sourcev1.LocalHelmChartSourceReference{
+ Kind: sourcev1.HelmRepositoryKind,
+ Name: "reconcile-delete",
+ },
+ },
+ }
+ g.Expect(testEnv.Create(context.TODO(), hc)).To(Succeed())
+ t.Cleanup(func() {
+ _ = testEnv.Delete(context.TODO(), hc)
+ })
+
+ // Create a test Helm release storage mock.
+ rls := testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: "reconcile-delete",
+ Namespace: ns.Name,
+ Version: 1,
+ Chart: testutil.BuildChart(testutil.ChartWithTestHook()),
+ Status: helmreleasecommon.StatusDeployed,
+ })
+
+ obj := &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "reconcile-delete",
+ Namespace: ns.Name,
+ Finalizers: []string{v2.HelmReleaseFinalizer},
+ DeletionTimestamp: &metav1.Time{Time: time.Now()},
+ },
+ Status: v2.HelmReleaseStatus{
+ StorageNamespace: ns.Name,
+ History: v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(rls)),
+ },
+ HelmChart: hc.Namespace + "/" + hc.Name,
+ },
+ }
+
+ r := &HelmReleaseReconciler{
+ Client: testEnv.Client,
+ APIReader: testEnv.Client,
+ 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())
+
+ // Reconcile the actual deletion of the Helm release.
+ res, err := r.reconcileDelete(context.TODO(), obj)
+ g.Expect(err).ToNot(HaveOccurred())
+ g.Expect(res.IsZero()).To(BeTrue())
+
+ // Verify Helm release has been uninstalled.
+ _, err = store.History(rls.Name)
+ g.Expect(err).To(MatchError(helmdriver.ErrReleaseNotFound))
+
+ // Verify Helm chart has been removed.
+ g.Eventually(func(g Gomega) {
+ err = testEnv.Get(context.TODO(), client.ObjectKey{
+ Namespace: hc.Namespace,
+ Name: hc.Name,
+ }, &sourcev1.HelmChart{})
+ g.Expect(err).To(HaveOccurred())
+ g.Expect(apierrors.IsNotFound(err)).To(BeTrue())
+ }).Should(Succeed())
+ })
+
+ t.Run("removes finalizer for suspended resource with DeletionTimestamp", func(t *testing.T) {
+ g := NewWithT(t)
+
+ obj := &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Finalizers: []string{v2.HelmReleaseFinalizer, "other-finalizer"},
+ DeletionTimestamp: &metav1.Time{Time: time.Now()},
+ },
+ Spec: v2.HelmReleaseSpec{
+ Suspend: true,
+ },
+ }
+
+ res, err := (&HelmReleaseReconciler{}).reconcileDelete(context.TODO(), obj)
+ g.Expect(err).ToNot(HaveOccurred())
+ g.Expect(res.IsZero()).To(BeTrue())
+
+ g.Expect(obj.GetFinalizers()).To(ConsistOf("other-finalizer"))
+ })
+
+ t.Run("does not remove finalizer when DeletionTimestamp is not set", func(t *testing.T) {
+ g := NewWithT(t)
+
+ obj := &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Finalizers: []string{v2.HelmReleaseFinalizer},
+ },
+ Spec: v2.HelmReleaseSpec{
+ Suspend: true,
+ },
+ }
+
+ res, err := (&HelmReleaseReconciler{}).reconcileDelete(context.TODO(), obj)
+ g.Expect(err).ToNot(HaveOccurred())
+ g.Expect(res.Requeue).To(BeTrue())
+
+ g.Expect(obj.GetFinalizers()).To(ConsistOf(v2.HelmReleaseFinalizer))
+ })
+}
+
+func TestHelmReleaseReconciler_reconcileReleaseDeletion(t *testing.T) {
+ t.Run("uninstalls Helm release", func(t *testing.T) {
+ g := NewWithT(t)
+
+ // Create a test namespace for storing the Helm release mock.
+ ns, err := testEnv.CreateNamespace(context.TODO(), "reconcile-release-deletion")
+ g.Expect(err).ToNot(HaveOccurred())
+ t.Cleanup(func() {
+ _ = testEnv.Delete(context.TODO(), ns)
+ })
+
+ // Create a test Helm release storage mock.
+ rls := testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: "reconcile-delete",
+ Namespace: ns.Name,
+ Version: 1,
+ Chart: testutil.BuildChart(testutil.ChartWithTestHook()),
+ Status: helmreleasecommon.StatusDeployed,
+ })
+
+ obj := &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "reconcile-delete",
+ Namespace: ns.Name,
+ DeletionTimestamp: &metav1.Time{Time: time.Now()},
+ },
+ Status: v2.HelmReleaseStatus{
+ StorageNamespace: ns.Name,
+ History: v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(rls)),
+ },
+ },
+ }
+
+ r := &HelmReleaseReconciler{
+ Client: testEnv.Client,
+ APIReader: testEnv.Client,
+ 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())
+
+ // Reconcile the actual deletion of the Helm release.
+ err = r.reconcileReleaseDeletion(context.TODO(), obj)
+ g.Expect(err).ToNot(HaveOccurred())
+
+ // Verify status of Helm release has been updated.
+ g.Expect(obj.Status.StorageNamespace).To(BeEmpty())
+ g.Expect(obj.Status.History).To(BeNil())
+
+ // Verify Helm release has been uninstalled.
+ _, err = store.History(rls.Name)
+ g.Expect(err).To(MatchError(helmdriver.ErrReleaseNotFound))
+ })
+
+ t.Run("skip uninstalling Helm release when KubeConfig Secret is missing", func(t *testing.T) {
+ g := NewWithT(t)
+
+ // Create a test namespace for storing the Helm release mock.
+ ns, err := testEnv.CreateNamespace(context.TODO(), "reconcile-release-deletion")
+ g.Expect(err).ToNot(HaveOccurred())
+ t.Cleanup(func() {
+ _ = testEnv.Delete(context.TODO(), ns)
+ })
+
+ // Create a test Helm release storage mock.
+ rls := testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: "reconcile-delete",
+ Namespace: ns.Name,
+ Version: 1,
+ Chart: testutil.BuildChart(testutil.ChartWithTestHook()),
+ Status: helmreleasecommon.StatusDeployed,
+ })
+
+ obj := &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "reconcile-delete",
+ Namespace: ns.Name,
+ DeletionTimestamp: &metav1.Time{Time: time.Now()},
+ },
+ Status: v2.HelmReleaseStatus{
+ StorageNamespace: ns.Name,
+ History: v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(rls)),
+ },
+ },
+ }
+
+ r := &HelmReleaseReconciler{
+ Client: testEnv.Client,
+ APIReader: testEnv.Client,
+ GetClusterConfig: GetTestClusterConfig,
+ }
+
+ // 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())
+
+ // Reconcile the actual deletion of the Helm release.
+ obj.Spec.KubeConfig = &meta.KubeConfigReference{
+ SecretRef: &meta.SecretKeyReference{
+ Name: "missing-secret",
+ },
+ }
+ err = r.reconcileReleaseDeletion(context.TODO(), obj)
+ g.Expect(err).ToNot(HaveOccurred())
+
+ // Verify status of Helm release has not been updated.
+ g.Expect(obj.Status.StorageNamespace).ToNot(BeEmpty())
+ g.Expect(obj.Status.History.Latest()).ToNot(BeNil())
+
+ // Verify Helm release has not been uninstalled.
+ _, err = store.History(rls.Name)
+ g.Expect(err).ToNot(HaveOccurred())
+ })
+
+ t.Run("error when REST client getter construction fails", func(t *testing.T) {
+ g := NewWithT(t)
+
+ mockErr := errors.New("mock error")
+ r := &HelmReleaseReconciler{
+ Client: testEnv.Client,
+ GetClusterConfig: func() (*rest.Config, error) {
+ return nil, mockErr
+ },
+ }
+
+ obj := &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "reconcile-delete",
+ Namespace: "mock",
+ DeletionTimestamp: &metav1.Time{Time: time.Now()},
+ },
+ Status: v2.HelmReleaseStatus{
+ StorageNamespace: "mock",
+ },
+ }
+
+ // Reconcile the actual deletion of the Helm release.
+ err := r.reconcileReleaseDeletion(context.TODO(), obj)
+ g.Expect(errors.Is(err, mockErr)).To(BeTrue())
+ g.Expect(obj.Status.Conditions).To(conditions.MatchConditions([]metav1.Condition{
+ *conditions.FalseCondition(meta.ReadyCondition, v2.UninstallFailedReason,
+ "failed to build REST client getter to uninstall release"),
+ }))
+
+ // Verify status of Helm release has not been updated.
+ g.Expect(obj.Status.StorageNamespace).ToNot(BeEmpty())
+ })
+
+ t.Run("skip uninstalling Helm release when ServiceAccount is missing", func(t *testing.T) {
+ g := NewWithT(t)
+
+ // Create a test namespace for storing the Helm release mock.
+ ns, err := testEnv.CreateNamespace(context.TODO(), "reconcile-release-deletion")
+ g.Expect(err).ToNot(HaveOccurred())
+ t.Cleanup(func() {
+ _ = testEnv.Delete(context.TODO(), ns)
+ })
+
+ // Create a test Helm release storage mock.
+ rls := testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: "reconcile-delete",
+ Namespace: ns.Name,
+ Version: 1,
+ Chart: testutil.BuildChart(testutil.ChartWithTestHook()),
+ Status: helmreleasecommon.StatusDeployed,
+ })
+
+ obj := &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "reconcile-delete",
+ Namespace: ns.Name,
+ DeletionTimestamp: &metav1.Time{Time: time.Now()},
+ },
+ Status: v2.HelmReleaseStatus{
+ StorageNamespace: ns.Name,
+ History: v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(rls)),
+ },
+ },
+ }
+
+ r := &HelmReleaseReconciler{
+ Client: testEnv.Client,
+ APIReader: testEnv.Client,
+ GetClusterConfig: GetTestClusterConfig,
+ }
+
+ // 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())
+
+ // Reconcile the actual deletion of the Helm release.
+ obj.Spec.ServiceAccountName = "missing-sa"
+ err = r.reconcileReleaseDeletion(context.TODO(), obj)
+ g.Expect(err).ToNot(HaveOccurred())
+
+ // Verify status of Helm release has not been updated.
+ g.Expect(obj.Status.StorageNamespace).ToNot(BeEmpty())
+ g.Expect(obj.Status.History.Latest()).ToNot(BeNil())
+
+ // Verify Helm release has not been uninstalled.
+ _, err = store.History(rls.Name)
+ g.Expect(err).ToNot(HaveOccurred())
+ })
+
+ t.Run("error when ServiceAccount existence check fails", func(t *testing.T) {
+ g := NewWithT(t)
+
+ var (
+ serviceAccount = "missing-sa"
+ namespace = "mock"
+ mockErr = errors.New("mock error")
+ )
+
+ c := fake.NewClientBuilder().WithInterceptorFuncs(interceptor.Funcs{
+ Get: func(ctx context.Context, client client.WithWatch, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error {
+ if key.Name == serviceAccount && key.Namespace == namespace {
+ return mockErr
+ }
+ return client.Get(ctx, key, obj, opts...)
+ },
+ })
+
+ r := &HelmReleaseReconciler{
+ Client: c.Build(),
+ GetClusterConfig: GetTestClusterConfig,
+ }
+
+ obj := &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "reconcile-delete",
+ Namespace: namespace,
+ DeletionTimestamp: &metav1.Time{Time: time.Now()},
+ },
+ Spec: v2.HelmReleaseSpec{
+ ServiceAccountName: serviceAccount,
+ },
+ Status: v2.HelmReleaseStatus{
+ StorageNamespace: namespace,
+ },
+ }
+
+ // Reconcile the actual deletion of the Helm release.
+ err := r.reconcileReleaseDeletion(context.TODO(), obj)
+ g.Expect(errors.Is(err, mockErr)).To(BeTrue())
+ g.Expect(obj.Status.Conditions).To(conditions.MatchConditions([]metav1.Condition{
+ *conditions.FalseCondition(meta.ReadyCondition, v2.UninstallFailedReason,
+ "failed to confirm ServiceAccount '%s' can be used to uninstall release", serviceAccount),
+ }))
+
+ // Verify status of Helm release has not been updated.
+ g.Expect(obj.Status.StorageNamespace).ToNot(BeEmpty())
+ })
+
+ t.Run("error when Helm release uninstallation fails", func(t *testing.T) {
+ g := NewWithT(t)
+
+ r := &HelmReleaseReconciler{
+ Client: testEnv.Client,
+ GetClusterConfig: func() (*rest.Config, error) {
+ return &rest.Config{
+ Host: "https://failing-mock.local",
+ }, nil
+ },
+ EventRecorder: record.NewFakeRecorder(32),
+ }
+
+ obj := &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "reconcile-delete",
+ Namespace: "mock",
+ DeletionTimestamp: &metav1.Time{Time: time.Now()},
+ },
+ Status: v2.HelmReleaseStatus{
+ StorageNamespace: "mock",
+ History: v2.Snapshots{
+ {},
+ },
+ },
+ }
+
+ err := r.reconcileReleaseDeletion(context.TODO(), obj)
+ g.Expect(err).To(HaveOccurred())
+ g.Expect(obj.Status.Conditions).To(conditions.MatchConditions([]metav1.Condition{
+ *conditions.FalseCondition(meta.ReadyCondition, v2.UninstallFailedReason, "kubernetes cluster unreachable"),
+ *conditions.FalseCondition(v2.ReleasedCondition, v2.UninstallFailedReason, "kubernetes cluster unreachable"),
+ }))
+ })
+
+ t.Run("ignores ErrNoLatest when uninstalling Helm release", func(t *testing.T) {
+ g := NewWithT(t)
+
+ obj := &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "reconcile-delete",
+ Namespace: "mock",
+ DeletionTimestamp: &metav1.Time{Time: time.Now()},
+ },
+ Status: v2.HelmReleaseStatus{
+ StorageNamespace: "mock",
+ },
+ }
+
+ r := &HelmReleaseReconciler{
+ Client: testEnv.Client,
+ APIReader: testEnv.Client,
+ GetClusterConfig: GetTestClusterConfig,
+ }
+
+ // Reconcile the actual deletion of the Helm release.
+ err := r.reconcileReleaseDeletion(context.TODO(), obj)
+ g.Expect(err).ToNot(HaveOccurred())
+
+ // Verify status of Helm release been updated.
+ g.Expect(obj.Status.StorageNamespace).To(BeEmpty())
+ })
+
+ t.Run("error when DeletionTimestamp is not set", func(t *testing.T) {
+ g := NewWithT(t)
+
+ obj := &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "reconcile-delete",
+ Namespace: "mock",
+ },
+ }
+
+ err := (&HelmReleaseReconciler{}).reconcileReleaseDeletion(context.TODO(), obj)
+ g.Expect(err).To(HaveOccurred())
+ g.Expect(err.Error()).To(ContainSubstring("deletion timestamp is not set"))
+ })
+
+ t.Run("skip uninstalling Helm release when StorageNamespace is missing", func(t *testing.T) {
+ g := NewWithT(t)
+
+ obj := &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "reconcile-delete",
+ Namespace: "mock",
+ DeletionTimestamp: &metav1.Time{Time: time.Now()},
+ },
+ }
+
+ err := (&HelmReleaseReconciler{}).reconcileReleaseDeletion(context.TODO(), obj)
+ g.Expect(err).ToNot(HaveOccurred())
+ })
+}
+
+func TestHelmReleaseReconciler_reconcileChartTemplate(t *testing.T) {
+ t.Run("attempts to reconcile chart template", func(t *testing.T) {
+ g := NewWithT(t)
+
+ r := &HelmReleaseReconciler{
+ Client: fake.NewClientBuilder().WithScheme(NewTestScheme()).Build(),
+ EventRecorder: record.NewFakeRecorder(32),
+ }
+
+ obj := &v2.HelmRelease{
+ Spec: v2.HelmReleaseSpec{
+ Chart: &v2.HelmChartTemplate{},
+ },
+ Status: v2.HelmReleaseStatus{
+ StorageNamespace: "default",
+ },
+ }
+
+ // We do not care about the result of the reconcile, only that it was attempted.
+ err := r.reconcileChartTemplate(context.TODO(), obj)
+ g.Expect(err).To(HaveOccurred())
+ g.Expect(err.Error()).To(ContainSubstring("failed to run server-side apply"))
+ })
+}
+
+func TestHelmReleaseReconciler_reconcileUninstall(t *testing.T) {
+ t.Run("attempts to uninstall release", func(t *testing.T) {
+ g := NewWithT(t)
+
+ getter := kube.NewMemoryRESTClientGetter(testEnv.GetConfig())
+
+ obj := &v2.HelmRelease{
+ Status: v2.HelmReleaseStatus{
+ StorageNamespace: "default",
+ },
+ }
+
+ // We do not care about the result of the uninstall, only that it was attempted.
+ err := (&HelmReleaseReconciler{}).reconcileUninstall(context.TODO(), getter, obj)
+ g.Expect(err).To(HaveOccurred())
+ g.Expect(errors.Is(err, intreconcile.ErrNoLatest)).To(BeTrue())
+ })
+
+ t.Run("error on empty storage namespace", func(t *testing.T) {
+ g := NewWithT(t)
+
+ obj := &v2.HelmRelease{
+ Status: v2.HelmReleaseStatus{
+ StorageNamespace: "",
+ },
+ }
+
+ err := (&HelmReleaseReconciler{}).reconcileUninstall(context.TODO(), nil, obj)
+ g.Expect(err).To(HaveOccurred())
+
+ g.Expect(conditions.IsFalse(obj, meta.ReadyCondition)).To(BeTrue())
+ g.Expect(conditions.GetReason(obj, meta.ReadyCondition)).To(Equal("ConfigFactoryErr"))
+ g.Expect(conditions.GetMessage(obj, meta.ReadyCondition)).To(ContainSubstring("no namespace provided"))
+ g.Expect(obj.GetConditions()).To(HaveLen(1))
+ })
+
+ t.Run("error due to failing delete hook", func(t *testing.T) {
+ g := NewWithT(t)
+
+ ns, err := testEnv.CreateNamespace(context.TODO(), "reconcile-uninstall")
+ g.Expect(err).ToNot(HaveOccurred())
+ t.Cleanup(func() {
+ _ = testEnv.Delete(context.TODO(), ns)
+ })
+
+ rls := testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: "reconcile-uninstall",
+ Namespace: ns.Name,
+ Version: 1,
+ Chart: testutil.BuildChart(testutil.ChartWithFailingHook()),
+ Status: helmreleasecommon.StatusDeployed,
+ }, testutil.ReleaseWithFailingHook())
+
+ obj := &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "reconcile-uninstall",
+ Namespace: ns.Name,
+ DeletionTimestamp: &metav1.Time{Time: time.Now()},
+ },
+ Spec: v2.HelmReleaseSpec{
+ Uninstall: &v2.Uninstall{
+ KeepHistory: true,
+ Timeout: &metav1.Duration{Duration: time.Millisecond},
+ },
+ },
+ Status: v2.HelmReleaseStatus{
+ StorageNamespace: ns.Name,
+ History: v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(rls)),
+ },
+ },
+ }
+
+ r := &HelmReleaseReconciler{
+ Client: testEnv.Client,
+ APIReader: testEnv.Client,
+ 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())
+ err = r.reconcileUninstall(context.TODO(), getter, obj)
+ g.Expect(err).To(HaveOccurred())
+
+ // Verify status of Helm release has not been updated.
+ g.Expect(obj.Status.StorageNamespace).ToNot(BeEmpty())
+
+ // Verify Helm release has not been uninstalled.
+ _, err = store.History(rls.Name)
+ g.Expect(err).ToNot(HaveOccurred())
+ })
+}
+
+func TestHelmReleaseReconciler_checkDependencies(t *testing.T) {
tests := []struct {
- name string
- resources []runtime.Object
- references []v2.ValuesReference
- values string
- want chartutil.Values
- wantErr bool
+ name string
+ obj *v2.HelmRelease
+ objects []client.Object
+ expect func(g *WithT, err error)
}{
{
- name: "merges",
- resources: []runtime.Object{
- valuesConfigMap("values", map[string]string{
- "values.yaml": `flat: value
-nested:
- configuration: value
-`,
- }),
- valuesSecret("values", map[string][]byte{
- "values.yaml": []byte(`flat:
- nested: value
-nested: value
-`),
- }),
- },
- references: []v2.ValuesReference{
- {
- Kind: "ConfigMap",
- Name: "values",
+ name: "all dependencies ready",
+ obj: &v2.HelmRelease{
+ TypeMeta: metav1.TypeMeta{
+ APIVersion: v2.GroupVersion.String(),
+ Kind: v2.HelmReleaseKind,
},
- {
- Kind: "Secret",
- Name: "values",
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "dependant",
+ Namespace: "some-namespace",
+ },
+ Spec: v2.HelmReleaseSpec{
+ DependsOn: []v2.DependencyReference{
+ {
+ Name: "dependency-1",
+ },
+ {
+ Name: "dependency-2",
+ Namespace: "some-other-namespace",
+ },
+ },
+ },
+ },
+ objects: []client.Object{
+ &v2.HelmRelease{
+ TypeMeta: metav1.TypeMeta{
+ APIVersion: v2.GroupVersion.String(),
+ Kind: v2.HelmReleaseKind,
+ },
+ ObjectMeta: metav1.ObjectMeta{
+ Generation: 1,
+ Name: "dependency-1",
+ Namespace: "some-namespace",
+ },
+ Status: v2.HelmReleaseStatus{
+ ObservedGeneration: 1,
+ Conditions: []metav1.Condition{
+ {Type: meta.ReadyCondition, Status: metav1.ConditionTrue},
+ },
+ },
+ },
+ &v2.HelmRelease{
+ TypeMeta: metav1.TypeMeta{
+ APIVersion: v2.GroupVersion.String(),
+ Kind: v2.HelmReleaseKind,
+ },
+ ObjectMeta: metav1.ObjectMeta{
+ Generation: 2,
+ Name: "dependency-2",
+ Namespace: "some-other-namespace",
+ },
+ Status: v2.HelmReleaseStatus{
+ ObservedGeneration: 2,
+ Conditions: []metav1.Condition{
+ {Type: meta.ReadyCondition, Status: metav1.ConditionTrue},
+ },
+ },
},
},
- values: `
-other: values
+ expect: func(g *WithT, err error) {
+ g.Expect(err).ToNot(HaveOccurred())
+ },
+ },
+ {
+ name: "all dependencies ready with readyExpr",
+ obj: &v2.HelmRelease{
+ TypeMeta: metav1.TypeMeta{
+ APIVersion: v2.GroupVersion.String(),
+ Kind: v2.HelmReleaseKind,
+ },
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "dependant",
+ Namespace: "some-namespace",
+ Labels: map[string]string{
+ "app/version": "v1.2.3",
+ },
+ },
+ Spec: v2.HelmReleaseSpec{
+ Values: &apiextensionsv1.JSON{
+ Raw: []byte(`{"version":"v1.2.3"}`),
+ },
+ DependsOn: []v2.DependencyReference{
+ {
+ Name: "dependency-1",
+ ReadyExpr: "self.spec.values.version == dep.spec.values.version",
+ },
+ {
+ Name: "dependency-2",
+ ReadyExpr: `
+dep.metadata.labels['app/version'] == self.metadata.labels['app/version'] &&
+dep.status.conditions.filter(e, e.type == 'Ready').all(e, e.status == 'True') &&
+dep.metadata.generation == dep.status.observedGeneration
`,
- want: chartutil.Values{
- "flat": map[string]interface{}{
- "nested": "value",
+ },
+ },
+ },
+ },
+ objects: []client.Object{
+ &v2.HelmRelease{
+ TypeMeta: metav1.TypeMeta{
+ APIVersion: v2.GroupVersion.String(),
+ Kind: v2.HelmReleaseKind,
+ },
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "dependency-1",
+ Namespace: "some-namespace",
+ },
+ Spec: v2.HelmReleaseSpec{
+ Values: &apiextensionsv1.JSON{
+ Raw: []byte(`{"version":"v1.2.3"}`),
+ },
+ },
+ },
+ &v2.HelmRelease{
+ TypeMeta: metav1.TypeMeta{
+ APIVersion: v2.GroupVersion.String(),
+ Kind: v2.HelmReleaseKind,
+ },
+ ObjectMeta: metav1.ObjectMeta{
+ Generation: 2,
+ Name: "dependency-2",
+ Namespace: "some-namespace",
+ Labels: map[string]string{
+ "app/version": "v1.2.3",
+ },
+ },
+ Status: v2.HelmReleaseStatus{
+ ObservedGeneration: 2,
+ Conditions: []metav1.Condition{
+ {Type: meta.ReadyCondition, Status: metav1.ConditionTrue},
+ },
+ },
},
- "nested": "value",
- "other": "values",
+ },
+ expect: func(g *WithT, err error) {
+ g.Expect(err).ToNot(HaveOccurred())
},
},
{
- name: "target path",
- resources: []runtime.Object{
- valuesSecret("values", map[string][]byte{"single": []byte("value")}),
+ name: "error on dependency with ObservedGeneration < Generation",
+ obj: &v2.HelmRelease{
+ TypeMeta: metav1.TypeMeta{
+ APIVersion: v2.GroupVersion.String(),
+ Kind: v2.HelmReleaseKind,
+ },
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "dependant",
+ Namespace: "some-namespace",
+ },
+ Spec: v2.HelmReleaseSpec{
+ DependsOn: []v2.DependencyReference{
+ {
+ Name: "dependency-1",
+ },
+ },
+ },
},
- references: []v2.ValuesReference{
- {
- Kind: "Secret",
- Name: "values",
- ValuesKey: "single",
- TargetPath: "merge.at.specific.path",
+ objects: []client.Object{
+ &v2.HelmRelease{
+ TypeMeta: metav1.TypeMeta{
+ APIVersion: v2.GroupVersion.String(),
+ Kind: v2.HelmReleaseKind,
+ },
+ ObjectMeta: metav1.ObjectMeta{
+ Generation: 2,
+ Name: "dependency-1",
+ Namespace: "some-namespace",
+ },
+ Status: v2.HelmReleaseStatus{
+ ObservedGeneration: 1,
+ Conditions: []metav1.Condition{
+ {Type: meta.ReadyCondition, Status: metav1.ConditionTrue},
+ },
+ },
+ },
+ },
+ expect: func(g *WithT, err error) {
+ g.Expect(err).To(HaveOccurred())
+ g.Expect(err.Error()).To(ContainSubstring("is not ready"))
+ },
+ },
+ {
+ name: "error on dependency with ObservedGeneration = Generation and ReadyCondition = False",
+ obj: &v2.HelmRelease{
+ TypeMeta: metav1.TypeMeta{
+ APIVersion: v2.GroupVersion.String(),
+ Kind: v2.HelmReleaseKind,
+ },
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "dependant",
+ Namespace: "some-namespace",
+ },
+ Spec: v2.HelmReleaseSpec{
+ DependsOn: []v2.DependencyReference{
+ {
+ Name: "dependency-1",
+ },
+ },
+ },
+ },
+ objects: []client.Object{
+ &v2.HelmRelease{
+ TypeMeta: metav1.TypeMeta{
+ APIVersion: v2.GroupVersion.String(),
+ Kind: v2.HelmReleaseKind,
+ },
+ ObjectMeta: metav1.ObjectMeta{
+ Generation: 1,
+ Name: "dependency-1",
+ Namespace: "some-namespace",
+ },
+ Status: v2.HelmReleaseStatus{
+ ObservedGeneration: 1,
+ Conditions: []metav1.Condition{
+ {Type: meta.ReadyCondition, Status: metav1.ConditionFalse},
+ },
+ },
+ },
+ },
+ expect: func(g *WithT, err error) {
+ g.Expect(err).To(HaveOccurred())
+ g.Expect(err.Error()).To(ContainSubstring("is not ready"))
+ },
+ },
+ {
+ name: "error on dependency without conditions",
+ obj: &v2.HelmRelease{
+ TypeMeta: metav1.TypeMeta{
+ APIVersion: v2.GroupVersion.String(),
+ Kind: v2.HelmReleaseKind,
+ },
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "dependant",
+ Namespace: "some-namespace",
+ },
+ Spec: v2.HelmReleaseSpec{
+ DependsOn: []v2.DependencyReference{
+ {
+ Name: "dependency-1",
+ },
+ },
+ },
+ },
+ objects: []client.Object{
+ &v2.HelmRelease{
+ TypeMeta: metav1.TypeMeta{
+ APIVersion: v2.GroupVersion.String(),
+ Kind: v2.HelmReleaseKind,
+ },
+ ObjectMeta: metav1.ObjectMeta{
+ Generation: 1,
+ Name: "dependency-1",
+ Namespace: "some-namespace",
+ },
+ Status: v2.HelmReleaseStatus{
+ ObservedGeneration: 1,
+ },
+ },
+ },
+ expect: func(g *WithT, err error) {
+ g.Expect(err).To(HaveOccurred())
+ g.Expect(err.Error()).To(ContainSubstring("is not ready"))
+ },
+ },
+ {
+ name: "error on missing dependency",
+ obj: &v2.HelmRelease{
+ TypeMeta: metav1.TypeMeta{
+ APIVersion: v2.GroupVersion.String(),
+ Kind: v2.HelmReleaseKind,
+ },
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "dependant",
+ Namespace: "some-namespace",
+ },
+ Spec: v2.HelmReleaseSpec{
+ DependsOn: []v2.DependencyReference{
+ {
+ Name: "dependency-1",
+ },
+ },
+ },
+ },
+ expect: func(g *WithT, err error) {
+ g.Expect(err).To(HaveOccurred())
+ g.Expect(apierrors.IsNotFound(err)).To(BeTrue())
+ },
+ },
+ {
+ name: "error on dependency with readyExpr",
+ obj: &v2.HelmRelease{
+ TypeMeta: metav1.TypeMeta{
+ APIVersion: v2.GroupVersion.String(),
+ Kind: v2.HelmReleaseKind,
+ },
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "dependant",
+ Namespace: "some-namespace",
+ },
+ Spec: v2.HelmReleaseSpec{
+ DependsOn: []v2.DependencyReference{
+ {
+ Name: "dependency-1",
+ ReadyExpr: "self.metadata.name == dep.metadata.name",
+ },
+ },
+ },
+ },
+ objects: []client.Object{
+ &v2.HelmRelease{
+ TypeMeta: metav1.TypeMeta{
+ APIVersion: v2.GroupVersion.String(),
+ Kind: v2.HelmReleaseKind,
+ },
+ ObjectMeta: metav1.ObjectMeta{
+ Generation: 1,
+ Name: "dependency-1",
+ Namespace: "some-namespace",
+ },
+ Status: v2.HelmReleaseStatus{
+ ObservedGeneration: 1,
+ },
},
},
- want: chartutil.Values{
- "merge": map[string]interface{}{
- "at": map[string]interface{}{
- "specific": map[string]interface{}{
- "path": "value",
+ expect: func(g *WithT, err error) {
+ g.Expect(err).To(HaveOccurred())
+ g.Expect(err.Error()).To(ContainSubstring("is not ready according to readyExpr eval"))
+ },
+ },
+ {
+ name: "terminal error on dependency with invalid readyExpr",
+ obj: &v2.HelmRelease{
+ TypeMeta: metav1.TypeMeta{
+ APIVersion: v2.GroupVersion.String(),
+ Kind: v2.HelmReleaseKind,
+ },
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "dependant",
+ Namespace: "some-namespace",
+ },
+ Spec: v2.HelmReleaseSpec{
+ DependsOn: []v2.DependencyReference{
+ {
+ Name: "dependency-1",
+ ReadyExpr: "self.generation == deps.generation",
},
},
},
},
+ objects: []client.Object{
+ &v2.HelmRelease{
+ TypeMeta: metav1.TypeMeta{
+ APIVersion: v2.GroupVersion.String(),
+ Kind: v2.HelmReleaseKind,
+ },
+ ObjectMeta: metav1.ObjectMeta{
+ Generation: 1,
+ Name: "dependency-1",
+ Namespace: "some-namespace",
+ },
+ Status: v2.HelmReleaseStatus{
+ ObservedGeneration: 1,
+ },
+ },
+ },
+ expect: func(g *WithT, err error) {
+ g.Expect(err).To(HaveOccurred())
+ g.Expect(errors.Is(err, reconcile.TerminalError(nil))).To(BeTrue())
+ g.Expect(err.Error()).To(ContainSubstring("failed to parse"))
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ g := NewWithT(t)
+
+ c := fake.NewClientBuilder().
+ WithScheme(NewTestScheme()).
+ WithStatusSubresource(&v2.HelmRelease{})
+ if len(tt.objects) > 0 {
+ c.WithObjects(tt.objects...)
+ }
+
+ fakeClient := c.Build()
+ r := &HelmReleaseReconciler{
+ Client: fakeClient,
+ APIReader: fakeClient,
+ }
+
+ err := r.checkDependencies(context.TODO(), tt.obj)
+ tt.expect(g, err)
+ })
+ }
+}
+
+func TestHelmReleaseReconciler_buildRESTClientGetter(t *testing.T) {
+ const (
+ namespace = "some-namespace"
+ kubeCfg = `apiVersion: v1
+kind: Config
+clusters:
+- cluster:
+ insecure-skip-tls-verify: true
+ server: https://1.2.3.4
+ name: development
+contexts:
+- context:
+ cluster: development
+ namespace: frontend
+ user: developer
+ name: dev-frontend
+current-context: dev-frontend
+preferences: {}
+users:
+- name: developer
+ user:
+ password: some-password
+ username: exp`
+ )
+
+ tests := []struct {
+ name string
+ env map[string]string
+ getConfig func() (*rest.Config, error)
+ spec v2.HelmReleaseSpec
+ secret *corev1.Secret
+ want genericclioptions.RESTClientGetter
+ wantErr string
+ }{
+ {
+ name: "builds in-cluster RESTClientGetter for HelmRelease",
+ getConfig: func() (*rest.Config, error) {
+ return clientcmd.RESTConfigFromKubeConfig([]byte(kubeCfg))
+ },
+ spec: v2.HelmReleaseSpec{},
+ want: &kube.MemoryRESTClientGetter{},
+ },
+ {
+ name: "returns error when in-cluster GetClusterConfig fails",
+ getConfig: func() (*rest.Config, error) {
+ return nil, errors.New("some-error")
+ },
+ wantErr: "some-error",
+ },
+ {
+ name: "builds RESTClientGetter from HelmRelease with KubeConfig",
+ spec: v2.HelmReleaseSpec{
+ KubeConfig: &meta.KubeConfigReference{
+ SecretRef: &meta.SecretKeyReference{
+ Name: "kubeconfig",
+ },
+ },
+ },
+ secret: &corev1.Secret{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "kubeconfig",
+ Namespace: namespace,
+ },
+ Data: map[string][]byte{
+ kube.DefaultKubeConfigSecretKey: []byte(kubeCfg),
+ },
+ },
+ want: &kube.MemoryRESTClientGetter{},
},
{
- name: "target path with boolean value",
- resources: []runtime.Object{
- valuesSecret("values", map[string][]byte{"single": []byte("true")}),
- },
- references: []v2.ValuesReference{
- {
- Kind: "Secret",
- Name: "values",
- ValuesKey: "single",
- TargetPath: "merge.at.specific.path",
- },
- },
- want: chartutil.Values{
- "merge": map[string]interface{}{
- "at": map[string]interface{}{
- "specific": map[string]interface{}{
- "path": true,
- },
+ name: "error on missing KubeConfig secret",
+ spec: v2.HelmReleaseSpec{
+ KubeConfig: &meta.KubeConfigReference{
+ SecretRef: &meta.SecretKeyReference{
+ Name: "kubeconfig",
},
},
},
+ wantErr: "could not get KubeConfig secret",
},
{
- name: "target path with set-string behavior",
- resources: []runtime.Object{
- valuesSecret("values", map[string][]byte{"single": []byte("\"true\"")}),
- },
- references: []v2.ValuesReference{
- {
- Kind: "Secret",
- Name: "values",
- ValuesKey: "single",
- TargetPath: "merge.at.specific.path",
+ name: "error on invalid KubeConfig secret",
+ spec: v2.HelmReleaseSpec{
+ KubeConfig: &meta.KubeConfigReference{
+ SecretRef: &meta.SecretKeyReference{
+ Name: "kubeconfig",
+ Key: "invalid-key",
+ },
},
},
- want: chartutil.Values{
- "merge": map[string]interface{}{
- "at": map[string]interface{}{
- "specific": map[string]interface{}{
- "path": "true",
- },
- },
+ secret: &corev1.Secret{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "kubeconfig",
+ Namespace: namespace,
},
},
+ wantErr: "does not contain a 'invalid-key' key",
},
- {
- name: "values reference to non existing secret",
- references: []v2.ValuesReference{
- {
- Kind: "Secret",
- Name: "missing",
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ g := NewWithT(t)
+
+ for k, v := range tt.env {
+ t.Setenv(k, v)
+ }
+
+ c := fake.NewClientBuilder()
+ if tt.secret != nil {
+ c.WithObjects(tt.secret)
+ }
+
+ r := &HelmReleaseReconciler{
+ Client: c.Build(),
+ GetClusterConfig: tt.getConfig,
+ }
+
+ getter, err := r.buildRESTClientGetter(context.Background(), &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "some-name",
+ Namespace: namespace,
},
- },
- wantErr: true,
+ Spec: tt.spec,
+ })
+ if len(tt.wantErr) > 0 {
+ g.Expect(err).To(HaveOccurred())
+ g.Expect(err.Error()).To(ContainSubstring(tt.wantErr))
+ } else {
+ g.Expect(err).ToNot(HaveOccurred())
+ g.Expect(getter).To(BeAssignableToTypeOf(tt.want))
+ }
+ })
+ }
+}
+
+func TestHelmReleaseReconciler_getHelmChart(t *testing.T) {
+ g := NewWithT(t)
+
+ chart := &sourcev1.HelmChart{
+ ObjectMeta: metav1.ObjectMeta{
+ Namespace: "some-namespace",
+ Name: "some-chart-name",
},
+ }
+
+ tests := []struct {
+ name string
+ rel *v2.HelmRelease
+ chart *sourcev1.HelmChart
+ expectChart bool
+ wantErr bool
+ disallowCrossNS bool
+ }{
{
- name: "optional values reference to non existing secret",
- references: []v2.ValuesReference{
- {
- Kind: "Secret",
- Name: "missing",
- Optional: true,
+ name: "retrieves HelmChart object from Status",
+ rel: &v2.HelmRelease{
+ Status: v2.HelmReleaseStatus{
+ HelmChart: "some-namespace/some-chart-name",
},
},
- want: chartutil.Values{},
- wantErr: false,
+ chart: chart,
+ expectChart: true,
},
{
- name: "values reference to non existing config map",
- references: []v2.ValuesReference{
- {
- Kind: "ConfigMap",
- Name: "missing",
+ name: "no HelmChart found",
+ rel: &v2.HelmRelease{
+ Status: v2.HelmReleaseStatus{
+ HelmChart: "some-namespace/some-chart-name",
},
},
- wantErr: true,
+ chart: nil,
+ expectChart: false,
+ wantErr: true,
},
{
- name: "optional values reference to non existing config map",
- references: []v2.ValuesReference{
- {
- Kind: "ConfigMap",
- Name: "missing",
- Optional: true,
+ name: "no HelmChart in Status",
+ rel: &v2.HelmRelease{
+ Status: v2.HelmReleaseStatus{
+ HelmChart: "",
},
},
- want: chartutil.Values{},
- wantErr: false,
+ chart: chart,
+ expectChart: false,
+ wantErr: true,
},
{
- name: "missing secret key",
- resources: []runtime.Object{
- valuesSecret("values", nil),
- },
- references: []v2.ValuesReference{
- {
- Kind: "Secret",
- Name: "values",
- ValuesKey: "nonexisting",
+ name: "ACL disallows cross namespace",
+ rel: &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Namespace: "default",
+ },
+ Status: v2.HelmReleaseStatus{
+ HelmChart: "some-namespace/some-chart-name",
},
},
- wantErr: true,
+ chart: chart,
+ expectChart: false,
+ wantErr: true,
+ disallowCrossNS: true,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ c := fake.NewClientBuilder()
+ c.WithScheme(NewTestScheme())
+ if tt.chart != nil {
+ c.WithObjects(tt.chart)
+ }
+
+ r := &HelmReleaseReconciler{
+ Client: c.Build(),
+ EventRecorder: record.NewFakeRecorder(32),
+ }
+
+ curAllow := intacl.AllowCrossNamespaceRef
+ intacl.AllowCrossNamespaceRef = !tt.disallowCrossNS
+ t.Cleanup(func() { intacl.AllowCrossNamespaceRef = !curAllow })
+
+ got, err := r.getSource(context.TODO(), tt.rel)
+ if tt.wantErr {
+ g.Expect(err).To(HaveOccurred())
+ g.Expect(got).To(BeNil())
+ return
+ }
+ g.Expect(err).ToNot(HaveOccurred())
+ hc, ok := got.(*sourcev1.HelmChart)
+ g.Expect(ok).To(BeTrue())
+ expect := g.Expect(hc.ObjectMeta)
+ if tt.expectChart {
+ expect.To(BeEquivalentTo(tt.chart.ObjectMeta))
+ } else {
+ expect.To(BeNil())
+ }
+ })
+ }
+}
+
+func TestHelmReleaseReconciler_getSourceClient(t *testing.T) {
+ g := NewWithT(t)
+
+ // Create a fake client and a separate fake API reader
+ fakeClient := fake.NewClientBuilder().WithScheme(NewTestScheme()).Build()
+ fakeAPIReader := fake.NewClientBuilder().WithScheme(NewTestScheme()).Build()
+
+ tests := []struct {
+ name string
+ directSourceFetch bool
+ wantAPIReader bool
+ }{
+ {
+ name: "returns Client when DirectSourceFetch is disabled",
+ directSourceFetch: false,
+ wantAPIReader: false,
+ },
+ {
+ name: "returns APIReader when DirectSourceFetch is enabled",
+ directSourceFetch: true,
+ wantAPIReader: true,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ r := &HelmReleaseReconciler{
+ Client: fakeClient,
+ APIReader: fakeAPIReader,
+ DirectSourceFetch: tt.directSourceFetch,
+ }
+
+ got := r.getSourceClient()
+ if tt.wantAPIReader {
+ g.Expect(got).To(BeIdenticalTo(fakeAPIReader))
+ } else {
+ g.Expect(got).To(BeIdenticalTo(fakeClient))
+ }
+ })
+ }
+}
+
+func TestHelmReleaseReconciler_getSourceFromOCIRef_DirectSourceFetch(t *testing.T) {
+ g := NewWithT(t)
+
+ ociRepo := &sourcev1.OCIRepository{
+ ObjectMeta: metav1.ObjectMeta{
+ Namespace: "default",
+ Name: "test-oci-repo",
+ },
+ }
+
+ tests := []struct {
+ name string
+ directSourceFetch bool
+ repoInClient bool
+ repoInAPIReader bool
+ wantErr bool
+ }{
+ {
+ name: "uses Client when DirectSourceFetch is disabled",
+ directSourceFetch: false,
+ repoInClient: true,
+ repoInAPIReader: false,
+ wantErr: false,
+ },
+ {
+ name: "uses APIReader when DirectSourceFetch is enabled",
+ directSourceFetch: true,
+ repoInClient: false,
+ repoInAPIReader: true,
+ wantErr: false,
},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ clientBuilder := fake.NewClientBuilder().WithScheme(NewTestScheme())
+ if tt.repoInClient {
+ clientBuilder.WithObjects(ociRepo.DeepCopy())
+ }
+ fakeClient := clientBuilder.Build()
+
+ apiReaderBuilder := fake.NewClientBuilder().WithScheme(NewTestScheme())
+ if tt.repoInAPIReader {
+ apiReaderBuilder.WithObjects(ociRepo.DeepCopy())
+ }
+ fakeAPIReader := apiReaderBuilder.Build()
+
+ r := &HelmReleaseReconciler{
+ Client: fakeClient,
+ APIReader: fakeAPIReader,
+ DirectSourceFetch: tt.directSourceFetch,
+ EventRecorder: record.NewFakeRecorder(32),
+ }
+
+ rel := &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Namespace: "default",
+ },
+ Spec: v2.HelmReleaseSpec{
+ ChartRef: &v2.CrossNamespaceSourceReference{
+ Kind: sourcev1.OCIRepositoryKind,
+ Name: "test-oci-repo",
+ Namespace: "default",
+ },
+ },
+ }
+
+ got, err := r.getSource(context.TODO(), rel)
+ if tt.wantErr {
+ g.Expect(err).To(HaveOccurred())
+ g.Expect(got).To(BeNil())
+ return
+ }
+ g.Expect(err).ToNot(HaveOccurred())
+ g.Expect(got).ToNot(BeNil())
+ or, ok := got.(*sourcev1.OCIRepository)
+ g.Expect(ok).To(BeTrue())
+ g.Expect(or.Name).To(Equal(ociRepo.Name))
+ g.Expect(or.Namespace).To(Equal(ociRepo.Namespace))
+ })
+ }
+}
+
+func Test_waitForHistoryCacheSync(t *testing.T) {
+ tests := []struct {
+ name string
+ rel *v2.HelmRelease
+ cacheRel *v2.HelmRelease
+ want bool
+ }{
{
- name: "missing config map key",
- resources: []runtime.Object{
- valuesConfigMap("values", nil),
+ name: "different history",
+ rel: &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "some-name",
+ },
+ Status: v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ {
+ Version: 2,
+ Status: "deployed",
+ },
+ {
+ Version: 1,
+ Status: "failed",
+ },
+ },
+ },
},
- references: []v2.ValuesReference{
- {
- Kind: "ConfigMap",
- Name: "values",
- ValuesKey: "nonexisting",
+ cacheRel: &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "some-name",
+ },
+ Status: v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ {
+ Version: 1,
+ Status: "deployed",
+ },
+ },
},
},
- wantErr: true,
+ want: false,
},
{
- name: "unsupported values reference kind",
- references: []v2.ValuesReference{
- {
- Kind: "Unsupported",
+ name: "same history",
+ rel: &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "some-name",
+ },
+ Status: v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ {
+ Version: 2,
+ Status: "deployed",
+ },
+ {
+ Version: 1,
+ Status: "failed",
+ },
+ },
},
},
- wantErr: true,
+ cacheRel: &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "some-name",
+ },
+ Status: v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ {
+ Version: 2,
+ Status: "deployed",
+ },
+ {
+ Version: 1,
+ Status: "failed",
+ },
+ },
+ },
+ },
+ want: true,
},
{
- name: "invalid values",
- resources: []runtime.Object{
- valuesConfigMap("values", map[string]string{
- "values.yaml": `
-invalid`,
- }),
- },
- references: []v2.ValuesReference{
- {
- Kind: "ConfigMap",
- Name: "values",
+ name: "does not exist",
+ rel: &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "some-name",
},
},
- wantErr: true,
+ cacheRel: nil,
+ want: true,
},
}
-
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- c := fake.NewFakeClientWithScheme(scheme, tt.resources...)
- r := &HelmReleaseReconciler{Client: c}
- var values *apiextensionsv1.JSON
- if tt.values != "" {
- v, _ := yaml.YAMLToJSON([]byte(tt.values))
- values = &apiextensionsv1.JSON{Raw: v}
- }
- hr := v2.HelmRelease{
- Spec: v2.HelmReleaseSpec{
- ValuesFrom: tt.references,
- Values: values,
- },
- }
- got, err := r.composeValues(logr.NewContext(context.TODO(), logr.Discard()), hr)
- if (err != nil) != tt.wantErr {
- t.Errorf("composeValues() error = %v, wantErr %v", err, tt.wantErr)
- return
+ g := NewWithT(t)
+
+ c := fake.NewClientBuilder()
+ c.WithScheme(NewTestScheme())
+ if tt.cacheRel != nil {
+ c.WithObjects(tt.cacheRel)
}
- if !reflect.DeepEqual(got, tt.want) {
- t.Errorf("composeValues() got = %v, want %v", got, tt.want)
+ r := &HelmReleaseReconciler{
+ Client: c.Build(),
}
+
+ got, err := r.waitForHistoryCacheSync(tt.rel)(context.Background())
+ g.Expect(err).ToNot(HaveOccurred())
+ g.Expect(got == tt.want).To(BeTrue())
})
}
}
@@ -284,12 +3669,12 @@ invalid`,
func TestValuesReferenceValidation(t *testing.T) {
tests := []struct {
name string
- references []v2.ValuesReference
+ references []meta.ValuesReference
wantErr bool
}{
{
name: "valid ValuesKey",
- references: []v2.ValuesReference{
+ references: []meta.ValuesReference{
{
Kind: "Secret",
Name: "values",
@@ -300,7 +3685,7 @@ func TestValuesReferenceValidation(t *testing.T) {
},
{
name: "valid ValuesKey: empty",
- references: []v2.ValuesReference{
+ references: []meta.ValuesReference{
{
Kind: "Secret",
Name: "values",
@@ -311,7 +3696,7 @@ func TestValuesReferenceValidation(t *testing.T) {
},
{
name: "valid ValuesKey: long",
- references: []v2.ValuesReference{
+ references: []meta.ValuesReference{
{
Kind: "Secret",
Name: "values",
@@ -322,7 +3707,7 @@ func TestValuesReferenceValidation(t *testing.T) {
},
{
name: "invalid ValuesKey",
- references: []v2.ValuesReference{
+ references: []meta.ValuesReference{
{
Kind: "Secret",
Name: "values",
@@ -333,7 +3718,7 @@ func TestValuesReferenceValidation(t *testing.T) {
},
{
name: "invalid ValuesKey: too long",
- references: []v2.ValuesReference{
+ references: []meta.ValuesReference{
{
Kind: "Secret",
Name: "values",
@@ -344,7 +3729,7 @@ func TestValuesReferenceValidation(t *testing.T) {
},
{
name: "valid target path: empty",
- references: []v2.ValuesReference{
+ references: []meta.ValuesReference{
{
Kind: "Secret",
Name: "values",
@@ -355,7 +3740,7 @@ func TestValuesReferenceValidation(t *testing.T) {
},
{
name: "valid target path",
- references: []v2.ValuesReference{
+ references: []meta.ValuesReference{
{
Kind: "Secret",
Name: "values",
@@ -366,7 +3751,7 @@ func TestValuesReferenceValidation(t *testing.T) {
},
{
name: "valid target path: long",
- references: []v2.ValuesReference{
+ references: []meta.ValuesReference{
{
Kind: "Secret",
Name: "values",
@@ -377,7 +3762,7 @@ func TestValuesReferenceValidation(t *testing.T) {
},
{
name: "invalid target path: too long",
- references: []v2.ValuesReference{
+ references: []meta.ValuesReference{
{
Kind: "Secret",
Name: "values",
@@ -388,7 +3773,7 @@ func TestValuesReferenceValidation(t *testing.T) {
},
{
name: "invalid target path: opened index",
- references: []v2.ValuesReference{
+ references: []meta.ValuesReference{
{
Kind: "Secret",
Name: "values",
@@ -400,7 +3785,7 @@ func TestValuesReferenceValidation(t *testing.T) {
},
{
name: "invalid target path: incorrect index syntax",
- references: []v2.ValuesReference{
+ references: []meta.ValuesReference{
{
Kind: "Secret",
Name: "values",
@@ -425,10 +3810,12 @@ func TestValuesReferenceValidation(t *testing.T) {
},
Spec: v2.HelmReleaseSpec{
Interval: metav1.Duration{Duration: 5 * time.Minute},
- Chart: v2.HelmChartTemplate{
+ Chart: &v2.HelmChartTemplate{
Spec: v2.HelmChartTemplateSpec{
+ Chart: "mychart",
SourceRef: v2.CrossNamespaceObjectReference{
Name: "something",
+ Kind: "HelmRepository",
},
},
},
@@ -437,7 +3824,7 @@ func TestValuesReferenceValidation(t *testing.T) {
},
}
- err := k8sClient.Create(context.TODO(), &hr, client.DryRunAll)
+ err := testEnv.Create(context.TODO(), &hr, client.DryRunAll)
if (err != nil) != tt.wantErr {
t.Errorf("composeValues() error = %v, wantErr %v", err, tt.wantErr)
return
@@ -446,16 +3833,251 @@ func TestValuesReferenceValidation(t *testing.T) {
}
}
-func valuesSecret(name string, data map[string][]byte) *corev1.Secret {
- return &corev1.Secret{
- ObjectMeta: metav1.ObjectMeta{Name: name},
- Data: data,
+func Test_isHelmChartReady(t *testing.T) {
+ mock := &sourcev1.HelmChart{
+ TypeMeta: metav1.TypeMeta{
+ Kind: "HelmChart",
+ APIVersion: sourcev1.GroupVersion.String(),
+ },
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "mock",
+ Namespace: "default",
+ Generation: 2,
+ },
+ Status: sourcev1.HelmChartStatus{
+ ObservedGeneration: 2,
+ Conditions: []metav1.Condition{
+ {
+ Type: meta.ReadyCondition,
+ Status: metav1.ConditionTrue,
+ },
+ },
+ Artifact: &meta.Artifact{},
+ },
+ }
+
+ tests := []struct {
+ name string
+ obj *sourcev1.HelmChart
+ want bool
+ wantReason string
+ }{
+ {
+ name: "chart is ready",
+ obj: mock.DeepCopy(),
+ want: true,
+ },
+ {
+ name: "chart generation differs from observed generation while Ready=True",
+ obj: func() *sourcev1.HelmChart {
+ m := mock.DeepCopy()
+ m.Generation = 3
+ return m
+ }(),
+ want: false,
+ wantReason: "HelmChart 'default/mock' is not ready: latest generation of object has not been reconciled",
+ },
+ {
+ name: "chart generation differs from observed generation while Ready=False",
+ obj: func() *sourcev1.HelmChart {
+ m := mock.DeepCopy()
+ m.Generation = 3
+ conditions.MarkFalse(m, meta.ReadyCondition, "Reason", "some reason")
+ return m
+ }(),
+ want: false,
+ wantReason: "HelmChart 'default/mock' is not ready: some reason",
+ },
+ {
+ name: "chart has Stalled=True",
+ obj: func() *sourcev1.HelmChart {
+ m := mock.DeepCopy()
+ conditions.MarkFalse(m, meta.ReadyCondition, "Reason", "some reason")
+ conditions.MarkStalled(m, "Reason", "some stalled reason")
+ return m
+ }(),
+ want: false,
+ wantReason: "HelmChart 'default/mock' is not ready: some stalled reason",
+ },
+ {
+ name: "chart does not have an Artifact",
+ obj: func() *sourcev1.HelmChart {
+ m := mock.DeepCopy()
+ m.Status.Artifact = nil
+ return m
+ }(),
+ want: false,
+ wantReason: "HelmChart 'default/mock' is not ready: does not have an artifact",
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got, gotReason := isReady(tt.obj, tt.obj.GetArtifact())
+ if got != tt.want {
+ t.Errorf("isHelmChartReady() got = %v, want %v", got, tt.want)
+ }
+ if gotReason != tt.wantReason {
+ t.Errorf("isHelmChartReady() reason = %v, want %v", gotReason, tt.wantReason)
+ }
+ })
+ }
+}
+
+func Test_isOCIRepositoryReady(t *testing.T) {
+ mock := &sourcev1.OCIRepository{
+ TypeMeta: metav1.TypeMeta{
+ Kind: sourcev1.OCIRepositoryKind,
+ APIVersion: sourcev1.GroupVersion.String(),
+ },
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "mock",
+ Namespace: "default",
+ Generation: 2,
+ },
+ Status: sourcev1.OCIRepositoryStatus{
+ ObservedGeneration: 2,
+ Conditions: []metav1.Condition{
+ {
+ Type: meta.ReadyCondition,
+ Status: metav1.ConditionTrue,
+ },
+ },
+ Artifact: &meta.Artifact{},
+ },
+ }
+
+ tests := []struct {
+ name string
+ obj *sourcev1.OCIRepository
+ want bool
+ wantReason string
+ }{
+ {
+ name: "OCIRepository is ready",
+ obj: mock.DeepCopy(),
+ want: true,
+ },
+ {
+ name: "OCIRepository generation differs from observed generation while Ready=True",
+ obj: func() *sourcev1.OCIRepository {
+ m := mock.DeepCopy()
+ m.Generation = 3
+ return m
+ }(),
+ want: false,
+ wantReason: "OCIRepository 'default/mock' is not ready: latest generation of object has not been reconciled",
+ },
+ {
+ name: "OCIRepository generation differs from observed generation while Ready=False",
+ obj: func() *sourcev1.OCIRepository {
+ m := mock.DeepCopy()
+ m.Generation = 3
+ conditions.MarkFalse(m, meta.ReadyCondition, "Reason", "some reason")
+ return m
+ }(),
+ want: false,
+ wantReason: "OCIRepository 'default/mock' is not ready: some reason",
+ },
+ {
+ name: "OCIRepository has Stalled=True",
+ obj: func() *sourcev1.OCIRepository {
+ m := mock.DeepCopy()
+ conditions.MarkFalse(m, meta.ReadyCondition, "Reason", "some reason")
+ conditions.MarkStalled(m, "Reason", "some stalled reason")
+ return m
+ }(),
+ want: false,
+ wantReason: "OCIRepository 'default/mock' is not ready: some stalled reason",
+ },
+ {
+ name: "OCIRepository does not have an Artifact",
+ obj: func() *sourcev1.OCIRepository {
+ m := mock.DeepCopy()
+ m.Status.Artifact = nil
+ return m
+ }(),
+ want: false,
+ wantReason: "OCIRepository 'default/mock' is not ready: does not have an artifact",
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got, gotReason := isReady(tt.obj, tt.obj.GetArtifact())
+ if got != tt.want {
+ t.Errorf("isOCIRepositoryReady() got = %v, want %v", got, tt.want)
+ }
+ if gotReason != tt.wantReason {
+ t.Errorf("isOCIRepositoryReady() reason = %v, want %v", gotReason, tt.wantReason)
+ }
+ })
}
}
-func valuesConfigMap(name string, data map[string]string) *corev1.ConfigMap {
- return &corev1.ConfigMap{
- ObjectMeta: metav1.ObjectMeta{Name: name},
- Data: data,
+func Test_TryMutateChartWithSourceRevision(t *testing.T) {
+ tests := []struct {
+ name string
+ version string
+ revision string
+ wantVersion string
+ wantErr bool
+ }{
+ {
+ name: "valid version and revision",
+ version: "1.2.3",
+ revision: "1.2.3@sha256:9933f58f8bf459eb199d59ebc8a05683f3944e1242d9f5467d99aa2cf08a5370",
+ wantVersion: "1.2.3+9933f58f8bf4",
+ wantErr: false,
+ },
+ {
+ name: "valid version and invalid revision",
+ version: "1.2.3",
+ revision: "1.2.4@sha256:9933f58f8bf459eb199d59ebc8a05683f3944e1242d9f5467d99aa2cf08a5370",
+ wantVersion: "",
+ wantErr: true,
+ },
+ {
+ name: "valid version and revision without version",
+ version: "1.2.3",
+ revision: "sha256:9933f58f8bf459eb199d59ebc8a05683f3944e1242d9f5467d99aa2cf08a5370",
+ wantVersion: "1.2.3+9933f58f8bf4",
+ wantErr: false,
+ },
+ {
+ name: "invalid version",
+ version: "sha:123456",
+ revision: "1.2.3@sha:123456",
+ wantVersion: "",
+ wantErr: true,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ g := NewWithT(t)
+
+ c := &chart.Chart{
+ Metadata: &chart.Metadata{
+ Version: tt.version,
+ },
+ }
+
+ s := &sourcev1.OCIRepository{
+ Status: sourcev1.OCIRepositoryStatus{
+ Artifact: &meta.Artifact{
+ Revision: tt.revision,
+ },
+ },
+ }
+
+ r := &HelmReleaseReconciler{}
+ _, err := r.mutateChartWithSourceRevision(c, s)
+ if tt.wantErr {
+ g.Expect(err).To(HaveOccurred())
+ } else {
+ g.Expect(err).ToNot(HaveOccurred())
+ g.Expect(c.Metadata.Version).To(Equal(tt.wantVersion))
+ }
+ })
}
+
}
diff --git a/internal/controller/helmrelease_indexers.go b/internal/controller/helmrelease_indexers.go
new file mode 100644
index 000000000..d39731a62
--- /dev/null
+++ b/internal/controller/helmrelease_indexers.go
@@ -0,0 +1,186 @@
+/*
+Copyright 2025 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package controller
+
+import (
+ "context"
+ "fmt"
+ "strings"
+
+ "github.com/fluxcd/pkg/runtime/conditions"
+ sourcev1 "github.com/fluxcd/source-controller/api/v1"
+ ctrl "sigs.k8s.io/controller-runtime"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+ "sigs.k8s.io/controller-runtime/pkg/reconcile"
+
+ v2 "github.com/fluxcd/helm-controller/api/v2"
+)
+
+func isReadyOrReconciling(from conditions.Getter) bool {
+ return conditions.IsReady(from) || conditions.IsReconciling(from)
+}
+
+// requestsForHelmChartChange enqueues requests for watched HelmCharts
+// according to the specified index.
+func (r *HelmReleaseReconciler) requestsForHelmChartChange(ctx context.Context, o client.Object) []reconcile.Request {
+ hc, ok := o.(*sourcev1.HelmChart)
+ if !ok {
+ err := fmt.Errorf("expected a HelmChart, got %T", o)
+ ctrl.LoggerFrom(ctx).Error(err, "failed to get requests for HelmChart change")
+ return nil
+ }
+ // If we do not have an artifact, we have no requests to make
+ if hc.GetArtifact() == nil {
+ return nil
+ }
+
+ var list v2.HelmReleaseList
+ if err := r.List(ctx, &list, client.MatchingFields{
+ v2.SourceIndexKey: sourcev1.HelmChartKind + "/" + client.ObjectKeyFromObject(hc).String(),
+ }); err != nil {
+ ctrl.LoggerFrom(ctx).Error(err, "failed to list HelmReleases for HelmChart change")
+ return nil
+ }
+
+ var reqs []reconcile.Request
+ for i, hr := range list.Items {
+ // If the HelmRelease is ready or reconciling and the revision of the artifact equals to the
+ // last attempted revision, we should not make a request for this HelmRelease
+ if isReadyOrReconciling(&list.Items[i]) && hc.GetArtifact().HasRevision(hr.Status.GetLastAttemptedRevision()) {
+ continue
+ }
+ reqs = append(reqs, reconcile.Request{NamespacedName: client.ObjectKeyFromObject(&list.Items[i])})
+ }
+ return reqs
+}
+
+// requestsForOCIRepositoryChange enqueues requests for watched OCIRepositories
+// according to the specified index.
+func (r *HelmReleaseReconciler) requestsForOCIRepositoryChange(ctx context.Context, o client.Object) []reconcile.Request {
+ or, ok := o.(*sourcev1.OCIRepository)
+ if !ok {
+ err := fmt.Errorf("expected an OCIRepository, got %T", o)
+ ctrl.LoggerFrom(ctx).Error(err, "failed to get requests for OCIRepository change")
+ return nil
+ }
+ // If we do not have an artifact, we have no requests to make
+ if or.GetArtifact() == nil {
+ return nil
+ }
+
+ var list v2.HelmReleaseList
+ if err := r.List(ctx, &list, client.MatchingFields{
+ v2.SourceIndexKey: sourcev1.OCIRepositoryKind + "/" + client.ObjectKeyFromObject(or).String(),
+ }); err != nil {
+ ctrl.LoggerFrom(ctx).Error(err, "failed to list HelmReleases for OCIRepository change")
+ return nil
+ }
+
+ var reqs []reconcile.Request
+ for i, hr := range list.Items {
+ // If the HelmRelease is ready or reconciling and the digest of the artifact equals to the
+ // last attempted revision digest, we should not make a request for this HelmRelease,
+ // likewise if we cannot retrieve the artifact digest.
+ digest := extractDigest(or.GetArtifact().Revision)
+ if digest == "" {
+ ctrl.LoggerFrom(ctx).Error(fmt.Errorf("wrong digest for %T", or), "failed to get requests for OCIRepository change")
+ continue
+ }
+
+ // Skip if the HelmRelease is ready or reconciling and the digest matches the last attempted revision digest.
+ if isReadyOrReconciling(&list.Items[i]) && digest == hr.Status.LastAttemptedRevisionDigest {
+ continue
+ }
+
+ reqs = append(reqs, reconcile.Request{NamespacedName: client.ObjectKeyFromObject(&list.Items[i])})
+ }
+ return reqs
+}
+
+// requestsForExternalArtifactChange enqueues requests for watched ExternalArtifacts
+// according to the specified index.
+func (r *HelmReleaseReconciler) requestsForExternalArtifactChange(ctx context.Context, o client.Object) []reconcile.Request {
+ log := ctrl.LoggerFrom(ctx)
+ ea, ok := o.(*sourcev1.ExternalArtifact)
+ if !ok {
+ err := fmt.Errorf("expected an ExternalArtifact, got %T", o)
+ log.Error(err, "failed to get requests for ExternalArtifact change")
+ return nil
+ }
+ // If we do not have an artifact, we have no requests to make
+ if ea.GetArtifact() == nil {
+ return nil
+ }
+
+ var list v2.HelmReleaseList
+ if err := r.List(ctx, &list, client.MatchingFields{
+ v2.SourceIndexKey: sourcev1.ExternalArtifactKind + "/" + client.ObjectKeyFromObject(ea).String(),
+ }); err != nil {
+ log.Error(err, "failed to list HelmReleases for ExternalArtifact change")
+ return nil
+ }
+ var reqs []reconcile.Request
+ for i, hr := range list.Items {
+ revision := ea.GetArtifact().Revision
+
+ // Handle both revision formats: digest or semantic version.
+ if strings.Contains(revision, ":") {
+ // Skip if the HelmRelease is ready and the digest matches the last attempted revision digest.
+ if isReadyOrReconciling(&list.Items[i]) && extractDigest(revision) == hr.Status.LastAttemptedRevisionDigest {
+ continue
+ }
+ } else {
+ // Skip if the HelmRelease is ready and the revision matches the last attempted revision.
+ if isReadyOrReconciling(&list.Items[i]) && revision == hr.Status.LastAttemptedRevision {
+ continue
+ }
+ }
+
+ reqs = append(reqs, reconcile.Request{NamespacedName: client.ObjectKeyFromObject(&list.Items[i])})
+ }
+ return reqs
+}
+
+// requestsForConfigDependency enqueues requests for watched ConfigMaps or Secrets
+// according to the specified index.
+func (r *HelmReleaseReconciler) requestsForConfigDependency(
+ index string) func(ctx context.Context, o client.Object) []reconcile.Request {
+
+ return func(ctx context.Context, o client.Object) []reconcile.Request {
+ // List HelmReleases that have a dependency on the ConfigMap or Secret.
+ var list v2.HelmReleaseList
+ if err := r.List(ctx, &list, client.MatchingFields{
+ index: client.ObjectKeyFromObject(o).String(),
+ }); err != nil {
+ ctrl.LoggerFrom(ctx).Error(err, "failed to list HelmReleases for config dependency change",
+ "index", index, "objectRef", map[string]string{
+ "name": o.GetName(),
+ "namespace": o.GetNamespace(),
+ })
+ return nil
+ }
+
+ // Enqueue requests for each HelmRelease in the list.
+ reqs := make([]reconcile.Request, 0, len(list.Items))
+ for i := range list.Items {
+ reqs = append(reqs, reconcile.Request{
+ NamespacedName: client.ObjectKeyFromObject(&list.Items[i]),
+ })
+ }
+ return reqs
+ }
+}
diff --git a/internal/controller/helmrelease_manager.go b/internal/controller/helmrelease_manager.go
new file mode 100644
index 000000000..aa1f6d537
--- /dev/null
+++ b/internal/controller/helmrelease_manager.go
@@ -0,0 +1,180 @@
+/*
+Copyright 2025 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package controller
+
+import (
+ "context"
+ "fmt"
+
+ corev1 "k8s.io/api/core/v1"
+ "k8s.io/client-go/util/workqueue"
+ ctrl "sigs.k8s.io/controller-runtime"
+ "sigs.k8s.io/controller-runtime/pkg/builder"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+ "sigs.k8s.io/controller-runtime/pkg/controller"
+ "sigs.k8s.io/controller-runtime/pkg/handler"
+ "sigs.k8s.io/controller-runtime/pkg/predicate"
+ "sigs.k8s.io/controller-runtime/pkg/reconcile"
+
+ runtimeCtrl "github.com/fluxcd/pkg/runtime/controller"
+ "github.com/fluxcd/pkg/runtime/predicates"
+ sourcev1 "github.com/fluxcd/source-controller/api/v1"
+
+ v2 "github.com/fluxcd/helm-controller/api/v2"
+ intpredicates "github.com/fluxcd/helm-controller/internal/predicates"
+)
+
+type HelmReleaseReconcilerOptions struct {
+ RateLimiter workqueue.TypedRateLimiter[reconcile.Request]
+ WatchConfigs bool
+ WatchConfigsPredicate predicate.Predicate
+ WatchExternalArtifacts bool
+ CancelHealthCheckOnRequeue bool
+}
+
+func (r *HelmReleaseReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager, opts HelmReleaseReconcilerOptions) error {
+ const (
+ indexConfigMap = ".metadata.configMap"
+ indexSecret = ".metadata.secret"
+ )
+
+ // Index the HelmRelease by the Source reference they point to.
+ if err := mgr.GetFieldIndexer().IndexField(ctx, &v2.HelmRelease{}, v2.SourceIndexKey,
+ func(o client.Object) []string {
+ obj := o.(*v2.HelmRelease)
+ var kind, name, namespace string
+ switch {
+ case obj.HasChartRef() && !obj.HasChartTemplate():
+ kind = obj.Spec.ChartRef.Kind
+ name = obj.Spec.ChartRef.Name
+ namespace = obj.Spec.ChartRef.Namespace
+ if namespace == "" {
+ namespace = obj.GetNamespace()
+ }
+ case !obj.HasChartRef() && obj.HasChartTemplate():
+ kind = sourcev1.HelmChartKind
+ name = obj.GetHelmChartName()
+ namespace = obj.Spec.Chart.GetNamespace(obj.GetNamespace())
+ default:
+ return nil
+ }
+ return []string{fmt.Sprintf("%s/%s/%s", kind, namespace, name)}
+ },
+ ); err != nil {
+ return err
+ }
+
+ // Index the HelmRelease by the ConfigMap references they point to.
+ if err := mgr.GetFieldIndexer().IndexField(ctx, &v2.HelmRelease{}, indexConfigMap,
+ func(o client.Object) []string {
+ obj := o.(*v2.HelmRelease)
+ namespace := obj.GetNamespace()
+ var keys []string
+ if kc := obj.Spec.KubeConfig; kc != nil && kc.ConfigMapRef != nil {
+ keys = append(keys, fmt.Sprintf("%s/%s", namespace, kc.ConfigMapRef.Name))
+ }
+ for _, ref := range obj.Spec.ValuesFrom {
+ if ref.Kind == "ConfigMap" {
+ keys = append(keys, fmt.Sprintf("%s/%s", namespace, ref.Name))
+ }
+ }
+ return keys
+ },
+ ); err != nil {
+ return err
+ }
+
+ // Index the HelmRelease by the Secret references they point to.
+ if err := mgr.GetFieldIndexer().IndexField(ctx, &v2.HelmRelease{}, indexSecret,
+ func(o client.Object) []string {
+ obj := o.(*v2.HelmRelease)
+ namespace := obj.GetNamespace()
+ var keys []string
+ if kc := obj.Spec.KubeConfig; kc != nil && kc.SecretRef != nil {
+ keys = append(keys, fmt.Sprintf("%s/%s", namespace, kc.SecretRef.Name))
+ }
+ for _, ref := range obj.Spec.ValuesFrom {
+ if ref.Kind == "Secret" {
+ keys = append(keys, fmt.Sprintf("%s/%s", namespace, ref.Name))
+ }
+ }
+ return keys
+ },
+ ); err != nil {
+ return err
+ }
+
+ var blder *builder.Builder
+ var toComplete reconcile.TypedReconciler[reconcile.Request]
+ var enqueueRequestsFromMapFunc func(objKind string, fn handler.MapFunc) handler.EventHandler
+
+ hrPredicate := predicate.Or(
+ predicate.GenerationChangedPredicate{},
+ predicates.ReconcileRequestedPredicate{},
+ )
+
+ if !opts.CancelHealthCheckOnRequeue {
+ toComplete = r
+ enqueueRequestsFromMapFunc = func(objKind string, fn handler.MapFunc) handler.EventHandler {
+ return handler.EnqueueRequestsFromMapFunc(fn)
+ }
+ blder = ctrl.NewControllerManagedBy(mgr).
+ For(&v2.HelmRelease{}, builder.WithPredicates(hrPredicate))
+ } else {
+ wr := runtimeCtrl.WrapReconciler(r)
+ toComplete = wr
+ enqueueRequestsFromMapFunc = wr.EnqueueRequestsFromMapFunc
+ blder = runtimeCtrl.NewControllerManagedBy(mgr, wr).
+ For(&v2.HelmRelease{}, hrPredicate).Builder
+ }
+
+ blder.
+ Watches(
+ &sourcev1.HelmChart{},
+ enqueueRequestsFromMapFunc(sourcev1.HelmChartKind, r.requestsForHelmChartChange),
+ builder.WithPredicates(intpredicates.SourceRevisionChangePredicate{}),
+ ).
+ Watches(
+ &sourcev1.OCIRepository{},
+ enqueueRequestsFromMapFunc(sourcev1.OCIRepositoryKind, r.requestsForOCIRepositoryChange),
+ builder.WithPredicates(intpredicates.SourceRevisionChangePredicate{}),
+ )
+
+ if opts.WatchConfigs {
+ blder = blder.
+ WatchesMetadata(
+ &corev1.ConfigMap{},
+ enqueueRequestsFromMapFunc("ConfigMap", r.requestsForConfigDependency(indexConfigMap)),
+ builder.WithPredicates(predicate.ResourceVersionChangedPredicate{}, opts.WatchConfigsPredicate),
+ ).
+ WatchesMetadata(
+ &corev1.Secret{},
+ enqueueRequestsFromMapFunc("Secret", r.requestsForConfigDependency(indexSecret)),
+ builder.WithPredicates(predicate.ResourceVersionChangedPredicate{}, opts.WatchConfigsPredicate),
+ )
+ }
+
+ if opts.WatchExternalArtifacts {
+ blder = blder.Watches(
+ &sourcev1.ExternalArtifact{},
+ enqueueRequestsFromMapFunc(sourcev1.ExternalArtifactKind, r.requestsForExternalArtifactChange),
+ builder.WithPredicates(intpredicates.SourceRevisionChangePredicate{}),
+ )
+ }
+
+ return blder.WithOptions(controller.Options{RateLimiter: opts.RateLimiter}).Complete(toComplete)
+}
diff --git a/internal/controller/suite_test.go b/internal/controller/suite_test.go
index 764d29787..2eed01da6 100644
--- a/internal/controller/suite_test.go
+++ b/internal/controller/suite_test.go
@@ -22,43 +22,78 @@ import (
"path/filepath"
"testing"
+ corev1 "k8s.io/api/core/v1"
+ apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
+ "k8s.io/apimachinery/pkg/runtime"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
- "k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
- "sigs.k8s.io/controller-runtime/pkg/client"
- "sigs.k8s.io/controller-runtime/pkg/envtest"
+ ctrl "sigs.k8s.io/controller-runtime"
- "github.com/fluxcd/helm-controller/api/v2beta1"
+ "github.com/fluxcd/pkg/runtime/testenv"
+ "github.com/fluxcd/pkg/testserver"
+ sourcev1 "github.com/fluxcd/source-controller/api/v1"
+
+ v2 "github.com/fluxcd/helm-controller/api/v2"
// +kubebuilder:scaffold:imports
)
-var cfg *rest.Config
-var k8sClient client.Client
-var testEnv *envtest.Environment
+var (
+ testEnv *testenv.Environment
+ testServer *testserver.HTTPServer
+
+ testCtx = ctrl.SetupSignalHandler()
+)
+
+func NewTestScheme() *runtime.Scheme {
+ s := runtime.NewScheme()
+ utilruntime.Must(corev1.AddToScheme(s))
+ utilruntime.Must(apiextensionsv1.AddToScheme(s))
+ utilruntime.Must(sourcev1.AddToScheme(s))
+ utilruntime.Must(v2.AddToScheme(s))
+ return s
+}
func TestMain(m *testing.M) {
- testEnv = &envtest.Environment{
- CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd", "bases")},
- }
+ testEnv = testenv.New(
+ testenv.WithCRDPath(
+ filepath.Join("..", "..", "build", "config", "crd", "bases"),
+ filepath.Join("..", "..", "config", "crd", "bases"),
+ ),
+ testenv.WithScheme(NewTestScheme()),
+ )
var err error
- cfg, err = testEnv.Start()
- if err != nil {
- panic(fmt.Errorf("failed to start testenv: %v", err))
+ if testServer, err = testserver.NewTempHTTPServer(); err != nil {
+ panic(fmt.Sprintf("Failed to create a temporary storage server: %v", err))
}
+ fmt.Println("Starting the test storage server")
+ testServer.Start()
- utilruntime.Must(v2beta1.AddToScheme(scheme.Scheme))
- k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
- if err != nil {
- panic(fmt.Errorf("failed to create k8s client: %v", err))
- }
+ go func() {
+ fmt.Println("Starting the test environment")
+ if err := testEnv.Start(testCtx); err != nil {
+ panic(fmt.Sprintf("Failed to start the test environment manager: %v", err))
+ }
+ }()
+ <-testEnv.Manager.Elected()
code := m.Run()
- err = testEnv.Stop()
- if err != nil {
- panic(fmt.Errorf("failed to stop testenv: %v", err))
+ fmt.Println("Stopping the test environment")
+ if err := testEnv.Stop(); err != nil {
+ panic(fmt.Sprintf("Failed to stop the test environment: %v", err))
+ }
+
+ fmt.Println("Stopping the test storage server")
+ testServer.Stop()
+ if err := os.RemoveAll(testServer.Root()); err != nil {
+ panic(fmt.Sprintf("Failed to remove storage server dir: %v", err))
}
os.Exit(code)
}
+
+// GetTestClusterConfig returns a copy of the test cluster config.
+func GetTestClusterConfig() (*rest.Config, error) {
+ return rest.CopyConfig(testEnv.GetConfig()), nil
+}
diff --git a/internal/controller_test/external_artifact_test.go b/internal/controller_test/external_artifact_test.go
new file mode 100644
index 000000000..ae5d03327
--- /dev/null
+++ b/internal/controller_test/external_artifact_test.go
@@ -0,0 +1,241 @@
+/*
+Copyright 2025 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package controller_test
+
+import (
+ "context"
+ "fmt"
+ "testing"
+ "time"
+
+ aclv1 "github.com/fluxcd/pkg/apis/acl"
+ "github.com/fluxcd/pkg/apis/meta"
+ sourcev1 "github.com/fluxcd/source-controller/api/v1"
+ . "github.com/onsi/gomega"
+ "github.com/opencontainers/go-digest"
+ v1 "k8s.io/api/core/v1"
+ apimeta "k8s.io/apimachinery/pkg/api/meta"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+
+ v2 "github.com/fluxcd/helm-controller/api/v2"
+ "github.com/fluxcd/helm-controller/internal/testutil"
+)
+
+func TestExternalArtifact_LifeCycle(t *testing.T) {
+ g := NewWithT(t)
+ reconciler.AllowExternalArtifact = true
+
+ // Create a namespace for the test
+ ns := v1.Namespace{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "external-artifact-test",
+ },
+ }
+ err := k8sClient.Create(context.Background(), &ns)
+ g.Expect(err).ToNot(HaveOccurred())
+
+ // Create an ExternalArtifact containing a Helm chart
+ revision := "1.0.0"
+ eaKey := client.ObjectKey{
+ Namespace: ns.Name,
+ Name: "test-ea",
+ }
+ ea, err := applyExternalArtifact(eaKey, revision, "")
+ g.Expect(err).ToNot(HaveOccurred(), "failed to create ExternalArtifact")
+
+ // Create a HelmRelease that references the ExternalArtifact
+ hr := &v2.HelmRelease{
+ TypeMeta: metav1.TypeMeta{
+ APIVersion: v2.GroupVersion.String(),
+ Kind: v2.HelmReleaseKind,
+ },
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test",
+ Namespace: ns.Name,
+ },
+ Spec: v2.HelmReleaseSpec{
+ ChartRef: &v2.CrossNamespaceSourceReference{
+ Kind: sourcev1.ExternalArtifactKind,
+ Name: ea.Name,
+ },
+ Interval: metav1.Duration{Duration: time.Hour},
+ },
+ }
+
+ err = k8sClient.Create(context.Background(), hr)
+ g.Expect(err).ToNot(HaveOccurred())
+
+ t.Run("installs from external artifact", func(t *testing.T) {
+ gt := NewWithT(t)
+ gt.Eventually(func() bool {
+ err = testEnv.Get(context.Background(), client.ObjectKeyFromObject(hr), hr)
+ if err != nil {
+ return false
+ }
+ return apimeta.IsStatusConditionTrue(hr.Status.Conditions, meta.ReadyCondition)
+ }, 5*time.Second, time.Second).Should(BeTrue(), "HelmRelease did not become ready")
+
+ gt.Expect(hr.Status.LastAttemptedRevision).To(Equal(revision))
+ gt.Expect(hr.Status.LastAttemptedReleaseAction).To(Equal(v2.ReleaseActionInstall))
+ })
+
+ t.Run("upgrades at external artifact revision change", func(t *testing.T) {
+ gt := NewWithT(t)
+ newRevision := "2.0.0"
+ ea, err = applyExternalArtifact(eaKey, newRevision, "")
+ gt.Expect(err).ToNot(HaveOccurred())
+
+ gt.Eventually(func() bool {
+ err = testEnv.Get(context.Background(), client.ObjectKeyFromObject(hr), hr)
+ if err != nil {
+ return false
+ }
+ return apimeta.IsStatusConditionTrue(hr.Status.Conditions, meta.ReadyCondition) &&
+ hr.Status.LastAttemptedRevision == newRevision
+ }, 5*time.Second, time.Second).Should(BeTrue(), "HelmRelease did not upgrade")
+
+ gt.Expect(hr.Status.LastAttemptedReleaseAction).To(Equal(v2.ReleaseActionUpgrade))
+ })
+
+ t.Run("upgrades at external artifact revision switch to digest", func(t *testing.T) {
+ gt := NewWithT(t)
+ fixedRevision := "2.0.0"
+ newDigest := fmt.Sprintf("latest@%s", digest.FromString("1"))
+ ea, err = applyExternalArtifact(eaKey, fixedRevision, newDigest)
+ gt.Expect(err).ToNot(HaveOccurred())
+
+ gt.Eventually(func() bool {
+ err = testEnv.Get(context.Background(), client.ObjectKeyFromObject(hr), hr)
+ if err != nil {
+ return false
+ }
+ return apimeta.IsStatusConditionTrue(hr.Status.Conditions, meta.ReadyCondition) &&
+ hr.Status.LastAttemptedRevisionDigest == newDigest
+ }, 5*time.Second, time.Second).Should(BeTrue(), "HelmRelease did not upgrade")
+
+ gt.Expect(hr.Status.LastAttemptedReleaseAction).To(Equal(v2.ReleaseActionUpgrade))
+ })
+
+ t.Run("upgrades at external artifact digest change", func(t *testing.T) {
+ gt := NewWithT(t)
+ fixedRevision := "2.0.0"
+ newDigest := digest.FromString("2").String()
+ ea, err = applyExternalArtifact(eaKey, fixedRevision, newDigest)
+ gt.Expect(err).ToNot(HaveOccurred())
+
+ gt.Eventually(func() bool {
+ err = testEnv.Get(context.Background(), client.ObjectKeyFromObject(hr), hr)
+ if err != nil {
+ return false
+ }
+ return apimeta.IsStatusConditionTrue(hr.Status.Conditions, meta.ReadyCondition) &&
+ hr.Status.LastAttemptedRevisionDigest == newDigest
+ }, 5*time.Second, time.Second).Should(BeTrue(), "HelmRelease did not upgrade")
+
+ gt.Expect(hr.Status.LastAttemptedReleaseAction).To(Equal(v2.ReleaseActionUpgrade))
+ })
+
+ t.Run("fails when external artifact feature gate is disable", func(t *testing.T) {
+ gt := NewWithT(t)
+ reconciler.AllowExternalArtifact = false
+ newRevision := "3.0.0"
+ ea, err = applyExternalArtifact(eaKey, newRevision, "")
+ gt.Expect(err).ToNot(HaveOccurred())
+
+ gt.Eventually(func() bool {
+ err = testEnv.Get(context.Background(), client.ObjectKeyFromObject(hr), hr)
+ if err != nil {
+ return false
+ }
+ return apimeta.IsStatusConditionFalse(hr.Status.Conditions, meta.ReadyCondition)
+ }, 5*time.Second, time.Second).Should(BeTrue())
+
+ gt.Expect(apimeta.IsStatusConditionTrue(hr.Status.Conditions, meta.StalledCondition)).Should(BeTrue())
+ readyCondition := apimeta.FindStatusCondition(hr.Status.Conditions, meta.ReadyCondition)
+ gt.Expect(readyCondition.Reason).To(Equal(aclv1.AccessDeniedReason))
+ })
+
+ t.Run("uninstalls successfully", func(t *testing.T) {
+ gt := NewWithT(t)
+ err = k8sClient.Delete(context.Background(), hr)
+ gt.Expect(err).ToNot(HaveOccurred())
+
+ gt.Eventually(func() bool {
+ err = testEnv.Get(context.Background(), client.ObjectKeyFromObject(hr), hr)
+ return err != nil && client.IgnoreNotFound(err) == nil
+ }, 5*time.Second, time.Second).Should(BeTrue(), "HelmRelease was not deleted")
+ })
+}
+
+func applyExternalArtifact(objKey client.ObjectKey, aVersion, aDigest string) (*sourcev1.ExternalArtifact, error) {
+ chart := testutil.BuildChart(testutil.ChartWithVersion(aVersion))
+ artifact, err := testutil.SaveChartAsArtifact(chart, digest.SHA256, testServer.URL(), testServer.Root())
+ if err != nil {
+ return nil, err
+ }
+
+ ea := &sourcev1.ExternalArtifact{
+ TypeMeta: metav1.TypeMeta{
+ APIVersion: sourcev1.GroupVersion.String(),
+ Kind: sourcev1.ExternalArtifactKind,
+ },
+ ObjectMeta: metav1.ObjectMeta{
+ Name: objKey.Name,
+ Namespace: objKey.Namespace,
+ },
+ }
+
+ patchOpts := []client.PatchOption{
+ client.ForceOwnership,
+ client.FieldOwner("kustomize-controller"),
+ }
+
+ err = k8sClient.Patch(context.Background(), ea, client.Apply, patchOpts...)
+ if err != nil {
+ return nil, err
+ }
+
+ ea.ManagedFields = nil
+ if aDigest != "" {
+ artifact.Revision = aDigest
+ }
+ ea.Status = sourcev1.ExternalArtifactStatus{
+ Artifact: artifact,
+ Conditions: []metav1.Condition{
+ {
+ Type: meta.ReadyCondition,
+ Status: metav1.ConditionTrue,
+ LastTransitionTime: metav1.Now(),
+ Reason: meta.SucceededReason,
+ ObservedGeneration: 1,
+ },
+ },
+ }
+
+ statusOpts := &client.SubResourcePatchOptions{
+ PatchOptions: client.PatchOptions{
+ FieldManager: "source-controller",
+ },
+ }
+
+ err = k8sClient.Status().Patch(context.Background(), ea, client.Apply, statusOpts)
+ if err != nil {
+ return nil, err
+ }
+ return ea, nil
+}
diff --git a/internal/controller_test/suite_test.go b/internal/controller_test/suite_test.go
new file mode 100644
index 000000000..e35b45e06
--- /dev/null
+++ b/internal/controller_test/suite_test.go
@@ -0,0 +1,301 @@
+/*
+Copyright 2025 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package controller_test
+
+import (
+ "context"
+ "fmt"
+ "math/rand"
+ "os"
+ "path/filepath"
+ "testing"
+ "time"
+
+ "go.uber.org/zap/zapcore"
+ "helm.sh/helm/v4/pkg/kube"
+ corev1 "k8s.io/api/core/v1"
+ apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/runtime"
+ utilruntime "k8s.io/apimachinery/pkg/util/runtime"
+ "k8s.io/client-go/rest"
+ ctrl "sigs.k8s.io/controller-runtime"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+ "sigs.k8s.io/controller-runtime/pkg/envtest"
+ controllerLog "sigs.k8s.io/controller-runtime/pkg/log"
+ "sigs.k8s.io/controller-runtime/pkg/log/zap"
+
+ "github.com/fluxcd/pkg/apis/meta"
+ rclient "github.com/fluxcd/pkg/runtime/client"
+ helper "github.com/fluxcd/pkg/runtime/controller"
+ "github.com/fluxcd/pkg/runtime/testenv"
+ "github.com/fluxcd/pkg/testserver"
+ sourcev1 "github.com/fluxcd/source-controller/api/v1"
+
+ v2 "github.com/fluxcd/helm-controller/api/v2"
+ "github.com/fluxcd/helm-controller/internal/controller"
+ // +kubebuilder:scaffold:imports
+)
+
+var (
+ controllerName = "helm-controller"
+ testEnv *testenv.Environment
+ testServer *testserver.HTTPServer
+ k8sClient client.Client
+ reconciler *controller.HelmReleaseReconciler
+ kubeConfig []byte
+
+ testCtx = ctrl.SetupSignalHandler()
+)
+
+var letterRunes = []rune("abcdefghijklmnopqrstuvwxyz1234567890")
+
+func randStringRunes(n int) string {
+ b := make([]rune, n)
+ for i := range b {
+ b[i] = letterRunes[rand.Intn(len(letterRunes))]
+ }
+ return string(b)
+}
+
+func NewTestScheme() *runtime.Scheme {
+ s := runtime.NewScheme()
+ utilruntime.Must(corev1.AddToScheme(s))
+ utilruntime.Must(apiextensionsv1.AddToScheme(s))
+ utilruntime.Must(sourcev1.AddToScheme(s))
+ utilruntime.Must(v2.AddToScheme(s))
+ return s
+}
+
+func TestMain(m *testing.M) {
+ // Set logs on development mode and debug level.
+ controllerLog.SetLogger(zap.New(zap.WriteTo(os.Stderr), zap.UseDevMode(true), zap.Level(zapcore.DebugLevel)))
+
+ // Set the managedFields owner for resources reconciled from Helm charts.
+ kube.ManagedFieldsManager = controllerName
+
+ // Initialize the test environment with source and helm controller CRDs.
+ testEnv = testenv.New(
+ testenv.WithCRDPath(
+ filepath.Join("..", "..", "build", "config", "crd", "bases"),
+ filepath.Join("..", "..", "config", "crd", "bases"),
+ ),
+ testenv.WithScheme(NewTestScheme()),
+ )
+
+ // Start a local test HTTP server to serve chart and artifact files.
+ var err error
+ if testServer, err = testserver.NewTempHTTPServer(); err != nil {
+ panic(fmt.Sprintf("Failed to create a temporary storage server: %v", err))
+ }
+ fmt.Println("Starting the test storage server")
+ testServer.Start()
+
+ // Start the test environment.
+ go func() {
+ fmt.Println("Starting the test environment")
+ if err := testEnv.Start(testCtx); err != nil {
+ panic(fmt.Sprintf("Failed to start the test environment manager: %v", err))
+ }
+ }()
+ <-testEnv.Manager.Elected()
+
+ // Client with caching disabled.
+ k8sClient, err = client.New(testEnv.Config, client.Options{Scheme: NewTestScheme()})
+ if err != nil {
+ panic(fmt.Sprintf("Failed to create client: %v", err))
+ }
+
+ // Create a user for generating kubeconfig.
+ user, err := testEnv.AddUser(envtest.User{
+ Name: "testenv-admin",
+ Groups: []string{"system:masters"},
+ }, nil)
+ if err != nil {
+ panic(fmt.Sprintf("Failed to create testenv-admin user: %v", err))
+ }
+
+ kubeConfig, err = user.KubeConfig()
+ if err != nil {
+ panic(fmt.Sprintf("Failed to create the testenv-admin user kubeconfig: %v", err))
+ }
+
+ // Start the HelmRelease controller.
+ if err := StartController(); err != nil {
+ panic(fmt.Sprintf("Failed to start HelmRelease controller: %v", err))
+ }
+
+ code := m.Run()
+
+ fmt.Println("Stopping the test environment")
+ if err := testEnv.Stop(); err != nil {
+ panic(fmt.Sprintf("Failed to stop the test environment: %v", err))
+ }
+
+ fmt.Println("Stopping the test storage server")
+ testServer.Stop()
+ if err := os.RemoveAll(testServer.Root()); err != nil {
+ panic(fmt.Sprintf("Failed to remove storage server dir: %v", err))
+ }
+
+ os.Exit(code)
+}
+
+// GetTestClusterConfig returns a copy of the test cluster config.
+func GetTestClusterConfig() (*rest.Config, error) {
+ return rest.CopyConfig(testEnv.GetConfig()), nil
+}
+
+// StartController adds the HelmReleaseReconciler to the test controller manager
+// and starts the reconciliation loops.
+func StartController() error {
+ timeout := time.Second * 10
+ reconciler = &controller.HelmReleaseReconciler{
+ Client: testEnv,
+ EventRecorder: testEnv.GetEventRecorderFor(controllerName),
+ Metrics: helper.Metrics{},
+ GetClusterConfig: GetTestClusterConfig,
+ ClientOpts: rclient.Options{
+ QPS: 50.0,
+ Burst: 300,
+ },
+ KubeConfigOpts: rclient.KubeConfigOptions{
+ InsecureExecProvider: false,
+ InsecureTLS: false,
+ UserAgent: controllerName,
+ Timeout: &timeout,
+ },
+ APIReader: testEnv,
+ TokenCache: nil,
+ FieldManager: controllerName,
+ DependencyRequeueInterval: 5 * time.Second,
+ ArtifactFetchRetries: 1,
+ DefaultServiceAccount: "",
+ DisableChartDigestTracking: false,
+ AdditiveCELDependencyCheck: false,
+ AllowExternalArtifact: false,
+ }
+
+ watchConfigsPredicate, err := helper.GetWatchConfigsPredicate(helper.WatchOptions{})
+ if err != nil {
+ return err
+ }
+
+ return (reconciler).SetupWithManager(testCtx, testEnv, controller.HelmReleaseReconcilerOptions{
+ WatchConfigsPredicate: watchConfigsPredicate,
+ WatchExternalArtifacts: true,
+ CancelHealthCheckOnRequeue: true,
+ })
+}
+
+func createNamespace(name string) error {
+ namespace := &corev1.Namespace{
+ ObjectMeta: metav1.ObjectMeta{Name: name},
+ }
+ return k8sClient.Create(context.Background(), namespace)
+}
+
+func createKubeConfigSecret(namespace string) error {
+ secret := &corev1.Secret{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "kubeconfig",
+ Namespace: namespace,
+ },
+ Data: map[string][]byte{
+ "value.yaml": kubeConfig,
+ },
+ }
+ return k8sClient.Create(context.Background(), secret)
+}
+
+func applyHelmChart(objKey client.ObjectKey, artifact *meta.Artifact) error {
+ chart := &sourcev1.HelmChart{
+ TypeMeta: metav1.TypeMeta{
+ Kind: sourcev1.HelmChartKind,
+ APIVersion: sourcev1.GroupVersion.String(),
+ },
+ ObjectMeta: metav1.ObjectMeta{
+ Name: objKey.Name,
+ Namespace: objKey.Namespace,
+ },
+ Spec: sourcev1.HelmChartSpec{
+ Interval: metav1.Duration{Duration: time.Minute},
+ Chart: "test-chart",
+ SourceRef: sourcev1.LocalHelmChartSourceReference{
+ Kind: sourcev1.HelmRepositoryKind,
+ Name: "test-repo",
+ },
+ },
+ }
+
+ status := sourcev1.HelmChartStatus{
+ Conditions: []metav1.Condition{
+ {
+ Type: meta.ReadyCondition,
+ Status: metav1.ConditionTrue,
+ LastTransitionTime: metav1.Now(),
+ Reason: sourcev1.ChartPullSucceededReason,
+ },
+ },
+ Artifact: artifact,
+ ObservedGeneration: 1,
+ }
+
+ patchOpts := []client.PatchOption{
+ client.ForceOwnership,
+ client.FieldOwner("helm-controller"),
+ }
+
+ if err := k8sClient.Patch(context.Background(), chart, client.Apply, patchOpts...); err != nil {
+ return err
+ }
+
+ chart.ManagedFields = nil
+ chart.Status = status
+
+ statusOpts := &client.SubResourcePatchOptions{
+ PatchOptions: client.PatchOptions{
+ FieldManager: "source-controller",
+ },
+ }
+
+ if err := k8sClient.Status().Patch(context.Background(), chart, client.Apply, statusOpts); err != nil {
+ return err
+ }
+ return nil
+}
+
+func getEvents(objName string, annotations map[string]string) []corev1.Event {
+ var result []corev1.Event
+ events := &corev1.EventList{}
+ _ = k8sClient.List(testCtx, events)
+ for _, event := range events.Items {
+ if event.InvolvedObject.Name == objName {
+ if len(annotations) == 0 {
+ result = append(result, event)
+ } else {
+ for ak, av := range annotations {
+ if event.GetAnnotations()[ak] == av {
+ result = append(result, event)
+ break
+ }
+ }
+ }
+ }
+ }
+ return result
+}
diff --git a/internal/controller_test/wait_test.go b/internal/controller_test/wait_test.go
new file mode 100644
index 000000000..3c03dc868
--- /dev/null
+++ b/internal/controller_test/wait_test.go
@@ -0,0 +1,257 @@
+/*
+Copyright 2026 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package controller_test
+
+import (
+ "context"
+ "fmt"
+ "testing"
+ "time"
+
+ "github.com/opencontainers/go-digest"
+ corev1 "k8s.io/api/core/v1"
+ apimeta "k8s.io/apimachinery/pkg/api/meta"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/types"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+
+ "github.com/fluxcd/pkg/apis/kustomize"
+ "github.com/fluxcd/pkg/apis/meta"
+ sourcev1 "github.com/fluxcd/source-controller/api/v1"
+ . "github.com/onsi/gomega"
+
+ v2 "github.com/fluxcd/helm-controller/api/v2"
+ "github.com/fluxcd/helm-controller/internal/testutil"
+)
+
+func TestHelmReleaseReconciler_WaitsForCustomHealthChecks(t *testing.T) {
+ g := NewWithT(t)
+ id := "cel-" + randStringRunes(5)
+ timeout := 60 * time.Second
+
+ err := createNamespace(id)
+ g.Expect(err).NotTo(HaveOccurred(), "failed to create test namespace")
+
+ err = createKubeConfigSecret(id)
+ g.Expect(err).NotTo(HaveOccurred(), "failed to create kubeconfig secret")
+
+ // Create a Helm chart that deploys a ConfigMap
+ chart := testutil.BuildChart(
+ testutil.ChartWithVersion("1.0.0"),
+ testutil.ChartWithName("test-cel-chart"),
+ )
+ chartArtifact, err := testutil.SaveChartAsArtifact(chart, digest.SHA256, testServer.URL(), testServer.Root())
+ g.Expect(err).NotTo(HaveOccurred())
+
+ chartKey := types.NamespacedName{
+ Name: fmt.Sprintf("cel-%s", randStringRunes(5)),
+ Namespace: id,
+ }
+
+ err = applyHelmChart(chartKey, chartArtifact)
+ g.Expect(err).NotTo(HaveOccurred())
+
+ hrKey := types.NamespacedName{
+ Name: fmt.Sprintf("cel-%s", randStringRunes(5)),
+ Namespace: id,
+ }
+
+ hr := &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: hrKey.Name,
+ Namespace: hrKey.Namespace,
+ },
+ Spec: v2.HelmReleaseSpec{
+ Interval: metav1.Duration{Duration: 10 * time.Minute},
+ ChartRef: &v2.CrossNamespaceSourceReference{
+ Kind: sourcev1.HelmChartKind,
+ Name: chartKey.Name,
+ Namespace: chartKey.Namespace,
+ },
+ KubeConfig: &meta.KubeConfigReference{
+ SecretRef: &meta.SecretKeyReference{
+ Name: "kubeconfig",
+ },
+ },
+ TargetNamespace: id,
+ Timeout: &metav1.Duration{Duration: 30 * time.Second},
+ // Use a CEL expression that references a non-existent field
+ // This will fail because 'data.foo.bar' doesn't exist
+ HealthCheckExprs: []kustomize.CustomHealthCheck{{
+ APIVersion: "v1",
+ Kind: "ConfigMap",
+ HealthCheckExpressions: kustomize.HealthCheckExpressions{
+ InProgress: "has(data.foo.bar)",
+ Current: "true",
+ },
+ }},
+ },
+ }
+
+ err = k8sClient.Create(context.Background(), hr)
+ g.Expect(err).NotTo(HaveOccurred())
+
+ resultHR := &v2.HelmRelease{}
+ g.Eventually(func() bool {
+ _ = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(hr), resultHR)
+ return apimeta.IsStatusConditionFalse(resultHR.Status.Conditions, meta.ReadyCondition)
+ }, timeout, time.Second).Should(BeTrue())
+
+ readyCondition := apimeta.FindStatusCondition(resultHR.Status.Conditions, meta.ReadyCondition)
+ g.Expect(readyCondition).NotTo(BeNil())
+ // The health check should fail with the CEL expression returning Unknown status
+ // because the expression tries to access 'data.foo.bar' which doesn't exist.
+ g.Expect(readyCondition.Message).
+ To(ContainSubstring("failed to evaluate the CEL expression"))
+}
+
+func TestHelmReleaseReconciler_CancelHealthCheckOnNewRevision(t *testing.T) {
+ g := NewWithT(t)
+ id := "cancel-" + randStringRunes(5)
+ timeout := 120 * time.Second
+
+ err := createNamespace(id)
+ g.Expect(err).NotTo(HaveOccurred(), "failed to create test namespace")
+
+ err = createKubeConfigSecret(id)
+ g.Expect(err).NotTo(HaveOccurred(), "failed to create kubeconfig secret")
+
+ // Create initial successful chart
+ successChart := testutil.BuildChart(
+ testutil.ChartWithVersion("1.0.0"),
+ testutil.ChartWithName("test-cancel-chart"),
+ )
+ successArtifact, err := testutil.SaveChartAsArtifact(successChart, digest.SHA256, testServer.URL(), testServer.Root())
+ g.Expect(err).NotTo(HaveOccurred())
+
+ chartKey := types.NamespacedName{
+ Name: fmt.Sprintf("cancel-%s", randStringRunes(5)),
+ Namespace: id,
+ }
+
+ err = applyHelmChart(chartKey, successArtifact)
+ g.Expect(err).NotTo(HaveOccurred())
+
+ hrKey := types.NamespacedName{
+ Name: fmt.Sprintf("cancel-%s", randStringRunes(5)),
+ Namespace: id,
+ }
+
+ hr := &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: hrKey.Name,
+ Namespace: hrKey.Namespace,
+ },
+ Spec: v2.HelmReleaseSpec{
+ Interval: metav1.Duration{Duration: 10 * time.Minute},
+ ChartRef: &v2.CrossNamespaceSourceReference{
+ Kind: sourcev1.HelmChartKind,
+ Name: chartKey.Name,
+ Namespace: chartKey.Namespace,
+ },
+ KubeConfig: &meta.KubeConfigReference{
+ SecretRef: &meta.SecretKeyReference{
+ Name: "kubeconfig",
+ },
+ },
+ TargetNamespace: id,
+ Timeout: &metav1.Duration{Duration: 5 * time.Minute},
+ },
+ }
+
+ err = k8sClient.Create(context.Background(), hr)
+ g.Expect(err).NotTo(HaveOccurred())
+
+ // Wait for initial reconciliation to succeed
+ resultHR := &v2.HelmRelease{}
+ g.Eventually(func() bool {
+ _ = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(hr), resultHR)
+ return apimeta.IsStatusConditionTrue(resultHR.Status.Conditions, meta.ReadyCondition)
+ }, timeout, time.Second).Should(BeTrue(), "HelmRelease did not become ready")
+
+ // Create a failing chart (deployment with bad image that will timeout)
+ failingChart := testutil.BuildChart(
+ testutil.ChartWithVersion("2.0.0"),
+ testutil.ChartWithName("test-cancel-chart"),
+ testutil.ChartWithFailingDeployment(),
+ )
+ failingArtifact, err := testutil.SaveChartAsArtifact(failingChart, digest.SHA256, testServer.URL(), testServer.Root())
+ g.Expect(err).NotTo(HaveOccurred())
+
+ // Apply failing revision
+ err = applyHelmChart(chartKey, failingArtifact)
+ g.Expect(err).NotTo(HaveOccurred())
+
+ // Wait for reconciliation to start on failing revision
+ g.Eventually(func() bool {
+ _ = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(hr), resultHR)
+ return resultHR.Status.LastAttemptedRevision == failingChart.Metadata.Version
+ }, timeout, time.Second).Should(BeTrue(), "HelmRelease did not start reconciling failing revision")
+
+ // Now quickly apply a fixed revision while health check should be in progress
+ fixedChart := testutil.BuildChart(
+ testutil.ChartWithVersion("3.0.0"),
+ testutil.ChartWithName("test-cancel-chart"),
+ )
+ fixedArtifact, err := testutil.SaveChartAsArtifact(fixedChart, digest.SHA256, testServer.URL(), testServer.Root())
+ g.Expect(err).NotTo(HaveOccurred())
+
+ // Give some time for health check to start
+ time.Sleep(2 * time.Second)
+
+ // Apply the fixed revision
+ err = applyHelmChart(chartKey, fixedArtifact)
+ g.Expect(err).NotTo(HaveOccurred())
+
+ // The key test: verify that the fixed revision gets attempted
+ // and that the health check cancellation worked
+ g.Eventually(func() bool {
+ _ = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(hr), resultHR)
+ return resultHR.Status.LastAttemptedRevision == fixedChart.Metadata.Version
+ }, timeout, time.Second).Should(BeTrue(), "HelmRelease did not attempt the fixed revision")
+
+ // Verify the HealthCheckCanceled event was emitted.
+ g.Eventually(func() bool {
+ events := getEvents(resultHR.GetName(), nil)
+ for _, event := range events {
+ if event.Reason == meta.HealthCheckCanceledReason {
+ t.Logf("Found HealthCheckCanceled event: %s", event.Message)
+ return true
+ }
+ }
+ return false
+ }, timeout, time.Second).Should(BeTrue(), "HealthCheckCanceled event should be recorded")
+
+ // Verify the event message indicates the trigger source.
+ events := getEvents(resultHR.GetName(), nil)
+ var cancelEvent *corev1.Event
+ for i := range events {
+ if events[i].Reason == meta.HealthCheckCanceledReason {
+ cancelEvent = &events[i]
+ break
+ }
+ }
+ g.Expect(cancelEvent).ToNot(BeNil())
+ g.Expect(cancelEvent.Message).To(ContainSubstring("Health checks canceled"))
+ g.Expect(cancelEvent.Message).To(ContainSubstring("HelmChart"))
+
+ // Verify the HelmRelease becomes Ready after the fixed revision is reconciled.
+ g.Eventually(func() bool {
+ _ = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(hr), resultHR)
+ return apimeta.IsStatusConditionTrue(resultHR.Status.Conditions, meta.ReadyCondition)
+ }, timeout, time.Second).Should(BeTrue(), "HelmRelease did not become ready after fixed revision")
+}
diff --git a/internal/diff/differ.go b/internal/diff/differ.go
deleted file mode 100644
index 9359fa3f6..000000000
--- a/internal/diff/differ.go
+++ /dev/null
@@ -1,159 +0,0 @@
-/*
-Copyright 2023 The Flux authors
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package diff
-
-import (
- "context"
- "fmt"
- "strings"
-
- "github.com/fluxcd/pkg/runtime/client"
- "github.com/fluxcd/pkg/ssa"
- "github.com/google/go-cmp/cmp"
- "helm.sh/helm/v3/pkg/release"
- "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
- "k8s.io/apimachinery/pkg/util/errors"
- ctrl "sigs.k8s.io/controller-runtime"
-
- "github.com/fluxcd/pkg/runtime/logger"
-
- helmv1 "github.com/fluxcd/helm-controller/api/v2beta1"
- intcmp "github.com/fluxcd/helm-controller/internal/cmp"
- "github.com/fluxcd/helm-controller/internal/util"
-)
-
-var (
- // MetadataKey is the label or annotation key used to disable the diffing
- // of an object.
- MetadataKey = helmv1.GroupVersion.Group + "/driftDetection"
- // MetadataDisabledValue is the value used to disable the diffing of an
- // object using MetadataKey.
- MetadataDisabledValue = "disabled"
-)
-
-type Differ struct {
- impersonator *client.Impersonator
- controllerName string
-}
-
-func NewDiffer(impersonator *client.Impersonator, controllerName string) *Differ {
- return &Differ{
- impersonator: impersonator,
- controllerName: controllerName,
- }
-}
-
-// Manager returns a new ssa.ResourceManager constructed using the client.Impersonator.
-func (d *Differ) Manager(ctx context.Context) (*ssa.ResourceManager, error) {
- c, poller, err := d.impersonator.GetClient(ctx)
- if err != nil {
- return nil, fmt.Errorf("failed to get client to configure resource manager: %w", err)
- }
- owner := ssa.Owner{
- Field: d.controllerName,
- }
- return ssa.NewResourceManager(c, poller, owner), nil
-}
-
-func (d *Differ) Diff(ctx context.Context, rel *release.Release) (*ssa.ChangeSet, bool, error) {
- objects, err := ssa.ReadObjects(strings.NewReader(rel.Manifest))
- if err != nil {
- return nil, false, fmt.Errorf("failed to read objects from release manifest: %w", err)
- }
-
- if err := ssa.SetNativeKindsDefaults(objects); err != nil {
- return nil, false, fmt.Errorf("failed to set native kind defaults on release objects: %w", err)
- }
-
- resourceManager, err := d.Manager(ctx)
- if err != nil {
- return nil, false, err
- }
-
- var (
- changeSet = ssa.NewChangeSet()
- isNamespacedGVK = map[string]bool{}
- diff bool
- errs []error
- )
- for _, obj := range objects {
- if obj.GetNamespace() == "" {
- // Manifest does not contain the namespace of the release.
- // Figure out if the object is namespaced if the namespace is not
- // explicitly set, and configure the namespace accordingly.
- objGVK := obj.GetObjectKind().GroupVersionKind().String()
- if _, ok := isNamespacedGVK[objGVK]; !ok {
- namespaced, err := util.IsAPINamespaced(obj, resourceManager.Client().Scheme(), resourceManager.Client().RESTMapper())
- if err != nil {
- errs = append(errs, fmt.Errorf("failed to determine if %s is namespace scoped: %w",
- obj.GetObjectKind().GroupVersionKind().Kind, err))
- continue
- }
- // Cache the result, so we don't have to do this for every object
- isNamespacedGVK[objGVK] = namespaced
- }
- if isNamespacedGVK[objGVK] {
- obj.SetNamespace(rel.Namespace)
- }
- }
-
- entry, releaseObject, clusterObject, err := resourceManager.Diff(ctx, obj, ssa.DiffOptions{
- Exclusions: map[string]string{
- MetadataKey: MetadataDisabledValue,
- },
- })
- if err != nil {
- errs = append(errs, err)
- }
-
- if entry == nil {
- continue
- }
-
- switch entry.Action {
- case ssa.CreatedAction, ssa.ConfiguredAction:
- diff = true
- changeSet.Add(*entry)
-
- if entry.Action == ssa.ConfiguredAction {
- // TODO: remove this once we have a better way to log the diff
- // for example using a custom dyff reporter, or a flux CLI command
- r := intcmp.SimpleUnstructuredReporter{}
- if diff := cmp.Diff(
- unstructuredWithoutStatus(releaseObject).UnstructuredContent(),
- unstructuredWithoutStatus(clusterObject).UnstructuredContent(),
- cmp.Reporter(&r)); diff != "" {
- ctrl.LoggerFrom(ctx).V(logger.DebugLevel).Info(entry.Subject + " diff:\n" + r.String())
- }
- }
- case ssa.SkippedAction:
- changeSet.Add(*entry)
- }
- }
-
- err = errors.Reduce(errors.Flatten(errors.NewAggregate(errs)))
- if len(changeSet.Entries) == 0 {
- return nil, diff, err
- }
- return changeSet, diff, err
-}
-
-func unstructuredWithoutStatus(obj *unstructured.Unstructured) *unstructured.Unstructured {
- obj = obj.DeepCopy()
- delete(obj.Object, "status")
- return obj
-}
diff --git a/internal/diff/differ_test.go b/internal/diff/differ_test.go
deleted file mode 100644
index f3a9bdf7d..000000000
--- a/internal/diff/differ_test.go
+++ /dev/null
@@ -1,250 +0,0 @@
-/*
-Copyright 2023 The Flux authors
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package diff
-
-import (
- "context"
- "fmt"
- "testing"
-
- . "github.com/onsi/gomega"
- corev1 "k8s.io/api/core/v1"
- "k8s.io/apimachinery/pkg/api/meta"
- "k8s.io/apimachinery/pkg/runtime"
- "k8s.io/apimachinery/pkg/runtime/schema"
- "sigs.k8s.io/cli-utils/pkg/kstatus/polling"
- "sigs.k8s.io/cli-utils/pkg/object"
- "sigs.k8s.io/controller-runtime/pkg/client"
- "sigs.k8s.io/controller-runtime/pkg/client/fake"
-
- runtimeClient "github.com/fluxcd/pkg/runtime/client"
- "github.com/fluxcd/pkg/ssa"
-
- "helm.sh/helm/v3/pkg/release"
-)
-
-func TestDiffer_Diff(t *testing.T) {
- scheme, mapper := testSchemeWithMapper()
-
- // We do not test all the possible scenarios here, as the ssa package is
- // already tested in depth. We only test the integration with the ssa package.
- tests := []struct {
- name string
- client client.Client
- rel *release.Release
- want *ssa.ChangeSet
- wantDrift bool
- wantErr string
- }{
- {
- name: "manifest read error",
- client: fake.NewClientBuilder().Build(),
- rel: &release.Release{
- Manifest: "invalid",
- },
- wantErr: "failed to read objects from release manifest",
- },
- {
- name: "error on failure to determine namespace scope",
- client: fake.NewClientBuilder().Build(),
- rel: &release.Release{
- Namespace: "release",
- Manifest: `apiVersion: v1
-kind: Secret
-metadata:
- name: test
-stringData:
- foo: bar
-`,
- },
- wantErr: "failed to determine if Secret is namespace scoped",
- },
- {
- name: "detects changes",
- client: fake.NewClientBuilder().
- WithScheme(scheme).
- WithRESTMapper(mapper).
- Build(),
- rel: &release.Release{
- Namespace: "release",
- Manifest: `---
-apiVersion: v1
-kind: Secret
-metadata:
- name: test
-stringData:
- foo: bar
----
-apiVersion: v1
-kind: Secret
-metadata:
- name: test-ns
- namespace: other
-stringData:
- foo: bar
-`,
- },
- want: &ssa.ChangeSet{
- Entries: []ssa.ChangeSetEntry{
- {
- ObjMetadata: object.ObjMetadata{
- Namespace: "release",
- Name: "test",
- GroupKind: schema.GroupKind{
- Kind: "Secret",
- },
- },
- GroupVersion: "v1",
- Subject: "Secret/release/test",
- Action: ssa.CreatedAction,
- },
- {
- ObjMetadata: object.ObjMetadata{
- Namespace: "other",
- Name: "test-ns",
- GroupKind: schema.GroupKind{
- Kind: "Secret",
- },
- },
- GroupVersion: "v1",
- Subject: "Secret/other/test-ns",
- Action: ssa.CreatedAction,
- },
- },
- },
- wantDrift: true,
- },
- {
- name: "ignores exclusions",
- client: fake.NewClientBuilder().
- WithScheme(scheme).
- WithRESTMapper(mapper).
- Build(),
- rel: &release.Release{
- Namespace: "release",
- Manifest: fmt.Sprintf(`---
-apiVersion: v1
-kind: Secret
-metadata:
- name: test
- labels:
- %[1]s: %[2]s
-stringData:
- foo: bar
----
-apiVersion: v1
-kind: Secret
-metadata:
- name: test2
-stringData:
- foo: bar
-`, MetadataKey, MetadataDisabledValue),
- },
- want: &ssa.ChangeSet{
- Entries: []ssa.ChangeSetEntry{
- {
- ObjMetadata: object.ObjMetadata{
- Namespace: "release",
- Name: "test",
- GroupKind: schema.GroupKind{
- Kind: "Secret",
- },
- },
- GroupVersion: "v1",
- Subject: "Secret/release/test",
- Action: ssa.SkippedAction,
- },
- {
- ObjMetadata: object.ObjMetadata{
- Namespace: "release",
- Name: "test2",
- GroupKind: schema.GroupKind{
- Kind: "Secret",
- },
- },
- GroupVersion: "v1",
- Subject: "Secret/release/test2",
- Action: ssa.CreatedAction,
- },
- },
- },
- wantDrift: true,
- },
- {
- name: "ignores exclusions (without diff)",
- client: fake.NewClientBuilder().
- WithScheme(scheme).
- WithRESTMapper(mapper).
- Build(),
- rel: &release.Release{
- Namespace: "release",
- Manifest: fmt.Sprintf(`---
-apiVersion: v1
-kind: Secret
-metadata:
- name: test
- labels:
- %[1]s: %[2]s
-stringData:
- foo: bar`, MetadataKey, MetadataDisabledValue),
- },
- want: &ssa.ChangeSet{
- Entries: []ssa.ChangeSetEntry{
- {
- ObjMetadata: object.ObjMetadata{
- Namespace: "release",
- Name: "test",
- GroupKind: schema.GroupKind{
- Kind: "Secret",
- },
- },
- GroupVersion: "v1",
- Subject: "Secret/release/test",
- Action: ssa.SkippedAction,
- },
- },
- },
- wantDrift: false,
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- g := NewWithT(t)
-
- d := NewDiffer(runtimeClient.NewImpersonator(tt.client, nil, polling.Options{}, nil, runtimeClient.KubeConfigOptions{}, "", "", ""), "test-controller")
- got, drift, err := d.Diff(context.TODO(), tt.rel)
-
- if tt.wantErr != "" {
- g.Expect(err).To(HaveOccurred())
- g.Expect(err.Error()).To(ContainSubstring(tt.wantErr))
- } else {
- g.Expect(err).NotTo(HaveOccurred())
- }
-
- g.Expect(got).To(Equal(tt.want))
- g.Expect(drift).To(Equal(tt.wantDrift))
- })
- }
-}
-
-func testSchemeWithMapper() (*runtime.Scheme, meta.RESTMapper) {
- scheme := runtime.NewScheme()
- _ = corev1.AddToScheme(scheme)
- mapper := meta.NewDefaultRESTMapper([]schema.GroupVersion{corev1.SchemeGroupVersion})
- mapper.Add(corev1.SchemeGroupVersion.WithKind("Secret"), meta.RESTScopeNamespace)
- return scheme, mapper
-}
diff --git a/internal/diff/summarize.go b/internal/diff/summarize.go
new file mode 100644
index 000000000..9b1d80d9b
--- /dev/null
+++ b/internal/diff/summarize.go
@@ -0,0 +1,176 @@
+/*
+Copyright 2023 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package diff
+
+import (
+ "fmt"
+ "strings"
+
+ extjsondiff "github.com/wI2L/jsondiff"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+
+ "github.com/fluxcd/pkg/ssa/jsondiff"
+)
+
+// DefaultDiffTypes is the default set of jsondiff.DiffType types to include in
+// summaries.
+var DefaultDiffTypes = []jsondiff.DiffType{
+ jsondiff.DiffTypeCreate,
+ jsondiff.DiffTypeUpdate,
+ jsondiff.DiffTypeExclude,
+}
+
+// SummarizeDiffSet returns a summary of the given DiffSet, including only
+// the given jsondiff.DiffType types. If no types are given, the
+// DefaultDiffTypes set is used.
+//
+// The summary is a string with one line per Diff, in the format:
+// `Kind/namespace/name: `
+//
+// Where summary is one of:
+//
+// - unchanged
+// - removed
+// - excluded
+// - changed (x added, y changed, z removed)
+//
+// For example:
+//
+// Deployment/default/hello-world changed (1 added, 1 changed, 1 removed)
+// Deployment/default/hello-world2 removed
+// Deployment/default/hello-world3 excluded
+// Deployment/default/hello-world4 unchanged
+func SummarizeDiffSet(set jsondiff.DiffSet, include ...jsondiff.DiffType) string {
+ if include == nil {
+ include = DefaultDiffTypes
+ }
+
+ var summary strings.Builder
+ for _, diff := range set {
+ if diff == nil || !typeInSlice(diff.Type, include) {
+ continue
+ }
+
+ switch diff.Type {
+ case jsondiff.DiffTypeNone:
+ writeResourceName(diff.DesiredObject, &summary)
+ summary.WriteString(" unchanged\n")
+ case jsondiff.DiffTypeCreate:
+ writeResourceName(diff.DesiredObject, &summary)
+ summary.WriteString(" removed\n")
+ case jsondiff.DiffTypeExclude:
+ writeResourceName(diff.DesiredObject, &summary)
+ summary.WriteString(" excluded\n")
+ case jsondiff.DiffTypeUpdate:
+ writeResourceName(diff.DesiredObject, &summary)
+ added, changed, removed := summarizeUpdate(diff)
+ summary.WriteString(fmt.Sprintf(" changed (%d additions, %d changes, %d removals)\n", added, changed, removed))
+ }
+ }
+ return strings.TrimSpace(summary.String())
+}
+
+// SummarizeDiffSetBrief returns a brief summary of the given DiffSet.
+//
+// The summary is a string in the format:
+//
+// removed: x, changed: y, excluded: z, unchanged: w
+//
+// For example:
+//
+// removed: 1, changed: 3, excluded: 1, unchanged: 2
+func SummarizeDiffSetBrief(set jsondiff.DiffSet, include ...jsondiff.DiffType) string {
+ var removed, changed, excluded, unchanged int
+ for _, diff := range set {
+ switch diff.Type {
+ case jsondiff.DiffTypeCreate:
+ removed++
+ case jsondiff.DiffTypeUpdate:
+ changed++
+ case jsondiff.DiffTypeExclude:
+ excluded++
+ case jsondiff.DiffTypeNone:
+ unchanged++
+ }
+ }
+
+ if include == nil {
+ include = DefaultDiffTypes
+ }
+
+ var summary strings.Builder
+ for _, t := range include {
+ switch t {
+ case jsondiff.DiffTypeCreate:
+ summary.WriteString(fmt.Sprintf("removed: %d, ", removed))
+ case jsondiff.DiffTypeUpdate:
+ summary.WriteString(fmt.Sprintf("changed: %d, ", changed))
+ case jsondiff.DiffTypeExclude:
+ summary.WriteString(fmt.Sprintf("excluded: %d, ", excluded))
+ case jsondiff.DiffTypeNone:
+ summary.WriteString(fmt.Sprintf("unchanged: %d, ", unchanged))
+ }
+ }
+ return strings.TrimSuffix(summary.String(), ", ")
+}
+
+// ResourceName returns the resource name in the format `kind/namespace/name`.
+func ResourceName(obj client.Object) string {
+ var summary strings.Builder
+ writeResourceName(obj, &summary)
+ return summary.String()
+}
+
+const resourceSeparator = "/"
+
+// writeResourceName writes the resource name in the format
+// `kind/namespace/name` to the given strings.Builder.
+func writeResourceName(obj client.Object, summary *strings.Builder) {
+ summary.WriteString(obj.GetObjectKind().GroupVersionKind().Kind)
+ summary.WriteString(resourceSeparator)
+ if ns := obj.GetNamespace(); ns != "" {
+ summary.WriteString(ns)
+ summary.WriteString(resourceSeparator)
+ }
+ summary.WriteString(obj.GetName())
+}
+
+// SummarizeUpdate returns the number of added, changed and removed fields
+// in the given update patch.
+func summarizeUpdate(diff *jsondiff.Diff) (added, changed, removed int) {
+ for _, p := range diff.Patch {
+ switch p.Type {
+ case extjsondiff.OperationAdd:
+ added++
+ case extjsondiff.OperationReplace:
+ changed++
+ case extjsondiff.OperationRemove:
+ removed++
+ }
+ }
+ return
+}
+
+// typeInSlice returns true if the given jsondiff.DiffType is in the slice.
+func typeInSlice(t jsondiff.DiffType, slice []jsondiff.DiffType) bool {
+ for _, s := range slice {
+ if t == s {
+ return true
+ }
+ }
+ return false
+}
diff --git a/internal/diff/summarize_test.go b/internal/diff/summarize_test.go
new file mode 100644
index 000000000..749b1c30d
--- /dev/null
+++ b/internal/diff/summarize_test.go
@@ -0,0 +1,248 @@
+/*
+Copyright 2023 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package diff
+
+import (
+ "testing"
+
+ extjsondiff "github.com/wI2L/jsondiff"
+ "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+
+ "github.com/fluxcd/pkg/ssa/jsondiff"
+)
+
+func TestSummarizeDiffSet(t *testing.T) {
+ diffSet := jsondiff.DiffSet{
+ &jsondiff.Diff{
+ DesiredObject: &unstructured.Unstructured{
+ Object: map[string]any{
+ "kind": "ConfigMap",
+ "metadata": map[string]any{
+ "name": "config",
+ "namespace": "namespace-1",
+ },
+ },
+ },
+ Type: jsondiff.DiffTypeNone,
+ },
+ &jsondiff.Diff{
+ DesiredObject: &unstructured.Unstructured{
+ Object: map[string]any{
+ "kind": "Secret",
+ "metadata": map[string]any{
+ "name": "naughty",
+ "namespace": "namespace-x",
+ },
+ },
+ },
+ Type: jsondiff.DiffTypeCreate,
+ },
+ &jsondiff.Diff{
+ DesiredObject: &unstructured.Unstructured{
+ Object: map[string]any{
+ "kind": "StatefulSet",
+ "metadata": map[string]any{
+ "name": "hello-world",
+ "namespace": "default",
+ },
+ },
+ },
+ Type: jsondiff.DiffTypeExclude,
+ },
+ &jsondiff.Diff{
+ DesiredObject: &unstructured.Unstructured{
+ Object: map[string]any{
+ "kind": "Deployment",
+ "metadata": map[string]any{
+ "name": "touched-me",
+ "namespace": "tenant-y",
+ },
+ },
+ },
+ Type: jsondiff.DiffTypeUpdate,
+ Patch: extjsondiff.Patch{
+ {Type: extjsondiff.OperationAdd},
+ {Type: extjsondiff.OperationReplace},
+ {Type: extjsondiff.OperationReplace},
+ {Type: extjsondiff.OperationReplace},
+ {Type: extjsondiff.OperationRemove},
+ {Type: extjsondiff.OperationRemove},
+ },
+ },
+ }
+
+ tests := []struct {
+ name string
+ include []jsondiff.DiffType
+ want string
+ }{
+ {
+ name: "default",
+ include: nil,
+ want: `Secret/namespace-x/naughty removed
+StatefulSet/default/hello-world excluded
+Deployment/tenant-y/touched-me changed (1 additions, 3 changes, 2 removals)`,
+ },
+ {
+ name: "include unchanged",
+ include: []jsondiff.DiffType{
+ jsondiff.DiffTypeNone,
+ },
+ want: "ConfigMap/namespace-1/config unchanged",
+ },
+ {
+ name: "include removed",
+ include: []jsondiff.DiffType{
+ jsondiff.DiffTypeCreate,
+ },
+ want: "Secret/namespace-x/naughty removed",
+ },
+ {
+ name: "include excluded",
+ include: []jsondiff.DiffType{
+ jsondiff.DiffTypeExclude,
+ },
+ want: "StatefulSet/default/hello-world excluded",
+ },
+ {
+ name: "include changed",
+ include: []jsondiff.DiffType{
+ jsondiff.DiffTypeUpdate,
+ },
+ want: "Deployment/tenant-y/touched-me changed (1 additions, 3 changes, 2 removals)",
+ },
+ {
+ name: "include multiple types",
+ include: []jsondiff.DiffType{
+ jsondiff.DiffTypeNone,
+ jsondiff.DiffTypeUpdate,
+ },
+ want: `ConfigMap/namespace-1/config unchanged
+Deployment/tenant-y/touched-me changed (1 additions, 3 changes, 2 removals)`,
+ },
+ {
+ name: "empty set",
+ include: []jsondiff.DiffType{},
+ want: "",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got := SummarizeDiffSet(diffSet, tt.include...)
+ if got != tt.want {
+ t.Errorf("SummarizeDiffSet() =\n\n%v\n\nwant\n\n%v", got, tt.want)
+ }
+ })
+ }
+}
+
+func TestSummarizeDiffSetBrief(t *testing.T) {
+ diffSet := jsondiff.DiffSet{
+ &jsondiff.Diff{Type: jsondiff.DiffTypeCreate},
+ &jsondiff.Diff{Type: jsondiff.DiffTypeUpdate},
+ &jsondiff.Diff{Type: jsondiff.DiffTypeExclude},
+ &jsondiff.Diff{Type: jsondiff.DiffTypeNone},
+ &jsondiff.Diff{Type: jsondiff.DiffTypeNone},
+ }
+
+ tests := []struct {
+ name string
+ include []jsondiff.DiffType
+ want string
+ }{
+ {
+ name: "default include",
+ include: nil,
+ want: "removed: 1, changed: 1, excluded: 1",
+ },
+ {
+ name: "include create and update",
+ include: []jsondiff.DiffType{
+ jsondiff.DiffTypeCreate,
+ jsondiff.DiffTypeUpdate,
+ },
+ want: "removed: 1, changed: 1",
+ },
+ {
+ name: "include all types",
+ include: []jsondiff.DiffType{
+ jsondiff.DiffTypeCreate,
+ jsondiff.DiffTypeUpdate,
+ jsondiff.DiffTypeExclude,
+ jsondiff.DiffTypeNone,
+ },
+ want: "removed: 1, changed: 1, excluded: 1, unchanged: 2",
+ },
+ {
+ name: "include none",
+ include: []jsondiff.DiffType{},
+ want: "",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got := SummarizeDiffSetBrief(diffSet, tt.include...)
+ if got != tt.want {
+ t.Errorf("SummarizeDiffSetBrief() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
+
+func TestResourceName(t *testing.T) {
+ tests := []struct {
+ name string
+ resource client.Object
+ want string
+ }{
+ {
+ name: "with namespace",
+ resource: &unstructured.Unstructured{
+ Object: map[string]any{
+ "kind": "Deployment",
+ "metadata": map[string]any{
+ "name": "touched-me",
+ "namespace": "tenant-y",
+ },
+ },
+ },
+ want: "Deployment/tenant-y/touched-me",
+ },
+ {
+ name: "without namespace",
+ resource: &unstructured.Unstructured{
+ Object: map[string]any{
+ "kind": "ClusterIssuer",
+ "metadata": map[string]any{
+ "name": "letsencrypt",
+ },
+ },
+ },
+ want: "ClusterIssuer/letsencrypt",
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if got := ResourceName(tt.resource); got != tt.want {
+ t.Errorf("ResourceName() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
diff --git a/internal/diff/unstructured.go b/internal/diff/unstructured.go
new file mode 100644
index 000000000..a61ed04db
--- /dev/null
+++ b/internal/diff/unstructured.go
@@ -0,0 +1,54 @@
+/*
+Copyright 2023 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package diff
+
+import (
+ "github.com/google/go-cmp/cmp"
+ "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+
+ intcmp "github.com/fluxcd/helm-controller/internal/cmp"
+)
+
+// CompareOption is a function that modifies the unstructured object before
+// comparing.
+type CompareOption func(u *unstructured.Unstructured)
+
+// WithoutStatus removes the status field from the unstructured object
+// before comparing.
+func WithoutStatus() CompareOption {
+ return func(u *unstructured.Unstructured) {
+ delete(u.Object, "status")
+ }
+}
+
+// Unstructured compares two unstructured objects and returns a diff and
+// a bool indicating whether the objects are equal.
+func Unstructured(x, y *unstructured.Unstructured, opts ...CompareOption) (string, bool) {
+ if len(opts) > 0 {
+ x = x.DeepCopy()
+ y = y.DeepCopy()
+ }
+
+ for _, opt := range opts {
+ opt(x)
+ opt(y)
+ }
+
+ r := intcmp.SimpleUnstructuredReporter{}
+ _ = cmp.Diff(x.UnstructuredContent(), y.UnstructuredContent(), cmp.Reporter(&r))
+ return r.String(), r.String() == ""
+}
diff --git a/internal/diff/unstructured_test.go b/internal/diff/unstructured_test.go
new file mode 100644
index 000000000..2b3cfd2ab
--- /dev/null
+++ b/internal/diff/unstructured_test.go
@@ -0,0 +1,162 @@
+/*
+Copyright 2023 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package diff
+
+import (
+ "testing"
+
+ . "github.com/onsi/gomega"
+ "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+)
+
+func TestWithoutStatus(t *testing.T) {
+ g := NewWithT(t)
+
+ u := unstructured.Unstructured{
+ Object: map[string]any{
+ "status": "test",
+ },
+ }
+ WithoutStatus()(&u)
+ g.Expect(u.Object["status"]).To(BeNil())
+}
+
+func TestUnstructured(t *testing.T) {
+ tests := []struct {
+ name string
+ x *unstructured.Unstructured
+ y *unstructured.Unstructured
+ opts []CompareOption
+ want string
+ equal bool
+ }{
+ {
+ name: "equal objects",
+ x: &unstructured.Unstructured{Object: map[string]any{
+ "spec": map[string]any{
+ "replicas": int64(4),
+ },
+ "status": map[string]any{
+ "readyReplicas": int64(4),
+ },
+ }},
+ y: &unstructured.Unstructured{Object: map[string]any{
+ "spec": map[string]any{
+ "replicas": int64(4),
+ },
+ "status": map[string]any{
+ "readyReplicas": int64(4),
+ },
+ }},
+ want: "",
+ equal: true,
+ },
+ {
+ name: "added simple value",
+ x: &unstructured.Unstructured{Object: map[string]any{
+ "spec": map[string]any{
+ "replicas": int64(1),
+ },
+ "status": map[string]any{},
+ }},
+ y: &unstructured.Unstructured{Object: map[string]any{
+ "spec": map[string]any{
+ "replicas": int64(1),
+ },
+ "status": map[string]any{
+ "readyReplicas": int64(1),
+ },
+ }},
+ want: `.status.readyReplicas
++1`,
+ equal: false,
+ },
+ {
+ name: "removed simple value",
+ x: &unstructured.Unstructured{Object: map[string]any{
+ "spec": map[string]any{
+ "replicas": int64(1),
+ },
+ "status": map[string]any{
+ "readyReplicas": int64(4),
+ },
+ }},
+ y: &unstructured.Unstructured{Object: map[string]any{
+ "spec": map[string]any{},
+ "status": map[string]any{
+ "readyReplicas": int64(4),
+ },
+ }},
+ want: `.spec.replicas
+-1`,
+ equal: false,
+ },
+ {
+ name: "changed simple value",
+ x: &unstructured.Unstructured{Object: map[string]any{
+ "spec": map[string]any{
+ "replicas": int64(3),
+ },
+ "status": map[string]any{
+ "readyReplicas": int64(1),
+ },
+ }},
+ y: &unstructured.Unstructured{Object: map[string]any{
+ "spec": map[string]any{
+ "replicas": int64(3),
+ },
+ "status": map[string]any{
+ "readyReplicas": int64(3),
+ },
+ }},
+ want: `.status.readyReplicas
+-1
++3`,
+ equal: false,
+ },
+ {
+ name: "with options",
+ opts: []CompareOption{WithoutStatus()},
+ x: &unstructured.Unstructured{Object: map[string]any{
+ "spec": map[string]any{
+ "replicas": int64(3),
+ },
+ "status": map[string]any{
+ "readyReplicas": int64(4),
+ },
+ }},
+ y: &unstructured.Unstructured{Object: map[string]any{
+ "spec": map[string]any{
+ "replicas": int64(3),
+ },
+ "status": map[string]any{
+ "readyReplicas": int64(1),
+ },
+ }},
+ equal: true,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ g := NewWithT(t)
+
+ got, equal := Unstructured(tt.x, tt.y, tt.opts...)
+ g.Expect(got).To(Equal(tt.want))
+ g.Expect(equal).To(Equal(tt.equal))
+ })
+ }
+}
diff --git a/internal/digest/digest.go b/internal/digest/digest.go
new file mode 100644
index 000000000..f1e5154b6
--- /dev/null
+++ b/internal/digest/digest.go
@@ -0,0 +1,52 @@
+/*
+Copyright 2022 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package digest
+
+import (
+ "crypto"
+ _ "crypto/sha256"
+ _ "crypto/sha512"
+ "fmt"
+
+ "github.com/opencontainers/go-digest"
+ _ "github.com/opencontainers/go-digest/blake3"
+)
+
+const (
+ SHA1 digest.Algorithm = "sha1"
+)
+
+var (
+ // Canonical is the primary digest algorithm used to calculate checksums
+ // for e.g. Helm release objects and config values.
+ Canonical = digest.SHA256
+)
+
+func init() {
+ // Register SHA-1 algorithm for support of legacy values checksums.
+ digest.RegisterAlgorithm(SHA1, crypto.SHA1)
+}
+
+// AlgorithmForName returns the digest algorithm for the given name, or an
+// error of type digest.ErrDigestUnsupported if the algorithm is unavailable.
+func AlgorithmForName(name string) (digest.Algorithm, error) {
+ a := digest.Algorithm(name)
+ if !a.Available() {
+ return "", fmt.Errorf("%w: %s", digest.ErrDigestUnsupported, name)
+ }
+ return a, nil
+}
diff --git a/internal/digest/digest_test.go b/internal/digest/digest_test.go
new file mode 100644
index 000000000..fb56e65d9
--- /dev/null
+++ b/internal/digest/digest_test.go
@@ -0,0 +1,71 @@
+/*
+Copyright 2023 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package digest
+
+import (
+ "errors"
+ "testing"
+
+ . "github.com/onsi/gomega"
+ "github.com/opencontainers/go-digest"
+)
+
+func TestAlgorithmForName(t *testing.T) {
+ tests := []struct {
+ name string
+ want digest.Algorithm
+ wantErr error
+ }{
+ {
+ name: "sha256",
+ want: digest.SHA256,
+ },
+ {
+ name: "sha384",
+ want: digest.SHA384,
+ },
+ {
+ name: "sha512",
+ want: digest.SHA512,
+ },
+ {
+ name: "blake3",
+ want: digest.BLAKE3,
+ },
+ {
+ name: "sha1",
+ want: SHA1,
+ },
+ {
+ name: "not-available",
+ wantErr: digest.ErrDigestUnsupported,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ g := NewWithT(t)
+ got, err := AlgorithmForName(tt.name)
+ if tt.wantErr != nil {
+ g.Expect(err).To(HaveOccurred())
+ g.Expect(errors.Is(err, tt.wantErr)).To(BeTrue())
+ return
+ }
+ g.Expect(err).ToNot(HaveOccurred())
+ g.Expect(got).To(Equal(tt.want))
+ })
+ }
+}
diff --git a/internal/errors/ignore.go b/internal/errors/ignore.go
new file mode 100644
index 000000000..8cd247c92
--- /dev/null
+++ b/internal/errors/ignore.go
@@ -0,0 +1,25 @@
+/*
+Copyright 2023 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package errors
+
+// Ignore returns nil if err is equal to any of the errs.
+func Ignore(err error, errs ...error) error {
+ if IsOneOf(err, errs...) {
+ return nil
+ }
+ return err
+}
diff --git a/internal/errors/ignore_test.go b/internal/errors/ignore_test.go
new file mode 100644
index 000000000..bcf204ea3
--- /dev/null
+++ b/internal/errors/ignore_test.go
@@ -0,0 +1,40 @@
+/*
+Copyright 2023 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package errors
+
+import (
+ "errors"
+ "testing"
+)
+
+func TestIgnore(t *testing.T) {
+ err1 := errors.New("error1")
+ err2 := errors.New("error2")
+
+ if err := Ignore(err1, err1, err2); err != nil {
+ t.Errorf("Expected Ignore to return nil when the error is in the list, but got %v", err)
+ }
+
+ err3 := errors.New("error3")
+ if err := Ignore(err3, err1, err2); !errors.Is(err, err3) {
+ t.Errorf("Expected Ignore to return the error when it is not in the list, but got %v", err)
+ }
+
+ if err := Ignore(err1); !errors.Is(err, err1) {
+ t.Errorf("Expected Ignore to return the error with an empty list of errors, but got %v", err)
+ }
+}
diff --git a/internal/errors/is.go b/internal/errors/is.go
new file mode 100644
index 000000000..f2bfb09d0
--- /dev/null
+++ b/internal/errors/is.go
@@ -0,0 +1,29 @@
+/*
+Copyright 2023 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package errors
+
+import "errors"
+
+// IsOneOf returns true if err is equal to any of the errs.
+func IsOneOf(err error, errs ...error) bool {
+ for _, e := range errs {
+ if errors.Is(err, e) {
+ return true
+ }
+ }
+ return false
+}
diff --git a/internal/errors/is_test.go b/internal/errors/is_test.go
new file mode 100644
index 000000000..a3a358be7
--- /dev/null
+++ b/internal/errors/is_test.go
@@ -0,0 +1,40 @@
+/*
+Copyright 2023 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package errors
+
+import (
+ "errors"
+ "testing"
+)
+
+func TestIsOneOf(t *testing.T) {
+ err1 := errors.New("error1")
+ err2 := errors.New("error2")
+
+ if !IsOneOf(err1, err1, err2) {
+ t.Errorf("Expected IsOneOf to return true when the error is in the list, but got false")
+ }
+
+ err3 := errors.New("error3")
+ if IsOneOf(err3, err1, err2) {
+ t.Errorf("Expected IsOneOf to return false when the error is not in the list, but got true")
+ }
+
+ if IsOneOf(err1) {
+ t.Errorf("Expected IsOneOf to return false with an empty list of errors, but got true")
+ }
+}
diff --git a/internal/features/features.go b/internal/features/features.go
index 43e7a4425..e385bc731 100644
--- a/internal/features/features.go
+++ b/internal/features/features.go
@@ -18,24 +18,25 @@ limitations under the License.
// helm-controller supports, and their default states.
package features
-import feathelper "github.com/fluxcd/pkg/runtime/features"
+import (
+ "github.com/fluxcd/pkg/auth"
+ "github.com/fluxcd/pkg/runtime/controller"
+ feathelper "github.com/fluxcd/pkg/runtime/features"
+)
const (
- // CacheSecretsAndConfigMaps configures the caching of Secrets and ConfigMaps
- // by the controller-runtime client.
- //
- // When enabled, it will cache both object types, resulting in increased memory
- // usage and cluster-wide RBAC permissions (list and watch).
- CacheSecretsAndConfigMaps = "CacheSecretsAndConfigMaps"
-
// DetectDrift configures the detection of cluster state drift compared to
// the desired state as described in the manifest of the Helm release
// storage object.
+ // Deprecated in v0.37.0, use the drift detection mode on the HelmRelease
+ // object instead.
DetectDrift = "DetectDrift"
// CorrectDrift configures the correction of cluster state drift compared to
// the desired state as described in the manifest of the Helm release. It
// is only effective when DetectDrift is enabled.
+ // Deprecated in v0.37.0, use the drift detection mode on the HelmRelease
+ // object instead.
CorrectDrift = "CorrectDrift"
// AllowDNSLookups allows the controller to perform DNS lookups when rendering Helm
@@ -47,24 +48,94 @@ const (
// OOMWatch enables the OOM watcher, which will gracefully shut down the controller
// when the memory usage exceeds the configured limit. This is disabled by default.
OOMWatch = "OOMWatch"
+
+ // AdoptLegacyReleases enables the adoption of the historical Helm release
+ // based on the status fields from a v2beta1 HelmRelease object.
+ // This is enabled by default to support an upgrade path from v2beta1 to v2
+ // without the need to upgrade the Helm release. But it can be disabled to
+ // avoid potential abuse of the adoption mechanism.
+ //
+ // Ignored from v1.5.0, prints a warning if set.
+ AdoptLegacyReleases = "AdoptLegacyReleases"
+
+ // DisableChartDigestTracking disables the tracking of digest changes
+ // for Helm OCI charts. When enabled, the controller will not trigger
+ // a Helm release upgrade if the chart version stays the same, but its
+ // digest changes. When enabled, the controller will not
+ // append the digest to the chart version in Chart.yaml.
+ DisableChartDigestTracking = "DisableChartDigestTracking"
+
+ // UseHelm3Defaults makes the controller use the Helm 3 default behaviors
+ // when defaults are used.
+ UseHelm3Defaults = "UseHelm3Defaults"
+
+ // CancelHealthCheckOnNewRevision controls whether ongoing health checks
+ // should be cancelled when a new reconciliation is triggered for the
+ // same HelmRelease, regardless of the reason. The name does not match
+ // this behavior exactly for historical reasons.
+ //
+ // When enabled, if a new reconciliation request is detected while waiting
+ // for resources to become ready, the current health check will be cancelled
+ // to allow immediate processing of the new reconciliation request. This can
+ // help avoid getting stuck on failing deployments when fixes are available.
+ CancelHealthCheckOnNewRevision = "CancelHealthCheckOnNewRevision"
+
+ // DefaultToRetryOnFailure changes the default install/upgrade strategy
+ // from RemediateOnFailure to RetryOnFailure when the user has not
+ // explicitly configured a strategy. Unlike RemediateOnFailure, which
+ // has a retry budget, RetryOnFailure retries indefinitely and
+ // auto-clears failures on success, providing better UX especially
+ // when CancelHealthCheckOnNewRevision is enabled.
+ DefaultToRetryOnFailure = "DefaultToRetryOnFailure"
)
var features = map[string]bool{
// CacheSecretsAndConfigMaps
// opt-in from v0.28
- CacheSecretsAndConfigMaps: false,
+ controller.FeatureGateCacheSecretsAndConfigMaps: false,
// DetectDrift
- // opt-in from v0.31
+ // deprecated in v0.37.0
DetectDrift: false,
// CorrectDrift,
- // opt-out from v0.31.2
- CorrectDrift: true,
+ // deprecated in v0.37.0
+ CorrectDrift: false,
// AllowDNSLookups
// opt-in from v0.31
AllowDNSLookups: false,
// OOMWatch
// opt-in from v0.31
OOMWatch: false,
+ // AdoptLegacyReleases
+ // ignored, prints warning from v1.5.0
+ AdoptLegacyReleases: false,
+ // DisableChartDigestTracking
+ // opt-in from v1.3.0
+ DisableChartDigestTracking: false,
+ // AdditiveCELDependencyCheck
+ // opt-in from v1.4.0
+ controller.FeatureGateAdditiveCELDependencyCheck: false,
+ // ExternalArtifact
+ // opt-in from v1.4.0
+ controller.FeatureGateExternalArtifact: false,
+ // DisableConfigWatchers
+ // opt-in from v1.4.4
+ controller.FeatureGateDisableConfigWatchers: false,
+ // DirectSourceFetch
+ // opt-in from v1.5.0
+ controller.FeatureGateDirectSourceFetch: false,
+ // UseHelm3Defaults
+ // opt-in from v1.5.0
+ UseHelm3Defaults: false,
+ // CancelHealthCheckOnNewRevision
+ // opt-in from v1.5.0
+ CancelHealthCheckOnNewRevision: false,
+ // DefaultToRetryOnFailure
+ // opt-in from v1.5.2
+ DefaultToRetryOnFailure: false,
+}
+
+func init() {
+ auth.SetFeatureGates(features)
}
// FeatureGates contains a list of all supported feature gates and
diff --git a/internal/inventory/inventory.go b/internal/inventory/inventory.go
new file mode 100644
index 000000000..8e918a5d7
--- /dev/null
+++ b/internal/inventory/inventory.go
@@ -0,0 +1,125 @@
+/*
+Copyright 2026 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package inventory
+
+import (
+ "bytes"
+ "fmt"
+ "slices"
+ "strings"
+
+ "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+ "k8s.io/apimachinery/pkg/runtime/schema"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+ "sigs.k8s.io/controller-runtime/pkg/client/apiutil"
+
+ "github.com/fluxcd/cli-utils/pkg/object"
+ ssautil "github.com/fluxcd/pkg/ssa/utils"
+
+ v2 "github.com/fluxcd/helm-controller/api/v2"
+ helmchart "helm.sh/helm/v4/pkg/chart/v2"
+ helmrelease "helm.sh/helm/v4/pkg/release/v1"
+)
+
+// New returns a new ResourceInventory with an empty Entries slice.
+func New() *v2.ResourceInventory {
+ return &v2.ResourceInventory{
+ Entries: []v2.ResourceRef{},
+ }
+}
+
+// AddManifest parses the manifest, complements namespaces, and adds the objects to the inventory.
+func AddManifest(inv *v2.ResourceInventory, manifest string, releaseNamespace string, c client.Client) (warnings []string, err error) {
+ objects, err := parseManifest(manifest)
+ if err != nil {
+ return nil, fmt.Errorf("failed to parse manifest: %w", err)
+ }
+
+ objects = slices.DeleteFunc(objects, func(obj *unstructured.Unstructured) bool {
+ annotations := obj.GetAnnotations()
+ if annotations == nil {
+ return false
+ }
+ _, isHook := annotations[helmrelease.HookAnnotation]
+ return isHook
+ })
+
+ warnings = setNamespaces(objects, releaseNamespace, c)
+
+ for _, obj := range objects {
+ objMeta := object.UnstructuredToObjMetadata(obj)
+ inv.Entries = append(inv.Entries, v2.ResourceRef{
+ ID: objMeta.String(),
+ Version: obj.GroupVersionKind().Version,
+ })
+ }
+ return warnings, nil
+}
+
+func parseManifest(manifest string) ([]*unstructured.Unstructured, error) {
+ return ssautil.ReadObjects(strings.NewReader(manifest))
+}
+
+// setNamespaces complements namespace for namespaced objects that don't have one set.
+// This is necessary because Helm manifests don't include namespace for namespaced resources.
+func setNamespaces(objects []*unstructured.Unstructured, releaseNamespace string, c client.Client) []string {
+ var warnings []string
+ isNamespacedGK := map[schema.GroupKind]bool{}
+
+ for _, obj := range objects {
+ if obj.GetNamespace() != "" {
+ continue
+ }
+
+ objGK := obj.GetObjectKind().GroupVersionKind().GroupKind()
+ if _, ok := isNamespacedGK[objGK]; !ok {
+ namespaced, err := apiutil.IsObjectNamespaced(obj, c.Scheme(), c.RESTMapper())
+ if err != nil {
+ warnings = append(warnings, fmt.Sprintf(
+ "failed to determine if %s is namespace scoped, skipping namespace: %s",
+ objGK.Kind, err.Error()))
+ continue
+ }
+ isNamespacedGK[objGK] = namespaced
+ }
+
+ if isNamespacedGK[objGK] {
+ obj.SetNamespace(releaseNamespace)
+ }
+ }
+ return warnings
+}
+
+// AddCRDs adds CRDs from the chart's crds/ directory to the inventory.
+// CRDs are cluster-scoped, so no namespace complement is needed.
+func AddCRDs(inv *v2.ResourceInventory, chart *helmchart.Chart) error {
+ for _, crd := range chart.CRDObjects() {
+ objects, err := ssautil.ReadObjects(bytes.NewBuffer(crd.File.Data))
+ if err != nil {
+ return fmt.Errorf("failed to parse CRD %s: %w", crd.Name, err)
+ }
+
+ for _, obj := range objects {
+ objMeta := object.UnstructuredToObjMetadata(obj)
+ inv.Entries = append(inv.Entries, v2.ResourceRef{
+ ID: objMeta.String(),
+ Version: obj.GroupVersionKind().Version,
+ })
+ }
+ }
+ return nil
+}
diff --git a/internal/inventory/inventory_test.go b/internal/inventory/inventory_test.go
new file mode 100644
index 000000000..761b65a92
--- /dev/null
+++ b/internal/inventory/inventory_test.go
@@ -0,0 +1,348 @@
+/*
+Copyright 2026 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package inventory
+
+import (
+ "testing"
+ "time"
+
+ . "github.com/onsi/gomega"
+ appsv1 "k8s.io/api/apps/v1"
+ corev1 "k8s.io/api/core/v1"
+ rbacv1 "k8s.io/api/rbac/v1"
+ "k8s.io/apimachinery/pkg/api/meta"
+ "k8s.io/apimachinery/pkg/runtime"
+ "k8s.io/apimachinery/pkg/runtime/schema"
+ utilruntime "k8s.io/apimachinery/pkg/util/runtime"
+ "sigs.k8s.io/controller-runtime/pkg/client/fake"
+
+ "helm.sh/helm/v4/pkg/chart/common"
+ helmchart "helm.sh/helm/v4/pkg/chart/v2"
+)
+
+func TestNew(t *testing.T) {
+ g := NewWithT(t)
+
+ inv := New()
+
+ g.Expect(inv).ToNot(BeNil())
+ g.Expect(inv.Entries).To(BeEmpty())
+}
+
+func TestAddManifest(t *testing.T) {
+ scheme := runtime.NewScheme()
+ utilruntime.Must(corev1.AddToScheme(scheme))
+ utilruntime.Must(appsv1.AddToScheme(scheme))
+ utilruntime.Must(rbacv1.AddToScheme(scheme))
+
+ fakeMapper := newFakeRESTMapper()
+
+ fakeClient := fake.NewClientBuilder().
+ WithScheme(scheme).
+ WithRESTMapper(fakeMapper).
+ Build()
+
+ tests := []struct {
+ name string
+ manifest string
+ releaseNamespace string
+ expectedLen int
+ expectedIDs []string
+ expectError bool
+ }{
+ {
+ name: "namespace specified in manifest",
+ manifest: `apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: mycm
+ namespace: default
+`,
+ releaseNamespace: "other",
+ expectedLen: 1,
+ expectedIDs: []string{"default_mycm__ConfigMap"},
+ },
+ {
+ name: "namespace complement for namespaced resource",
+ manifest: `apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: myapp
+`,
+ releaseNamespace: "foo",
+ expectedLen: 1,
+ expectedIDs: []string{"foo_myapp_apps_Deployment"},
+ },
+ {
+ name: "cluster-scoped resource not complemented",
+ manifest: `apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+ name: myrole
+`,
+ releaseNamespace: "foo",
+ expectedLen: 1,
+ expectedIDs: []string{"_myrole_rbac.authorization.k8s.io_ClusterRole"},
+ },
+ {
+ name: "multiple documents",
+ manifest: `apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: mycm
+ namespace: default
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: myapp
+`,
+ releaseNamespace: "bar",
+ expectedLen: 2,
+ expectedIDs: []string{"default_mycm__ConfigMap", "bar_myapp_apps_Deployment"},
+ },
+ {
+ name: "hook resources excluded",
+ manifest: `apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: hook
+ namespace: default
+ annotations:
+ "helm.sh/hook": post-install
+data:
+ foo: bar
+---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: regular
+ namespace: default
+data:
+ foo: bar
+`,
+ releaseNamespace: "default",
+ expectedLen: 1,
+ expectedIDs: []string{"default_regular__ConfigMap"},
+ },
+ {
+ name: "all hooks excluded results in empty inventory",
+ manifest: `apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: pre-install-hook
+ namespace: default
+ annotations:
+ "helm.sh/hook": pre-install
+`,
+ releaseNamespace: "default",
+ expectedLen: 0,
+ expectedIDs: []string{},
+ },
+ {
+ name: "invalid YAML returns error",
+ manifest: "not: valid: yaml: content",
+ releaseNamespace: "default",
+ expectError: true,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ g := NewWithT(t)
+
+ inv := New()
+ _, err := AddManifest(inv, tt.manifest, tt.releaseNamespace, fakeClient)
+
+ if tt.expectError {
+ g.Expect(err).To(HaveOccurred())
+ g.Expect(err.Error()).To(ContainSubstring("failed to parse manifest"))
+ return
+ }
+
+ g.Expect(err).ToNot(HaveOccurred())
+ g.Expect(inv.Entries).To(HaveLen(tt.expectedLen))
+ for i, entry := range inv.Entries {
+ g.Expect(entry.ID).To(Equal(tt.expectedIDs[i]))
+ }
+ })
+ }
+}
+
+func TestAddManifest_RESTMapperError(t *testing.T) {
+ g := NewWithT(t)
+
+ scheme := runtime.NewScheme()
+ emptyMapper := meta.NewDefaultRESTMapper([]schema.GroupVersion{})
+ fakeClient := fake.NewClientBuilder().
+ WithScheme(scheme).
+ WithRESTMapper(emptyMapper).
+ Build()
+
+ manifest := `apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: mycm
+`
+
+ inv := New()
+ warnings, err := AddManifest(inv, manifest, "default", fakeClient)
+
+ g.Expect(err).ToNot(HaveOccurred())
+ g.Expect(inv.Entries).To(HaveLen(1))
+ g.Expect(inv.Entries[0].ID).To(Equal("_mycm__ConfigMap"))
+ g.Expect(warnings).To(HaveLen(1))
+ g.Expect(warnings[0]).To(ContainSubstring("failed to determine if ConfigMap is namespace scoped"))
+}
+
+// newFakeRESTMapper creates a RESTMapper with common Kubernetes resource types,
+// correctly distinguishing between namespaced and cluster-scoped resources.
+func newFakeRESTMapper() meta.RESTMapper {
+ mapper := meta.NewDefaultRESTMapper([]schema.GroupVersion{
+ corev1.SchemeGroupVersion,
+ appsv1.SchemeGroupVersion,
+ rbacv1.SchemeGroupVersion,
+ })
+
+ // Namespaced resources
+ mapper.Add(corev1.SchemeGroupVersion.WithKind("ConfigMap"), meta.RESTScopeNamespace)
+ mapper.Add(appsv1.SchemeGroupVersion.WithKind("Deployment"), meta.RESTScopeNamespace)
+
+ // Cluster-scoped resources
+ mapper.Add(rbacv1.SchemeGroupVersion.WithKind("ClusterRole"), meta.RESTScopeRoot)
+
+ return mapper
+}
+
+func TestAddCRDs(t *testing.T) {
+ modTime := time.Now()
+
+ tests := []struct {
+ name string
+ crdFiles []*common.File
+ expectedLen int
+ expectedIDs []string
+ }{
+ {
+ name: "CRD is added to inventory",
+ crdFiles: []*common.File{
+ {
+ Name: "crds/mycrd.yaml",
+ ModTime: modTime,
+ Data: []byte(`apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+ name: myresources.example.com
+spec:
+ group: example.com
+ names:
+ kind: MyResource
+ plural: myresources
+ scope: Namespaced
+ versions:
+ - name: v1
+ served: true
+ storage: true
+`),
+ },
+ },
+ expectedLen: 1,
+ expectedIDs: []string{"_myresources.example.com_apiextensions.k8s.io_CustomResourceDefinition"},
+ },
+ {
+ name: "multiple CRDs in single file",
+ crdFiles: []*common.File{
+ {
+ Name: "crds/crds.yaml",
+ ModTime: modTime,
+ Data: []byte(`apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+ name: foos.example.com
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+ name: bars.example.com
+`),
+ },
+ },
+ expectedLen: 2,
+ expectedIDs: []string{
+ "_foos.example.com_apiextensions.k8s.io_CustomResourceDefinition",
+ "_bars.example.com_apiextensions.k8s.io_CustomResourceDefinition",
+ },
+ },
+ {
+ name: "CRDs in multiple files",
+ crdFiles: []*common.File{
+ {
+ Name: "crds/crd1.yaml",
+ ModTime: modTime,
+ Data: []byte(`apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+ name: foos.example.com
+`),
+ },
+ {
+ Name: "crds/crd2.yaml",
+ ModTime: modTime,
+ Data: []byte(`apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+ name: bars.example.com
+`),
+ },
+ },
+ expectedLen: 2,
+ expectedIDs: []string{
+ "_foos.example.com_apiextensions.k8s.io_CustomResourceDefinition",
+ "_bars.example.com_apiextensions.k8s.io_CustomResourceDefinition",
+ },
+ },
+ {
+ name: "no CRDs",
+ crdFiles: []*common.File{},
+ expectedLen: 0,
+ expectedIDs: []string{},
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ g := NewWithT(t)
+
+ chart := &helmchart.Chart{
+ Metadata: &helmchart.Metadata{
+ Name: "test-chart",
+ Version: "1.0.0",
+ },
+ Files: tt.crdFiles,
+ }
+
+ inv := New()
+ err := AddCRDs(inv, chart)
+
+ g.Expect(err).ToNot(HaveOccurred())
+ g.Expect(inv.Entries).To(HaveLen(tt.expectedLen))
+ for i, entry := range inv.Entries {
+ g.Expect(entry.ID).To(Equal(tt.expectedIDs[i]))
+ }
+ })
+ }
+}
diff --git a/internal/kube/client.go b/internal/kube/client.go
index 91441ee16..788f0b27e 100644
--- a/internal/kube/client.go
+++ b/internal/kube/client.go
@@ -18,6 +18,7 @@ package kube
import (
"fmt"
+ "net/http"
"sync"
"k8s.io/apimachinery/pkg/api/meta"
@@ -130,6 +131,10 @@ func (c *MemoryRESTClientGetter) ToRESTConfig() (*rest.Config, error) {
if c.cfg == nil {
return nil, fmt.Errorf("MemoryRESTClientGetter has no REST config")
}
+ // add retries to fix temporary "etcdserver: leader changed" errors from kube-apiserver
+ c.cfg.Wrap(func(rt http.RoundTripper) http.RoundTripper {
+ return &retryingRoundTripper{wrapped: rt}
+ })
return c.cfg, nil
}
@@ -198,7 +203,7 @@ func (c *MemoryRESTClientGetter) toRESTMapper() (meta.RESTMapper, error) {
return nil, err
}
mapper := restmapper.NewDeferredDiscoveryRESTMapper(discoveryClient)
- return restmapper.NewShortcutExpander(mapper, discoveryClient), nil
+ return restmapper.NewShortcutExpander(mapper, discoveryClient, nil), nil
}
// ToRawKubeConfigLoader returns a clientcmd.ClientConfig using
diff --git a/internal/kube/client_test.go b/internal/kube/client_test.go
index 24fbfd910..fa7792131 100644
--- a/internal/kube/client_test.go
+++ b/internal/kube/client_test.go
@@ -250,7 +250,7 @@ func TestMemoryRESTClientGetter_ToRESTMapper(t *testing.T) {
// Calling it again should return the same instance.
rm2, err := c.ToRESTMapper()
g.Expect(err).ToNot(HaveOccurred())
- g.Expect(rm2).To(BeIdenticalTo(rm))
+ g.Expect(rm2).To(BeEquivalentTo(rm))
})
t.Run("returns a REST mapper", func(t *testing.T) {
@@ -268,7 +268,7 @@ func TestMemoryRESTClientGetter_ToRESTMapper(t *testing.T) {
// Calling it again should return a new instance.
rm2, err := c.ToRESTMapper()
g.Expect(err).ToNot(HaveOccurred())
- g.Expect(rm2).ToNot(BeIdenticalTo(rm))
+ g.Expect(rm2).ToNot(BeEquivalentTo(rm))
})
}
diff --git a/internal/kube/config.go b/internal/kube/config.go
index 7991fd649..189390c5f 100644
--- a/internal/kube/config.go
+++ b/internal/kube/config.go
@@ -17,6 +17,7 @@ limitations under the License.
package kube
import (
+ "context"
"fmt"
corev1 "k8s.io/api/core/v1"
@@ -40,7 +41,7 @@ const (
// given Secret, or attempts to load the data from the default `value` and
// `value.yaml` keys. If a Secret is provided but no key with data can be
// found, an error is returned.
-func ConfigFromSecret(secret *corev1.Secret, key string, opts client.KubeConfigOptions) (*rest.Config, error) {
+func ConfigFromSecret(ctx context.Context, secret *corev1.Secret, key string, opts client.KubeConfigOptions) (*rest.Config, error) {
if secret == nil {
return nil, fmt.Errorf("KubeConfig secret is nil")
}
@@ -68,6 +69,6 @@ func ConfigFromSecret(secret *corev1.Secret, key string, opts client.KubeConfigO
if err != nil {
return nil, fmt.Errorf("failed to load KubeConfig from secret '%s': %w", secretName, err)
}
- cfg = client.KubeConfig(cfg, opts)
+ cfg = client.KubeConfig(ctx, cfg, opts)
return cfg, nil
}
diff --git a/internal/kube/config_test.go b/internal/kube/config_test.go
index eb075d504..bbb37b7fb 100644
--- a/internal/kube/config_test.go
+++ b/internal/kube/config_test.go
@@ -17,9 +17,10 @@ limitations under the License.
package kube
import (
- "github.com/fluxcd/pkg/runtime/client"
"testing"
+ "github.com/fluxcd/pkg/runtime/client"
+
. "github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -63,7 +64,7 @@ func TestConfigFromSecret(t *testing.T) {
DefaultKubeConfigSecretKeyExt: []byte("bad"),
},
}
- got, err := ConfigFromSecret(secret, "", client.KubeConfigOptions{})
+ got, err := ConfigFromSecret(t.Context(), secret, "", client.KubeConfigOptions{})
g.Expect(err).ToNot(HaveOccurred())
g.Expect(got).ToNot(BeNil())
})
@@ -80,7 +81,7 @@ func TestConfigFromSecret(t *testing.T) {
DefaultKubeConfigSecretKeyExt: []byte(kubeCfg),
},
}
- got, err := ConfigFromSecret(secret, "", client.KubeConfigOptions{})
+ got, err := ConfigFromSecret(t.Context(), secret, "", client.KubeConfigOptions{})
g.Expect(err).ToNot(HaveOccurred())
g.Expect(got).ToNot(BeNil())
})
@@ -98,7 +99,7 @@ func TestConfigFromSecret(t *testing.T) {
key: []byte(kubeCfg),
},
}
- got, err := ConfigFromSecret(secret, key, client.KubeConfigOptions{})
+ got, err := ConfigFromSecret(t.Context(), secret, key, client.KubeConfigOptions{})
g.Expect(err).ToNot(HaveOccurred())
g.Expect(got).ToNot(BeNil())
})
@@ -114,7 +115,7 @@ func TestConfigFromSecret(t *testing.T) {
},
Data: map[string][]byte{},
}
- got, err := ConfigFromSecret(secret, key, client.KubeConfigOptions{})
+ got, err := ConfigFromSecret(t.Context(), secret, key, client.KubeConfigOptions{})
g.Expect(err).To(HaveOccurred())
g.Expect(got).To(BeNil())
g.Expect(err.Error()).To(ContainSubstring("secret 'vault/super-secret' does not contain a 'black-hole' key "))
@@ -133,7 +134,7 @@ func TestConfigFromSecret(t *testing.T) {
key: nil,
},
}
- got, err := ConfigFromSecret(secret, key, client.KubeConfigOptions{})
+ got, err := ConfigFromSecret(t.Context(), secret, key, client.KubeConfigOptions{})
g.Expect(err).To(HaveOccurred())
g.Expect(got).To(BeNil())
g.Expect(err.Error()).To(ContainSubstring("does not contain a 'void' key with data"))
@@ -150,7 +151,7 @@ func TestConfigFromSecret(t *testing.T) {
Data: map[string][]byte{},
}
- got, err := ConfigFromSecret(secret, "", client.KubeConfigOptions{})
+ got, err := ConfigFromSecret(t.Context(), secret, "", client.KubeConfigOptions{})
g.Expect(err).To(HaveOccurred())
g.Expect(got).To(BeNil())
g.Expect(err.Error()).To(ContainSubstring("does not contain a 'value' or 'value.yaml'"))
@@ -159,7 +160,7 @@ func TestConfigFromSecret(t *testing.T) {
t.Run("nil secret", func(t *testing.T) {
g := NewWithT(t)
- got, err := ConfigFromSecret(nil, "", client.KubeConfigOptions{})
+ got, err := ConfigFromSecret(t.Context(), nil, "", client.KubeConfigOptions{})
g.Expect(err).To(HaveOccurred())
g.Expect(got).To(BeNil())
g.Expect(err.Error()).To(ContainSubstring("secret is nil"))
@@ -178,7 +179,7 @@ func TestConfigFromSecret(t *testing.T) {
},
}
- got, err := ConfigFromSecret(secret, "", client.KubeConfigOptions{})
+ got, err := ConfigFromSecret(t.Context(), secret, "", client.KubeConfigOptions{})
g.Expect(err).To(HaveOccurred())
g.Expect(got).To(BeNil())
g.Expect(err.Error()).To(ContainSubstring("couldn't get version/kind"))
@@ -196,7 +197,7 @@ func TestConfigFromSecret(t *testing.T) {
DefaultKubeConfigSecretKey: []byte(kubeCfg),
},
}
- got, err := ConfigFromSecret(secret, "", client.KubeConfigOptions{
+ got, err := ConfigFromSecret(t.Context(), secret, "", client.KubeConfigOptions{
UserAgent: "test",
})
g.Expect(err).ToNot(HaveOccurred())
diff --git a/internal/kube/roundtripper.go b/internal/kube/roundtripper.go
new file mode 100644
index 000000000..34e5acdae
--- /dev/null
+++ b/internal/kube/roundtripper.go
@@ -0,0 +1,83 @@
+/*
+Copyright The Helm Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// TODO: this file was taken from https://github.com/helm/helm/blob/main/pkg/cli/roundtripper.go
+// We should be able to get rid of it once https://github.com/helm/helm/issues/13052 is addressed
+// A PR (https://github.com/helm/helm/pull/13383) was open and merged but not yet picked into any release
+package kube
+
+import (
+ "bytes"
+ "encoding/json"
+ "io"
+ "net/http"
+ "strings"
+)
+
+type retryingRoundTripper struct {
+ wrapped http.RoundTripper
+}
+
+func (rt *retryingRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
+ return rt.roundTrip(req, 1, nil)
+}
+
+func (rt *retryingRoundTripper) roundTrip(req *http.Request, retry int, prevResp *http.Response) (*http.Response, error) {
+ if retry < 0 {
+ return prevResp, nil
+ }
+ resp, rtErr := rt.wrapped.RoundTrip(req)
+ if rtErr != nil {
+ return resp, rtErr
+ }
+ if resp.StatusCode < 500 {
+ return resp, nil
+ }
+ if resp.Header.Get("content-type") != "application/json" {
+ return resp, nil
+ }
+ b, err := io.ReadAll(resp.Body)
+ resp.Body.Close()
+ if err != nil {
+ return resp, err
+ }
+
+ var ke kubernetesError
+ r := bytes.NewReader(b)
+ err = json.NewDecoder(r).Decode(&ke)
+ r.Seek(0, io.SeekStart)
+ resp.Body = io.NopCloser(r)
+ if err != nil {
+ return resp, err
+ }
+ if ke.Code < 500 {
+ return resp, nil
+ }
+ // Matches messages like "etcdserver: leader changed"
+ if strings.HasSuffix(ke.Message, "etcdserver: leader changed") {
+ return rt.roundTrip(req, retry-1, resp)
+ }
+ // Matches messages like "rpc error: code = Unknown desc = raft proposal dropped"
+ if strings.HasSuffix(ke.Message, "raft proposal dropped") {
+ return rt.roundTrip(req, retry-1, resp)
+ }
+ return resp, nil
+}
+
+type kubernetesError struct {
+ Message string `json:"message"`
+ Code int `json:"code"`
+}
diff --git a/internal/loader/artifact_url.go b/internal/loader/artifact_url.go
new file mode 100644
index 000000000..af63a4f5e
--- /dev/null
+++ b/internal/loader/artifact_url.go
@@ -0,0 +1,136 @@
+/*
+Copyright 2022 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package loader
+
+import (
+ "bytes"
+ _ "crypto/sha256"
+ _ "crypto/sha512"
+ "errors"
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+ "os"
+ "strings"
+
+ "github.com/hashicorp/go-retryablehttp"
+ digestlib "github.com/opencontainers/go-digest"
+ _ "github.com/opencontainers/go-digest/blake3"
+ chart "helm.sh/helm/v4/pkg/chart/v2"
+ "helm.sh/helm/v4/pkg/chart/v2/loader"
+)
+
+const (
+ // envSourceControllerLocalhost is the name of the environment variable
+ // used to override the hostname of the source-controller from which
+ // the chart is downloaded.
+ envSourceControllerLocalhost = "SOURCE_CONTROLLER_LOCALHOST"
+
+ // envSourceWatcherLocalhost is the name of the environment variable
+ // used to override the hostname of the source-watcher from which
+ // the chart is downloaded.
+ envSourceWatcherLocalhost = "SOURCE_WATCHER_LOCALHOST"
+)
+
+var (
+ // ErrFileNotFound is an error type used to signal 404 HTTP status code responses.
+ ErrFileNotFound = errors.New("file not found")
+ // ErrIntegrity signals a chart loader failed to verify the integrity of
+ // a chart, for example due to a digest mismatch.
+ ErrIntegrity = errors.New("integrity failure")
+)
+
+// SecureLoadChartFromURL attempts to download a Helm chart from the given URL
+// using the provided client. The retrieved data is verified against the given
+// digest before loading the chart. It returns the loaded chart.Chart, or an
+// error. The error may be of type ErrIntegrity if the integrity check fails.
+func SecureLoadChartFromURL(client *retryablehttp.Client, URL, digest string) (*chart.Chart, error) {
+ sourceLocalhost := os.Getenv(envSourceControllerLocalhost)
+ if strings.Contains(URL, "//source-watcher") {
+ sourceLocalhost = os.Getenv(envSourceWatcherLocalhost)
+ }
+
+ u, err := overwriteHostname(URL, sourceLocalhost)
+ if err != nil {
+ return nil, err
+ }
+
+ req, err := retryablehttp.NewRequest(http.MethodGet, u, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ resp, err := client.Do(req)
+ if err != nil || resp != nil && resp.StatusCode != http.StatusOK {
+ if err != nil {
+ return nil, err
+ }
+ _ = resp.Body.Close()
+ if resp.StatusCode == http.StatusNotFound {
+ return nil, fmt.Errorf("failed to download chart from '%s': %w", URL, ErrFileNotFound)
+ }
+ return nil, fmt.Errorf("failed to download chart from '%s' (status: %s)", URL, resp.Status)
+ }
+
+ var c bytes.Buffer
+ if err := copyAndVerify(digest, resp.Body, &c); err != nil {
+ _ = resp.Body.Close()
+ return nil, err
+ }
+
+ if err := resp.Body.Close(); err != nil {
+ return nil, err
+ }
+ return loader.LoadArchive(&c)
+}
+
+// copyAndVerify copies the contents of reader to writer, and verifies the
+// integrity of the data using the given digest. It returns an error if the
+// integrity check fails.
+func copyAndVerify(digest string, reader io.Reader, writer io.Writer) error {
+ dig, err := digestlib.Parse(digest)
+ if err != nil {
+ return fmt.Errorf("failed to parse digest '%s': %w", digest, err)
+ }
+
+ verifier := dig.Verifier()
+ mw := io.MultiWriter(verifier, writer)
+ if _, err := io.Copy(mw, reader); err != nil {
+ return fmt.Errorf("failed to copy and verify chart artifact: %w", err)
+ }
+
+ if !verifier.Verified() {
+ return fmt.Errorf("%w: computed digest doesn't match '%s'", ErrIntegrity, dig)
+ }
+ return nil
+}
+
+// overwriteHostname overwrites the hostname of the given URL with the given
+// hostname. If the hostname is empty, the URL is returned unmodified.
+func overwriteHostname(URL, hostname string) (string, error) {
+ if hostname == "" {
+ return URL, nil
+ }
+
+ u, err := url.Parse(URL)
+ if err != nil {
+ return "", fmt.Errorf("failed to parse URL to overwrite hostname: %w", err)
+ }
+ u.Host = hostname
+ return u.String(), nil
+}
diff --git a/internal/loader/artifact_url_test.go b/internal/loader/artifact_url_test.go
new file mode 100644
index 000000000..9f4a3b291
--- /dev/null
+++ b/internal/loader/artifact_url_test.go
@@ -0,0 +1,219 @@
+/*
+Copyright 2022 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package loader
+
+import (
+ "bytes"
+ "errors"
+ "io"
+ "net/http"
+ "net/http/httptest"
+ "os"
+ "strings"
+ "testing"
+
+ "github.com/hashicorp/go-retryablehttp"
+ . "github.com/onsi/gomega"
+ digestlib "github.com/opencontainers/go-digest"
+)
+
+func TestSecureLoadChartFromURL(t *testing.T) {
+ g := NewWithT(t)
+
+ b, err := os.ReadFile("testdata/chart-0.1.0.tgz")
+ g.Expect(err).ToNot(HaveOccurred())
+ g.Expect(b).ToNot(BeNil())
+ digest := digestlib.SHA256.FromBytes(b)
+
+ const chartPath = "/chart.tgz"
+ const notFoundPath = "/not-found.tgz"
+ server := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
+ if req.URL.Path == chartPath {
+ res.WriteHeader(http.StatusOK)
+ _, _ = res.Write(b)
+ return
+ }
+ if req.URL.Path == notFoundPath {
+ res.WriteHeader(http.StatusNotFound)
+ return
+ }
+ res.WriteHeader(http.StatusInternalServerError)
+ }))
+ t.Cleanup(func() {
+ server.Close()
+ })
+
+ chartURL := server.URL + chartPath
+
+ client := retryablehttp.NewClient()
+ client.Logger = nil
+ client.RetryMax = 2
+
+ t.Run("loads Helm chart from URL", func(t *testing.T) {
+ g := NewWithT(t)
+
+ got, err := SecureLoadChartFromURL(client, chartURL, digest.String())
+ g.Expect(err).ToNot(HaveOccurred())
+ g.Expect(got).ToNot(BeNil())
+ g.Expect(got.Name()).To(Equal("chart"))
+ g.Expect(got.Metadata.Version).To(Equal("0.1.0"))
+ })
+
+ t.Run("overwrites hostname", func(t *testing.T) {
+ g := NewWithT(t)
+
+ t.Setenv(envSourceControllerLocalhost, strings.TrimPrefix(server.URL, "http://"))
+ wrongHostnameURL := "http://invalid.com" + chartPath
+
+ got, err := SecureLoadChartFromURL(client, wrongHostnameURL, digest.String())
+ g.Expect(err).ToNot(HaveOccurred())
+ g.Expect(got).ToNot(BeNil())
+ g.Expect(got.Name()).To(Equal("chart"))
+ g.Expect(got.Metadata.Version).To(Equal("0.1.0"))
+ })
+
+ t.Run("error on chart data digest mismatch", func(t *testing.T) {
+ g := NewWithT(t)
+
+ got, err := SecureLoadChartFromURL(client, chartURL, digestlib.SHA256.FromString("invalid").String())
+ g.Expect(err).To(HaveOccurred())
+ g.Expect(errors.Is(err, ErrIntegrity)).To(BeTrue())
+ g.Expect(got).To(BeNil())
+ })
+
+ t.Run("file not found error on 404", func(t *testing.T) {
+ g := NewWithT(t)
+
+ got, err := SecureLoadChartFromURL(client, server.URL+notFoundPath, digest.String())
+ g.Expect(errors.Is(err, ErrFileNotFound)).To(BeTrue())
+ g.Expect(got).To(BeNil())
+ })
+
+ t.Run("error on HTTP request failure", func(t *testing.T) {
+ g := NewWithT(t)
+
+ got, err := SecureLoadChartFromURL(client, server.URL+"/invalid.tgz", digest.String())
+ g.Expect(err).To(HaveOccurred())
+ g.Expect(errors.Is(err, ErrFileNotFound)).To(BeFalse())
+ g.Expect(got).To(BeNil())
+ })
+}
+
+func Test_copyAndVerify(t *testing.T) {
+ g := NewWithT(t)
+
+ tmpDir := t.TempDir()
+ closedF, err := os.CreateTemp(tmpDir, "closed.txt")
+ g.Expect(err).ToNot(HaveOccurred())
+ g.Expect(closedF.Close()).ToNot(HaveOccurred())
+
+ tests := []struct {
+ name string
+ digest string
+ in io.Reader
+ out io.Writer
+ wantErr bool
+ }{
+ {
+ name: "digest match (SHA256)",
+ digest: digestlib.SHA256.FromString("foo").String(),
+ in: bytes.NewReader([]byte("foo")),
+ out: bytes.NewBuffer(nil),
+ },
+ {
+ name: "digest match (SHA384)",
+ digest: digestlib.SHA384.FromString("foo").String(),
+ in: bytes.NewReader([]byte("foo")),
+ out: bytes.NewBuffer(nil),
+ },
+ {
+ name: "digest match (SHA512)",
+ digest: digestlib.SHA512.FromString("foo").String(),
+ in: bytes.NewReader([]byte("foo")),
+ out: bytes.NewBuffer(nil),
+ },
+ {
+ name: "digest match (BLAKE3)",
+ digest: digestlib.BLAKE3.FromString("foo").String(),
+ in: bytes.NewReader([]byte("foo")),
+ out: bytes.NewBuffer(nil),
+ },
+ {
+ name: "digest mismatch",
+ digest: digestlib.SHA256.FromString("foo").String(),
+ in: bytes.NewReader([]byte("bar")),
+ out: io.Discard,
+ wantErr: true,
+ },
+ {
+ name: "copy failure (closed file)",
+ digest: digestlib.SHA256.FromString("foo").String(),
+ in: bytes.NewReader([]byte("foo")),
+ out: closedF,
+ wantErr: true,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ g := NewWithT(t)
+
+ err := copyAndVerify(tt.digest, tt.in, tt.out)
+ g.Expect(err != nil).To(Equal(tt.wantErr), err)
+ })
+ }
+}
+
+func Test_overwriteHostname(t *testing.T) {
+ tests := []struct {
+ name string
+ URL string
+ hostname string
+ want string
+ wantErr bool
+ }{
+ {
+ name: "overwrite hostname",
+ URL: "http://example.com",
+ hostname: "localhost",
+ want: "http://localhost",
+ },
+ {
+ name: "overwrite hostname with port",
+ URL: "http://example.com",
+ hostname: "localhost:9090",
+ want: "http://localhost:9090",
+ },
+ {
+ name: "no hostname",
+ URL: "http://example.com",
+ hostname: "",
+ want: "http://example.com",
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got, err := overwriteHostname(tt.URL, tt.hostname)
+ if (err != nil) != tt.wantErr {
+ t.Errorf("overwriteHostname() error = %v, wantErr %v", err, tt.wantErr)
+ return
+ }
+ if got != tt.want {
+ t.Errorf("overwriteHostname() got = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
diff --git a/internal/loader/client.go b/internal/loader/client.go
new file mode 100644
index 000000000..867c89292
--- /dev/null
+++ b/internal/loader/client.go
@@ -0,0 +1,64 @@
+/*
+Copyright 2023 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package loader
+
+import (
+ "context"
+ "time"
+
+ "github.com/go-logr/logr"
+ "github.com/hashicorp/go-retryablehttp"
+ ctrl "sigs.k8s.io/controller-runtime"
+)
+
+// NewRetryableHTTPClient returns a new retrying HTTP client for loading
+// artifacts. The client will retry up to the given number of times before
+// giving up. The context is used to log errors.
+func NewRetryableHTTPClient(ctx context.Context, retries int) *retryablehttp.Client {
+ httpClient := retryablehttp.NewClient()
+ httpClient.RetryWaitMin = 5 * time.Second
+ httpClient.RetryWaitMax = 30 * time.Second
+ httpClient.RetryMax = retries
+ httpClient.Logger = newLoggerForContext(ctx)
+ return httpClient
+}
+
+func newLoggerForContext(ctx context.Context) retryablehttp.LeveledLogger {
+ return &errorLogger{log: ctrl.LoggerFrom(ctx)}
+}
+
+// errorLogger is a wrapper around logr.Logger that implements the
+// retryablehttp.LeveledLogger interface while only logging errors.
+type errorLogger struct {
+ log logr.Logger
+}
+
+func (l *errorLogger) Error(msg string, keysAndValues ...any) {
+ l.log.Info(msg, keysAndValues...)
+}
+
+func (l *errorLogger) Info(msg string, keysAndValues ...any) {
+ // Do nothing.
+}
+
+func (l *errorLogger) Debug(msg string, keysAndValues ...any) {
+ // Do nothing.
+}
+
+func (l *errorLogger) Warn(msg string, keysAndValues ...any) {
+ // Do nothing.
+}
diff --git a/internal/loader/testdata/chart-0.1.0.tgz b/internal/loader/testdata/chart-0.1.0.tgz
new file mode 100644
index 000000000..b5dca7618
Binary files /dev/null and b/internal/loader/testdata/chart-0.1.0.tgz differ
diff --git a/internal/postrender/build.go b/internal/postrender/build.go
new file mode 100644
index 000000000..4209eb1a0
--- /dev/null
+++ b/internal/postrender/build.go
@@ -0,0 +1,71 @@
+/*
+Copyright 2022 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package postrender
+
+import (
+ "encoding/json"
+
+ "github.com/opencontainers/go-digest"
+ helmpostrender "helm.sh/helm/v4/pkg/postrenderer"
+
+ v2 "github.com/fluxcd/helm-controller/api/v2"
+)
+
+// BuildPostRenderers creates the post-renderer instances from a HelmRelease
+// and combines them into a single Combined post renderer.
+func BuildPostRenderers(rel *v2.HelmRelease) helmpostrender.PostRenderer {
+ if rel == nil {
+ return nil
+ }
+ renderers := make([]helmpostrender.PostRenderer, 0)
+ for _, r := range rel.Spec.PostRenderers {
+ if r.Kustomize != nil {
+ renderers = append(renderers, &Kustomize{
+ Patches: r.Kustomize.Patches,
+ Images: r.Kustomize.Images,
+ })
+ }
+ }
+ if rel.Spec.CommonMetadata != nil {
+ renderers = append(renderers, NewCommonRenderer(rel.Spec.CommonMetadata))
+ }
+ // OriginLabels takes precedence over CommonMetadata renderer, so that in case of label key collision,
+ // the label from common metadata is skipped
+ renderers = append(renderers, NewOriginLabels(v2.GroupVersion.Group, rel.Namespace, rel.Name))
+ if len(renderers) == 0 {
+ return nil
+ }
+ 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()
+}
+
+func CommonMetadataDigest(algo digest.Algorithm, commonMetadata *v2.CommonMetadata) digest.Digest {
+ digester := algo.Digester()
+ enc := json.NewEncoder(digester.Hash())
+ if err := enc.Encode(commonMetadata); err != nil {
+ return ""
+ }
+ return digester.Digest()
+}
diff --git a/internal/runner/post_renderer.go b/internal/postrender/combined.go
similarity index 56%
rename from internal/runner/post_renderer.go
rename to internal/postrender/combined.go
index 45ad3c501..6de4506c8 100644
--- a/internal/runner/post_renderer.go
+++ b/internal/postrender/combined.go
@@ -14,32 +14,30 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-package runner
+package postrender
import (
"bytes"
- "helm.sh/helm/v3/pkg/postrender"
+ helmpostrender "helm.sh/helm/v4/pkg/postrenderer"
)
-// combinedPostRenderer, a collection of Helm PostRenders which are
+// Combined is a collection of Helm PostRenders which are
// invoked in the order of insertion.
-type combinedPostRenderer struct {
- renderers []postrender.PostRenderer
+type Combined struct {
+ renderers []helmpostrender.PostRenderer
}
-func newCombinedPostRenderer() combinedPostRenderer {
- return combinedPostRenderer{
- renderers: make([]postrender.PostRenderer, 0),
+func NewCombined(renderer ...helmpostrender.PostRenderer) *Combined {
+ pr := make([]helmpostrender.PostRenderer, 0)
+ pr = append(pr, renderer...)
+ return &Combined{
+ renderers: pr,
}
}
-func (c *combinedPostRenderer) addRenderer(renderer postrender.PostRenderer) {
- c.renderers = append(c.renderers, renderer)
-}
-
-func (c *combinedPostRenderer) Run(renderedManifests *bytes.Buffer) (modifiedManifests *bytes.Buffer, err error) {
- var result *bytes.Buffer = renderedManifests
+func (c *Combined) Run(renderedManifests *bytes.Buffer) (modifiedManifests *bytes.Buffer, err error) {
+ var result = renderedManifests
for _, renderer := range c.renderers {
result, err = renderer.Run(result)
if err != nil {
diff --git a/internal/postrender/common_renderer.go b/internal/postrender/common_renderer.go
new file mode 100644
index 000000000..ffe1edb9f
--- /dev/null
+++ b/internal/postrender/common_renderer.go
@@ -0,0 +1,83 @@
+/*
+Copyright 2025 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package postrender
+
+import (
+ "bytes"
+ "fmt"
+
+ v2 "github.com/fluxcd/helm-controller/api/v2"
+)
+
+type OriginLabels struct {
+ group string
+ name string
+ namespace string
+}
+
+type CommonRenderer struct {
+ labels map[string]string // Origin labels + Common labels to be applied to all resources
+ annotations map[string]string // Common annotations to be applied to all resources
+}
+
+func NewOriginLabels(group, namespace, name string) *OriginLabels {
+ return &OriginLabels{
+ group: group,
+ name: name,
+ namespace: namespace,
+ }
+}
+
+func (k *OriginLabels) Run(renderedManifests *bytes.Buffer) (modifiedManifests *bytes.Buffer, err error) {
+ labels := originLabels(k.group, k.namespace, k.name)
+ return transform(renderedManifests, labelTransformer(labels))
+}
+
+func NewCommonRenderer(commonMetadata *v2.CommonMetadata) *CommonRenderer {
+ renderer := &CommonRenderer{}
+ if commonMetadata.Labels != nil {
+ renderer.labels = commonMetadata.Labels
+ }
+ if commonMetadata.Annotations != nil {
+ renderer.annotations = commonMetadata.Annotations
+ }
+ return renderer
+}
+
+func (k *CommonRenderer) Run(renderedManifests *bytes.Buffer) (modifiedManifests *bytes.Buffer, err error) {
+ var transformers []ResMapTransformer
+
+ if k.labels != nil {
+ transformers = append(transformers, labelTransformer(k.labels))
+ }
+ if k.annotations != nil {
+ transformers = append(transformers, annotationTransformer(k.annotations))
+ }
+
+ if len(transformers) == 0 {
+ return renderedManifests, nil
+ }
+
+ return transform(renderedManifests, transformers...)
+}
+
+func originLabels(group, namespace, name string) map[string]string {
+ return map[string]string{
+ fmt.Sprintf("%s/name", group): name,
+ fmt.Sprintf("%s/namespace", group): namespace,
+ }
+}
diff --git a/internal/postrender/common_renderer_test.go b/internal/postrender/common_renderer_test.go
new file mode 100644
index 000000000..ff1c168a3
--- /dev/null
+++ b/internal/postrender/common_renderer_test.go
@@ -0,0 +1,190 @@
+/*
+Copyright 2025 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package postrender
+
+import (
+ "bytes"
+ "testing"
+
+ . "github.com/onsi/gomega"
+
+ v2 "github.com/fluxcd/helm-controller/api/v2"
+)
+
+const mixedResourceMock = `apiVersion: v1
+kind: Pod
+metadata:
+ name: pod-without-labels
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: service-with-labels
+ labels:
+ existing: label
+`
+
+func Test_OriginLabels_Run(t *testing.T) {
+ tests := []struct {
+ name string
+ renderedManifests string
+ expectManifests string
+ expectErr bool
+ }{
+ {
+ name: "labels",
+ renderedManifests: mixedResourceMock,
+ expectManifests: `apiVersion: v1
+kind: Pod
+metadata:
+ labels:
+ helm.toolkit.fluxcd.io/name: name
+ helm.toolkit.fluxcd.io/namespace: namespace
+ name: pod-without-labels
+---
+apiVersion: v1
+kind: Service
+metadata:
+ labels:
+ existing: label
+ helm.toolkit.fluxcd.io/name: name
+ helm.toolkit.fluxcd.io/namespace: namespace
+ name: service-with-labels
+`,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ g := NewWithT(t)
+
+ k := NewOriginLabels("helm.toolkit.fluxcd.io", "namespace", "name")
+ gotModifiedManifests, err := k.Run(bytes.NewBufferString(tt.renderedManifests))
+ if tt.expectErr {
+ g.Expect(err).To(HaveOccurred())
+ g.Expect(gotModifiedManifests.String()).To(BeEmpty())
+ return
+ }
+ g.Expect(err).ToNot(HaveOccurred())
+ g.Expect(gotModifiedManifests).To(Equal(bytes.NewBufferString(tt.expectManifests)))
+ })
+ }
+}
+
+func Test_CommonRenderer_Run(t *testing.T) {
+ tests := []struct {
+ name string
+ renderedManifests string
+ expectManifests string
+ labels map[string]string
+ annotations map[string]string
+ expectErr bool
+ }{
+ {
+ name: "labels and annotations",
+ labels: map[string]string{"foo": "bar", "baz": "qux"},
+ annotations: map[string]string{"annotation1": "value1", "annotation2": "value2"},
+ renderedManifests: mixedResourceMock,
+ expectManifests: `apiVersion: v1
+kind: Pod
+metadata:
+ annotations:
+ annotation1: value1
+ annotation2: value2
+ labels:
+ baz: qux
+ foo: bar
+ name: pod-without-labels
+---
+apiVersion: v1
+kind: Service
+metadata:
+ annotations:
+ annotation1: value1
+ annotation2: value2
+ labels:
+ baz: qux
+ existing: label
+ foo: bar
+ name: service-with-labels
+`,
+ },
+ {
+ name: "labels only",
+ labels: map[string]string{"foo": "bar"},
+ renderedManifests: mixedResourceMock,
+ expectManifests: `apiVersion: v1
+kind: Pod
+metadata:
+ labels:
+ foo: bar
+ name: pod-without-labels
+---
+apiVersion: v1
+kind: Service
+metadata:
+ labels:
+ existing: label
+ foo: bar
+ name: service-with-labels
+`,
+ },
+ {
+ name: "annotations only",
+ annotations: map[string]string{"bar": "baz"},
+ renderedManifests: mixedResourceMock,
+ expectManifests: `apiVersion: v1
+kind: Pod
+metadata:
+ annotations:
+ bar: baz
+ name: pod-without-labels
+---
+apiVersion: v1
+kind: Service
+metadata:
+ annotations:
+ bar: baz
+ labels:
+ existing: label
+ name: service-with-labels
+`,
+ },
+ {
+ name: "no annotations and labels",
+ renderedManifests: mixedResourceMock,
+ expectManifests: mixedResourceMock,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ g := NewWithT(t)
+ commonMetadata := &v2.CommonMetadata{
+ Labels: tt.labels,
+ Annotations: tt.annotations,
+ }
+ k := NewCommonRenderer(commonMetadata)
+ gotModifiedManifests, err := k.Run(bytes.NewBufferString(tt.renderedManifests))
+ if tt.expectErr {
+ g.Expect(err).To(HaveOccurred())
+ g.Expect(gotModifiedManifests.String()).To(BeEmpty())
+ return
+ }
+ g.Expect(err).ToNot(HaveOccurred())
+ g.Expect(gotModifiedManifests).To(Equal(bytes.NewBufferString(tt.expectManifests)))
+ })
+ }
+}
diff --git a/internal/runner/post_renderer_kustomize.go b/internal/postrender/kustomize.go
similarity index 76%
rename from internal/runner/post_renderer_kustomize.go
rename to internal/postrender/kustomize.go
index 89147a514..5e74954f4 100644
--- a/internal/runner/post_renderer_kustomize.go
+++ b/internal/postrender/kustomize.go
@@ -14,31 +14,68 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-package runner
+package postrender
import (
"bytes"
"encoding/json"
"sync"
- "sigs.k8s.io/kustomize/api/filesys"
"sigs.k8s.io/kustomize/api/krusty"
"sigs.k8s.io/kustomize/api/resmap"
kustypes "sigs.k8s.io/kustomize/api/types"
+ "sigs.k8s.io/kustomize/kyaml/filesys"
"github.com/fluxcd/pkg/apis/kustomize"
-
- v2 "github.com/fluxcd/helm-controller/api/v2beta1"
)
-type postRendererKustomize struct {
- spec *v2.Kustomize
+// Kustomize is a Helm post-render plugin that runs Kustomize.
+type Kustomize struct {
+ // Patches is a list of patches to apply to the rendered manifests.
+ Patches []kustomize.Patch
+ // Images is a list of images to replace in the rendered manifests.
+ Images []kustomize.Image
}
-func newPostRendererKustomize(spec *v2.Kustomize) *postRendererKustomize {
- return &postRendererKustomize{
- spec: spec,
+func (k *Kustomize) Run(renderedManifests *bytes.Buffer) (modifiedManifests *bytes.Buffer, err error) {
+ fs := filesys.MakeFsInMemory()
+ cfg := kustypes.Kustomization{}
+ cfg.APIVersion = kustypes.KustomizationVersion
+ cfg.Kind = kustypes.KustomizationKind
+ cfg.Images = adaptImages(k.Images)
+
+ // Add rendered Helm output as input resource to the Kustomization.
+ const input = "helm-output.yaml"
+ cfg.Resources = append(cfg.Resources, input)
+ if err := writeFile(fs, input, renderedManifests); err != nil {
+ return nil, err
+ }
+
+ // Add patches.
+ for _, m := range k.Patches {
+ cfg.Patches = append(cfg.Patches, kustypes.Patch{
+ Patch: m.Patch,
+ Target: adaptSelector(m.Target),
+ })
+ }
+
+ // Write kustomization config to file.
+ kustomization, err := json.Marshal(cfg)
+ if err != nil {
+ return nil, err
+ }
+ if err := writeToFile(fs, "kustomization.yaml", kustomization); err != nil {
+ return nil, err
}
+ resMap, err := buildKustomization(fs, ".")
+ if err != nil {
+ return nil, err
+ }
+ yaml, err := resMap.AsYaml()
+ if err != nil {
+ return nil, err
+ }
+ return bytes.NewBuffer(yaml), nil
}
func writeToFile(fs filesys.FileSystem, path string, content []byte) error {
@@ -46,8 +83,10 @@ func writeToFile(fs filesys.FileSystem, path string, content []byte) error {
if err != nil {
return err
}
- helmOutput.Write(content)
- if err := helmOutput.Close(); err != nil {
+ if _, err = helmOutput.Write(content); err != nil {
+ return err
+ }
+ if err = helmOutput.Close(); err != nil {
return err
}
return nil
@@ -58,8 +97,10 @@ func writeFile(fs filesys.FileSystem, path string, content *bytes.Buffer) error
if err != nil {
return err
}
- content.WriteTo(helmOutput)
- if err := helmOutput.Close(); err != nil {
+ if _, err = content.WriteTo(helmOutput); err != nil {
+ return err
+ }
+ if err = helmOutput.Close(); err != nil {
return err
}
return nil
@@ -91,64 +132,6 @@ func adaptSelector(selector *kustomize.Selector) (output *kustypes.Selector) {
return
}
-func (k *postRendererKustomize) Run(renderedManifests *bytes.Buffer) (modifiedManifests *bytes.Buffer, err error) {
- fs := filesys.MakeFsInMemory()
- cfg := kustypes.Kustomization{}
- cfg.APIVersion = kustypes.KustomizationVersion
- cfg.Kind = kustypes.KustomizationKind
- cfg.Images = adaptImages(k.spec.Images)
-
- // Add rendered Helm output as input resource to the Kustomization.
- const input = "helm-output.yaml"
- cfg.Resources = append(cfg.Resources, input)
- if err := writeFile(fs, input, renderedManifests); err != nil {
- return nil, err
- }
-
- // Add patches.
- for _, m := range k.spec.Patches {
- cfg.Patches = append(cfg.Patches, kustypes.Patch{
- Patch: m.Patch,
- Target: adaptSelector(m.Target),
- })
- }
-
- // Add strategic merge patches.
- for _, m := range k.spec.PatchesStrategicMerge {
- cfg.PatchesStrategicMerge = append(cfg.PatchesStrategicMerge, kustypes.PatchStrategicMerge(m.Raw))
- }
-
- // Add JSON 6902 patches.
- for _, m := range k.spec.PatchesJSON6902 {
- patch, err := json.Marshal(m.Patch)
- if err != nil {
- return nil, err
- }
- cfg.PatchesJson6902 = append(cfg.PatchesJson6902, kustypes.Patch{
- Patch: string(patch),
- Target: adaptSelector(&m.Target),
- })
- }
-
- // Write kustomization config to file.
- kustomization, err := json.Marshal(cfg)
- if err != nil {
- return nil, err
- }
- if err := writeToFile(fs, "kustomization.yaml", kustomization); err != nil {
- return nil, err
- }
- resMap, err := buildKustomization(fs, ".")
- if err != nil {
- return nil, err
- }
- yaml, err := resMap.AsYaml()
- if err != nil {
- return nil, err
- }
- return bytes.NewBuffer(yaml), nil
-}
-
// TODO: remove mutex when kustomize fixes the concurrent map read/write panic
var kustomizeRenderMutex sync.Mutex
diff --git a/internal/runner/post_renderer_kustomize_test.go b/internal/postrender/kustomize_test.go
similarity index 69%
rename from internal/runner/post_renderer_kustomize_test.go
rename to internal/postrender/kustomize_test.go
index 31f322e82..7681603c1 100644
--- a/internal/runner/post_renderer_kustomize_test.go
+++ b/internal/postrender/kustomize_test.go
@@ -14,20 +14,18 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-package runner
+package postrender
import (
"bytes"
- "encoding/json"
- "reflect"
"testing"
- v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
+ . "github.com/onsi/gomega"
"sigs.k8s.io/yaml"
"github.com/fluxcd/pkg/apis/kustomize"
- v2 "github.com/fluxcd/helm-controller/api/v2beta1"
+ v2 "github.com/fluxcd/helm-controller/api/v2"
)
const replaceImageMock = `apiVersion: v1
@@ -61,14 +59,12 @@ spec:
func Test_postRendererKustomize_Run(t *testing.T) {
tests := []struct {
- name string
- renderedManifests string
- patches string
- patchesStrategicMerge string
- patchesJson6902 string
- images string
- expectManifests string
- expectErr bool
+ name string
+ renderedManifests string
+ patches string
+ images string
+ expectManifests string
+ expectErr bool
}{
{
name: "image tag",
@@ -121,12 +117,12 @@ spec:
{
name: "json 6902",
renderedManifests: json6902Mock,
- patchesJson6902: `
+ patches: `
- target:
version: v1
kind: Pod
name: json6902
- patch:
+ patch: |
- op: test
path: /metadata/annotations/c
value: foo
@@ -188,33 +184,6 @@ metadata:
d: "42"
e: "42"
name: json6902
-`,
- },
- {
- name: "strategic merge test",
- renderedManifests: strategicMergeMock,
- patchesStrategicMerge: `
-- apiVersion: apps/v1
- kind: Deployment
- metadata:
- name: nginx
- spec:
- template:
- spec:
- containers:
- - name: nginx
- image: nignx:latest
-`,
- expectManifests: `apiVersion: apps/v1
-kind: Deployment
-metadata:
- name: nginx
-spec:
- template:
- spec:
- containers:
- - image: nignx:latest
- name: nginx
`,
},
{
@@ -253,51 +222,39 @@ spec:
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- spec, err := mockKustomize(tt.patches, tt.patchesStrategicMerge, tt.patchesJson6902, tt.images)
- if err != nil {
- t.Errorf("Run() mockKustomize returned %v", err)
- return
- }
- k := &postRendererKustomize{
- spec: spec,
+ g := NewWithT(t)
+
+ spec, err := mockKustomize(tt.patches, tt.images)
+ g.Expect(err).ToNot(HaveOccurred())
+
+ k := &Kustomize{
+ Patches: spec.Patches,
+ Images: spec.Images,
}
gotModifiedManifests, err := k.Run(bytes.NewBufferString(tt.renderedManifests))
- if (err != nil) != tt.expectErr {
- t.Errorf("Run() error = %v, expectErr %v", err, tt.expectErr)
+ if tt.expectErr {
+ g.Expect(err).ToNot(HaveOccurred())
+ g.Expect(gotModifiedManifests.String()).To(BeEmpty())
return
}
- if !reflect.DeepEqual(gotModifiedManifests, bytes.NewBufferString(tt.expectManifests)) {
- t.Errorf("Run() gotModifiedManifests = %v, want %v", gotModifiedManifests, tt.expectManifests)
- }
+
+ g.Expect(err).ToNot(HaveOccurred())
+ g.Expect(gotModifiedManifests).To(Equal(bytes.NewBufferString(tt.expectManifests)))
})
}
}
-func mockKustomize(patches, patchesStrategicMerge, patchesJson6902, images string) (*v2.Kustomize, error) {
+func mockKustomize(patches, images string) (*v2.Kustomize, error) {
var targeted []kustomize.Patch
if err := yaml.Unmarshal([]byte(patches), &targeted); err != nil {
return nil, err
}
- b, err := yaml.YAMLToJSON([]byte(patchesStrategicMerge))
- if err != nil {
- return nil, err
- }
- var strategicMerge []v1.JSON
- if err := json.Unmarshal(b, &strategicMerge); err != nil {
- return nil, err
- }
- var json6902 []kustomize.JSON6902Patch
- if err := yaml.Unmarshal([]byte(patchesJson6902), &json6902); err != nil {
- return nil, err
- }
var imgs []kustomize.Image
if err := yaml.Unmarshal([]byte(images), &imgs); err != nil {
return nil, err
}
return &v2.Kustomize{
- Patches: targeted,
- PatchesStrategicMerge: strategicMerge,
- PatchesJSON6902: json6902,
- Images: imgs,
+ Patches: targeted,
+ Images: imgs,
}, nil
}
diff --git a/internal/runner/post_renderer_origin_labels.go b/internal/postrender/transformer.go
similarity index 56%
rename from internal/runner/post_renderer_origin_labels.go
rename to internal/postrender/transformer.go
index 47437de05..aeaab8498 100644
--- a/internal/runner/post_renderer_origin_labels.go
+++ b/internal/postrender/transformer.go
@@ -1,5 +1,5 @@
/*
-Copyright 2021 The Flux authors
+Copyright 2025 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -14,33 +14,22 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-package runner
+package postrender
import (
"bytes"
- "fmt"
"sigs.k8s.io/kustomize/api/builtins"
"sigs.k8s.io/kustomize/api/provider"
"sigs.k8s.io/kustomize/api/resmap"
kustypes "sigs.k8s.io/kustomize/api/types"
-
- v2 "github.com/fluxcd/helm-controller/api/v2beta1"
)
-func newPostRendererOriginLabels(release *v2.HelmRelease) *postRendererOriginLabels {
- return &postRendererOriginLabels{
- name: release.ObjectMeta.Name,
- namespace: release.ObjectMeta.Namespace,
- }
+type ResMapTransformer interface {
+ Transform(resmap.ResMap) error
}
-type postRendererOriginLabels struct {
- name string
- namespace string
-}
-
-func (k *postRendererOriginLabels) Run(renderedManifests *bytes.Buffer) (modifiedManifests *bytes.Buffer, err error) {
+func transform(renderedManifests *bytes.Buffer, transformers ...ResMapTransformer) (*bytes.Buffer, error) {
resFactory := provider.NewDefaultDepProvider().GetResourceFactory()
resMapFactory := resmap.NewFactory(resFactory)
@@ -49,14 +38,10 @@ func (k *postRendererOriginLabels) Run(renderedManifests *bytes.Buffer) (modifie
return nil, err
}
- labelTransformer := builtins.LabelTransformerPlugin{
- Labels: originLabels(k.name, k.namespace),
- FieldSpecs: []kustypes.FieldSpec{
- {Path: "metadata/labels", CreateIfNotPresent: true},
- },
- }
- if err := labelTransformer.Transform(resMap); err != nil {
- return nil, err
+ for _, t := range transformers {
+ if err := t.Transform(resMap); err != nil {
+ return nil, err
+ }
}
yaml, err := resMap.AsYaml()
@@ -67,9 +52,20 @@ func (k *postRendererOriginLabels) Run(renderedManifests *bytes.Buffer) (modifie
return bytes.NewBuffer(yaml), nil
}
-func originLabels(name, namespace string) map[string]string {
- return map[string]string{
- fmt.Sprintf("%s/name", v2.GroupVersion.Group): name,
- fmt.Sprintf("%s/namespace", v2.GroupVersion.Group): namespace,
+func labelTransformer(labels map[string]string) ResMapTransformer {
+ return &builtins.LabelTransformerPlugin{
+ Labels: labels,
+ FieldSpecs: []kustypes.FieldSpec{
+ {Path: "metadata/labels", CreateIfNotPresent: true},
+ },
+ }
+}
+
+func annotationTransformer(annotations map[string]string) ResMapTransformer {
+ return &builtins.AnnotationsTransformerPlugin{
+ Annotations: annotations,
+ FieldSpecs: []kustypes.FieldSpec{
+ {Path: "metadata/annotations", CreateIfNotPresent: true},
+ },
}
}
diff --git a/internal/controller/source_predicate.go b/internal/predicates/source_predicate.go
similarity index 78%
rename from internal/controller/source_predicate.go
rename to internal/predicates/source_predicate.go
index 8e5be1656..730abd5e2 100644
--- a/internal/controller/source_predicate.go
+++ b/internal/predicates/source_predicate.go
@@ -14,15 +14,18 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-package controller
+package predicates
import (
+ "github.com/fluxcd/pkg/runtime/conditions"
"sigs.k8s.io/controller-runtime/pkg/event"
"sigs.k8s.io/controller-runtime/pkg/predicate"
sourcev1 "github.com/fluxcd/source-controller/api/v1"
)
+// SourceRevisionChangePredicate detects revision changes to the v1.Artifact of
+// a v1.Source object.
type SourceRevisionChangePredicate struct {
predicate.Funcs
}
@@ -51,6 +54,20 @@ func (SourceRevisionChangePredicate) Update(e event.UpdateEvent) bool {
return true
}
+ oldConditions, ok := e.ObjectOld.(conditions.Getter)
+ if !ok {
+ return false
+ }
+
+ newConditions, ok := e.ObjectNew.(conditions.Getter)
+ if !ok {
+ return false
+ }
+
+ if !conditions.IsReady(oldConditions) && conditions.IsReady(newConditions) {
+ return true
+ }
+
return false
}
diff --git a/internal/predicates/source_predicate_test.go b/internal/predicates/source_predicate_test.go
new file mode 100644
index 000000000..89328ddf5
--- /dev/null
+++ b/internal/predicates/source_predicate_test.go
@@ -0,0 +1,136 @@
+/*
+Copyright 2022 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package predicates
+
+import (
+ "testing"
+ "time"
+
+ "github.com/onsi/gomega"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+ "sigs.k8s.io/controller-runtime/pkg/event"
+
+ "github.com/fluxcd/pkg/apis/meta"
+)
+
+func TestSourceRevisionChangePredicate_Update(t *testing.T) {
+ sourceA := &sourceMock{revision: "revision-a"}
+ sourceB := &sourceMock{revision: "revision-b"}
+ emptySource := &sourceMock{}
+ notASource := &unstructured.Unstructured{}
+
+ tests := []struct {
+ name string
+ old client.Object
+ new client.Object
+ want bool
+ }{
+ {name: "same artifact revision", old: sourceA, new: sourceA, want: false},
+ {name: "diff artifact revision", old: sourceA, new: sourceB, want: true},
+ {name: "new with artifact", old: emptySource, new: sourceA, want: true},
+ {name: "old with artifact", old: sourceA, new: emptySource, want: false},
+ {name: "old not a source", old: notASource, new: sourceA, want: false},
+ {name: "new not a source", old: sourceA, new: notASource, want: false},
+ {
+ name: "old not ready and new ready",
+ old: &sourceMock{
+ revision: "revision-a",
+ conditions: []metav1.Condition{{Type: meta.ReadyCondition, Status: metav1.ConditionFalse}},
+ },
+ new: &sourceMock{
+ revision: "revision-a",
+ conditions: []metav1.Condition{{Type: meta.ReadyCondition, Status: metav1.ConditionTrue}},
+ },
+ want: true,
+ },
+ {
+ name: "old ready and new not ready",
+ old: &sourceMock{
+ revision: "revision-a",
+ conditions: []metav1.Condition{{Type: meta.ReadyCondition, Status: metav1.ConditionTrue}},
+ },
+ new: &sourceMock{
+ revision: "revision-a",
+ conditions: []metav1.Condition{{Type: meta.ReadyCondition, Status: metav1.ConditionFalse}},
+ },
+ want: false,
+ },
+ {
+ name: "old not ready and new not ready",
+ old: &sourceMock{
+ revision: "revision-a",
+ conditions: []metav1.Condition{{Type: meta.ReadyCondition, Status: metav1.ConditionFalse}},
+ },
+ new: &sourceMock{
+ revision: "revision-a",
+ conditions: []metav1.Condition{{Type: meta.ReadyCondition, Status: metav1.ConditionFalse}},
+ },
+ want: false,
+ },
+ {
+ name: "old ready and new ready",
+ old: &sourceMock{
+ revision: "revision-a",
+ conditions: []metav1.Condition{{Type: meta.ReadyCondition, Status: metav1.ConditionTrue}},
+ },
+ new: &sourceMock{
+ revision: "revision-a",
+ conditions: []metav1.Condition{{Type: meta.ReadyCondition, Status: metav1.ConditionTrue}},
+ },
+ want: false,
+ },
+ {name: "old nil", old: nil, new: sourceA, want: false},
+ {name: "new nil", old: sourceA, new: nil, want: false},
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ g := gomega.NewWithT(t)
+
+ so := SourceRevisionChangePredicate{}
+ e := event.UpdateEvent{
+ ObjectOld: tt.old,
+ ObjectNew: tt.new,
+ }
+ g.Expect(so.Update(e)).To(gomega.Equal(tt.want))
+ })
+ }
+}
+
+type sourceMock struct {
+ unstructured.Unstructured
+ revision string
+ conditions []metav1.Condition
+}
+
+func (m sourceMock) GetRequeueAfter() time.Duration {
+ return time.Second * 0
+}
+
+func (m *sourceMock) GetArtifact() *meta.Artifact {
+ if m.revision != "" {
+ return &meta.Artifact{
+ Revision: m.revision,
+ }
+ }
+ return nil
+}
+
+func (m *sourceMock) GetConditions() []metav1.Condition {
+ return m.conditions
+}
diff --git a/internal/reconcile/atomic_release.go b/internal/reconcile/atomic_release.go
new file mode 100644
index 000000000..6872e77cd
--- /dev/null
+++ b/internal/reconcile/atomic_release.go
@@ -0,0 +1,612 @@
+/*
+Copyright 2022 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package reconcile
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "strings"
+ "time"
+
+ "helm.sh/helm/v4/pkg/kube"
+ corev1 "k8s.io/api/core/v1"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/client-go/tools/record"
+ ctrl "sigs.k8s.io/controller-runtime"
+
+ "github.com/fluxcd/pkg/apis/meta"
+ "github.com/fluxcd/pkg/runtime/conditions"
+ "github.com/fluxcd/pkg/runtime/logger"
+ "github.com/fluxcd/pkg/runtime/patch"
+ "github.com/fluxcd/pkg/ssa/jsondiff"
+
+ v2 "github.com/fluxcd/helm-controller/api/v2"
+ "github.com/fluxcd/helm-controller/internal/action"
+ "github.com/fluxcd/helm-controller/internal/diff"
+ interrors "github.com/fluxcd/helm-controller/internal/errors"
+)
+
+// OwnedConditions is a list of Condition types owned by the HelmRelease object.
+var OwnedConditions = []string{
+ v2.ReleasedCondition,
+ v2.RemediatedCondition,
+ v2.TestSuccessCondition,
+ meta.ReconcilingCondition,
+ meta.ReadyCondition,
+ meta.StalledCondition,
+}
+
+var (
+ // ErrExceededMaxRetries is returned when there are no remaining retry
+ // attempts for the provided release config.
+ ErrExceededMaxRetries = errors.New("exceeded maximum retries")
+
+ // ErrRetryAfterInterval is returned when the action strategy is RetryOnFailure
+ // and the current AtomicRelease has already reconciled at least one action,
+ // in which case the action must be retried after the configured retry interval.
+ ErrRetryAfterInterval = errors.New("retry after interval")
+
+ // ErrMustRequeue is returned when the caller must requeue the object
+ // to continue the reconciliation process.
+ ErrMustRequeue = errors.New("must requeue")
+
+ // ErrMissingRollbackTarget is returned when the rollback target is missing.
+ ErrMissingRollbackTarget = errors.New("missing target release for rollback")
+
+ // ErrUnknownReleaseStatus is returned when the release status is unknown
+ // and cannot be acted upon.
+ ErrUnknownReleaseStatus = errors.New("unknown release status")
+
+ // ErrUnknownRemediationStrategy is returned when the remediation strategy
+ // is unknown.
+ ErrUnknownRemediationStrategy = errors.New("unknown remediation strategy")
+)
+
+// AtomicRelease is an ActionReconciler which implements an atomic release
+// strategy similar to Helm's `--atomic`, but with more advanced state
+// determination. It determines the next action to take based on the current
+// state of the Request.Object and other data, and the state of the Helm
+// release.
+//
+// This process will continue until an action is called multiple times, no
+// action remains, or a remediation action is called. In which case, the process
+// will stop to be resumed at a later time or be checked upon again, by e.g. a
+// requeue.
+//
+// Before running the ActionReconciler for the next action, the object is
+// marked with Reconciling=True and the status is patched.
+// This condition is removed when the ActionReconciler process is done.
+//
+// When it determines the object is out of remediation retries, the object
+// is marked with Stalled=True.
+//
+// The status conditions are summarized into a Ready condition when no actions
+// to be run remain, to ensure any transient error is cleared.
+//
+// Any returned error other than ErrExceededMaxRetries should be retried by the
+// caller as soon as possible, preferably with a backoff strategy. In case of
+// ErrMustRequeue, it is advised to requeue the object outside the interval
+// to ensure continued progress.
+//
+// The caller is expected to patch the object one last time with the
+// Request.Object result to persist the final observation. As there is an
+// expectation they will need to patch the object anyway to e.g. update the
+// ObservedGeneration.
+//
+// For more information on the individual ActionReconcilers, refer to their
+// documentation.
+type AtomicRelease struct {
+ patchHelper *patch.SerialPatcher
+ configFactory *action.ConfigFactory
+ eventRecorder record.EventRecorder
+ strategy releaseStrategy
+ fieldManager string
+ disallowedFieldManagers []string
+ defaultToRetryOnFailure bool
+}
+
+// NewAtomicRelease returns a new AtomicRelease reconciler configured with the
+// provided values.
+func NewAtomicRelease(patchHelper *patch.SerialPatcher, cfg *action.ConfigFactory, recorder record.EventRecorder, fieldManager string, disallowedFieldManagers []string, defaultToRetryOnFailure bool) *AtomicRelease {
+ return &AtomicRelease{
+ patchHelper: patchHelper,
+ eventRecorder: recorder,
+ configFactory: cfg,
+ strategy: &cleanReleaseStrategy{},
+ fieldManager: fieldManager,
+ disallowedFieldManagers: disallowedFieldManagers,
+ defaultToRetryOnFailure: defaultToRetryOnFailure,
+ }
+}
+
+// releaseStrategy defines the continue-stop behavior of the reconcile loop.
+type releaseStrategy interface {
+ // MustContinue should be called before running the current action, and
+ // returns true if the caller must proceed.
+ MustContinue(current ReconcilerType, previous ReconcilerTypeSet) bool
+ // MustStop should be called after running the current action, and returns
+ // true if the caller must stop.
+ MustStop(current ReconcilerType, previous ReconcilerTypeSet) bool
+}
+
+// cleanReleaseStrategy is a releaseStrategy which will only execute the
+// (remaining) actions for a single release. Effectively, this means it will
+// only run any action once during a reconcile attempt, and stops after running
+// a remediation action.
+type cleanReleaseStrategy ReconcilerTypeSet
+
+// MustContinue returns if previous does not contain current.
+func (cleanReleaseStrategy) MustContinue(current ReconcilerType, previous ReconcilerTypeSet) bool {
+ return !previous.Contains(current)
+}
+
+// MustStop returns true if current equals ReconcilerTypeRemediate.
+func (cleanReleaseStrategy) MustStop(current ReconcilerType, _ ReconcilerTypeSet) bool {
+ switch current {
+ case ReconcilerTypeRemediate:
+ return true
+ default:
+ return false
+ }
+}
+
+func (r *AtomicRelease) Reconcile(ctx context.Context, req *Request) error {
+ log := ctrl.LoggerFrom(ctx).V(logger.InfoLevel)
+
+ var (
+ previous ReconcilerTypeSet
+ next ActionReconciler
+ )
+ for {
+ select {
+ case <-ctx.Done():
+ if errors.Is(ctx.Err(), context.Canceled) || errors.Is(ctx.Err(), context.DeadlineExceeded) {
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ // If the context is canceled, we still need to persist any
+ // last observation before returning. If the patch fails, we
+ // log the error and return the original context cancellation
+ // error.
+ if err := r.patchHelper.Patch(ctx, req.Object, patch.WithOwnedConditions{Conditions: OwnedConditions}, patch.WithFieldOwner(r.fieldManager)); err != nil {
+ log.Error(err, "failed to patch HelmRelease after context cancellation")
+ }
+ cancel()
+ }
+ return fmt.Errorf("atomic release canceled: %w", ctx.Err())
+ default:
+ // Determine the current state of the Helm release.
+ log.V(logger.DebugLevel).Info("determining current state of Helm release")
+ state, err := DetermineReleaseState(ctx, r.configFactory, req, r.disallowedFieldManagers)
+ if err != nil {
+ conditions.MarkFalse(req.Object, meta.ReadyCondition, "StateError", "Could not determine release state: %s", err)
+ return fmt.Errorf("cannot determine release state: %w", err)
+ }
+
+ // Determine the next action to run based on the current state.
+ log.V(logger.DebugLevel).Info(
+ fmt.Sprintf("determining next Helm action based on state: '%s' reason '%s'", state.Status, state.Reason),
+ )
+ if next, err = r.actionForState(ctx, req, state); err != nil {
+ if errors.Is(err, ErrExceededMaxRetries) {
+ conditions.MarkStalled(req.Object, "RetriesExceeded", "Failed to %s after %d attempt(s)",
+ req.Object.Status.LastAttemptedReleaseAction, req.Object.GetActiveRemediation().GetFailureCount(req.Object))
+ return err
+ }
+ if errors.Is(err, ErrMissingRollbackTarget) {
+ conditions.MarkStalled(req.Object, "MissingRollbackTarget", "Failed to perform remediation: %s", err)
+ return err
+ }
+ return err
+ }
+
+ // If there is no next action, we are done.
+ if next == nil {
+ log.V(logger.DebugLevel).Info("no further action to take, atomic release completed")
+ conditions.Delete(req.Object, meta.ReconcilingCondition)
+
+ // Always summarize; this ensures we restore transient errors
+ // written to Ready, and updates the observed post-renderers
+ // and common-metadata digests.
+ summarize(req)
+
+ return nil
+ }
+
+ // If we are not allowed to run the next action, we are done for now...
+ if !r.strategy.MustContinue(next.Type(), previous) {
+ log.V(logger.DebugLevel).Info(
+ fmt.Sprintf("instructed to stop before running %s action reconciler %s", next.Type(), next.Name()),
+ )
+
+ if retry := req.Object.GetActiveRetry(r.defaultToRetryOnFailure); retry != nil {
+ conditions.MarkReconciling(req.Object, meta.ProgressingWithRetryReason, "retrying after %s", retry.GetRetryInterval().String())
+ return ErrRetryAfterInterval
+ }
+
+ if remediation := req.Object.GetActiveRemediation(); remediation == nil || !remediation.RetriesExhausted(req.Object) {
+ conditions.MarkReconciling(req.Object, meta.ProgressingWithRetryReason, "%s", conditions.GetMessage(req.Object, meta.ReadyCondition))
+ return ErrMustRequeue
+ }
+
+ conditions.Delete(req.Object, meta.ReconcilingCondition)
+ return nil
+ }
+
+ // Mark the release as reconciling before we attempt to run the action.
+ // This to show continuous progress, as Helm actions can be long-running.
+ reconcilingMsg := fmt.Sprintf("Running '%s' action with timeout of %s",
+ next.Name(), timeoutForAction(next, req.Object).String())
+ conditions.MarkReconciling(req.Object, meta.ProgressingReason, "%s", reconcilingMsg)
+
+ // If the next action is a release action, we can mark the release
+ // as progressing in terms of readiness as well. Doing this for any
+ // other action type is not useful, as it would potentially
+ // overwrite more important failure state from an earlier action.
+ if next.Type() == ReconcilerTypeRelease {
+ conditions.MarkUnknown(req.Object, meta.ReadyCondition, meta.ProgressingReason, "%s", reconcilingMsg)
+ }
+
+ // Patch the object to reflect the new condition.
+ if err = r.patchHelper.Patch(ctx, req.Object, patch.WithOwnedConditions{Conditions: OwnedConditions}, patch.WithFieldOwner(r.fieldManager)); err != nil {
+ return err
+ }
+
+ // Run the action sub-reconciler.
+ log.Info(fmt.Sprintf("running '%s' action with timeout of %s", next.Name(), timeoutForAction(next, req.Object).String()))
+ if err = next.Reconcile(ctx, req); err != nil {
+ log.V(logger.DebugLevel).Info(
+ fmt.Sprintf("action reconciler %s of type %s returned error: %s", next.Name(), next.Type(), err),
+ )
+
+ if retry := req.Object.GetActiveRetry(r.defaultToRetryOnFailure); retry != nil {
+ log.Error(err, fmt.Sprintf("failed to run '%s' action", next.Name()))
+ conditions.MarkReconciling(req.Object, meta.ProgressingWithRetryReason, "retrying after %s", retry.GetRetryInterval().String())
+ return ErrRetryAfterInterval
+ }
+
+ if conditions.IsReady(req.Object) {
+ conditions.MarkFalse(req.Object, meta.ReadyCondition, "ReconcileError", "%s", err)
+ }
+ return err
+ }
+
+ // If we must stop after running the action, we are done for now...
+ if r.strategy.MustStop(next.Type(), previous) {
+ log.V(logger.DebugLevel).Info(fmt.Sprintf(
+ "instructed to stop after running %s action reconciler %s", next.Type(), next.Name()),
+ )
+
+ if retry := req.Object.GetActiveRetry(r.defaultToRetryOnFailure); retry != nil {
+ conditions.MarkReconciling(req.Object, meta.ProgressingWithRetryReason, "retrying after %s", retry.GetRetryInterval().String())
+ return ErrRetryAfterInterval
+ }
+
+ remediation := req.Object.GetActiveRemediation()
+ if remediation == nil || !remediation.RetriesExhausted(req.Object) || remediation.IsUninstallAfterUpgrade() {
+ conditions.MarkReconciling(req.Object, meta.ProgressingWithRetryReason, "%s", conditions.GetMessage(req.Object, meta.ReadyCondition))
+ return ErrMustRequeue
+ }
+
+ // Retries have exhausted after remediation for early stall condition detection.
+ conditions.MarkStalled(req.Object, "RetriesExceeded", "Failed to %s after %d attempt(s)",
+ req.Object.Status.LastAttemptedReleaseAction, req.Object.GetActiveRemediation().GetFailureCount(req.Object))
+ return ErrExceededMaxRetries
+ }
+
+ // Append the type to the set of action types we have performed.
+ previous = append(previous, next.Type())
+
+ // Patch the release to reflect progress.
+ if err = r.patchHelper.Patch(ctx, req.Object, patch.WithOwnedConditions{Conditions: OwnedConditions}, patch.WithFieldOwner(r.fieldManager)); err != nil {
+ return err
+ }
+ }
+ }
+}
+
+// actionForState determines the next action to run based on the current state.
+func (r *AtomicRelease) actionForState(ctx context.Context, req *Request, state ReleaseState) (ActionReconciler, error) {
+ log := ctrl.LoggerFrom(ctx)
+
+ // Determine whether we may need to force a release action.
+ // We do this before determining the next action to run, as otherwise we may
+ // end up running a Helm upgrade (due to e.g. ReleaseStatusUnmanaged) and
+ // then forcing an upgrade (due to the release now being in
+ // ReleaseStatusInSync with a yet unhandled force request).
+ forceRequested := meta.ShouldHandleForceRequest(req.Object)
+
+ switch state.Status {
+ case ReleaseStatusInSync:
+ log.Info("release in-sync with desired state")
+
+ if retry := req.Object.GetActiveRetry(r.defaultToRetryOnFailure); retry != nil {
+ req.Object.Status.History.TruncateIgnoringPreviousSnapshots()
+ } else {
+ // Remove all history up to the previous release action.
+ // We need to continue to hold on to the previous release result
+ // to ensure we can e.g. roll back when tests are enabled without
+ // any further changes to the release.
+ ignoreFailures := req.Object.GetTest().IgnoreFailures
+ if remediation := req.Object.GetActiveRemediation(); remediation != nil {
+ ignoreFailures = remediation.MustIgnoreTestFailures(req.Object.GetTest().IgnoreFailures)
+ }
+ req.Object.Status.History.Truncate(ignoreFailures)
+ }
+
+ if forceRequested {
+ log.Info(msgWithReason("forcing upgrade for in-sync release", "force requested through annotation"))
+ return NewUpgrade(r.configFactory, r.eventRecorder, r.defaultToRetryOnFailure), nil
+ }
+
+ // Since the release is in-sync, remove any remediated condition if
+ // present and replace it with upgrade succeeded condition.
+ // This can happen when the current release, which is the result of a
+ // rollback remediation, matches the new desired configuration due to
+ // having the same chart version and values. As a result, we are already
+ // in-sync without performing a release action.
+ if conditions.IsTrue(req.Object, v2.RemediatedCondition) {
+ cur := req.Object.Status.History.Latest()
+ msg := fmt.Sprintf(fmtUpgradeSuccess, cur.FullReleaseName(), cur.VersionedChartName())
+ replaceCondition(req.Object, v2.RemediatedCondition, v2.ReleasedCondition, v2.UpgradeSucceededReason, msg, metav1.ConditionTrue)
+ }
+
+ // Set Released and Ready to reflect the in-sync state if needed.
+ if !conditions.IsReady(req.Object) || !conditions.IsTrue(req.Object, v2.ReleasedCondition) {
+ var reason, msgFmt string
+ switch conditions.GetReason(req.Object, v2.ReleasedCondition) {
+ case v2.InstallFailedReason:
+ reason, msgFmt = v2.InstallSucceededReason, fmtInstallSuccess
+ case v2.UpgradeFailedReason:
+ reason, msgFmt = v2.UpgradeSucceededReason, fmtUpgradeSuccess
+ }
+ if reason != "" {
+ cur := req.Object.Status.History.Latest()
+ msg := fmt.Sprintf(msgFmt, cur.FullReleaseName(), cur.VersionedChartName())
+ conditions.MarkTrue(req.Object, v2.ReleasedCondition, reason, "%s", msg)
+ summarize(req)
+ }
+ }
+
+ if req.Object.GetDriftDetection().MustDetectChanges() {
+ conditions.MarkFalse(req.Object, v2.DriftedCondition, v2.NoDriftDetectedReason, "No drift detected against the cluster state")
+ }
+
+ return nil, nil
+ case ReleaseStatusLocked:
+ log.Info(msgWithReason("release locked", state.Reason))
+ return NewUnlock(r.configFactory, r.eventRecorder), nil
+ case ReleaseStatusAbsent:
+ log.Info(msgWithReason("release not installed", state.Reason))
+
+ if req.Object.GetInstall().GetRemediation().RetriesExhausted(req.Object) {
+ if forceRequested {
+ log.Info(msgWithReason("forcing install while out of retries", "force requested through annotation"))
+ return NewInstall(r.configFactory, r.eventRecorder, r.defaultToRetryOnFailure), nil
+ }
+
+ return nil, fmt.Errorf("%w: cannot install release", ErrExceededMaxRetries)
+ }
+
+ return NewInstall(r.configFactory, r.eventRecorder, r.defaultToRetryOnFailure), nil
+ case ReleaseStatusUnmanaged:
+ log.Info(msgWithReason("release not managed by controller", state.Reason))
+
+ // Clear the history as we can no longer rely on it.
+ req.Object.Status.ClearHistory()
+
+ return NewUpgrade(r.configFactory, r.eventRecorder, r.defaultToRetryOnFailure), nil
+ case ReleaseStatusOutOfSync:
+ log.Info(msgWithReason("release out-of-sync with desired state", state.Reason))
+
+ if req.Object.GetUpgrade().GetRemediation().RetriesExhausted(req.Object) {
+ if forceRequested {
+ log.Info(msgWithReason("forcing upgrade while out of retries", "force requested through annotation"))
+ return NewUpgrade(r.configFactory, r.eventRecorder, r.defaultToRetryOnFailure), nil
+ }
+
+ return nil, fmt.Errorf("%w: cannot upgrade release", ErrExceededMaxRetries)
+ }
+
+ return NewUpgrade(r.configFactory, r.eventRecorder, r.defaultToRetryOnFailure), nil
+ case ReleaseStatusDrifted:
+ log.Info(msgWithReason("detected changes in cluster state", diff.SummarizeDiffSetBrief(state.Diff)))
+ for _, change := range state.Diff {
+ switch change.Type {
+ case jsondiff.DiffTypeCreate:
+ log.V(logger.DebugLevel).Info("resource deleted",
+ "resource", diff.ResourceName(change.DesiredObject))
+ case jsondiff.DiffTypeUpdate:
+ patch := change.Patch
+ if change.DesiredObject.GetObjectKind().GroupVersionKind().Kind == "Secret" {
+ patch = jsondiff.MaskSecretPatchData(change.Patch)
+ }
+ log.V(logger.DebugLevel).Info("resource modified",
+ "resource", diff.ResourceName(change.DesiredObject),
+ "patch", patch)
+ }
+ }
+
+ msg := fmt.Sprintf("Cluster state of release %s has drifted from the desired state:\n%s",
+ req.Object.Status.History.Latest().FullReleaseName(), diff.SummarizeDiffSet(state.Diff))
+ r.eventRecorder.Eventf(req.Object, corev1.EventTypeWarning, v2.DriftDetectedReason, msg)
+ conditions.MarkTrue(req.Object, v2.DriftedCondition, v2.DriftDetectedReason, "%s", msg)
+
+ if req.Object.GetDriftDetection().GetMode() == v2.DriftDetectionEnabled {
+ return NewCorrectClusterDrift(r.configFactory, r.eventRecorder, state.Diff, kube.ManagedFieldsManager), nil
+ }
+
+ return nil, nil
+ case ReleaseStatusUntested:
+ log.Info(msgWithReason("release has not been tested", state.Reason))
+
+ // Since an untested release indicates that the release is already
+ // in-sync, remove any remediated condition if present and replace it
+ // with upgrade succeeded condition.
+ // This can happen when an untested current release, which is the result
+ // of a rollback remediation, matches the new desired configuration due
+ // to having the same chart version and values, and has test enabled. As
+ // a result, we are already in-sync without performing a release action,
+ // the existing release needs to undergo testing.
+ if conditions.IsTrue(req.Object, v2.RemediatedCondition) {
+ cur := req.Object.Status.History.Latest()
+ msg := fmt.Sprintf(fmtUpgradeSuccess, cur.FullReleaseName(), cur.VersionedChartName())
+ replaceCondition(req.Object, v2.RemediatedCondition, v2.ReleasedCondition, v2.UpgradeSucceededReason, msg, metav1.ConditionTrue)
+ }
+
+ return NewTest(r.configFactory, r.eventRecorder), nil
+ case ReleaseStatusFailed:
+ log.Info(msgWithReason("release is in a failed state", state.Reason))
+
+ // If the action strategy is to retry (and not remediate), we behave just like
+ // "flux reconcile hr --force" and .spec..remediation.retries set to 0.
+ if req.Object.GetActiveRetry(r.defaultToRetryOnFailure) != nil {
+ req.Object.Status.History.TruncateIgnoringPreviousSnapshots()
+
+ log.V(logger.DebugLevel).Info("retrying upgrade for failed release")
+ return NewUpgrade(r.configFactory, r.eventRecorder, r.defaultToRetryOnFailure), nil
+ }
+
+ remediation := req.Object.GetActiveRemediation()
+
+ // If there is no active remediation strategy, we can only attempt to
+ // upgrade the release to see if that fixes the problem.
+ if remediation == nil {
+ log.V(logger.DebugLevel).Info("no active remediation strategy")
+ return NewUpgrade(r.configFactory, r.eventRecorder, r.defaultToRetryOnFailure), nil
+ }
+
+ // If there is no failure count, the conditions under which the failure
+ // occurred must have changed.
+ // Attempt to upgrade the release to see if the problem is resolved.
+ // This ensures that after a configuration change, the release is
+ // attempted again.
+ if remediation.GetFailureCount(req.Object) <= 0 {
+ log.Info("release conditions have changed since last failure")
+ return NewUpgrade(r.configFactory, r.eventRecorder, r.defaultToRetryOnFailure), nil
+ }
+
+ // If the force annotation is set, we can attempt to upgrade the release
+ // without any further checks.
+ if forceRequested {
+ log.Info(msgWithReason("forcing upgrade for failed release", "force requested through annotation"))
+ return NewUpgrade(r.configFactory, r.eventRecorder, r.defaultToRetryOnFailure), nil
+ }
+
+ // We have exhausted the number of retries for the remediation
+ // strategy.
+ if remediation.RetriesExhausted(req.Object) && !remediation.MustRemediateLastFailure() {
+ return nil, fmt.Errorf("%w: cannot remediate failed release", ErrExceededMaxRetries)
+ }
+
+ // Reset the history up to the point where the failure occurred.
+ // This ensures we do not accumulate a long history of failures.
+ req.Object.Status.History.Truncate(remediation.MustIgnoreTestFailures(req.Object.GetTest().IgnoreFailures))
+
+ switch remediation.GetStrategy() {
+ case v2.RollbackRemediationStrategy:
+ // Verify the previous release is still in storage and unmodified
+ // before instructing to roll back to it.
+ prev := req.Object.Status.History.Previous(remediation.MustIgnoreTestFailures(req.Object.GetTest().IgnoreFailures))
+ if _, err := action.VerifySnapshot(r.configFactory.Build(nil), prev); err != nil {
+ if errors.Is(err, action.ErrReleaseNotFound) {
+ // If the rollback target is missing, we cannot roll back
+ // to it and must fail.
+ return nil, fmt.Errorf("%w: cannot remediate failed release", ErrMissingRollbackTarget)
+ }
+
+ if interrors.IsOneOf(err, action.ErrReleaseDisappeared, action.ErrReleaseNotObserved, action.ErrReleaseDigest) {
+ // If the rollback target is in any way corrupt,
+ // the most correct remediation is to reattempt the upgrade.
+ log.Info(msgWithReason("unable to verify previous release in storage to roll back to", err.Error()))
+ return NewUpgrade(r.configFactory, r.eventRecorder, r.defaultToRetryOnFailure), nil
+ }
+
+ // This may be a temporary error, return it to retry.
+ return nil, fmt.Errorf("cannot verify previous release to roll back to: %w", err)
+ }
+ return NewRollbackRemediation(r.configFactory, r.eventRecorder), nil
+ case v2.UninstallRemediationStrategy:
+ return NewUninstallRemediation(r.configFactory, r.eventRecorder), nil
+ default:
+ return nil, fmt.Errorf("%w: %s", ErrUnknownRemediationStrategy, remediation.GetStrategy())
+ }
+ default:
+ return nil, fmt.Errorf("%w: %s", ErrUnknownReleaseStatus, state.Status)
+ }
+}
+
+func (r *AtomicRelease) Name() string {
+ return "atomic-release"
+}
+
+func (r *AtomicRelease) Type() ReconcilerType {
+ return ReconcilerTypeRelease
+}
+
+func msgWithReason(msg, reason string) string {
+ if reason != "" {
+ return fmt.Sprintf("%s: %s", msg, reason)
+ }
+ return msg
+}
+
+func inStringSlice(ss []string, str string) (pos int, ok bool) {
+ for k, s := range ss {
+ if strings.EqualFold(s, str) {
+ return k, true
+ }
+ }
+ return -1, false
+}
+
+func timeoutForAction(action ActionReconciler, obj *v2.HelmRelease) time.Duration {
+ switch action.(type) {
+ case *Install:
+ return obj.GetInstall().GetTimeout(obj.GetTimeout()).Duration
+ case *Upgrade:
+ return obj.GetUpgrade().GetTimeout(obj.GetTimeout()).Duration
+ case *Test:
+ return obj.GetTest().GetTimeout(obj.GetTimeout()).Duration
+ case *RollbackRemediation:
+ return obj.GetRollback().GetTimeout(obj.GetTimeout()).Duration
+ case *UninstallRemediation:
+ return obj.GetUninstall().GetTimeout(obj.GetTimeout()).Duration
+ default:
+ return obj.GetTimeout().Duration
+ }
+}
+
+// replaceCondition replaces existing target condition with replacement
+// condition, if present, for the given values, retaining the
+// LastTransitionTime.
+func replaceCondition(obj *v2.HelmRelease, target string, replacement string, reason string, msg string, status metav1.ConditionStatus) {
+ c := conditions.Get(obj, target)
+ if c != nil {
+ // Remove any existing replacement condition to retain the
+ // LastTransitionTime set here. If the state of the new condition
+ // changes an existing condition, the LastTransitionTime is updated to
+ // the current time.
+ // Refer https://github.com/fluxcd/pkg/blob/runtime/v0.43.0/runtime/conditions/setter.go#L54-L55.
+ conditions.Delete(obj, replacement)
+ c.Status = status
+ c.Type = replacement
+ c.Reason = reason
+ c.Message = msg
+ conditions.Set(obj, c)
+ conditions.Delete(obj, target)
+ }
+}
diff --git a/internal/reconcile/atomic_release_test.go b/internal/reconcile/atomic_release_test.go
new file mode 100644
index 000000000..7637c3bc4
--- /dev/null
+++ b/internal/reconcile/atomic_release_test.go
@@ -0,0 +1,2463 @@
+/*
+Copyright 2022 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package reconcile
+
+import (
+ "context"
+ "fmt"
+ "testing"
+ "time"
+
+ . "github.com/onsi/gomega"
+ extjsondiff "github.com/wI2L/jsondiff"
+ helmchart "helm.sh/helm/v4/pkg/chart/v2"
+ helmreleasecommon "helm.sh/helm/v4/pkg/release/common"
+ helmrelease "helm.sh/helm/v4/pkg/release/v1"
+ releaseutil "helm.sh/helm/v4/pkg/release/v1/util"
+ helmstorage "helm.sh/helm/v4/pkg/storage"
+ helmdriver "helm.sh/helm/v4/pkg/storage/driver"
+ corev1 "k8s.io/api/core/v1"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+ "k8s.io/client-go/tools/record"
+ "k8s.io/utils/ptr"
+ "sigs.k8s.io/controller-runtime/pkg/client/fake"
+
+ "github.com/fluxcd/pkg/apis/meta"
+ "github.com/fluxcd/pkg/runtime/conditions"
+ "github.com/fluxcd/pkg/runtime/patch"
+ "github.com/fluxcd/pkg/ssa/jsondiff"
+
+ 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"
+)
+
+func TestReleaseStrategy_CleanRelease_MustContinue(t *testing.T) {
+ tests := []struct {
+ name string
+ current ReconcilerType
+ previous ReconcilerTypeSet
+ want bool
+ }{
+ {
+ name: "continue if not in previous",
+ current: ReconcilerTypeRemediate,
+ previous: []ReconcilerType{
+ ReconcilerTypeRelease,
+ },
+ want: true,
+ },
+ {
+ name: "do not continue if in previous",
+ current: ReconcilerTypeRemediate,
+ previous: []ReconcilerType{
+ ReconcilerTypeRemediate,
+ },
+ want: false,
+ },
+ {
+ name: "do continue on nil",
+ current: ReconcilerTypeRemediate,
+ previous: nil,
+ want: true,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ at := &cleanReleaseStrategy{}
+ if got := at.MustContinue(tt.current, tt.previous); got != tt.want {
+ g := NewWithT(t)
+ g.Expect(got).To(Equal(tt.want))
+ }
+ })
+ }
+}
+
+func TestReleaseStrategy_CleanRelease_MustStop(t *testing.T) {
+ tests := []struct {
+ name string
+ current ReconcilerType
+ previous ReconcilerTypeSet
+ want bool
+ }{
+ {
+ name: "stop if current is remediate",
+ current: ReconcilerTypeRemediate,
+ want: true,
+ },
+ {
+ name: "do not stop if current is not remediate",
+ current: ReconcilerTypeRelease,
+ want: false,
+ },
+ {
+ name: "do not stop if current is not remediate",
+ current: ReconcilerTypeUnlock,
+ want: false,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ at := &cleanReleaseStrategy{}
+ if got := at.MustStop(tt.current, tt.previous); got != tt.want {
+ g := NewWithT(t)
+ g.Expect(got).To(Equal(tt.want))
+ }
+ })
+ }
+}
+
+func TestAtomicRelease_Reconcile(t *testing.T) {
+ t.Run("runs a series of actions", 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
+
+ obj := &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: mockReleaseName,
+ Namespace: releaseNamespace,
+ },
+ Spec: v2.HelmReleaseSpec{
+ ReleaseName: mockReleaseName,
+ TargetNamespace: releaseNamespace,
+ Test: &v2.Test{
+ Enable: true,
+ },
+ StorageNamespace: releaseNamespace,
+ Timeout: &metav1.Duration{Duration: 200 * time.Second},
+ },
+ }
+
+ 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())
+
+ // 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(testutil.ChartWithTestHook()),
+ Values: nil,
+ }
+ g.Expect(NewAtomicRelease(patchHelper, cfg, recorder, testFieldManager, nil, false).Reconcile(context.TODO(), req)).ToNot(HaveOccurred())
+
+ g.Expect(obj.Status.Conditions).To(conditions.MatchConditions([]metav1.Condition{
+ {
+ Type: meta.ReadyCondition,
+ Status: metav1.ConditionTrue,
+ Reason: v2.TestSucceededReason,
+ Message: "test hook completed successfully",
+ },
+ {
+ Type: v2.ReleasedCondition,
+ Status: metav1.ConditionTrue,
+ Reason: v2.InstallSucceededReason,
+ Message: "Helm install succeeded",
+ },
+ {
+ Type: v2.TestSuccessCondition,
+ Status: metav1.ConditionTrue,
+ Reason: v2.TestSucceededReason,
+ Message: "test hook completed successfully",
+ },
+ }))
+ g.Expect(obj.Status.History.Latest()).ToNot(BeNil(), "expected current to not be nil")
+ g.Expect(obj.Status.History.Previous(false)).To(BeNil(), "expected previous to be nil")
+
+ g.Expect(obj.Status.Failures).To(BeZero())
+ g.Expect(obj.Status.InstallFailures).To(BeZero())
+ g.Expect(obj.Status.UpgradeFailures).To(BeZero())
+
+ endState, err := DetermineReleaseState(ctx, cfg, req, nil)
+ g.Expect(err).ToNot(HaveOccurred())
+ g.Expect(endState).To(Equal(ReleaseState{Status: ReleaseStatusInSync}))
+ })
+}
+
+func TestAtomicRelease_Reconcile_Scenarios(t *testing.T) {
+ tests := []struct {
+ name string
+ releases func(namespace string) []*helmrelease.Release
+ spec func(spec *v2.HelmReleaseSpec)
+ status func(namespace string, releases []*helmrelease.Release) v2.HelmReleaseStatus
+ chart *helmchart.Chart
+ values map[string]any
+ expectHistory func(releases []*helmrelease.Release) v2.Snapshots
+ wantErr error
+ }{
+ {
+ name: "release is in-sync",
+ releases: func(namespace string) []*helmrelease.Release {
+ return []*helmrelease.Release{
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: namespace,
+ Version: 1,
+ Chart: testutil.BuildChart(),
+ Status: helmreleasecommon.StatusDeployed,
+ }, testutil.ReleaseWithConfig(nil)),
+ }
+ },
+ status: func(namespace string, releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ return v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
+ },
+ }
+ },
+ chart: testutil.BuildChart(),
+ values: nil,
+ expectHistory: func(releases []*helmrelease.Release) v2.Snapshots {
+ return v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
+ }
+ },
+ },
+ {
+ name: "release is out-of-sync (chart)",
+ releases: func(namespace string) []*helmrelease.Release {
+ return []*helmrelease.Release{
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: namespace,
+ Version: 1,
+ Chart: testutil.BuildChart(),
+ Status: helmreleasecommon.StatusDeployed,
+ }, testutil.ReleaseWithConfig(nil)),
+ }
+ },
+ status: func(namespace string, releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ return v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
+ },
+ }
+ },
+ chart: testutil.BuildChart(testutil.ChartWithVersion("0.2.0")),
+ values: nil,
+ expectHistory: func(releases []*helmrelease.Release) v2.Snapshots {
+ return v2.Snapshots{
+ observeReleaseWithAction(releases[1], v2.ReleaseActionUpgrade),
+ release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
+ }
+ },
+ },
+ {
+ name: "release is out-of-sync (values)",
+ releases: func(namespace string) []*helmrelease.Release {
+ return []*helmrelease.Release{
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: namespace,
+ Version: 1,
+ Chart: testutil.BuildChart(),
+ Status: helmreleasecommon.StatusDeployed,
+ }, testutil.ReleaseWithConfig(map[string]any{"foo": "bar"})),
+ }
+ },
+ status: func(namespace string, releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ return v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
+ },
+ }
+ },
+ chart: testutil.BuildChart(),
+ values: map[string]any{"foo": "baz"},
+ expectHistory: func(releases []*helmrelease.Release) v2.Snapshots {
+ return v2.Snapshots{
+ observeReleaseWithAction(releases[1], v2.ReleaseActionUpgrade),
+ release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
+ }
+ },
+ },
+ {
+ name: "release is locked (pending-install)",
+ releases: func(namespace string) []*helmrelease.Release {
+ return []*helmrelease.Release{
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: namespace,
+ Version: 1,
+ Chart: testutil.BuildChart(),
+ Status: helmreleasecommon.StatusPendingInstall,
+ }, testutil.ReleaseWithConfig(nil)),
+ }
+ },
+ status: func(namespace string, releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ return v2.HelmReleaseStatus{
+ History: nil,
+ }
+ },
+ chart: testutil.BuildChart(),
+ expectHistory: func(releases []*helmrelease.Release) v2.Snapshots {
+ return v2.Snapshots{
+ observeReleaseWithAction(releases[len(releases)-1], v2.ReleaseActionUpgrade),
+ }
+ },
+ },
+ {
+ name: "release is locked (pending-upgrade)",
+ releases: func(namespace string) []*helmrelease.Release {
+ return []*helmrelease.Release{
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: namespace,
+ Version: 1,
+ Chart: testutil.BuildChart(),
+ Status: helmreleasecommon.StatusDeployed,
+ }, testutil.ReleaseWithConfig(nil)),
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: namespace,
+ Version: 2,
+ Chart: testutil.BuildChart(),
+ Status: helmreleasecommon.StatusPendingUpgrade,
+ }, testutil.ReleaseWithConfig(nil)),
+ }
+ },
+ status: func(namespace string, releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ return v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
+ },
+ }
+ },
+ chart: testutil.BuildChart(),
+ expectHistory: func(releases []*helmrelease.Release) v2.Snapshots {
+ return v2.Snapshots{
+ observeReleaseWithAction(releases[len(releases)-1], v2.ReleaseActionUpgrade),
+ }
+ },
+ },
+ {
+ name: "release is locked (pending-rollback)",
+ releases: func(namespace string) []*helmrelease.Release {
+ return []*helmrelease.Release{
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: namespace,
+ Version: 1,
+ Chart: testutil.BuildChart(),
+ Status: helmreleasecommon.StatusDeployed,
+ }, testutil.ReleaseWithConfig(nil)),
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: namespace,
+ Version: 2,
+ Chart: testutil.BuildChart(),
+ Status: helmreleasecommon.StatusFailed,
+ }, testutil.ReleaseWithConfig(nil)),
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: namespace,
+ Version: 3,
+ Chart: testutil.BuildChart(),
+ Status: helmreleasecommon.StatusPendingRollback,
+ }, testutil.ReleaseWithConfig(nil)),
+ }
+ },
+ status: func(namespace string, releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ return v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(releases[1])),
+ release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
+ },
+ }
+ },
+ chart: testutil.BuildChart(),
+ expectHistory: func(releases []*helmrelease.Release) v2.Snapshots {
+ return v2.Snapshots{
+ observeReleaseWithAction(releases[len(releases)-1], v2.ReleaseActionUpgrade),
+ }
+ },
+ },
+ {
+ name: "release is not installed",
+ chart: testutil.BuildChart(),
+ expectHistory: func(releases []*helmrelease.Release) v2.Snapshots {
+ return v2.Snapshots{
+ observeReleaseWithAction(releases[0], v2.ReleaseActionInstall),
+ }
+ },
+ },
+ {
+ name: "release exists but is not managed",
+ releases: func(namespace string) []*helmrelease.Release {
+ return []*helmrelease.Release{
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: namespace,
+ Version: 1,
+ Chart: testutil.BuildChart(),
+ Status: helmreleasecommon.StatusDeployed,
+ }),
+ }
+ },
+ chart: testutil.BuildChart(),
+ expectHistory: func(releases []*helmrelease.Release) v2.Snapshots {
+ return v2.Snapshots{
+ observeReleaseWithAction(releases[1], v2.ReleaseActionUpgrade),
+ }
+ },
+ },
+ {
+ name: "release was upgraded outside of the reconciler",
+ releases: func(namespace string) []*helmrelease.Release {
+ return []*helmrelease.Release{
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: namespace,
+ Version: 3,
+ Chart: testutil.BuildChart(),
+ Status: helmreleasecommon.StatusDeployed,
+ }),
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: namespace,
+ Version: 2,
+ Chart: testutil.BuildChart(),
+ Status: helmreleasecommon.StatusSuperseded,
+ }),
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: namespace,
+ Version: 1,
+ Chart: testutil.BuildChart(),
+ Status: helmreleasecommon.StatusSuperseded,
+ }),
+ }
+ },
+ status: func(namespace string, releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ previousDeployed := release.ObserveRelease(releases[1])
+ previousDeployed.Info.Status = helmreleasecommon.StatusDeployed
+
+ return v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ release.ObservedToSnapshot(previousDeployed),
+ },
+ }
+ },
+ chart: testutil.BuildChart(),
+ expectHistory: func(releases []*helmrelease.Release) v2.Snapshots {
+ return v2.Snapshots{
+ observeReleaseWithAction(releases[len(releases)-1], v2.ReleaseActionUpgrade),
+ }
+ },
+ },
+ {
+ name: "release was rolled back outside of the reconciler",
+ releases: func(namespace string) []*helmrelease.Release {
+ return []*helmrelease.Release{
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: namespace,
+ Version: 3,
+ Chart: testutil.BuildChart(),
+ Status: helmreleasecommon.StatusDeployed,
+ }),
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: namespace,
+ Version: 2,
+ Chart: testutil.BuildChart(),
+ Status: helmreleasecommon.StatusSuperseded,
+ }),
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: namespace,
+ Version: 1,
+ Chart: testutil.BuildChart(),
+ Status: helmreleasecommon.StatusSuperseded,
+ }),
+ }
+ },
+ status: func(namespace string, releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ modifiedRelease := release.ObserveRelease(releases[1])
+ modifiedRelease.Info.Status = helmreleasecommon.StatusFailed
+
+ return v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ release.ObservedToSnapshot(modifiedRelease),
+ },
+ }
+ },
+ chart: testutil.BuildChart(),
+ expectHistory: func(releases []*helmrelease.Release) v2.Snapshots {
+ return v2.Snapshots{
+ observeReleaseWithAction(releases[len(releases)-1], v2.ReleaseActionUpgrade),
+ }
+ },
+ },
+ {
+ name: "release was deleted outside of the reconciler",
+ status: func(namespace string, releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ return v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: namespace,
+ Version: 1,
+ Chart: testutil.BuildChart(),
+ Status: helmreleasecommon.StatusDeployed,
+ }),
+ )),
+ },
+ }
+ },
+ chart: testutil.BuildChart(),
+ expectHistory: func(releases []*helmrelease.Release) v2.Snapshots {
+ return v2.Snapshots{
+ observeReleaseWithAction(releases[0], v2.ReleaseActionInstall),
+ }
+ },
+ },
+ {
+ name: "part of the release history was deleted outside of the reconciler",
+ releases: func(namespace string) []*helmrelease.Release {
+ return []*helmrelease.Release{
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: namespace,
+ Version: 1,
+ Chart: testutil.BuildChart(),
+ Status: helmreleasecommon.StatusSuperseded,
+ }),
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: namespace,
+ Version: 2,
+ Chart: testutil.BuildChart(),
+ Status: helmreleasecommon.StatusSuperseded,
+ }),
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: namespace,
+ Version: 3,
+ Chart: testutil.BuildChart(),
+ Status: helmreleasecommon.StatusDeployed,
+ }),
+ }
+ },
+ status: func(namespace string, releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ deletedRelease := release.ObservedToSnapshot(release.ObserveRelease(
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: namespace,
+ Version: 4,
+ Chart: testutil.BuildChart(),
+ Status: helmreleasecommon.StatusFailed,
+ }),
+ ))
+
+ return v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ deletedRelease,
+ release.ObservedToSnapshot(release.ObserveRelease(releases[1])),
+ },
+ }
+ },
+ chart: testutil.BuildChart(),
+ expectHistory: func(releases []*helmrelease.Release) v2.Snapshots {
+ return v2.Snapshots{
+ observeReleaseWithAction(releases[len(releases)-1], v2.ReleaseActionUpgrade),
+ }
+ },
+ },
+ {
+ name: "install failure",
+ chart: testutil.BuildChart(testutil.ChartWithFailingHook()),
+ expectHistory: func(releases []*helmrelease.Release) v2.Snapshots {
+ return v2.Snapshots{
+ observeReleaseWithAction(releases[0], v2.ReleaseActionInstall),
+ }
+ },
+ wantErr: ErrExceededMaxRetries,
+ },
+ {
+ name: "install failure with remediation",
+ spec: func(spec *v2.HelmReleaseSpec) {
+ spec.Install = &v2.Install{
+ Remediation: &v2.InstallRemediation{
+ RemediateLastFailure: ptr.To(true),
+ },
+ }
+ spec.Uninstall = &v2.Uninstall{
+ KeepHistory: true,
+ }
+ },
+ chart: testutil.BuildChart(testutil.ChartWithFailingHook()),
+ expectHistory: func(releases []*helmrelease.Release) v2.Snapshots {
+ return v2.Snapshots{
+ observeReleaseWithAction(releases[0], v2.ReleaseActionUninstallRemediation),
+ }
+ },
+ wantErr: ErrExceededMaxRetries,
+ },
+ {
+ name: "install failure with retry",
+ spec: func(spec *v2.HelmReleaseSpec) {
+ spec.Install = &v2.Install{
+ Strategy: &v2.InstallStrategy{
+ Name: "RetryOnFailure",
+ RetryInterval: &metav1.Duration{Duration: time.Minute},
+ },
+ }
+ spec.Uninstall = &v2.Uninstall{
+ KeepHistory: true,
+ }
+ },
+ chart: testutil.BuildChart(testutil.ChartWithFailingHook()),
+ expectHistory: func(releases []*helmrelease.Release) v2.Snapshots {
+ return v2.Snapshots{
+ observeReleaseWithAction(releases[0], v2.ReleaseActionInstall),
+ }
+ },
+ wantErr: ErrRetryAfterInterval,
+ },
+ {
+ name: "install test failure with remediation",
+ spec: func(spec *v2.HelmReleaseSpec) {
+ spec.Install = &v2.Install{
+ Remediation: &v2.InstallRemediation{
+ RemediateLastFailure: ptr.To(true),
+ },
+ }
+ spec.Test = &v2.Test{
+ Enable: true,
+ }
+ spec.Uninstall = &v2.Uninstall{
+ KeepHistory: true,
+ }
+ },
+ chart: testutil.BuildChart(testutil.ChartWithFailingTestHook()),
+ expectHistory: func(releases []*helmrelease.Release) v2.Snapshots {
+ snap := observeReleaseWithAction(releases[0], v2.ReleaseActionUninstallRemediation)
+ snap.SetTestHooks(release.TestHooksFromRelease(releases[0]))
+
+ return v2.Snapshots{
+ snap,
+ }
+ },
+ wantErr: ErrExceededMaxRetries,
+ },
+ {
+ name: "install test failure with retry",
+ spec: func(spec *v2.HelmReleaseSpec) {
+ spec.Install = &v2.Install{
+ Strategy: &v2.InstallStrategy{
+ Name: "RetryOnFailure",
+ RetryInterval: &metav1.Duration{Duration: time.Minute},
+ },
+ }
+ spec.Test = &v2.Test{
+ Enable: true,
+ }
+ spec.Uninstall = &v2.Uninstall{
+ KeepHistory: true,
+ }
+ },
+ chart: testutil.BuildChart(testutil.ChartWithFailingTestHook()),
+ expectHistory: func(releases []*helmrelease.Release) v2.Snapshots {
+ snap := observeReleaseWithAction(releases[0], v2.ReleaseActionInstall)
+ snap.SetTestHooks(release.TestHooksFromRelease(releases[0]))
+
+ return v2.Snapshots{
+ snap,
+ }
+ },
+ wantErr: ErrRetryAfterInterval,
+ },
+ {
+ name: "install test failure with test ignore",
+ spec: func(spec *v2.HelmReleaseSpec) {
+ spec.Test = &v2.Test{
+ Enable: true,
+ IgnoreFailures: true,
+ }
+ },
+ chart: testutil.BuildChart(testutil.ChartWithFailingTestHook()),
+ expectHistory: func(releases []*helmrelease.Release) v2.Snapshots {
+ snap := observeReleaseWithAction(releases[0], v2.ReleaseActionInstall)
+ snap.SetTestHooks(release.TestHooksFromRelease(releases[0]))
+
+ return v2.Snapshots{
+ snap,
+ }
+ },
+ },
+ {
+ name: "install with exhausted retries after remediation",
+ status: func(namespace string, releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ return v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: namespace,
+ Version: 1,
+ Chart: testutil.BuildChart(),
+ Status: helmreleasecommon.StatusUninstalling,
+ }),
+ )),
+ },
+ LastAttemptedReleaseAction: v2.ReleaseActionInstall,
+ Failures: 1,
+ InstallFailures: 1,
+ }
+ },
+ wantErr: ErrExceededMaxRetries,
+ },
+ {
+ name: "upgrade failure",
+ releases: func(namespace string) []*helmrelease.Release {
+ return []*helmrelease.Release{
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: namespace,
+ Version: 1,
+ Chart: testutil.BuildChart(),
+ Status: helmreleasecommon.StatusDeployed,
+ }),
+ }
+ },
+ status: func(namespace string, releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ return v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
+ },
+ }
+ },
+ chart: testutil.BuildChart(testutil.ChartWithFailingHook()),
+ expectHistory: func(releases []*helmrelease.Release) v2.Snapshots {
+ return v2.Snapshots{
+ observeReleaseWithAction(releases[1], v2.ReleaseActionUpgrade),
+ release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
+ }
+ },
+ wantErr: ErrExceededMaxRetries,
+ },
+ {
+ name: "upgrade failure with rollback remediation",
+ releases: func(namespace string) []*helmrelease.Release {
+ return []*helmrelease.Release{
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: namespace,
+ Version: 1,
+ Chart: testutil.BuildChart(),
+ Status: helmreleasecommon.StatusDeployed,
+ }),
+ }
+ },
+ spec: func(spec *v2.HelmReleaseSpec) {
+ spec.Upgrade = &v2.Upgrade{
+ Remediation: &v2.UpgradeRemediation{
+ RemediateLastFailure: ptr.To(true),
+ },
+ }
+ },
+ status: func(namespace string, releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ return v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
+ },
+ }
+ },
+ chart: testutil.BuildChart(testutil.ChartWithFailingHook()),
+ expectHistory: func(releases []*helmrelease.Release) v2.Snapshots {
+ return v2.Snapshots{
+ observeReleaseWithAction(releases[2], v2.ReleaseActionRollback),
+ observeReleaseWithAction(releases[1], v2.ReleaseActionUpgrade),
+ observeReleaseWithAction(releases[0], v2.ReleaseActionRollback),
+ }
+ },
+ wantErr: ErrExceededMaxRetries,
+ },
+ {
+ name: "upgrade failure with uninstall remediation",
+ releases: func(namespace string) []*helmrelease.Release {
+ return []*helmrelease.Release{
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: namespace,
+ Version: 1,
+ Chart: testutil.BuildChart(),
+ Status: helmreleasecommon.StatusDeployed,
+ }),
+ }
+ },
+ spec: func(spec *v2.HelmReleaseSpec) {
+ strategy := v2.UninstallRemediationStrategy
+ spec.Upgrade = &v2.Upgrade{
+ Remediation: &v2.UpgradeRemediation{
+ Strategy: &strategy,
+ RemediateLastFailure: ptr.To(true),
+ },
+ }
+ spec.Uninstall = &v2.Uninstall{
+ KeepHistory: true,
+ }
+ },
+ status: func(namespace string, releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ return v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
+ },
+ }
+ },
+ chart: testutil.BuildChart(testutil.ChartWithFailingHook()),
+ expectHistory: func(releases []*helmrelease.Release) v2.Snapshots {
+ return v2.Snapshots{
+ observeReleaseWithAction(releases[1], v2.ReleaseActionUninstallRemediation),
+ release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
+ }
+ },
+ wantErr: ErrMustRequeue,
+ },
+ {
+ name: "upgrade failure with retry",
+ releases: func(namespace string) []*helmrelease.Release {
+ return []*helmrelease.Release{
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: namespace,
+ Version: 1,
+ Chart: testutil.BuildChart(),
+ Status: helmreleasecommon.StatusDeployed,
+ }),
+ }
+ },
+ spec: func(spec *v2.HelmReleaseSpec) {
+ spec.Upgrade = &v2.Upgrade{
+ Strategy: &v2.UpgradeStrategy{
+ Name: "RetryOnFailure",
+ RetryInterval: &metav1.Duration{Duration: time.Minute},
+ },
+ }
+ },
+ status: func(namespace string, releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ return v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
+ },
+ }
+ },
+ chart: testutil.BuildChart(testutil.ChartWithFailingHook()),
+ expectHistory: func(releases []*helmrelease.Release) v2.Snapshots {
+ return v2.Snapshots{
+ observeReleaseWithAction(releases[1], v2.ReleaseActionUpgrade),
+ release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
+ }
+ },
+ wantErr: ErrRetryAfterInterval,
+ },
+ {
+ name: "upgrade test failure with remediation",
+ releases: func(namespace string) []*helmrelease.Release {
+ return []*helmrelease.Release{
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: namespace,
+ Version: 1,
+ Chart: testutil.BuildChart(),
+ Status: helmreleasecommon.StatusDeployed,
+ }),
+ }
+ },
+ spec: func(spec *v2.HelmReleaseSpec) {
+ spec.Upgrade = &v2.Upgrade{
+ Remediation: &v2.UpgradeRemediation{
+ RemediateLastFailure: ptr.To(true),
+ },
+ }
+ spec.Test = &v2.Test{
+ Enable: true,
+ }
+ },
+ status: func(namespace string, releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ return v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
+ },
+ }
+ },
+ chart: testutil.BuildChart(testutil.ChartWithFailingTestHook()),
+ expectHistory: func(releases []*helmrelease.Release) v2.Snapshots {
+ testedSnap := observeReleaseWithAction(releases[1], v2.ReleaseActionRollback)
+ testedSnap.SetTestHooks(release.TestHooksFromRelease(releases[1]))
+
+ return v2.Snapshots{
+ observeReleaseWithAction(releases[2], v2.ReleaseActionRollback),
+ testedSnap,
+ release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
+ }
+ },
+ wantErr: ErrExceededMaxRetries,
+ },
+ {
+ name: "upgrade test failure with retry",
+ releases: func(namespace string) []*helmrelease.Release {
+ return []*helmrelease.Release{
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: namespace,
+ Version: 1,
+ Chart: testutil.BuildChart(),
+ Status: helmreleasecommon.StatusDeployed,
+ }),
+ }
+ },
+ spec: func(spec *v2.HelmReleaseSpec) {
+ spec.Upgrade = &v2.Upgrade{
+ Strategy: &v2.UpgradeStrategy{
+ Name: "RetryOnFailure",
+ RetryInterval: &metav1.Duration{Duration: time.Minute},
+ },
+ }
+ spec.Test = &v2.Test{
+ Enable: true,
+ }
+ },
+ status: func(namespace string, releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ return v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
+ },
+ }
+ },
+ chart: testutil.BuildChart(testutil.ChartWithFailingTestHook()),
+ expectHistory: func(releases []*helmrelease.Release) v2.Snapshots {
+ testedSnap := observeReleaseWithAction(releases[1], v2.ReleaseActionUpgrade)
+ testedSnap.SetTestHooks(release.TestHooksFromRelease(releases[1]))
+
+ return v2.Snapshots{
+ testedSnap,
+ release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
+ }
+ },
+ wantErr: ErrRetryAfterInterval,
+ },
+ {
+ name: "upgrade test failure with test ignore",
+ releases: func(namespace string) []*helmrelease.Release {
+ return []*helmrelease.Release{
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: namespace,
+ Version: 1,
+ Chart: testutil.BuildChart(),
+ Status: helmreleasecommon.StatusDeployed,
+ }),
+ }
+ },
+ spec: func(spec *v2.HelmReleaseSpec) {
+ spec.Test = &v2.Test{
+ Enable: true,
+ IgnoreFailures: true,
+ }
+ },
+ status: func(namespace string, releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ return v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
+ },
+ }
+ },
+ chart: testutil.BuildChart(testutil.ChartWithFailingTestHook()),
+ expectHistory: func(releases []*helmrelease.Release) v2.Snapshots {
+ testedSnap := observeReleaseWithAction(releases[1], v2.ReleaseActionUpgrade)
+ testedSnap.SetTestHooks(release.TestHooksFromRelease(releases[1]))
+
+ return v2.Snapshots{
+ testedSnap,
+ release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
+ }
+ },
+ },
+ {
+ name: "upgrade with exhausted retries after remediation",
+ releases: func(namespace string) []*helmrelease.Release {
+ return []*helmrelease.Release{
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: namespace,
+ Version: 1,
+ Chart: testutil.BuildChart(),
+ Status: helmreleasecommon.StatusSuperseded,
+ }),
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: namespace,
+ Version: 2,
+ Chart: testutil.BuildChart(),
+ Status: helmreleasecommon.StatusSuperseded,
+ }),
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: namespace,
+ Version: 3,
+ Chart: testutil.BuildChart(),
+ Status: helmreleasecommon.StatusDeployed,
+ }),
+ }
+ },
+ status: func(namespace string, releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ return v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(releases[2])),
+ release.ObservedToSnapshot(release.ObserveRelease(releases[1])),
+ release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
+ },
+ LastAttemptedReleaseAction: v2.ReleaseActionUpgrade,
+ Failures: 1,
+ UpgradeFailures: 1,
+ }
+ },
+ chart: testutil.BuildChart(),
+ expectHistory: func(releases []*helmrelease.Release) v2.Snapshots {
+ return v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(releases[2])),
+ release.ObservedToSnapshot(release.ObserveRelease(releases[1])),
+ release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
+ }
+ },
+ wantErr: ErrExceededMaxRetries,
+ },
+ {
+ name: "upgrade remediation results in exhausted retries",
+ releases: func(namespace string) []*helmrelease.Release {
+ return []*helmrelease.Release{
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: namespace,
+ Version: 1,
+ Chart: testutil.BuildChart(),
+ Status: helmreleasecommon.StatusSuperseded,
+ }),
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: namespace,
+ Version: 2,
+ Chart: testutil.BuildChart(),
+ Status: helmreleasecommon.StatusSuperseded,
+ }),
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: namespace,
+ Version: 3,
+ Chart: testutil.BuildChart(),
+ Status: helmreleasecommon.StatusFailed,
+ }),
+ }
+ },
+ spec: func(spec *v2.HelmReleaseSpec) {
+ spec.Upgrade = &v2.Upgrade{
+ Remediation: &v2.UpgradeRemediation{
+ Retries: 1,
+ },
+ }
+ },
+ status: func(namespace string, releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ return v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(releases[2])),
+ release.ObservedToSnapshot(release.ObserveRelease(releases[1])),
+ release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
+ },
+ LastAttemptedReleaseAction: v2.ReleaseActionUpgrade,
+ Failures: 2,
+ UpgradeFailures: 2,
+ }
+ },
+ chart: testutil.BuildChart(),
+ wantErr: ErrExceededMaxRetries,
+ },
+ {
+ name: "upgrade retry does not result in exhausted retries",
+ releases: func(namespace string) []*helmrelease.Release {
+ return []*helmrelease.Release{
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: namespace,
+ Version: 1,
+ Chart: testutil.BuildChart(),
+ Status: helmreleasecommon.StatusSuperseded,
+ }),
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: namespace,
+ Version: 2,
+ Chart: testutil.BuildChart(),
+ Status: helmreleasecommon.StatusSuperseded,
+ }),
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: namespace,
+ Version: 3,
+ Chart: testutil.BuildChart(),
+ Status: helmreleasecommon.StatusFailed,
+ }),
+ }
+ },
+ spec: func(spec *v2.HelmReleaseSpec) {
+ spec.Upgrade = &v2.Upgrade{
+ Strategy: &v2.UpgradeStrategy{
+ Name: "RetryOnFailure",
+ RetryInterval: &metav1.Duration{Duration: time.Minute},
+ },
+ }
+ },
+ status: func(namespace string, releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ return v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(releases[2])),
+ release.ObservedToSnapshot(release.ObserveRelease(releases[1])),
+ release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
+ },
+ LastAttemptedReleaseAction: v2.ReleaseActionUpgrade,
+ Failures: 2,
+ UpgradeFailures: 2,
+ }
+ },
+ chart: testutil.BuildChart(),
+ },
+ }
+ 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
+
+ var releases []*helmrelease.Release
+ if tt.releases != nil {
+ releases = tt.releases(releaseNamespace)
+ releaseutil.SortByRevision(releases)
+ }
+
+ obj := &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: mockReleaseName,
+ Namespace: releaseNamespace,
+ },
+ Spec: v2.HelmReleaseSpec{
+ ReleaseName: mockReleaseName,
+ TargetNamespace: releaseNamespace,
+ StorageNamespace: releaseNamespace,
+ Timeout: &metav1.Duration{Duration: 200 * time.Millisecond},
+ },
+ }
+ if tt.spec != nil {
+ tt.spec(&obj.Spec)
+ }
+ if tt.status != nil {
+ obj.Status = tt.status(releaseNamespace, 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: tt.chart,
+ Values: tt.values,
+ }
+
+ err = NewAtomicRelease(patchHelper, cfg, recorder, testFieldManager, nil, false).Reconcile(context.TODO(), req)
+ wantErr := BeNil()
+ if tt.wantErr != nil {
+ wantErr = MatchError(tt.wantErr)
+ }
+ g.Expect(err).To(wantErr)
+
+ if tt.expectHistory != nil {
+ history, _ := storeHistory(store, mockReleaseName)
+ releaseutil.SortByRevision(history)
+
+ g.Expect(req.Object.Status.History).To(testutil.Equal(tt.expectHistory(history)))
+ }
+ })
+ }
+}
+
+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]any
+ 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: helmreleasecommon.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: helmreleasecommon.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]any{"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: helmreleasecommon.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: helmreleasecommon.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{
+ ObservedGeneration: 2, // Matches obj.Generation to skip the digest check.
+ History: v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
+ },
+ Conditions: []metav1.Condition{
+ {
+ Type: meta.ReadyCondition,
+ Status: metav1.ConditionTrue,
+ ObservedGeneration: 2,
+ },
+ },
+ 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: 200 * 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, nil, false).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))
+ if tt.wantReleaseAction != "" {
+ g.Expect(obj.Status.LastAttemptedReleaseActionDuration).ToNot(BeNil())
+ }
+ })
+ }
+}
+
+func TestAtomicRelease_actionForState(t *testing.T) {
+ tests := []struct {
+ name string
+ releases []*helmrelease.Release
+ annotations map[string]string
+ spec func(spec *v2.HelmReleaseSpec)
+ status func(releases []*helmrelease.Release) v2.HelmReleaseStatus
+ state ReleaseState
+ want ActionReconciler
+ wantEvent *corev1.Event
+ wantErr error
+ assertConditions []metav1.Condition
+ }{
+ {
+ name: "in-sync release does not trigger any action",
+ status: func(releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ return v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ {Version: 1},
+ },
+ }
+ },
+ state: ReleaseState{Status: ReleaseStatusInSync},
+ want: nil,
+ },
+ {
+ name: "in-sync release with force annotation triggers upgrade action",
+ state: ReleaseState{Status: ReleaseStatusInSync},
+ annotations: map[string]string{
+ meta.ReconcileRequestAnnotation: "force",
+ meta.ForceRequestAnnotation: "force",
+ },
+ status: func(releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ return v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ {Version: 1},
+ },
+ }
+ },
+ want: &Upgrade{},
+ },
+ {
+ name: "in-sync release with stale remediated condition",
+ status: func(releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ return v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ {Version: 1},
+ },
+ Conditions: []metav1.Condition{
+ *conditions.FalseCondition(v2.ReleasedCondition, v2.UpgradeFailedReason, "upgrade failed"),
+ *conditions.TrueCondition(v2.RemediatedCondition, v2.RollbackSucceededReason, "rolled back"),
+ },
+ }
+ },
+ state: ReleaseState{Status: ReleaseStatusInSync},
+ want: nil,
+ assertConditions: []metav1.Condition{
+ *conditions.TrueCondition(v2.ReleasedCondition, v2.UpgradeSucceededReason, "upgrade succeeded"),
+ },
+ },
+ {
+ name: "in-sync release with stale ready condition",
+ status: func(releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ return v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ {Version: 1},
+ },
+ Conditions: []metav1.Condition{
+ *conditions.FalseCondition(v2.ReleasedCondition, v2.UpgradeFailedReason, "upgrade failed"),
+ *conditions.FalseCondition(meta.ReadyCondition, v2.UpgradeFailedReason, "upgrade failed"),
+ },
+ }
+ },
+ state: ReleaseState{Status: ReleaseStatusInSync},
+ want: nil,
+ assertConditions: []metav1.Condition{
+ *conditions.TrueCondition(v2.ReleasedCondition, v2.UpgradeSucceededReason, "upgrade succeeded"),
+ *conditions.TrueCondition(meta.ReadyCondition, v2.UpgradeSucceededReason, "upgrade succeeded"),
+ },
+ },
+ {
+ name: "in-sync release with stale install failed condition",
+ status: func(releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ return v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ {Version: 1},
+ },
+ Conditions: []metav1.Condition{
+ *conditions.FalseCondition(v2.ReleasedCondition, v2.InstallFailedReason, "install failed"),
+ *conditions.FalseCondition(meta.ReadyCondition, v2.InstallFailedReason, "install failed"),
+ },
+ }
+ },
+ state: ReleaseState{Status: ReleaseStatusInSync},
+ want: nil,
+ assertConditions: []metav1.Condition{
+ *conditions.TrueCondition(v2.ReleasedCondition, v2.InstallSucceededReason, "install succeeded"),
+ *conditions.TrueCondition(meta.ReadyCondition, v2.InstallSucceededReason, "install succeeded"),
+ },
+ },
+ {
+ name: "locked release triggers unlock action",
+ state: ReleaseState{Status: ReleaseStatusLocked},
+ want: &Unlock{},
+ },
+ {
+ name: "absent release triggers install action",
+ state: ReleaseState{Status: ReleaseStatusAbsent},
+ want: &Install{},
+ },
+ {
+ name: "absent release without remaining retries and force annotation triggers install",
+ annotations: map[string]string{
+ meta.ReconcileRequestAnnotation: "force",
+ meta.ForceRequestAnnotation: "force",
+ },
+ state: ReleaseState{Status: ReleaseStatusAbsent},
+ status: func(releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ return v2.HelmReleaseStatus{
+ InstallFailures: 1,
+ }
+ },
+ want: &Install{},
+ },
+ {
+ name: "absent release without remaining retries returns error",
+ state: ReleaseState{Status: ReleaseStatusAbsent},
+ status: func(releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ return v2.HelmReleaseStatus{
+ InstallFailures: 1,
+ }
+ },
+ wantErr: ErrExceededMaxRetries,
+ },
+ {
+ name: "unmanaged release triggers upgrade",
+ state: ReleaseState{Status: ReleaseStatusUnmanaged},
+ want: &Upgrade{},
+ },
+ {
+ name: "drifted release triggers correction if enabled",
+ state: ReleaseState{Status: ReleaseStatusDrifted, Diff: jsondiff.DiffSet{
+ {
+ Type: jsondiff.DiffTypeCreate,
+ DesiredObject: &unstructured.Unstructured{
+ Object: map[string]any{
+ "apiVersion": "apps/v1",
+ "kind": "Deployment",
+ "metadata": map[string]any{
+ "name": "mock",
+ "namespace": "something",
+ },
+ },
+ },
+ },
+ }},
+ spec: func(spec *v2.HelmReleaseSpec) {
+ spec.DriftDetection = &v2.DriftDetection{
+ Mode: v2.DriftDetectionEnabled,
+ }
+ },
+ status: func(releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ return v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ {
+ Name: mockReleaseName,
+ Namespace: mockReleaseNamespace,
+ Version: 1,
+ },
+ },
+ }
+ },
+ want: &CorrectClusterDrift{},
+ wantEvent: &corev1.Event{
+ Reason: "DriftDetected",
+ Type: corev1.EventTypeWarning,
+ Message: fmt.Sprintf(
+ "Cluster state of release %s has drifted from the desired state:\n%s",
+ mockReleaseNamespace+"/"+mockReleaseName+".v1",
+ "Deployment/something/mock removed",
+ ),
+ },
+ assertConditions: []metav1.Condition{
+ *conditions.TrueCondition(v2.DriftedCondition, v2.DriftDetectedReason, "Cluster state of release mock-ns/mock-release.v1 has drifted from the desired state:\nDeployment/something/mock removed"),
+ },
+ },
+ {
+ name: "drifted release only triggers event if mode is warn",
+ spec: func(spec *v2.HelmReleaseSpec) {
+ spec.DriftDetection = &v2.DriftDetection{
+ Mode: v2.DriftDetectionDisabled,
+ }
+ },
+ status: func(releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ return v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ {
+ Name: mockReleaseName,
+ Namespace: mockReleaseNamespace,
+ Version: 1,
+ },
+ },
+ }
+ },
+ state: ReleaseState{Status: ReleaseStatusDrifted, Diff: jsondiff.DiffSet{
+ {
+ Type: jsondiff.DiffTypeUpdate,
+ DesiredObject: &unstructured.Unstructured{
+ Object: map[string]any{
+ "apiVersion": "apps/v1",
+ "kind": "Deployment",
+ "metadata": map[string]any{
+ "name": "mock",
+ "namespace": "something",
+ },
+ "spec": map[string]any{
+ "replicas": 2,
+ },
+ },
+ },
+ ClusterObject: &unstructured.Unstructured{
+ Object: map[string]any{
+ "apiVersion": "apps/v1",
+ "kind": "Deployment",
+ "metadata": map[string]any{
+ "name": "mock",
+ "namespace": "something",
+ },
+ "spec": map[string]any{
+ "replicas": 1,
+ },
+ },
+ },
+ Patch: extjsondiff.Patch{
+ {
+ Type: extjsondiff.OperationReplace,
+ Path: "/spec/replicas",
+ OldValue: 1,
+ Value: 2,
+ },
+ },
+ },
+ }},
+ want: nil,
+ wantErr: nil,
+ wantEvent: &corev1.Event{
+ Reason: "DriftDetected",
+ Type: corev1.EventTypeWarning,
+ Message: fmt.Sprintf(
+ "Cluster state of release %s has drifted from the desired state:\n%s",
+ mockReleaseNamespace+"/"+mockReleaseName+".v1",
+ "Deployment/something/mock changed (0 additions, 1 changes, 0 removals)",
+ ),
+ },
+ assertConditions: []metav1.Condition{
+ *conditions.TrueCondition(v2.DriftedCondition, v2.DriftDetectedReason, "Cluster state of release mock-ns/mock-release.v1 has drifted from the desired state:\nDeployment/something/mock changed (0 additions, 1 changes, 0 removals)"),
+ },
+ },
+ {
+ name: "out-of-sync release triggers upgrade",
+ state: ReleaseState{
+ Status: ReleaseStatusOutOfSync,
+ },
+ want: &Upgrade{},
+ },
+ {
+ name: "out-of-sync release with no remaining retries and force annotation triggers upgrade",
+ state: ReleaseState{
+ Status: ReleaseStatusOutOfSync,
+ },
+ annotations: map[string]string{
+ meta.ReconcileRequestAnnotation: "force",
+ meta.ForceRequestAnnotation: "force",
+ },
+ status: func(releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ return v2.HelmReleaseStatus{
+ UpgradeFailures: 1,
+ }
+ },
+ want: &Upgrade{},
+ },
+ {
+ name: "out-of-sync release with no remaining retries returns error",
+ state: ReleaseState{
+ Status: ReleaseStatusOutOfSync,
+ },
+ status: func(releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ return v2.HelmReleaseStatus{
+ UpgradeFailures: 1,
+ }
+ },
+ wantErr: ErrExceededMaxRetries,
+ },
+ {
+ name: "untested release triggers test action",
+ state: ReleaseState{Status: ReleaseStatusUntested},
+ want: &Test{},
+ },
+ {
+ name: "untested release with stale remediated condition",
+ status: func(releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ return v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ {Version: 1},
+ },
+ Conditions: []metav1.Condition{
+ *conditions.FalseCondition(v2.ReleasedCondition, v2.UpgradeFailedReason, "upgrade failed"),
+ *conditions.TrueCondition(v2.RemediatedCondition, v2.RollbackSucceededReason, "rolled back"),
+ },
+ }
+ },
+ state: ReleaseState{Status: ReleaseStatusUntested},
+ want: &Test{},
+ assertConditions: []metav1.Condition{
+ *conditions.TrueCondition(v2.ReleasedCondition, v2.UpgradeSucceededReason, "upgrade succeeded"),
+ },
+ },
+ {
+ name: "failed release without active remediation triggers upgrade",
+ state: ReleaseState{Status: ReleaseStatusFailed},
+ status: func(releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ return v2.HelmReleaseStatus{
+ LastAttemptedReleaseAction: "",
+ InstallFailures: 1,
+ }
+ },
+ want: &Upgrade{},
+ },
+ {
+ name: "failed release without failure count triggers upgrade",
+ state: ReleaseState{Status: ReleaseStatusFailed},
+ status: func(releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ return v2.HelmReleaseStatus{
+ LastAttemptedReleaseAction: v2.ReleaseActionUpgrade,
+ UpgradeFailures: 0,
+ }
+ },
+ want: &Upgrade{},
+ },
+ {
+ name: "failed release with exhausted retries and force annotation triggers upgrade",
+ state: ReleaseState{Status: ReleaseStatusFailed},
+ annotations: map[string]string{
+ meta.ReconcileRequestAnnotation: "force",
+ meta.ForceRequestAnnotation: "force",
+ },
+ status: func(releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ return v2.HelmReleaseStatus{
+ LastAttemptedReleaseAction: v2.ReleaseActionUpgrade,
+ UpgradeFailures: 1,
+ }
+ },
+ want: &Upgrade{},
+ },
+ {
+ name: "failed release with exhausted retries returns error",
+ state: ReleaseState{Status: ReleaseStatusFailed},
+ status: func(releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ return v2.HelmReleaseStatus{
+ LastAttemptedReleaseAction: v2.ReleaseActionUpgrade,
+ UpgradeFailures: 1,
+ }
+ },
+ wantErr: ErrExceededMaxRetries,
+ },
+ {
+ name: "failed release with active install remediation triggers uninstall",
+ state: ReleaseState{Status: ReleaseStatusFailed},
+ spec: func(spec *v2.HelmReleaseSpec) {
+ spec.Install = &v2.Install{
+ Remediation: &v2.InstallRemediation{
+ Retries: 3,
+ },
+ }
+ },
+ status: func(releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ return v2.HelmReleaseStatus{
+ LastAttemptedReleaseAction: v2.ReleaseActionInstall,
+ InstallFailures: 2,
+ }
+ },
+ want: &UninstallRemediation{},
+ },
+ {
+ name: "failed release with active install retry triggers upgrade",
+ state: ReleaseState{Status: ReleaseStatusFailed},
+ spec: func(spec *v2.HelmReleaseSpec) {
+ spec.Install = &v2.Install{
+ Strategy: &v2.InstallStrategy{
+ Name: "RetryOnFailure",
+ },
+ }
+ },
+ status: func(releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ return v2.HelmReleaseStatus{
+ LastAttemptedReleaseAction: v2.ReleaseActionInstall,
+ InstallFailures: 2,
+ }
+ },
+ want: &Upgrade{},
+ },
+ {
+ name: "failed release with active upgrade remediation triggers rollback",
+ state: ReleaseState{Status: ReleaseStatusFailed},
+ releases: []*helmrelease.Release{
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: mockReleaseNamespace,
+ Version: 1,
+ Status: helmreleasecommon.StatusSuperseded,
+ Chart: testutil.BuildChart(),
+ }),
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: mockReleaseNamespace,
+ Version: 2,
+ Status: helmreleasecommon.StatusFailed,
+ Chart: testutil.BuildChart(),
+ }),
+ },
+ spec: func(spec *v2.HelmReleaseSpec) {
+ spec.Upgrade = &v2.Upgrade{
+ Remediation: &v2.UpgradeRemediation{
+ Retries: 2,
+ },
+ }
+ },
+ status: func(releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ return v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(releases[1])),
+ release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
+ },
+ LastAttemptedReleaseAction: v2.ReleaseActionUpgrade,
+ UpgradeFailures: 1,
+ }
+ },
+ want: &RollbackRemediation{},
+ },
+ {
+ name: "failed release with active upgrade retry triggers upgrade",
+ state: ReleaseState{Status: ReleaseStatusFailed},
+ releases: []*helmrelease.Release{
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: mockReleaseNamespace,
+ Version: 1,
+ Status: helmreleasecommon.StatusSuperseded,
+ Chart: testutil.BuildChart(),
+ }),
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: mockReleaseNamespace,
+ Version: 2,
+ Status: helmreleasecommon.StatusFailed,
+ Chart: testutil.BuildChart(),
+ }),
+ },
+ spec: func(spec *v2.HelmReleaseSpec) {
+ spec.Upgrade = &v2.Upgrade{
+ Strategy: &v2.UpgradeStrategy{
+ Name: "RetryOnFailure",
+ },
+ }
+ },
+ status: func(releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ return v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(releases[1])),
+ release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
+ },
+ LastAttemptedReleaseAction: v2.ReleaseActionUpgrade,
+ UpgradeFailures: 1,
+ }
+ },
+ want: &Upgrade{},
+ },
+ {
+ name: "failed release with active upgrade remediation and no previous release triggers error",
+ state: ReleaseState{Status: ReleaseStatusFailed},
+ releases: []*helmrelease.Release{
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: mockReleaseNamespace,
+ Version: 2,
+ Status: helmreleasecommon.StatusFailed,
+ Chart: testutil.BuildChart(),
+ }),
+ },
+ spec: func(spec *v2.HelmReleaseSpec) {
+ spec.Upgrade = &v2.Upgrade{
+ Remediation: &v2.UpgradeRemediation{
+ Retries: 2,
+ },
+ }
+ },
+ status: func(releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ return v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
+ },
+ LastAttemptedReleaseAction: v2.ReleaseActionUpgrade,
+ UpgradeFailures: 1,
+ }
+ },
+ wantErr: ErrMissingRollbackTarget,
+ },
+ {
+ name: "failed release with active upgrade remediation and unverified previous triggers upgrade",
+ state: ReleaseState{Status: ReleaseStatusFailed},
+ releases: []*helmrelease.Release{
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: mockReleaseNamespace,
+ Version: 2,
+ Status: helmreleasecommon.StatusFailed,
+ Chart: testutil.BuildChart(),
+ }),
+ },
+ spec: func(spec *v2.HelmReleaseSpec) {
+ spec.Upgrade = &v2.Upgrade{
+ Remediation: &v2.UpgradeRemediation{
+ Retries: 2,
+ },
+ }
+ },
+ status: func(releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ return v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
+ release.ObservedToSnapshot(release.ObserveRelease(
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: mockReleaseNamespace,
+ Version: 1,
+ Status: helmreleasecommon.StatusSuperseded,
+ Chart: testutil.BuildChart(),
+ }),
+ )),
+ },
+ LastAttemptedReleaseAction: v2.ReleaseActionUpgrade,
+ UpgradeFailures: 1,
+ }
+ },
+ want: &Upgrade{},
+ },
+ {
+ name: "unknown remediation strategy returns error",
+ state: ReleaseState{
+ Status: ReleaseStatusFailed,
+ },
+ releases: []*helmrelease.Release{
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: mockReleaseNamespace,
+ Version: 1,
+ Status: helmreleasecommon.StatusSuperseded,
+ Chart: testutil.BuildChart(),
+ }),
+ },
+ spec: func(spec *v2.HelmReleaseSpec) {
+ strategy := v2.RemediationStrategy("invalid")
+ spec.Upgrade = &v2.Upgrade{
+ Remediation: &v2.UpgradeRemediation{
+ Strategy: &strategy,
+ Retries: 2,
+ },
+ }
+ },
+ status: func(releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ return v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
+ },
+ LastAttemptedReleaseAction: v2.ReleaseActionUpgrade,
+ UpgradeFailures: 1,
+ }
+ },
+ wantErr: ErrUnknownRemediationStrategy,
+ },
+ {
+ name: "invalid release status returns error",
+ state: ReleaseState{Status: "invalid"},
+ wantErr: ErrUnknownReleaseStatus,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ g := NewWithT(t)
+
+ obj := &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Annotations: tt.annotations,
+ },
+ Spec: v2.HelmReleaseSpec{
+ ReleaseName: mockReleaseName,
+ TargetNamespace: mockReleaseNamespace,
+ StorageNamespace: mockReleaseNamespace,
+ },
+ }
+ if tt.spec != nil {
+ tt.spec(&obj.Spec)
+ }
+ if tt.status != nil {
+ obj.Status = tt.status(tt.releases)
+ }
+
+ cfg, err := action.NewConfigFactory(&kube.MemoryRESTClientGetter{},
+ action.WithStorage(helmdriver.MemoryDriverName, mockReleaseNamespace),
+ )
+ g.Expect(err).ToNot(HaveOccurred())
+
+ if len(tt.releases) > 0 {
+ store := helmstorage.Init(cfg.Driver)
+ for _, i := range tt.releases {
+ g.Expect(store.Create(i)).To(Succeed())
+ }
+ }
+
+ recorder := testutil.NewFakeRecorder(1, false)
+ r := &AtomicRelease{configFactory: cfg, eventRecorder: recorder}
+ got, err := r.actionForState(context.TODO(), &Request{Object: obj}, tt.state)
+
+ if tt.wantErr != nil {
+ g.Expect(got).To(BeNil())
+ g.Expect(err).To(MatchError(tt.wantErr))
+ return
+ }
+ g.Expect(err).ToNot(HaveOccurred())
+
+ want := BeAssignableToTypeOf(tt.want)
+ if tt.want == nil {
+ want = BeNil()
+ }
+ g.Expect(got).To(want)
+
+ if tt.wantEvent != nil {
+ g.Expect(recorder.GetEvents()).To(ConsistOf([]corev1.Event{*tt.wantEvent}))
+ } else {
+ g.Expect(recorder.GetEvents()).To(BeEmpty())
+ }
+
+ g.Expect(obj.Status.Conditions).To(conditions.MatchConditions(tt.assertConditions))
+ })
+ }
+}
+
+func Test_replaceCondition(t *testing.T) {
+ g := NewWithT(t)
+ timestamp, err := time.Parse(time.UnixDate, "Wed Feb 25 11:06:39 GMT 2015")
+ g.Expect(err).ToNot(HaveOccurred())
+
+ tests := []struct {
+ name string
+ conditions []metav1.Condition
+ target string
+ replacement string
+ wantConditions []metav1.Condition
+ }{
+ {
+ name: "both conditions exist",
+ conditions: []metav1.Condition{
+ {
+ Type: v2.ReleasedCondition,
+ Status: metav1.ConditionFalse,
+ Reason: v2.UpgradeFailedReason,
+ Message: "upgrade failed",
+ ObservedGeneration: 1,
+ LastTransitionTime: metav1.NewTime(timestamp),
+ },
+ {
+ Type: v2.RemediatedCondition,
+ Status: metav1.ConditionTrue,
+ Reason: v2.RollbackSucceededReason,
+ Message: "rollback",
+ ObservedGeneration: 1,
+ LastTransitionTime: metav1.NewTime(timestamp),
+ },
+ },
+ target: v2.RemediatedCondition,
+ replacement: v2.ReleasedCondition,
+ wantConditions: []metav1.Condition{
+ {
+ Type: v2.ReleasedCondition,
+ Status: metav1.ConditionTrue,
+ Reason: v2.UpgradeSucceededReason,
+ Message: "foo",
+ ObservedGeneration: 1,
+ LastTransitionTime: metav1.NewTime(timestamp),
+ },
+ },
+ },
+ {
+ name: "no existing replacement condition",
+ conditions: []metav1.Condition{
+ {
+ Type: v2.RemediatedCondition,
+ Status: metav1.ConditionTrue,
+ Reason: v2.RollbackSucceededReason,
+ Message: "rollback",
+ ObservedGeneration: 1,
+ LastTransitionTime: metav1.NewTime(timestamp),
+ },
+ },
+ target: v2.RemediatedCondition,
+ replacement: v2.ReleasedCondition,
+ wantConditions: []metav1.Condition{
+ {
+ Type: v2.ReleasedCondition,
+ Status: metav1.ConditionTrue,
+ Reason: v2.UpgradeSucceededReason,
+ Message: "foo",
+ ObservedGeneration: 1,
+ LastTransitionTime: metav1.NewTime(timestamp),
+ },
+ },
+ },
+ {
+ name: "no existing target condition",
+ conditions: []metav1.Condition{
+ {
+ Type: v2.ReleasedCondition,
+ Status: metav1.ConditionFalse,
+ Reason: v2.UpgradeFailedReason,
+ Message: "upgrade failed",
+ ObservedGeneration: 1,
+ LastTransitionTime: metav1.NewTime(timestamp),
+ },
+ },
+ target: v2.RemediatedCondition,
+ replacement: v2.ReleasedCondition,
+ wantConditions: []metav1.Condition{
+ {
+ Type: v2.ReleasedCondition,
+ Status: metav1.ConditionFalse,
+ Reason: v2.UpgradeFailedReason,
+ Message: "upgrade failed",
+ ObservedGeneration: 1,
+ LastTransitionTime: metav1.NewTime(timestamp),
+ },
+ },
+ },
+ {
+ name: "no existing target and replacement conditions",
+ target: v2.RemediatedCondition,
+ replacement: v2.ReleasedCondition,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ g := NewWithT(t)
+
+ obj := &v2.HelmRelease{}
+ obj.Generation = 1
+ obj.Status.Conditions = tt.conditions
+ replaceCondition(obj, tt.target, tt.replacement, v2.UpgradeSucceededReason, "foo", metav1.ConditionTrue)
+ g.Expect(obj.Status.Conditions).To(Equal(tt.wantConditions))
+ })
+ }
+}
+
+func TestAtomicRelease_Reconcile_CommonMetadata_Scenarios(t *testing.T) {
+ tests := []struct {
+ name string
+ releases func(namespace string) []*helmrelease.Release
+ spec func(spec *v2.HelmReleaseSpec)
+ values map[string]any
+ status func(releases []*helmrelease.Release) v2.HelmReleaseStatus
+ wantDigest string
+ wantReleaseAction v2.ReleaseAction
+ }{
+ {
+ name: "addition of common metadata",
+ releases: func(namespace string) []*helmrelease.Release {
+ return []*helmrelease.Release{
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: namespace,
+ Version: 1,
+ Status: helmreleasecommon.StatusDeployed,
+ Chart: testutil.BuildChart(),
+ }, testutil.ReleaseWithConfig(nil)),
+ }
+ },
+ spec: func(spec *v2.HelmReleaseSpec) {
+ spec.CommonMetadata = commonMetadata
+ },
+ 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.CommonMetadataDigest(digest.Canonical, commonMetadata).String(),
+ wantReleaseAction: v2.ReleaseActionUpgrade,
+ },
+ {
+ name: "config change and addition of common metadata",
+ releases: func(namespace string) []*helmrelease.Release {
+ return []*helmrelease.Release{
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: namespace,
+ Version: 1,
+ Status: helmreleasecommon.StatusDeployed,
+ Chart: testutil.BuildChart(),
+ }, testutil.ReleaseWithConfig(nil)),
+ }
+ },
+ spec: func(spec *v2.HelmReleaseSpec) {
+ spec.CommonMetadata = commonMetadata
+ },
+ 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]any{"foo": "baz"},
+ wantDigest: postrender.CommonMetadataDigest(digest.Canonical, commonMetadata).String(),
+ wantReleaseAction: v2.ReleaseActionUpgrade,
+ },
+ {
+ name: "existing and new common metadata value",
+ releases: func(namespace string) []*helmrelease.Release {
+ return []*helmrelease.Release{
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: namespace,
+ Version: 1,
+ Status: helmreleasecommon.StatusDeployed,
+ Chart: testutil.BuildChart(),
+ }, testutil.ReleaseWithConfig(nil)),
+ }
+ },
+ spec: func(spec *v2.HelmReleaseSpec) {
+ spec.CommonMetadata = commonMetadata2
+ },
+ 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,
+ },
+ },
+ ObservedCommonMetadataDigest: postrender.CommonMetadataDigest(digest.Canonical, commonMetadata).String(),
+ }
+ },
+ wantDigest: postrender.CommonMetadataDigest(digest.Canonical, commonMetadata2).String(),
+ wantReleaseAction: v2.ReleaseActionUpgrade,
+ },
+ {
+ name: "common metadata 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: helmreleasecommon.StatusDeployed,
+ Chart: testutil.BuildChart(),
+ }, testutil.ReleaseWithConfig(nil)),
+ }
+ },
+ spec: func(spec *v2.HelmReleaseSpec) {
+ spec.CommonMetadata = commonMetadata2
+ },
+ status: func(releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ return v2.HelmReleaseStatus{
+ ObservedGeneration: 2, // Matches obj.Generation to skip the digest check.
+ History: v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
+ },
+ Conditions: []metav1.Condition{
+ {
+ Type: meta.ReadyCondition,
+ Status: metav1.ConditionTrue,
+ ObservedGeneration: 2,
+ },
+ },
+ ObservedPostRenderersDigest: postrender.CommonMetadataDigest(digest.Canonical, commonMetadata).String(),
+ }
+ },
+ wantDigest: postrender.CommonMetadataDigest(digest.Canonical, commonMetadata2).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: 200 * 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, nil, false).Reconcile(context.TODO(), req)
+ g.Expect(err).ToNot(HaveOccurred())
+
+ g.Expect(obj.Status.ObservedCommonMetadataDigest).To(Equal(tt.wantDigest))
+ g.Expect(obj.Status.LastAttemptedReleaseAction).To(Equal(tt.wantReleaseAction))
+ if tt.wantReleaseAction != "" {
+ g.Expect(obj.Status.LastAttemptedReleaseActionDuration).ToNot(BeNil())
+ }
+ })
+ }
+}
diff --git a/internal/reconcile/correct_cluster_drift.go b/internal/reconcile/correct_cluster_drift.go
new file mode 100644
index 000000000..a0cc483eb
--- /dev/null
+++ b/internal/reconcile/correct_cluster_drift.go
@@ -0,0 +1,124 @@
+/*
+Copyright 2023 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package reconcile
+
+import (
+ "context"
+ "strings"
+
+ corev1 "k8s.io/api/core/v1"
+ apierrutil "k8s.io/apimachinery/pkg/util/errors"
+ "k8s.io/client-go/tools/record"
+
+ "github.com/fluxcd/pkg/apis/meta"
+ "github.com/fluxcd/pkg/runtime/conditions"
+ "github.com/fluxcd/pkg/ssa"
+ "github.com/fluxcd/pkg/ssa/jsondiff"
+
+ v2 "github.com/fluxcd/helm-controller/api/v2"
+ "github.com/fluxcd/helm-controller/internal/action"
+)
+
+// CorrectClusterDrift is a reconciler that attempts to correct the cluster state
+// of a Helm release. It does so by applying the Helm release's desired state
+// to the cluster based on a jsondiff.DiffSet.
+//
+// The reconciler will only attempt to correct the cluster state if the Helm
+// release has drift detection enabled and the jsondiff.DiffSet is not empty.
+//
+// The reconciler will emit a Kubernetes event upon completion indicating
+// whether the cluster state was successfully corrected or not.
+type CorrectClusterDrift struct {
+ configFactory *action.ConfigFactory
+ eventRecorder record.EventRecorder
+ diff jsondiff.DiffSet
+ fieldManager string
+}
+
+func NewCorrectClusterDrift(configFactory *action.ConfigFactory, recorder record.EventRecorder, diff jsondiff.DiffSet, fieldManager string) *CorrectClusterDrift {
+ return &CorrectClusterDrift{
+ configFactory: configFactory,
+ eventRecorder: recorder,
+ diff: diff,
+ fieldManager: fieldManager,
+ }
+}
+
+func (r *CorrectClusterDrift) Reconcile(ctx context.Context, req *Request) error {
+ if req.Object.GetDriftDetection().GetMode() != v2.DriftDetectionEnabled || len(r.diff) == 0 {
+ return nil
+ }
+
+ ctx, cancel := context.WithTimeout(ctx, req.Object.GetTimeout().Duration)
+ defer cancel()
+
+ // Update condition to reflect the current status.
+ conditions.MarkUnknown(req.Object, meta.ReadyCondition, meta.ProgressingReason, "correcting cluster drift")
+
+ changeSet, err := action.ApplyDiff(ctx, r.configFactory.Build(nil), r.diff, r.fieldManager)
+ r.report(req.Object, changeSet, err)
+ return nil
+}
+
+func (r *CorrectClusterDrift) report(obj *v2.HelmRelease, changeSet *ssa.ChangeSet, err error) {
+ cur := obj.Status.History.Latest()
+
+ switch {
+ case err != nil:
+ var sb strings.Builder
+ sb.WriteString("Failed to ")
+ if changeSet != nil && len(changeSet.Entries) > 0 {
+ sb.WriteString("partially ")
+ }
+ sb.WriteString("correct cluster state of release ")
+ sb.WriteString(cur.FullReleaseName())
+ sb.WriteString(":\n")
+ if agErr, ok := err.(apierrutil.Aggregate); ok {
+ for i := range agErr.Errors() {
+ if i > 0 {
+ sb.WriteString("\n")
+ }
+ sb.WriteString(agErr.Errors()[i].Error())
+ }
+ } else {
+ sb.WriteString(err.Error())
+ }
+
+ if changeSet != nil && len(changeSet.Entries) > 0 {
+ sb.WriteString("\n\n")
+ sb.WriteString("Successful corrections:\n")
+ sb.WriteString(changeSet.String())
+ }
+
+ r.eventRecorder.AnnotatedEventf(obj, eventMeta(cur.ChartVersion, cur.ConfigDigest,
+ addAppVersion(cur.AppVersion), addOCIDigest(cur.OCIDigest)), corev1.EventTypeWarning,
+ "DriftCorrectionFailed", sb.String())
+ case changeSet != nil && len(changeSet.Entries) > 0:
+ r.eventRecorder.AnnotatedEventf(obj, eventMeta(cur.ChartVersion, cur.ConfigDigest,
+ addAppVersion(cur.AppVersion), addOCIDigest(cur.OCIDigest)), corev1.EventTypeNormal,
+ "DriftCorrected", "Cluster state of release %s has been corrected:\n%s",
+ obj.Status.History.Latest().FullReleaseName(), changeSet.String())
+ }
+}
+
+func (r *CorrectClusterDrift) Name() string {
+ return "correct cluster drift"
+}
+
+func (r *CorrectClusterDrift) Type() ReconcilerType {
+ return ReconcilerTypeDriftCorrection
+}
diff --git a/internal/reconcile/correct_cluster_drift_test.go b/internal/reconcile/correct_cluster_drift_test.go
new file mode 100644
index 000000000..b3d27ac45
--- /dev/null
+++ b/internal/reconcile/correct_cluster_drift_test.go
@@ -0,0 +1,321 @@
+/*
+Copyright 2023 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package reconcile
+
+import (
+ "context"
+ "errors"
+ "testing"
+
+ . "github.com/onsi/gomega"
+ extjsondiff "github.com/wI2L/jsondiff"
+ corev1 "k8s.io/api/core/v1"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+ apierrutil "k8s.io/apimachinery/pkg/util/errors"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+
+ "github.com/fluxcd/pkg/apis/meta"
+ "github.com/fluxcd/pkg/runtime/conditions"
+ "github.com/fluxcd/pkg/ssa"
+ "github.com/fluxcd/pkg/ssa/jsondiff"
+
+ v2 "github.com/fluxcd/helm-controller/api/v2"
+ "github.com/fluxcd/helm-controller/internal/action"
+ "github.com/fluxcd/helm-controller/internal/testutil"
+)
+
+func TestCorrectClusterDrift_Reconcile(t *testing.T) {
+ mockStatus := v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ {
+ Version: 2,
+ Name: mockReleaseName,
+ Namespace: mockReleaseNamespace,
+ },
+ },
+ }
+
+ tests := []struct {
+ name string
+ obj *v2.HelmRelease
+ diff func(namespace string) jsondiff.DiffSet
+ wantEvent bool
+ }{
+ {
+ name: "corrects cluster drift",
+ obj: &v2.HelmRelease{
+ Spec: v2.HelmReleaseSpec{
+ DriftDetection: &v2.DriftDetection{
+ Mode: v2.DriftDetectionEnabled,
+ },
+ },
+ Status: *mockStatus.DeepCopy(),
+ },
+ diff: func(namespace string) jsondiff.DiffSet {
+ return jsondiff.DiffSet{
+ {
+ Type: jsondiff.DiffTypeCreate,
+ DesiredObject: &unstructured.Unstructured{
+ Object: map[string]any{
+ "apiVersion": "v1",
+ "kind": "Secret",
+ "metadata": map[string]any{
+ "name": "secret",
+ "namespace": namespace,
+ },
+ },
+ },
+ },
+ {
+ Type: jsondiff.DiffTypeUpdate,
+ DesiredObject: &unstructured.Unstructured{
+ Object: map[string]any{
+ "apiVersion": "v1",
+ "kind": "ConfigMap",
+ "metadata": map[string]any{
+ "name": "configmap",
+ "namespace": namespace,
+ },
+ "data": map[string]any{
+ "key": "value",
+ },
+ },
+ },
+ ClusterObject: &unstructured.Unstructured{
+ Object: map[string]any{
+ "apiVersion": "v1",
+ "kind": "ConfigMap",
+ "metadata": map[string]any{
+ "name": "configmap",
+ "namespace": namespace,
+ },
+ },
+ },
+ Patch: extjsondiff.Patch{
+ {
+ Type: extjsondiff.OperationAdd,
+ Path: "/data",
+ Value: map[string]any{
+ "key": "value",
+ },
+ },
+ },
+ },
+ }
+ },
+ wantEvent: true,
+ },
+ }
+ 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)
+ })
+
+ diff := tt.diff(namedNS.Name)
+ for _, diff := range diff {
+ if diff.ClusterObject != nil {
+ obj := diff.ClusterObject.DeepCopyObject()
+ g.Expect(testEnv.Create(context.TODO(), obj.(client.Object))).To(Succeed())
+ }
+ }
+
+ getter, err := RESTClientGetterFromManager(testEnv.Manager, namedNS.Name)
+ g.Expect(err).ToNot(HaveOccurred())
+
+ cfg, err := action.NewConfigFactory(getter, action.WithStorage(action.DefaultStorageDriver, namedNS.Name))
+ g.Expect(err).ToNot(HaveOccurred())
+
+ recorder := testutil.NewFakeRecorder(10, false)
+
+ r := NewCorrectClusterDrift(cfg, recorder, tt.diff(namedNS.Name), testFieldManager)
+ g.Expect(r.Reconcile(context.TODO(), &Request{
+ Object: tt.obj,
+ })).ToNot(HaveOccurred())
+
+ if tt.wantEvent {
+ g.Expect(recorder.GetEvents()).To(HaveLen(1))
+ } else {
+ g.Expect(recorder.GetEvents()).To(BeEmpty())
+ }
+
+ g.Expect(tt.obj.Status.Conditions).To(conditions.MatchConditions([]metav1.Condition{
+ *conditions.UnknownCondition(meta.ReadyCondition, meta.ProgressingReason, "correcting cluster drift"),
+ }))
+ })
+ }
+}
+
+func TestCorrectClusterDrift_report(t *testing.T) {
+ mockObj := &v2.HelmRelease{
+ Status: v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ {
+ Version: 3,
+ Name: mockReleaseName,
+ Namespace: mockReleaseNamespace,
+ },
+ },
+ },
+ }
+
+ tests := []struct {
+ name string
+ obj *v2.HelmRelease
+ changeSet *ssa.ChangeSet
+ err error
+ wantEvent []corev1.Event
+ }{
+ {
+ name: "with multiple changes",
+ obj: mockObj.DeepCopy(),
+ changeSet: &ssa.ChangeSet{
+ Entries: []ssa.ChangeSetEntry{
+ {
+ Subject: "Secret/namespace/name",
+ Action: ssa.CreatedAction,
+ },
+ {
+ Subject: "Deployment/namespace/name",
+ Action: ssa.ConfiguredAction,
+ },
+ },
+ },
+ wantEvent: []corev1.Event{
+ {
+ Type: corev1.EventTypeNormal,
+ Reason: "DriftCorrected",
+ Message: `Cluster state of release mock-ns/mock-release.v3 has been corrected:
+Secret/namespace/name created
+Deployment/namespace/name configured`,
+ },
+ },
+ },
+ {
+ name: "with multiple changes and errors",
+ obj: mockObj.DeepCopy(),
+ changeSet: &ssa.ChangeSet{
+ Entries: []ssa.ChangeSetEntry{
+ {
+ Subject: "Secret/namespace/name",
+ Action: ssa.CreatedAction,
+ },
+ {
+ Subject: "Deployment/namespace/name",
+ Action: ssa.ConfiguredAction,
+ },
+ {
+ Subject: "ConfigMap/namespace/name",
+ Action: ssa.ConfiguredAction,
+ },
+ },
+ },
+ err: apierrutil.NewAggregate([]error{
+ errors.New("error 1"),
+ errors.New("error 2"),
+ }),
+ wantEvent: []corev1.Event{
+ {
+ Type: corev1.EventTypeWarning,
+ Reason: "DriftCorrectionFailed",
+ Message: `Failed to partially correct cluster state of release mock-ns/mock-release.v3:
+error 1
+error 2
+
+Successful corrections:
+Secret/namespace/name created
+Deployment/namespace/name configured
+ConfigMap/namespace/name configured`,
+ },
+ },
+ },
+ {
+ name: "with multiple errors",
+ obj: mockObj.DeepCopy(),
+ err: apierrutil.NewAggregate([]error{
+ errors.New("error 1"),
+ errors.New("error 2"),
+ }),
+ wantEvent: []corev1.Event{
+ {
+ Type: corev1.EventTypeWarning,
+ Reason: "DriftCorrectionFailed",
+ Message: `Failed to correct cluster state of release mock-ns/mock-release.v3:
+error 1
+error 2`,
+ },
+ },
+ },
+ {
+ name: "with single change",
+ obj: mockObj.DeepCopy(),
+ changeSet: &ssa.ChangeSet{
+ Entries: []ssa.ChangeSetEntry{
+ {
+ Subject: "Secret/namespace/name",
+ Action: ssa.CreatedAction,
+ },
+ },
+ },
+ wantEvent: []corev1.Event{
+ {
+ Type: corev1.EventTypeNormal,
+ Reason: "DriftCorrected",
+ Message: `Cluster state of release mock-ns/mock-release.v3 has been corrected:
+Secret/namespace/name created`,
+ },
+ },
+ },
+ {
+ name: "with single error",
+ obj: mockObj.DeepCopy(),
+ err: errors.New("error 1"),
+ wantEvent: []corev1.Event{
+ {
+ Type: corev1.EventTypeWarning,
+ Reason: "DriftCorrectionFailed",
+ Message: `Failed to correct cluster state of release mock-ns/mock-release.v3:
+error 1`,
+ },
+ },
+ },
+ {
+ name: "empty change set",
+ obj: mockObj.DeepCopy(),
+ wantEvent: []corev1.Event{},
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ g := NewWithT(t)
+
+ recorder := testutil.NewFakeRecorder(10, false)
+ r := &CorrectClusterDrift{
+ eventRecorder: recorder,
+ }
+
+ r.report(tt.obj, tt.changeSet, tt.err)
+ g.Expect(recorder.GetEvents()).To(ConsistOf(tt.wantEvent))
+ })
+ }
+}
diff --git a/internal/reconcile/helmchart_template.go b/internal/reconcile/helmchart_template.go
new file mode 100644
index 000000000..b58515a94
--- /dev/null
+++ b/internal/reconcile/helmchart_template.go
@@ -0,0 +1,258 @@
+/*
+Copyright 2023 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package reconcile
+
+import (
+ "context"
+ "fmt"
+
+ apierrors "k8s.io/apimachinery/pkg/api/errors"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+ "k8s.io/apimachinery/pkg/runtime"
+ "k8s.io/apimachinery/pkg/types"
+ "k8s.io/client-go/tools/record"
+ ctrl "sigs.k8s.io/controller-runtime"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+ "sigs.k8s.io/controller-runtime/pkg/client/apiutil"
+
+ eventv1 "github.com/fluxcd/pkg/apis/event/v1beta1"
+ "github.com/fluxcd/pkg/ssa"
+ sourcev1 "github.com/fluxcd/source-controller/api/v1"
+
+ v2 "github.com/fluxcd/helm-controller/api/v2"
+ "github.com/fluxcd/helm-controller/internal/acl"
+ "github.com/fluxcd/helm-controller/internal/strings"
+)
+
+// HelmChartTemplate attempts to create, update or delete a v1.HelmChart
+// based on the given Request data.
+//
+// It does this by building a v1.HelmChart from the template declared in
+// the v2.HelmRelease, and then reconciling that v1.HelmChart using
+// a server-side apply.
+//
+// When the server-side apply succeeds, the namespaced name of the chart is
+// written to the Status.HelmChart field of the v2.HelmRelease. If the
+// server-side apply fails, the error is returned to the caller and indicates
+// they should retry.
+//
+// When at the beginning of the reconciliation the deletion timestamp is set
+// on the v2.HelmRelease, or the Status.HelmChart differs from the
+// namespaced name of the chart to be applied, the existing chart is deleted.
+// The deletion is observed, and when it completes, the Status.HelmChart is
+// cleared. If the deletion fails, the error is returned to the caller and
+// indicates they should retry.
+//
+// In case the v2.HelmRelease is marked for deletion, the reconciler will
+// not continue to attempt to create or update the v1.HelmChart.
+type HelmChartTemplate struct {
+ client client.Client
+ eventRecorder record.EventRecorder
+ fieldManager string
+}
+
+// NewHelmChartTemplate returns a new HelmChartTemplate reconciler configured
+// with the provided values.
+func NewHelmChartTemplate(client client.Client, recorder record.EventRecorder, fieldManager string) *HelmChartTemplate {
+ return &HelmChartTemplate{
+ client: client,
+ eventRecorder: recorder,
+ fieldManager: fieldManager,
+ }
+}
+
+func (r *HelmChartTemplate) Reconcile(ctx context.Context, req *Request) error {
+ var (
+ obj = req.Object
+ chartRef = types.NamespacedName{}
+ )
+
+ if obj.Spec.Chart != nil {
+ chartRef.Name = obj.GetHelmChartName()
+ chartRef.Namespace = obj.Spec.Chart.GetNamespace(obj.Namespace)
+ }
+
+ // The HelmChart name and/or namespace diverges or the HelmRelease is
+ // being deleted, delete the HelmChart.
+ if (obj.Status.HelmChart != "" && obj.Status.HelmChart != chartRef.String()) || !obj.DeletionTimestamp.IsZero() {
+ // If the HelmRelease is being deleted, we need to short-circuit to
+ // avoid recreating the HelmChart.
+ if err := r.reconcileDelete(ctx, req.Object); err != nil || !obj.DeletionTimestamp.IsZero() {
+ return err
+ }
+ }
+
+ if mustCleanDeployedChart(obj) {
+ // If the HelmRelease has a ChartRef and no Chart template, and the
+ // HelmChart is present, we need to clean it up.
+ if err := r.reconcileDelete(ctx, req.Object); err != nil {
+ return err
+ }
+ return nil
+ }
+
+ if obj.HasChartRef() {
+ // if a chartRef is present, we do not need to reconcile the HelmChart from the template.
+ return nil
+ }
+
+ // Confirm we are allowed to fetch the HelmChart.
+ if err := acl.AllowsAccessTo(req.Object, sourcev1.HelmChartKind, chartRef); err != nil {
+ return err
+ }
+
+ // Build new HelmChart based on the declared template.
+ newChart := buildHelmChartFromTemplate(req.Object)
+
+ // Convert to an unstructured object to please the SSA library.
+ uo, err := runtime.DefaultUnstructuredConverter.ToUnstructured(newChart.DeepCopy())
+ if err != nil {
+ return fmt.Errorf("failed to convert HelmChart to unstructured: %w", err)
+ }
+ u := &unstructured.Unstructured{Object: uo}
+
+ // Get the GVK for the object according to the current scheme.
+ gvk, err := apiutil.GVKForObject(newChart, r.client.Scheme())
+ if err != nil {
+ return fmt.Errorf("unable to get GVK for HelmChart: %w", err)
+ }
+ u.SetGroupVersionKind(gvk)
+
+ rm := ssa.NewResourceManager(r.client, nil, ssa.Owner{
+ Group: v2.GroupVersion.Group,
+ Field: r.fieldManager,
+ })
+
+ // Mark the object as owned by the HelmRelease.
+ rm.SetOwnerLabels([]*unstructured.Unstructured{u}, obj.GetName(), obj.GetNamespace())
+
+ // Run using server-side apply.
+ entry, err := rm.Apply(ctx, u, ssa.DefaultApplyOptions())
+ if err != nil {
+ err = fmt.Errorf("failed to run server-side apply: %w", err)
+ r.eventRecorder.Eventf(req.Object, eventv1.EventTypeTrace, "HelmChartSyncErr", err.Error())
+ return err
+ }
+
+ // Consult the entry result and act accordingly.
+ switch entry.Action {
+ case ssa.CreatedAction, ssa.ConfiguredAction:
+ msg := strings.Normalize(fmt.Sprintf(
+ "%s %s with SourceRef '%s/%s/%s'", entry.Action.String(), entry.Subject,
+ newChart.Spec.SourceRef.Kind, newChart.GetNamespace(), newChart.Spec.SourceRef.Name,
+ ))
+
+ ctrl.LoggerFrom(ctx).Info(msg)
+ r.eventRecorder.Eventf(req.Object, eventv1.EventTypeTrace,
+ fmt.Sprintf("HelmChart%s", strings.Title(entry.Action.String())), msg)
+ case ssa.UnchangedAction:
+ msg := fmt.Sprintf("%s with SourceRef '%s/%s/%s' is in-sync", entry.Subject,
+ newChart.Spec.SourceRef.Kind, newChart.GetNamespace(), newChart.Spec.SourceRef.Name)
+
+ ctrl.LoggerFrom(ctx).Info(msg)
+ default:
+ err = fmt.Errorf("unexpected action '%s' for %s", entry.Action.String(), entry.Subject)
+ return err
+ }
+
+ // From this moment on, we know the HelmChart spec is up-to-date.
+ obj.Status.HelmChart = chartRef.String()
+
+ return nil
+}
+
+// reconcileDelete handles the garbage collection of the current HelmChart in
+// the Status object of the given HelmRelease.
+func (r *HelmChartTemplate) reconcileDelete(ctx context.Context, obj *v2.HelmRelease) error {
+ if !obj.Spec.Suspend && obj.Status.HelmChart != "" {
+ ns, name := obj.Status.GetHelmChart()
+ namespacedName := types.NamespacedName{Namespace: ns, Name: name}
+
+ // Confirm we are allowed to fetch the HelmChart.
+ if err := acl.AllowsAccessTo(obj, sourcev1.HelmChartKind, namespacedName); err != nil {
+ return err
+ }
+
+ // Fetch the HelmChart.
+ var chart sourcev1.HelmChart
+ err := r.client.Get(ctx, namespacedName, &chart)
+ if err != nil && !apierrors.IsNotFound(err) {
+ // Return error to retry until we succeed.
+ err = fmt.Errorf("failed to delete HelmChart '%s': %w", obj.Status.HelmChart, err)
+ return err
+ }
+ if err == nil {
+ // Delete the HelmChart.
+ if err = r.client.Delete(ctx, &chart); err != nil {
+ err = fmt.Errorf("failed to delete HelmChart '%s': %w", obj.Status.HelmChart, err)
+ return err
+ }
+ r.eventRecorder.Eventf(obj, eventv1.EventTypeTrace, "HelmChartDeleted", "deleted HelmChart '%s'", obj.Status.HelmChart)
+ }
+
+ // Truncate the chart reference in the status object.
+ obj.Status.HelmChart = ""
+ }
+
+ return nil
+}
+
+// buildHelmChartFromTemplate builds a v1.HelmChart from the
+// v2.HelmChartTemplate of the given v2.HelmRelease.
+func buildHelmChartFromTemplate(obj *v2.HelmRelease) *sourcev1.HelmChart {
+ template := obj.Spec.Chart.DeepCopy()
+ result := &sourcev1.HelmChart{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: obj.GetHelmChartName(),
+ Namespace: template.GetNamespace(obj.Namespace),
+ },
+ Spec: sourcev1.HelmChartSpec{
+ Chart: template.Spec.Chart,
+ Version: template.Spec.Version,
+ SourceRef: sourcev1.LocalHelmChartSourceReference{
+ Name: template.Spec.SourceRef.Name,
+ Kind: template.Spec.SourceRef.Kind,
+ },
+ Interval: template.GetInterval(obj.Spec.Interval),
+ ReconcileStrategy: template.Spec.ReconcileStrategy,
+ ValuesFiles: template.Spec.ValuesFiles,
+ IgnoreMissingValuesFiles: template.Spec.IgnoreMissingValuesFiles,
+ },
+ }
+ if verifyTpl := template.Spec.Verify; verifyTpl != nil {
+ result.Spec.Verify = &sourcev1.OCIRepositoryVerification{
+ Provider: verifyTpl.Provider,
+ SecretRef: verifyTpl.SecretRef,
+ }
+ }
+ if metaTpl := template.ObjectMeta; metaTpl != nil {
+ result.SetAnnotations(metaTpl.Annotations)
+ result.SetLabels(metaTpl.Labels)
+ }
+ return result
+}
+
+func mustCleanDeployedChart(obj *v2.HelmRelease) bool {
+ if obj.HasChartRef() && !obj.HasChartTemplate() {
+ if obj.Status.HelmChart != "" {
+ return true
+ }
+ }
+
+ return false
+}
diff --git a/internal/reconcile/helmchart_template_test.go b/internal/reconcile/helmchart_template_test.go
new file mode 100644
index 000000000..5cbb894e0
--- /dev/null
+++ b/internal/reconcile/helmchart_template_test.go
@@ -0,0 +1,826 @@
+/*
+Copyright 2023 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package reconcile
+
+import (
+ "context"
+ "fmt"
+ "testing"
+ "time"
+
+ . "github.com/onsi/gomega"
+ corev1 "k8s.io/api/core/v1"
+ apierrors "k8s.io/apimachinery/pkg/api/errors"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/types"
+ "k8s.io/client-go/tools/record"
+ "sigs.k8s.io/controller-runtime/pkg/client/fake"
+
+ "github.com/fluxcd/pkg/apis/meta"
+ sourcev1 "github.com/fluxcd/source-controller/api/v1"
+
+ v2 "github.com/fluxcd/helm-controller/api/v2"
+ "github.com/fluxcd/helm-controller/internal/acl"
+)
+
+func TestHelmChartTemplate_Reconcile(t *testing.T) {
+ g := NewWithT(t)
+
+ namespace := corev1.Namespace{
+ ObjectMeta: metav1.ObjectMeta{
+ GenerateName: "helm-release-chart-reconciler-",
+ },
+ }
+ g.Expect(testEnv.CreateAndWait(context.Background(), &namespace)).To(Succeed())
+ t.Cleanup(func() {
+ g.Expect(testEnv.Cleanup(context.Background(), &namespace)).To(Succeed())
+ })
+
+ t.Run("DeletionTimestamp triggers delete", func(t *testing.T) {
+ g := NewWithT(t)
+
+ releaseName := "deletion-timestamp"
+ existingChart := sourcev1.HelmChart{
+ ObjectMeta: metav1.ObjectMeta{
+ Namespace: namespace.GetName(),
+ Name: fmt.Sprintf("%s-%s", namespace.GetName(), releaseName),
+ Labels: map[string]string{
+ v2.GroupVersion.Group + "/name": releaseName,
+ v2.GroupVersion.Group + "/namespace": namespace.GetName(),
+ },
+ },
+ Spec: sourcev1.HelmChartSpec{
+ Interval: metav1.Duration{Duration: 1 * time.Hour},
+ Chart: "foo",
+ SourceRef: sourcev1.LocalHelmChartSourceReference{
+ Kind: sourcev1.HelmRepositoryKind,
+ Name: "foo-repository",
+ },
+ },
+ }
+ g.Expect(testEnv.CreateAndWait(context.Background(), &existingChart)).To(Succeed())
+ t.Cleanup(func() {
+ g.Expect(testEnv.Cleanup(context.Background(), &existingChart)).To(Succeed())
+ })
+
+ recorder := record.NewFakeRecorder(32)
+ r := &HelmChartTemplate{
+ client: testEnv,
+ eventRecorder: recorder,
+ fieldManager: testFieldManager,
+ }
+
+ ts := metav1.Now()
+ obj := &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Namespace: namespace.GetName(),
+ Name: releaseName,
+ DeletionTimestamp: &ts,
+ },
+ Status: v2.HelmReleaseStatus{
+ HelmChart: fmt.Sprintf("%s/%s", existingChart.GetNamespace(), existingChart.GetName()),
+ },
+ }
+
+ err := r.Reconcile(context.TODO(), &Request{Object: obj})
+ g.Expect(err).ToNot(HaveOccurred())
+ g.Expect(obj.Status.HelmChart).To(BeEmpty())
+
+ g.Eventually(func(g Gomega) {
+ g.Expect(apierrors.IsNotFound(testEnv.Get(context.TODO(),
+ types.NamespacedName{
+ Namespace: existingChart.GetNamespace(),
+ Name: existingChart.GetName(),
+ },
+ &existingChart,
+ ))).To(BeTrue())
+ }).Should(Succeed())
+ })
+
+ t.Run("Status.HelmChart divergence triggers delete and creates chart", func(t *testing.T) {
+ g := NewWithT(t)
+
+ existingChart := sourcev1.HelmChart{
+ ObjectMeta: metav1.ObjectMeta{
+ Namespace: namespace.GetName(),
+ GenerateName: "existing-chart-",
+ },
+ Spec: sourcev1.HelmChartSpec{
+ SourceRef: sourcev1.LocalHelmChartSourceReference{
+ Kind: sourcev1.HelmRepositoryKind,
+ Name: "mock",
+ },
+ },
+ }
+ g.Expect(testEnv.CreateAndWait(context.TODO(), &existingChart)).To(Succeed())
+ t.Cleanup(func() {
+ g.Expect(testEnv.Cleanup(context.Background(), &existingChart)).To(Succeed())
+ })
+
+ r := &HelmChartTemplate{
+ client: testEnv,
+ eventRecorder: record.NewFakeRecorder(32),
+ fieldManager: testFieldManager,
+ }
+
+ obj := &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Namespace: namespace.GetName(),
+ Name: "release-with-existing-chart",
+ },
+ Spec: v2.HelmReleaseSpec{
+ Chart: &v2.HelmChartTemplate{
+ Spec: v2.HelmChartTemplateSpec{
+ Chart: "foo",
+ SourceRef: v2.CrossNamespaceObjectReference{
+ Kind: sourcev1.HelmRepositoryKind,
+ Name: "foo-repository",
+ },
+ },
+ },
+ },
+ Status: v2.HelmReleaseStatus{
+ HelmChart: fmt.Sprintf("%s/%s", existingChart.GetNamespace(), existingChart.GetName()),
+ },
+ }
+
+ err := r.Reconcile(context.TODO(), &Request{Object: obj})
+ g.Expect(err).ToNot(HaveOccurred())
+ g.Expect(obj.Status.HelmChart).To(Equal(
+ fmt.Sprintf("%s/%s", namespace.GetName(), namespace.GetName()+"-"+obj.GetName()),
+ ))
+
+ g.Eventually(func(g Gomega) {
+ g.Expect(apierrors.IsNotFound(testEnv.Get(context.TODO(),
+ types.NamespacedName{
+ Namespace: existingChart.GetNamespace(),
+ Name: existingChart.GetName(),
+ },
+ &existingChart,
+ ))).To(BeTrue())
+ }).Should(Succeed())
+ })
+
+ t.Run("HelmChart NotFound creates HelmChart", func(t *testing.T) {
+ g := NewWithT(t)
+
+ recorder := record.NewFakeRecorder(32)
+ r := &HelmChartTemplate{
+ client: testEnv,
+ eventRecorder: recorder,
+ fieldManager: testFieldManager,
+ }
+
+ releaseName := "not-found"
+ obj := &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Namespace: namespace.GetName(),
+ Name: releaseName,
+ },
+ Spec: v2.HelmReleaseSpec{
+ Interval: metav1.Duration{Duration: 1 * time.Hour},
+ Chart: &v2.HelmChartTemplate{
+ Spec: v2.HelmChartTemplateSpec{
+ SourceRef: v2.CrossNamespaceObjectReference{
+ Kind: sourcev1.HelmRepositoryKind,
+ Name: "mock",
+ },
+ },
+ },
+ },
+ Status: v2.HelmReleaseStatus{
+ HelmChart: fmt.Sprintf("%s/%s", namespace.GetName(), namespace.GetName()+"-"+releaseName),
+ },
+ }
+ err := r.Reconcile(context.TODO(), &Request{Object: obj})
+ g.Expect(err).ToNot(HaveOccurred())
+ g.Expect(obj.Status.HelmChart).ToNot(BeEmpty())
+
+ expectChart := sourcev1.HelmChart{}
+ g.Eventually(func(g Gomega) {
+ g.Expect(testEnv.Get(context.TODO(), types.NamespacedName{
+ Namespace: obj.Spec.Chart.GetNamespace(obj.Namespace),
+ Name: obj.GetHelmChartName()},
+ &expectChart,
+ )).To(Succeed())
+ }).Should(Succeed())
+
+ t.Cleanup(func() {
+ g.Expect(testEnv.Cleanup(context.Background(), &expectChart)).To(Succeed())
+ })
+ })
+
+ t.Run("Spec divergence updates HelmChart", func(t *testing.T) {
+ g := NewWithT(t)
+
+ releaseName := "divergence"
+ existingChart := sourcev1.HelmChart{
+ ObjectMeta: metav1.ObjectMeta{
+ Namespace: namespace.GetName(),
+ Name: fmt.Sprintf("%s-%s", namespace.GetName(), releaseName),
+ Labels: map[string]string{
+ v2.GroupVersion.Group + "/name": releaseName,
+ v2.GroupVersion.Group + "/namespace": namespace.GetName(),
+ },
+ },
+ Spec: sourcev1.HelmChartSpec{
+ Chart: "./bar",
+ SourceRef: sourcev1.LocalHelmChartSourceReference{
+ Kind: sourcev1.HelmRepositoryKind,
+ Name: "bar-repository",
+ },
+ },
+ }
+ g.Expect(testEnv.CreateAndWait(context.TODO(), &existingChart)).To(Succeed())
+ t.Cleanup(func() {
+ g.Expect(testEnv.Cleanup(context.Background(), &existingChart)).To(Succeed())
+ })
+
+ recorder := record.NewFakeRecorder(32)
+ r := &HelmChartTemplate{
+ client: testEnv,
+ eventRecorder: recorder,
+ fieldManager: testFieldManager,
+ }
+
+ obj := &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Namespace: namespace.GetName(),
+ Name: releaseName,
+ },
+ Spec: v2.HelmReleaseSpec{
+ Interval: metav1.Duration{Duration: 1 * time.Hour},
+ Chart: &v2.HelmChartTemplate{
+ Spec: v2.HelmChartTemplateSpec{
+ Chart: "foo",
+ SourceRef: v2.CrossNamespaceObjectReference{
+ Kind: sourcev1.HelmRepositoryKind,
+ Name: "foo-repository",
+ },
+ },
+ },
+ },
+ Status: v2.HelmReleaseStatus{
+ HelmChart: fmt.Sprintf("%s/%s", existingChart.GetNamespace(), existingChart.GetName()),
+ },
+ }
+ err := r.Reconcile(context.TODO(), &Request{Object: obj})
+ g.Expect(err).ToNot(HaveOccurred())
+ g.Expect(obj.Status.HelmChart).ToNot(BeEmpty())
+
+ newChart := sourcev1.HelmChart{}
+ g.Eventually(func(g Gomega) {
+ g.Expect(testEnv.Get(context.TODO(), types.NamespacedName{
+ Namespace: obj.Spec.Chart.GetNamespace(obj.Namespace),
+ Name: obj.GetHelmChartName()}, &newChart)).To(Succeed())
+
+ g.Expect(newChart.Spec.Chart).To(Equal(obj.Spec.Chart.Spec.Chart))
+ g.Expect(newChart.Spec.SourceRef.Name).To(Equal(obj.Spec.Chart.Spec.SourceRef.Name))
+ g.Expect(newChart.Spec.SourceRef.Kind).To(Equal(obj.Spec.Chart.Spec.SourceRef.Kind))
+ }).Should(Succeed())
+ })
+
+ t.Run("no HelmChart divergence", func(t *testing.T) {
+ g := NewWithT(t)
+
+ releaseName := "no-divergence"
+ existingChart := &sourcev1.HelmChart{
+ ObjectMeta: metav1.ObjectMeta{
+ Namespace: namespace.GetName(),
+ Name: fmt.Sprintf("%s-%s", namespace.GetName(), releaseName),
+ Labels: map[string]string{
+ v2.GroupVersion.Group + "/name": releaseName,
+ v2.GroupVersion.Group + "/namespace": namespace.GetName(),
+ },
+ },
+ Spec: sourcev1.HelmChartSpec{
+ Interval: metav1.Duration{Duration: 1 * time.Hour},
+ Chart: "foo",
+ SourceRef: sourcev1.LocalHelmChartSourceReference{
+ Kind: sourcev1.HelmRepositoryKind,
+ Name: "foo-repository",
+ },
+ },
+ }
+ g.Expect(testEnv.CreateAndWait(context.Background(), existingChart)).To(Succeed())
+ t.Cleanup(func() {
+ g.Expect(testEnv.Cleanup(context.Background(), existingChart)).To(Succeed())
+ })
+
+ recorder := record.NewFakeRecorder(32)
+ r := &HelmChartTemplate{
+ client: testEnv,
+ eventRecorder: recorder,
+ fieldManager: testFieldManager,
+ }
+
+ obj := &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Namespace: namespace.GetName(),
+ Name: releaseName,
+ },
+ Spec: v2.HelmReleaseSpec{
+ Interval: existingChart.Spec.Interval,
+ Chart: &v2.HelmChartTemplate{
+ Spec: v2.HelmChartTemplateSpec{
+ Chart: existingChart.Spec.Chart,
+ SourceRef: v2.CrossNamespaceObjectReference{
+ Kind: existingChart.Spec.SourceRef.Kind,
+ Name: existingChart.Spec.SourceRef.Name,
+ },
+ },
+ },
+ },
+ Status: v2.HelmReleaseStatus{
+ HelmChart: fmt.Sprintf("%s/%s", existingChart.GetNamespace(), existingChart.GetName()),
+ },
+ }
+
+ err := r.Reconcile(context.TODO(), &Request{Object: obj})
+ g.Expect(err).ToNot(HaveOccurred())
+ g.Expect(obj.Status.HelmChart).ToNot(BeEmpty())
+
+ newChart := sourcev1.HelmChart{}
+ g.Expect(testEnv.Get(context.TODO(), types.NamespacedName{
+ Namespace: obj.Spec.Chart.GetNamespace(obj.Namespace),
+ Name: obj.GetHelmChartName()}, &newChart)).To(Succeed())
+ g.Expect(newChart.ResourceVersion).To(Equal(existingChart.ResourceVersion), "HelmChart should not have been updated")
+ })
+
+ t.Run("sets owner labels on HelmChart", func(t *testing.T) {
+ g := NewWithT(t)
+
+ recorder := record.NewFakeRecorder(32)
+ r := &HelmChartTemplate{
+ client: testEnv,
+ eventRecorder: recorder,
+ fieldManager: testFieldManager,
+ }
+
+ releaseName := "owner-labels"
+ obj := &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Namespace: namespace.GetName(),
+ Name: releaseName,
+ },
+ Spec: v2.HelmReleaseSpec{
+ Interval: metav1.Duration{Duration: 1 * time.Hour},
+ Chart: &v2.HelmChartTemplate{
+ Spec: v2.HelmChartTemplateSpec{
+ SourceRef: v2.CrossNamespaceObjectReference{
+ Kind: sourcev1.HelmRepositoryKind,
+ Name: "mock",
+ },
+ },
+ },
+ },
+ Status: v2.HelmReleaseStatus{
+ HelmChart: fmt.Sprintf("%s/%s", namespace.GetName(), namespace.GetName()+"-"+releaseName),
+ },
+ }
+ err := r.Reconcile(context.TODO(), &Request{Object: obj})
+ g.Expect(err).ToNot(HaveOccurred())
+ g.Expect(obj.Status.HelmChart).ToNot(BeEmpty())
+
+ expectChart := sourcev1.HelmChart{}
+ g.Eventually(func(g Gomega) {
+ g.Expect(r.client.Get(context.TODO(), types.NamespacedName{
+ Namespace: obj.Spec.Chart.GetNamespace(obj.Namespace),
+ Name: obj.GetHelmChartName()},
+ &expectChart,
+ )).To(Succeed())
+ g.Expect(testEnv.Cleanup(context.Background(), &expectChart)).To(Succeed())
+
+ g.Expect(expectChart.GetLabels()).To(HaveKeyWithValue(v2.GroupVersion.Group+"/name", obj.GetName()))
+ g.Expect(expectChart.GetLabels()).To(HaveKeyWithValue(v2.GroupVersion.Group+"/namespace", obj.GetNamespace()))
+ }).Should(Succeed())
+ })
+
+ t.Run("cross namespace disallow is respected", func(t *testing.T) {
+ g := NewWithT(t)
+
+ r := &HelmChartTemplate{
+ client: fake.NewClientBuilder().WithScheme(NewTestScheme()).Build(),
+ }
+
+ obj := &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "release",
+ Namespace: "default",
+ },
+ Spec: v2.HelmReleaseSpec{
+ Chart: &v2.HelmChartTemplate{
+ Spec: v2.HelmChartTemplateSpec{
+ SourceRef: v2.CrossNamespaceObjectReference{
+ Name: "chart",
+ Namespace: "other",
+ },
+ },
+ },
+ },
+ Status: v2.HelmReleaseStatus{},
+ }
+ err := r.Reconcile(context.TODO(), &Request{Object: obj})
+ g.Expect(err).To(HaveOccurred())
+ g.Expect(obj.Status.HelmChart).To(BeEmpty())
+
+ err = r.client.Get(context.TODO(), types.NamespacedName{Namespace: "other", Name: "chart"}, &sourcev1.HelmChart{})
+ g.Expect(err).To(HaveOccurred())
+ g.Expect(apierrors.IsNotFound(err)).To(BeTrue())
+ })
+
+ t.Run("Spec ChartRef and existing chart trigger delete", func(t *testing.T) {
+ g := NewWithT(t)
+
+ releaseName := "garbage-collection"
+ existingChart := sourcev1.HelmChart{
+ ObjectMeta: metav1.ObjectMeta{
+ Namespace: namespace.GetName(),
+ Name: fmt.Sprintf("%s-%s", namespace.GetName(), releaseName),
+ Labels: map[string]string{
+ v2.GroupVersion.Group + "/name": releaseName,
+ v2.GroupVersion.Group + "/namespace": namespace.GetName(),
+ },
+ },
+ Spec: sourcev1.HelmChartSpec{
+ Chart: "./bar",
+ SourceRef: sourcev1.LocalHelmChartSourceReference{
+ Kind: sourcev1.HelmRepositoryKind,
+ Name: "bar-repository",
+ },
+ },
+ }
+ g.Expect(testEnv.CreateAndWait(context.TODO(), &existingChart)).To(Succeed())
+ t.Cleanup(func() {
+ g.Expect(testEnv.Cleanup(context.Background(), &existingChart)).To(Succeed())
+ })
+
+ recorder := record.NewFakeRecorder(32)
+ r := &HelmChartTemplate{
+ client: testEnv,
+ eventRecorder: recorder,
+ fieldManager: testFieldManager,
+ }
+
+ obj := &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Namespace: namespace.GetName(),
+ Name: releaseName,
+ },
+ Spec: v2.HelmReleaseSpec{
+ Interval: metav1.Duration{Duration: 1 * time.Hour},
+ ChartRef: &v2.CrossNamespaceSourceReference{
+ Kind: sourcev1.OCIRepositoryKind,
+ Name: "oci-repository",
+ },
+ },
+ Status: v2.HelmReleaseStatus{
+ HelmChart: fmt.Sprintf("%s/%s", existingChart.GetNamespace(), existingChart.GetName()),
+ },
+ }
+ err := r.Reconcile(context.TODO(), &Request{Object: obj})
+ g.Expect(err).ToNot(HaveOccurred())
+ g.Expect(obj.Status.HelmChart).To(BeEmpty())
+ })
+}
+
+func TestHelmChartTemplate_reconcileDelete(t *testing.T) {
+ now := metav1.Now()
+
+ t.Run("Status.HelmChart is deleted", func(t *testing.T) {
+ g := NewWithT(t)
+
+ builder := fake.NewClientBuilder().
+ WithScheme(NewTestScheme()).
+ WithObjects(&sourcev1.HelmChart{
+ ObjectMeta: metav1.ObjectMeta{
+ Namespace: "default",
+ Name: "chart",
+ },
+ })
+
+ recorder := record.NewFakeRecorder(32)
+ r := &HelmChartTemplate{
+ client: builder.Build(),
+ eventRecorder: recorder,
+ }
+
+ obj := &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "release",
+ Namespace: "default",
+ },
+ Status: v2.HelmReleaseStatus{
+ HelmChart: "default/chart",
+ },
+ }
+ err := r.reconcileDelete(context.TODO(), obj)
+ g.Expect(err).ToNot(HaveOccurred())
+ g.Expect(obj.Status.HelmChart).To(BeEmpty())
+
+ err = r.client.Get(context.TODO(), types.NamespacedName{Namespace: "default", Name: "chart"}, &sourcev1.HelmChart{})
+ g.Expect(err).To(HaveOccurred())
+ g.Expect(apierrors.IsNotFound(err)).To(BeTrue())
+ })
+
+ t.Run("Status.HelmChart already deleted", func(t *testing.T) {
+ g := NewWithT(t)
+
+ r := &HelmChartTemplate{
+ client: fake.NewClientBuilder().WithScheme(NewTestScheme()).Build(),
+ }
+ obj := &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "release",
+ Namespace: "default",
+ },
+ Status: v2.HelmReleaseStatus{
+ HelmChart: "default/chart",
+ },
+ }
+ err := r.reconcileDelete(context.TODO(), obj)
+ g.Expect(err).ToNot(HaveOccurred())
+ g.Expect(obj.Status.HelmChart).To(BeEmpty())
+ })
+
+ t.Run("Spec.Suspend is respected", func(t *testing.T) {
+ g := NewWithT(t)
+
+ builder := fake.NewClientBuilder().
+ WithScheme(NewTestScheme()).
+ WithObjects(&sourcev1.HelmChart{
+ ObjectMeta: metav1.ObjectMeta{
+ Namespace: "default",
+ Name: "chart",
+ },
+ })
+
+ recorder := record.NewFakeRecorder(32)
+ r := &HelmChartTemplate{
+ client: builder.Build(),
+ eventRecorder: recorder,
+ }
+
+ obj := &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "release",
+ Namespace: "default",
+ DeletionTimestamp: &now,
+ },
+ Spec: v2.HelmReleaseSpec{
+ Suspend: true,
+ },
+ Status: v2.HelmReleaseStatus{
+ HelmChart: "default/chart",
+ },
+ }
+ err := r.reconcileDelete(context.TODO(), obj)
+ g.Expect(err).ToNot(HaveOccurred())
+ g.Expect(obj.Status.HelmChart).ToNot(BeEmpty())
+
+ g.Consistently(func(g Gomega) {
+ err = r.client.Get(context.TODO(), types.NamespacedName{Namespace: "default", Name: "chart"}, &sourcev1.HelmChart{})
+ g.Expect(err).ToNot(HaveOccurred())
+ }).Should(Succeed())
+ })
+
+ t.Run("cross namespace allow is respected", func(t *testing.T) {
+ g := NewWithT(t)
+
+ chart := &sourcev1.HelmChart{
+ ObjectMeta: metav1.ObjectMeta{
+ Namespace: "other",
+ Name: "chart",
+ },
+ }
+ builder := fake.NewClientBuilder().
+ WithScheme(NewTestScheme()).
+ WithObjects(chart)
+
+ r := &HelmChartTemplate{
+ client: builder.Build(),
+ }
+
+ obj := &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Namespace: "default",
+ },
+ Status: v2.HelmReleaseStatus{
+ HelmChart: "other/chart",
+ },
+ }
+
+ currentAllow := acl.AllowCrossNamespaceRef
+ acl.AllowCrossNamespaceRef = false
+ t.Cleanup(func() { acl.AllowCrossNamespaceRef = currentAllow })
+
+ err := r.reconcileDelete(context.TODO(), obj)
+ g.Expect(err).To(HaveOccurred())
+ g.Expect(obj.Status.HelmChart).ToNot(BeEmpty())
+
+ g.Expect(r.client.Get(context.TODO(),
+ types.NamespacedName{Namespace: chart.Namespace, Name: chart.Name},
+ &sourcev1.HelmChart{}),
+ ).To(Succeed())
+ })
+
+ t.Run("empty Status.HelmChart", func(t *testing.T) {
+ g := NewWithT(t)
+
+ r := &HelmChartTemplate{
+ client: fake.NewClientBuilder().WithScheme(NewTestScheme()).Build(),
+ }
+ obj := &v2.HelmRelease{
+ Status: v2.HelmReleaseStatus{},
+ }
+ err := r.reconcileDelete(context.TODO(), obj)
+ g.Expect(err).ToNot(HaveOccurred())
+ })
+}
+
+func Test_buildHelmChartFromTemplate(t *testing.T) {
+ hrWithChartTemplate := v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-release",
+ Namespace: "default",
+ },
+ Spec: v2.HelmReleaseSpec{
+ Interval: metav1.Duration{Duration: time.Minute},
+ Chart: &v2.HelmChartTemplate{
+ Spec: v2.HelmChartTemplateSpec{
+ Chart: "chart",
+ Version: "1.0.0",
+ SourceRef: v2.CrossNamespaceObjectReference{
+ Name: "test-repository",
+ Kind: "HelmRepository",
+ },
+ Interval: &metav1.Duration{Duration: 2 * time.Minute},
+ ValuesFiles: []string{"values.yaml"},
+ },
+ },
+ },
+ }
+
+ tests := []struct {
+ name string
+ modify func(release *v2.HelmRelease)
+ want *sourcev1.HelmChart
+ }{
+ {
+ name: "builds HelmChart from HelmChartTemplate",
+ modify: func(*v2.HelmRelease) {},
+ want: &sourcev1.HelmChart{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "default-test-release",
+ Namespace: "default",
+ },
+ Spec: sourcev1.HelmChartSpec{
+ Chart: "chart",
+ Version: "1.0.0",
+ SourceRef: sourcev1.LocalHelmChartSourceReference{
+ Name: "test-repository",
+ Kind: "HelmRepository",
+ },
+ Interval: metav1.Duration{Duration: 2 * time.Minute},
+ ValuesFiles: []string{"values.yaml"},
+ },
+ },
+ },
+ {
+ name: "takes SourceRef namespace into account",
+ modify: func(hr *v2.HelmRelease) {
+ hr.Spec.Chart.Spec.SourceRef.Namespace = "cross"
+ },
+ want: &sourcev1.HelmChart{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "default-test-release",
+ Namespace: "cross",
+ },
+ Spec: sourcev1.HelmChartSpec{
+ Chart: "chart",
+ Version: "1.0.0",
+ SourceRef: sourcev1.LocalHelmChartSourceReference{
+ Name: "test-repository",
+ Kind: "HelmRepository",
+ },
+ Interval: metav1.Duration{Duration: 2 * time.Minute},
+ ValuesFiles: []string{"values.yaml"},
+ },
+ },
+ },
+ {
+ name: "falls back to HelmRelease interval",
+ modify: func(hr *v2.HelmRelease) {
+ hr.Spec.Chart.Spec.Interval = nil
+ },
+ want: &sourcev1.HelmChart{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "default-test-release",
+ Namespace: "default",
+ },
+ Spec: sourcev1.HelmChartSpec{
+ Chart: "chart",
+ Version: "1.0.0",
+ SourceRef: sourcev1.LocalHelmChartSourceReference{
+ Name: "test-repository",
+ Kind: "HelmRepository",
+ },
+ Interval: metav1.Duration{Duration: time.Minute},
+ ValuesFiles: []string{"values.yaml"},
+ },
+ },
+ },
+ {
+ name: "take cosign verification into account",
+ modify: func(hr *v2.HelmRelease) {
+ hr.Spec.Chart.Spec.Verify = &v2.HelmChartTemplateVerification{
+ Provider: "cosign",
+ SecretRef: &meta.LocalObjectReference{
+ Name: "cosign-key",
+ },
+ }
+ },
+ want: &sourcev1.HelmChart{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "default-test-release",
+ Namespace: "default",
+ },
+ Spec: sourcev1.HelmChartSpec{
+ Chart: "chart",
+ Version: "1.0.0",
+ SourceRef: sourcev1.LocalHelmChartSourceReference{
+ Name: "test-repository",
+ Kind: "HelmRepository",
+ },
+ Interval: metav1.Duration{Duration: 2 * time.Minute},
+ ValuesFiles: []string{"values.yaml"},
+ Verify: &sourcev1.OCIRepositoryVerification{
+ Provider: "cosign",
+ SecretRef: &meta.LocalObjectReference{
+ Name: "cosign-key",
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "takes object meta into account",
+ modify: func(hr *v2.HelmRelease) {
+ hr.Spec.Chart.ObjectMeta = &v2.HelmChartTemplateObjectMeta{
+ Labels: map[string]string{
+ "foo": "bar",
+ },
+ Annotations: map[string]string{
+ "bar": "baz",
+ },
+ }
+ },
+ want: &sourcev1.HelmChart{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "default-test-release",
+ Namespace: "default",
+ Labels: map[string]string{
+ "foo": "bar",
+ },
+ Annotations: map[string]string{
+ "bar": "baz",
+ },
+ },
+ Spec: sourcev1.HelmChartSpec{
+ Chart: "chart",
+ Version: "1.0.0",
+ SourceRef: sourcev1.LocalHelmChartSourceReference{
+ Name: "test-repository",
+ Kind: "HelmRepository",
+ },
+ Interval: metav1.Duration{Duration: 2 * time.Minute},
+ ValuesFiles: []string{"values.yaml"},
+ },
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ g := NewWithT(t)
+
+ hr := hrWithChartTemplate.DeepCopy()
+ tt.modify(hr)
+
+ g.Expect(buildHelmChartFromTemplate(hr)).To(Equal(tt.want))
+ })
+ }
+}
diff --git a/internal/reconcile/install.go b/internal/reconcile/install.go
new file mode 100644
index 000000000..c7b1da56d
--- /dev/null
+++ b/internal/reconcile/install.go
@@ -0,0 +1,203 @@
+/*
+Copyright 2022 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package reconcile
+
+import (
+ "context"
+ "fmt"
+ "strings"
+ "time"
+
+ corev1 "k8s.io/api/core/v1"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/client-go/tools/record"
+
+ "github.com/fluxcd/pkg/chartutil"
+ "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"
+)
+
+// Install is an ActionReconciler which attempts to install a Helm release
+// based on the given Request data.
+//
+// Before the installation, the History in the Status of the Request.Object is
+// cleared to mark the start of a new release lifecycle. This ensures we never
+// attempt to roll back to a previous release before the install.
+//
+// During the installation process, the writes to the Helm storage are
+// observed and recorded in the Status.History field of the Request.Object.
+//
+// On installation success, the object is marked with Released=True and emits
+// an event. In addition, the object is marked with TestSuccess=False if tests
+// are enabled to indicate we are awaiting the results.
+// On failure, the object is marked with Released=False and emits a warning
+// event. Only an error which resulted in a modification to the Helm storage
+// counts towards a failure for the active remediation strategy.
+//
+// At the end of the reconciliation, the Status.Conditions are summarized and
+// propagated to the Ready condition on the Request.Object.
+//
+// The caller is assumed to have verified the integrity of Request.Object using
+// e.g. action.VerifySnapshot before calling Reconcile.
+type Install struct {
+ configFactory *action.ConfigFactory
+ eventRecorder record.EventRecorder
+ defaultToRetryOnFailure bool
+}
+
+// NewInstall returns a new Install reconciler configured with the provided
+// values.
+func NewInstall(cfg *action.ConfigFactory, recorder record.EventRecorder, defaultToRetryOnFailure bool) *Install {
+ return &Install{configFactory: cfg, eventRecorder: recorder, defaultToRetryOnFailure: defaultToRetryOnFailure}
+}
+
+func (r *Install) Reconcile(ctx context.Context, req *Request) error {
+ var (
+ logBuf = action.NewDebugLogBuffer(ctx)
+ obsReleases = make(observedReleases)
+ cfg = r.configFactory.Build(logBuf, observeRelease(obsReleases), observeInventory(req.Object, req.Chart, r.configFactory.Getter, r.eventRecorder))
+ startTime = time.Now()
+ )
+
+ defer summarize(req)
+
+ // Mark install attempt on object.
+ req.Object.Status.LastAttemptedReleaseAction = v2.ReleaseActionInstall
+
+ // An install is always considered a reset of any previous history.
+ // This ensures we never attempt to roll back to a previous release
+ // before the install.
+ req.Object.Status.ClearHistory()
+
+ // If we are installing, none of the previous conditions apply.
+ conditions.Delete(req.Object, v2.TestSuccessCondition)
+ conditions.Delete(req.Object, v2.RemediatedCondition)
+
+ // Run the Helm install action.
+ _, err := action.Install(ctx, cfg, req.Object, req.Chart, req.Values)
+
+ // Record the action duration in status.
+ req.Object.Status.LastAttemptedReleaseActionDuration = &metav1.Duration{Duration: time.Since(startTime)}
+
+ // Record the history of releases observed during the install.
+ obsReleases.recordOnObject(req.Object,
+ mutateOCIDigest,
+ mutateAction(v2.ReleaseActionInstall))
+
+ if err != nil {
+ r.failure(req, logBuf, err)
+
+ // Return error if we did not store a release, as this does not
+ // require remediation and the caller should e.g. retry.
+ if len(obsReleases) == 0 {
+ return err
+ }
+
+ // Count install failure on object, this is used to determine if
+ // we should retry the install and/or remediation. We only count
+ // attempts which did cause a modification to the storage, as
+ // without a new release in storage there is nothing to remediate,
+ // and the action can be retried immediately without causing
+ // storage drift.
+ req.Object.GetInstall().GetRemediation().IncrementFailureCount(req.Object)
+ return nil
+ }
+
+ r.success(req)
+ return nil
+}
+
+func (r *Install) Name() string {
+ return "install"
+}
+
+func (r *Install) Type() ReconcilerType {
+ return ReconcilerTypeRelease
+}
+
+const (
+ // fmtInstallFailure is the message format for an installation failure.
+ fmtInstallFailure = "Helm install failed for release %s/%s with chart %s@%s: %s"
+ // fmtInstallSuccess is the message format for a successful installation.
+ fmtInstallSuccess = "Helm install succeeded for release %s with chart %s"
+)
+
+// failure records the failure of a Helm installation action in the status of
+// the given Request.Object by marking ReleasedCondition=False and increasing
+// the failure counter. In addition, it emits a warning event for the
+// Request.Object.
+//
+// Increase of the failure counter for the active remediation strategy should
+// be done conditionally by the caller after verifying the failed action has
+// modified the Helm storage. This to avoid counting failures which do not
+// result in Helm storage drift.
+func (r *Install) failure(req *Request, buffer *action.LogBuffer, err error) {
+ // Compose failure message.
+ msg := fmt.Sprintf(fmtInstallFailure, req.Object.GetReleaseNamespace(), req.Object.GetReleaseName(), req.Chart.Name(),
+ req.Chart.Metadata.Version, strings.TrimSpace(err.Error()))
+
+ // Mark install failure on object.
+ req.Object.Status.Failures++
+ conditions.MarkFalse(req.Object, v2.ReleasedCondition, v2.InstallFailedReason, "%s", msg)
+
+ // Record warning event, this message contains more data than the
+ // Condition summary.
+ r.eventRecorder.AnnotatedEventf(
+ req.Object,
+ eventMeta(req.Chart.Metadata.Version, chartutil.DigestValues(digest.Canonical, req.Values).String(),
+ addAppVersion(req.Chart.AppVersion()), addOCIDigest(req.Object.Status.LastAttemptedRevisionDigest)),
+ corev1.EventTypeWarning,
+ v2.InstallFailedReason,
+ eventMessageWithLog(msg, buffer),
+ )
+}
+
+// success records the success of a Helm installation action in the status of
+// the given Request.Object by marking ReleasedCondition=True and emitting an
+// event. In addition, it marks TestSuccessCondition=False when tests are
+// enabled to indicate we are awaiting test results after having made the
+// release.
+func (r *Install) success(req *Request) {
+ // Compose success message.
+ cur := req.Object.Status.History.Latest()
+ msg := fmt.Sprintf(fmtInstallSuccess, cur.FullReleaseName(), cur.VersionedChartName())
+
+ // Mark install success on object.
+ conditions.MarkTrue(req.Object, v2.ReleasedCondition, v2.InstallSucceededReason, "%s", msg)
+ if req.Object.GetTest().Enable && !cur.HasBeenTested() {
+ conditions.MarkUnknown(req.Object, v2.TestSuccessCondition, "AwaitingTests", fmtTestPending,
+ cur.FullReleaseName(), cur.VersionedChartName())
+ }
+
+ // Failures are only relevant while the release is failed
+ // when a retry strategy is configured.
+ if req.Object.GetInstall().GetRetry(r.defaultToRetryOnFailure) != nil {
+ req.Object.Status.ClearFailures()
+ }
+
+ // Record event.
+ r.eventRecorder.AnnotatedEventf(
+ req.Object,
+ eventMeta(cur.ChartVersion, cur.ConfigDigest, addAppVersion(cur.AppVersion), addOCIDigest(cur.OCIDigest)),
+ corev1.EventTypeNormal,
+ v2.InstallSucceededReason,
+ msg,
+ )
+}
diff --git a/internal/reconcile/install_test.go b/internal/reconcile/install_test.go
new file mode 100644
index 000000000..1150d3297
--- /dev/null
+++ b/internal/reconcile/install_test.go
@@ -0,0 +1,700 @@
+/*
+Copyright 2022 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package reconcile
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "testing"
+ "time"
+
+ . "github.com/onsi/gomega"
+ helmchartutil "helm.sh/helm/v4/pkg/chart/common"
+ chart "helm.sh/helm/v4/pkg/chart/v2"
+ helmreleasecommon "helm.sh/helm/v4/pkg/release/common"
+ helmrelease "helm.sh/helm/v4/pkg/release/v1"
+ releaseutil "helm.sh/helm/v4/pkg/release/v1/util"
+ helmstorage "helm.sh/helm/v4/pkg/storage"
+ helmdriver "helm.sh/helm/v4/pkg/storage/driver"
+ corev1 "k8s.io/api/core/v1"
+ apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/client-go/tools/record"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+
+ eventv1 "github.com/fluxcd/pkg/apis/event/v1beta1"
+ "github.com/fluxcd/pkg/apis/meta"
+ "github.com/fluxcd/pkg/chartutil"
+ "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/release"
+ "github.com/fluxcd/helm-controller/internal/storage"
+ "github.com/fluxcd/helm-controller/internal/testutil"
+)
+
+func TestInstall_Reconcile(t *testing.T) {
+ tests := []struct {
+ name string
+ // driver allows for modifying the Helm storage driver.
+ driver func(driver helmdriver.Driver) helmdriver.Driver
+ // releases is the list of releases that are stored in the driver
+ // before install.
+ releases func(namespace string) []*helmrelease.Release
+ // chart to install.
+ chart *chart.Chart
+ // values to use during install.
+ values helmchartutil.Values
+ // spec modifies the HelmRelease object spec before install.
+ spec func(spec *v2.HelmReleaseSpec)
+ // status to configure on the HelmRelease object before install.
+ status func(releases []*helmrelease.Release) v2.HelmReleaseStatus
+ // wantErr is the error that is expected to be returned.
+ wantErr error
+ // expectedConditions are the conditions that are expected to be set on
+ // the HelmRelease after install.
+ expectConditions []metav1.Condition
+ // expectHistory is the expected History of the HelmRelease after
+ // install.
+ expectHistory func(releases []*helmrelease.Release) v2.Snapshots
+ // expectInventory is the expected Inventory of the HelmRelease after
+ // install.
+ expectInventory func(namespace string) *v2.ResourceInventory
+ // expectFailures is the expected Failures count of the HelmRelease.
+ expectFailures int64
+ // expectInstallFailures is the expected InstallFailures count of the
+ // HelmRelease.
+ expectInstallFailures int64
+ // expectUpgradeFailures is the expected UpgradeFailures count of the
+ // HelmRelease.
+ expectUpgradeFailures int64
+ // statusReader is an optional StatusReader to configure on the
+ // ConfigFactory.
+ statusReader bool
+ }{
+ {
+ name: "install success",
+ chart: testutil.BuildChart(),
+ expectConditions: []metav1.Condition{
+ *conditions.TrueCondition(meta.ReadyCondition, v2.InstallSucceededReason,
+ "Helm install succeeded"),
+ *conditions.TrueCondition(v2.ReleasedCondition, v2.InstallSucceededReason,
+ "Helm install succeeded"),
+ },
+ expectHistory: func(releases []*helmrelease.Release) v2.Snapshots {
+ return v2.Snapshots{
+ func() *v2.Snapshot {
+ obs := release.ObserveRelease(releases[0])
+ obs.Action = v2.ReleaseActionInstall
+ return release.ObservedToSnapshot(obs)
+ }(),
+ }
+ },
+ expectInventory: func(namespace string) *v2.ResourceInventory {
+ return &v2.ResourceInventory{
+ Entries: []v2.ResourceRef{
+ {
+ ID: namespace + "_cm__ConfigMap",
+ Version: "v1",
+ },
+ },
+ }
+ },
+ },
+ {
+ name: "install failure",
+ chart: testutil.BuildChart(testutil.ChartWithFailingHook()),
+ expectConditions: []metav1.Condition{
+ *conditions.FalseCondition(meta.ReadyCondition, v2.InstallFailedReason,
+ "failed post-install"),
+ *conditions.FalseCondition(v2.ReleasedCondition, v2.InstallFailedReason,
+ "failed post-install"),
+ },
+ expectHistory: func(releases []*helmrelease.Release) v2.Snapshots {
+ return v2.Snapshots{
+ func() *v2.Snapshot {
+ obs := release.ObserveRelease(releases[0])
+ obs.Action = v2.ReleaseActionInstall
+ return release.ObservedToSnapshot(obs)
+ }(),
+ }
+ },
+ expectFailures: 1,
+ expectInstallFailures: 1,
+ },
+ {
+ name: "install failure without storage update",
+ driver: func(driver helmdriver.Driver) helmdriver.Driver {
+ return &storage.Failing{
+ Driver: driver,
+ CreateErr: fmt.Errorf("storage create error"),
+ }
+ },
+ chart: testutil.BuildChart(),
+ wantErr: fmt.Errorf("storage create error"),
+ expectConditions: []metav1.Condition{
+ *conditions.FalseCondition(meta.ReadyCondition, v2.InstallFailedReason,
+ "storage create error"),
+ *conditions.FalseCondition(v2.ReleasedCondition, v2.InstallFailedReason,
+ "storage create error"),
+ },
+ expectFailures: 1,
+ expectInstallFailures: 0,
+ },
+ {
+ name: "install with current",
+ releases: func(namespace string) []*helmrelease.Release {
+ return []*helmrelease.Release{
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: namespace,
+ Chart: testutil.BuildChart(),
+ Version: 1,
+ Status: helmreleasecommon.StatusUninstalled,
+ }),
+ }
+ },
+ spec: func(spec *v2.HelmReleaseSpec) {
+ spec.Install = &v2.Install{
+ Replace: true,
+ }
+ },
+ status: func(releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ return v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
+ },
+ }
+ },
+ chart: testutil.BuildChart(),
+ expectConditions: []metav1.Condition{
+ *conditions.TrueCondition(meta.ReadyCondition, v2.InstallSucceededReason,
+ "Helm install succeeded"),
+ *conditions.TrueCondition(v2.ReleasedCondition, v2.InstallSucceededReason,
+ "Helm install succeeded"),
+ },
+ expectHistory: func(releases []*helmrelease.Release) v2.Snapshots {
+ return v2.Snapshots{
+ func() *v2.Snapshot {
+ obs := release.ObserveRelease(releases[1])
+ obs.Action = v2.ReleaseActionInstall
+ return release.ObservedToSnapshot(obs)
+ }(),
+ }
+ },
+ },
+ {
+ name: "install with stale current",
+ status: func(releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ return v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: "other",
+ Version: 1,
+ Status: helmreleasecommon.StatusUninstalled,
+ Chart: testutil.BuildChart(),
+ }))),
+ },
+ }
+ },
+ chart: testutil.BuildChart(),
+ expectConditions: []metav1.Condition{
+ *conditions.TrueCondition(meta.ReadyCondition, v2.InstallSucceededReason,
+ "Helm install succeeded"),
+ *conditions.TrueCondition(v2.ReleasedCondition, v2.InstallSucceededReason,
+ "Helm install succeeded"),
+ },
+ expectHistory: func(releases []*helmrelease.Release) v2.Snapshots {
+ return v2.Snapshots{
+ func() *v2.Snapshot {
+ obs := release.ObserveRelease(releases[0])
+ obs.Action = v2.ReleaseActionInstall
+ return release.ObservedToSnapshot(obs)
+ }(),
+ }
+ },
+ },
+ {
+ name: "install with stale conditions",
+ status: func(releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ return v2.HelmReleaseStatus{
+ Conditions: []metav1.Condition{
+ *conditions.FalseCondition(v2.TestSuccessCondition, v2.TestFailedReason, ""),
+ *conditions.TrueCondition(v2.RemediatedCondition, v2.UninstallSucceededReason, ""),
+ },
+ }
+ },
+ chart: testutil.BuildChart(),
+ expectConditions: []metav1.Condition{
+ *conditions.TrueCondition(meta.ReadyCondition, v2.InstallSucceededReason,
+ "Helm install succeeded"),
+ *conditions.TrueCondition(v2.ReleasedCondition, v2.InstallSucceededReason,
+ "Helm install succeeded"),
+ },
+ expectHistory: func(releases []*helmrelease.Release) v2.Snapshots {
+ return v2.Snapshots{
+ func() *v2.Snapshot {
+ obs := release.ObserveRelease(releases[0])
+ obs.Action = v2.ReleaseActionInstall
+ return release.ObservedToSnapshot(obs)
+ }(),
+ }
+ },
+ },
+ {
+ name: "install success with status reader",
+ chart: testutil.BuildChart(),
+ statusReader: true,
+ expectConditions: []metav1.Condition{
+ *conditions.TrueCondition(meta.ReadyCondition, v2.InstallSucceededReason,
+ "Helm install succeeded"),
+ *conditions.TrueCondition(v2.ReleasedCondition, v2.InstallSucceededReason,
+ "Helm install succeeded"),
+ },
+ expectHistory: func(releases []*helmrelease.Release) v2.Snapshots {
+ return v2.Snapshots{
+ func() *v2.Snapshot {
+ obs := release.ObserveRelease(releases[0])
+ obs.Action = v2.ReleaseActionInstall
+ return release.ObservedToSnapshot(obs)
+ }(),
+ }
+ },
+ expectInventory: func(namespace string) *v2.ResourceInventory {
+ return &v2.ResourceInventory{
+ Entries: []v2.ResourceRef{
+ {
+ ID: namespace + "_cm__ConfigMap",
+ Version: "v1",
+ },
+ },
+ }
+ },
+ },
+ }
+ 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
+
+ var releases []*helmrelease.Release
+ if tt.releases != nil {
+ releases = tt.releases(releaseNamespace)
+ releaseutil.SortByRevision(releases)
+ }
+
+ obj := &v2.HelmRelease{
+ Spec: v2.HelmReleaseSpec{
+ ReleaseName: mockReleaseName,
+ TargetNamespace: releaseNamespace,
+ StorageNamespace: releaseNamespace,
+ Timeout: &metav1.Duration{Duration: 200 * 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())
+
+ cfgOpts := []action.ConfigFactoryOption{
+ action.WithStorage(action.DefaultStorageDriver, obj.GetStorageNamespace()),
+ }
+ var mockSR *testutil.MockStatusReader
+ if tt.statusReader {
+ mockSR = &testutil.MockStatusReader{}
+ cfgOpts = append(cfgOpts, action.WithResourceManager(mockSR.NewResourceManagerFuncWithClient(testEnv.Client, testEnv.Manager.GetRESTMapper())))
+ }
+ cfg, err := action.NewConfigFactory(getter, cfgOpts...)
+ g.Expect(err).ToNot(HaveOccurred())
+
+ store := helmstorage.Init(cfg.Driver)
+ for _, r := range releases {
+ g.Expect(store.Create(r)).To(Succeed())
+ }
+
+ if tt.driver != nil {
+ cfg.Driver = tt.driver(cfg.Driver)
+ }
+
+ recorder := new(record.FakeRecorder)
+ got := (NewInstall(cfg, recorder, false)).Reconcile(context.TODO(), &Request{
+ Object: obj,
+ Chart: tt.chart,
+ Values: tt.values,
+ })
+ if tt.wantErr != nil {
+ g.Expect(got).To(Equal(tt.wantErr))
+ } else {
+ g.Expect(got).ToNot(HaveOccurred())
+ }
+
+ g.Expect(obj.Status.Conditions).To(conditions.MatchConditions(tt.expectConditions))
+
+ releases, _ = storeHistory(store, mockReleaseName)
+ releaseutil.SortByRevision(releases)
+
+ if tt.expectHistory != nil {
+ g.Expect(obj.Status.History).To(testutil.Equal(tt.expectHistory(releases)))
+ } else {
+ g.Expect(obj.Status.History).To(BeEmpty(), "expected history to be empty")
+ }
+
+ g.Expect(obj.Status.Failures).To(Equal(tt.expectFailures))
+ g.Expect(obj.Status.InstallFailures).To(Equal(tt.expectInstallFailures))
+ g.Expect(obj.Status.UpgradeFailures).To(Equal(tt.expectUpgradeFailures))
+ g.Expect(obj.Status.LastAttemptedReleaseAction).To(Equal(v2.ReleaseActionInstall))
+ g.Expect(obj.Status.LastAttemptedReleaseActionDuration).ToNot(BeNil())
+
+ if tt.expectInventory != nil {
+ g.Expect(obj.Status.Inventory).To(testutil.Equal(tt.expectInventory(releaseNamespace)))
+ }
+
+ if mockSR != nil {
+ g.Expect(mockSR.SupportsCalled()).To(BeNumerically(">", 0), "expected StatusReader.Supports to be called")
+ }
+ })
+ }
+}
+
+func TestInstall_Reconcile_withSubchartWithCRDs(t *testing.T) {
+ getValues := func(subchartValues map[string]any) helmchartutil.Values {
+ return helmchartutil.Values{"subchart": subchartValues}
+ }
+
+ expectConditions := []metav1.Condition{
+ *conditions.TrueCondition(meta.ReadyCondition, v2.InstallSucceededReason,
+ "Helm install succeeded"),
+ *conditions.TrueCondition(v2.ReleasedCondition, v2.InstallSucceededReason,
+ "Helm install succeeded"),
+ }
+
+ expectHistory := func(releases []*helmrelease.Release) v2.Snapshots {
+ obs := release.ObserveRelease(releases[0])
+ obs.Action = v2.ReleaseActionInstall
+ return v2.Snapshots{
+ release.ObservedToSnapshot(obs),
+ }
+ }
+
+ for _, tt := range []struct {
+ name string
+ subchartValues map[string]any
+ subchartResourcesPresent bool
+ expectedMainChartValues map[string]any
+ }{
+ {
+ name: "subchart disabled should not deploy resources, including CRDs",
+ subchartValues: map[string]any{"enabled": false},
+ subchartResourcesPresent: false,
+ expectedMainChartValues: map[string]any{
+ "foo": "baz",
+ "myimports": map[string]any{"myint": 0},
+ },
+ },
+ {
+ name: "subchart enabled should deploy resources, including CRDs",
+ subchartValues: map[string]any{"enabled": true},
+ subchartResourcesPresent: true,
+ expectedMainChartValues: map[string]any{
+ "foo": "baz",
+ "myint": 123,
+ "myimports": map[string]any{"myint": 0}, // should be 456: https://github.com/helm/helm/issues/13223
+ "subchart": map[string]any{
+ "foo": "bar",
+ "global": map[string]any{},
+ "exports": map[string]any{"data": map[string]any{"myint": 123}},
+ "default": map[string]any{"data": map[string]any{"myint": 456}},
+ },
+ },
+ },
+ } {
+ 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
+
+ obj := &v2.HelmRelease{
+ Spec: v2.HelmReleaseSpec{
+ ReleaseName: mockReleaseName,
+ TargetNamespace: releaseNamespace,
+ StorageNamespace: releaseNamespace,
+ Timeout: &metav1.Duration{Duration: 100 * time.Millisecond},
+ },
+ }
+
+ 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)
+
+ chart := testutil.BuildChartWithSubchartWithCRD()
+ recorder := new(record.FakeRecorder)
+ got := (NewInstall(cfg, recorder, false)).Reconcile(context.TODO(), &Request{
+ Object: obj,
+ Chart: chart,
+ Values: getValues(tt.subchartValues),
+ })
+ g.Expect(got).ToNot(HaveOccurred())
+
+ g.Expect(obj.Status.Conditions).To(conditions.MatchConditions(expectConditions))
+
+ releases, _ := storeHistory(store, mockReleaseName)
+ releaseutil.SortByRevision(releases)
+
+ g.Expect(obj.Status.History).To(testutil.Equal(expectHistory(releases)))
+
+ // Assert main chart configmap is present.
+ mainChartCM := &corev1.ConfigMap{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "cm-main-chart",
+ Namespace: releaseNamespace,
+ },
+ }
+ err = testEnv.Get(context.TODO(), client.ObjectKeyFromObject(mainChartCM), mainChartCM)
+ g.Expect(err).NotTo(HaveOccurred())
+
+ // Assert subchart configmap is absent or present.
+ subChartCM := &corev1.ConfigMap{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "cm-sub-chart",
+ Namespace: releaseNamespace,
+ },
+ }
+ err = testEnv.Get(context.TODO(), client.ObjectKeyFromObject(subChartCM), subChartCM)
+ if tt.subchartResourcesPresent {
+ g.Expect(err).NotTo(HaveOccurred())
+ } else {
+ g.Expect(err).To(HaveOccurred())
+ }
+
+ // Assert subchart CRD is absent or present.
+ subChartCRD := &apiextensionsv1.CustomResourceDefinition{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "crontabs.stable.example.com",
+ },
+ }
+ err = testEnv.Get(context.TODO(), client.ObjectKeyFromObject(subChartCRD), subChartCRD)
+ if tt.subchartResourcesPresent {
+ g.Expect(err).NotTo(HaveOccurred())
+ } else {
+ g.Expect(err).To(HaveOccurred())
+ }
+
+ // Assert main chart values.
+ g.Expect(chart.Values).To(testutil.Equal(tt.expectedMainChartValues))
+ })
+ }
+}
+
+func TestInstall_failure(t *testing.T) {
+ var (
+ obj = &v2.HelmRelease{
+ Spec: v2.HelmReleaseSpec{
+ ReleaseName: mockReleaseName,
+ TargetNamespace: mockReleaseNamespace,
+ },
+ Status: v2.HelmReleaseStatus{
+ LastAttemptedRevisionDigest: "sha256:1234567890",
+ },
+ }
+ chrt = testutil.BuildChart()
+ err = errors.New("installation error")
+ )
+
+ t.Run("records failure", func(t *testing.T) {
+ g := NewWithT(t)
+
+ recorder := testutil.NewFakeRecorder(10, false)
+ r := &Install{
+ eventRecorder: recorder,
+ }
+
+ req := &Request{Object: obj.DeepCopy(), Chart: chrt, Values: map[string]any{"foo": "bar"}}
+ r.failure(req, nil, err)
+
+ expectMsg := fmt.Sprintf(fmtInstallFailure, mockReleaseNamespace, mockReleaseName, chrt.Name(),
+ chrt.Metadata.Version, err.Error())
+
+ g.Expect(req.Object.Status.Conditions).To(conditions.MatchConditions([]metav1.Condition{
+ *conditions.FalseCondition(v2.ReleasedCondition, v2.InstallFailedReason, "%s", expectMsg),
+ }))
+ g.Expect(req.Object.Status.Failures).To(Equal(int64(1)))
+ g.Expect(recorder.GetEvents()).To(ConsistOf([]corev1.Event{
+ {
+ Type: corev1.EventTypeWarning,
+ Reason: v2.InstallFailedReason,
+ Message: expectMsg,
+ ObjectMeta: metav1.ObjectMeta{
+ Annotations: map[string]string{
+ eventMetaGroupKey(metaOCIDigestKey): obj.Status.LastAttemptedRevisionDigest,
+ eventMetaGroupKey(eventv1.MetaRevisionKey): chrt.Metadata.Version,
+ eventMetaGroupKey(metaAppVersionKey): chrt.Metadata.AppVersion,
+ eventMetaGroupKey(eventv1.MetaTokenKey): chartutil.DigestValues(digest.Canonical, req.Values).String(),
+ },
+ },
+ },
+ }))
+ })
+
+ t.Run("records failure with logs", func(t *testing.T) {
+ g := NewWithT(t)
+
+ recorder := testutil.NewFakeRecorder(10, false)
+ r := &Install{
+ eventRecorder: recorder,
+ }
+ req := &Request{Object: obj.DeepCopy(), Chart: chrt}
+ r.failure(req, mockLogBuffer(), err)
+
+ expectSubStr := "Last Helm logs"
+ g.Expect(conditions.IsFalse(req.Object, v2.ReleasedCondition)).To(BeTrue())
+ g.Expect(conditions.GetMessage(req.Object, v2.ReleasedCondition)).ToNot(ContainSubstring(expectSubStr))
+
+ events := recorder.GetEvents()
+ g.Expect(events).To(HaveLen(1))
+ g.Expect(events[0].Message).To(ContainSubstring(expectSubStr))
+ })
+}
+
+func TestInstall_success(t *testing.T) {
+ var (
+ cur = testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: mockReleaseNamespace,
+ Chart: testutil.BuildChart(),
+ })
+ obj = &v2.HelmRelease{
+ Status: v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(cur)),
+ },
+ },
+ }
+ )
+
+ t.Run("records success", func(t *testing.T) {
+ g := NewWithT(t)
+
+ recorder := testutil.NewFakeRecorder(10, false)
+ r := &Install{
+ eventRecorder: recorder,
+ }
+
+ req := &Request{
+ Object: obj.DeepCopy(),
+ }
+ r.success(req)
+
+ expectMsg := fmt.Sprintf(fmtInstallSuccess,
+ fmt.Sprintf("%s/%s.v%d", mockReleaseNamespace, mockReleaseName, obj.Status.History.Latest().Version),
+ fmt.Sprintf("%s@%s", obj.Status.History.Latest().ChartName, obj.Status.History.Latest().ChartVersion))
+
+ g.Expect(req.Object.Status.Conditions).To(conditions.MatchConditions([]metav1.Condition{
+ *conditions.TrueCondition(v2.ReleasedCondition, v2.InstallSucceededReason, "%s", expectMsg),
+ }))
+ g.Expect(recorder.GetEvents()).To(ConsistOf([]corev1.Event{
+ {
+ Type: corev1.EventTypeNormal,
+ Reason: v2.InstallSucceededReason,
+ Message: expectMsg,
+ ObjectMeta: metav1.ObjectMeta{
+ Annotations: map[string]string{
+ eventMetaGroupKey(eventv1.MetaRevisionKey): obj.Status.History.Latest().ChartVersion,
+ eventMetaGroupKey(metaAppVersionKey): obj.Status.History.Latest().AppVersion,
+ eventMetaGroupKey(eventv1.MetaTokenKey): obj.Status.History.Latest().ConfigDigest,
+ },
+ },
+ },
+ }))
+ })
+
+ t.Run("clears failures if retry strategy is configured", func(t *testing.T) {
+ g := NewWithT(t)
+
+ recorder := testutil.NewFakeRecorder(10, false)
+ r := &Install{
+ eventRecorder: recorder,
+ }
+
+ req := &Request{
+ Object: obj.DeepCopy(),
+ }
+ req.Object.Spec.Install = &v2.Install{
+ Strategy: &v2.InstallStrategy{
+ Name: "RetryOnFailure",
+ },
+ }
+ req.Object.Status.Failures = 3
+ req.Object.Status.InstallFailures = 3
+ req.Object.Status.UpgradeFailures = 3
+ r.success(req)
+
+ g.Expect(req.Object.Status.Failures).To(BeZero())
+ g.Expect(req.Object.Status.InstallFailures).To(BeZero())
+ g.Expect(req.Object.Status.UpgradeFailures).To(BeZero())
+ })
+
+ t.Run("records success with TestSuccess=False", func(t *testing.T) {
+ g := NewWithT(t)
+
+ recorder := testutil.NewFakeRecorder(10, false)
+ r := &Install{
+ eventRecorder: recorder,
+ }
+
+ obj := obj.DeepCopy()
+ obj.Spec.Test = &v2.Test{Enable: true}
+
+ req := &Request{Object: obj}
+ r.success(req)
+
+ g.Expect(conditions.IsTrue(req.Object, v2.ReleasedCondition)).To(BeTrue())
+
+ cond := conditions.Get(req.Object, v2.TestSuccessCondition)
+ g.Expect(cond).ToNot(BeNil())
+
+ expectMsg := fmt.Sprintf(fmtTestPending,
+ fmt.Sprintf("%s/%s.v%d", mockReleaseNamespace, mockReleaseName, obj.Status.History.Latest().Version),
+ fmt.Sprintf("%s@%s", obj.Status.History.Latest().ChartName, obj.Status.History.Latest().ChartVersion))
+ g.Expect(cond.Message).To(Equal(expectMsg))
+ })
+}
diff --git a/internal/reconcile/reconcile.go b/internal/reconcile/reconcile.go
new file mode 100644
index 000000000..de98349a9
--- /dev/null
+++ b/internal/reconcile/reconcile.go
@@ -0,0 +1,102 @@
+/*
+Copyright 2022 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package reconcile
+
+import (
+ "context"
+
+ helmchartutil "helm.sh/helm/v4/pkg/chart/common"
+ helmchart "helm.sh/helm/v4/pkg/chart/v2"
+
+ v2 "github.com/fluxcd/helm-controller/api/v2"
+)
+
+const (
+ // ReconcilerTypeRelease is an ActionReconciler which produces a new
+ // Helm release.
+ ReconcilerTypeRelease ReconcilerType = "release"
+ // ReconcilerTypeRemediate is an ActionReconciler which remediates a
+ // failed Helm release.
+ ReconcilerTypeRemediate ReconcilerType = "remediate"
+ // ReconcilerTypeTest is an ActionReconciler which tests a Helm release.
+ ReconcilerTypeTest ReconcilerType = "test"
+ // ReconcilerTypeUnlock is an ActionReconciler which unlocks a Helm
+ // release in a stale pending state. It differs from ReconcilerTypeRemediate
+ // in that it does not produce a new Helm release.
+ ReconcilerTypeUnlock ReconcilerType = "unlock"
+ // ReconcilerTypeDriftCorrection is an ActionReconciler which corrects
+ // Helm releases which have drifted from the cluster state.
+ ReconcilerTypeDriftCorrection ReconcilerType = "drift correction"
+)
+
+// ReconcilerType is a string which identifies the type of ActionReconciler.
+// It can be used to e.g. limiting the number of action (types) to be performed
+// in a single reconciliation.
+type ReconcilerType string
+
+// ReconcilerTypeSet is a set of ReconcilerType.
+type ReconcilerTypeSet []ReconcilerType
+
+// Contains returns true if the set contains the given type.
+func (s ReconcilerTypeSet) Contains(t ReconcilerType) bool {
+ for _, r := range s {
+ if r == t {
+ return true
+ }
+ }
+ return false
+}
+
+// Count returns the number of elements matching the given type.
+func (s ReconcilerTypeSet) Count(t ReconcilerType) int {
+ count := 0
+ for _, r := range s {
+ if r == t {
+ count++
+ }
+ }
+ return count
+}
+
+// Request is a request to be performed by an ActionReconciler. The reconciler
+// writes the result of the request to the Object's status.
+type Request struct {
+ // Object is the Helm release to be reconciled, and describes the desired
+ // state to the ActionReconciler.
+ Object *v2.HelmRelease
+ // Chart is the Helm chart to be installed or upgraded.
+ Chart *helmchart.Chart
+ // Values is the Helm chart values to be used for the installation or
+ // upgrade.
+ Values helmchartutil.Values
+}
+
+// ActionReconciler is an interface which defines the methods that a reconciler
+// of a Helm action must implement.
+type ActionReconciler interface {
+ // Reconcile performs the reconcile action for the given Request. The
+ // reconciler should write the result of the request to the Object's status.
+ // An error is returned if the reconcile action cannot be performed and did
+ // not result in a modification of the Helm storage. The caller should then
+ // either retry, or abort the operation.
+ Reconcile(ctx context.Context, req *Request) error
+ // Name returns the name of the ActionReconciler. Typically, this equals
+ // the name of the Helm action it performs.
+ Name() string
+ // Type returns the ReconcilerType of the ActionReconciler.
+ Type() ReconcilerType
+}
diff --git a/internal/reconcile/release.go b/internal/reconcile/release.go
new file mode 100644
index 000000000..fd3ceac46
--- /dev/null
+++ b/internal/reconcile/release.go
@@ -0,0 +1,342 @@
+/*
+Copyright 2022 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package reconcile
+
+import (
+ "errors"
+ "sort"
+ "strings"
+
+ corev1 "k8s.io/api/core/v1"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/cli-runtime/pkg/genericclioptions"
+ "k8s.io/client-go/tools/record"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+
+ eventv1 "github.com/fluxcd/pkg/apis/event/v1beta1"
+ "github.com/fluxcd/pkg/apis/meta"
+ "github.com/fluxcd/pkg/runtime/conditions"
+ helmchart "helm.sh/helm/v4/pkg/chart/v2"
+ helmrelease "helm.sh/helm/v4/pkg/release"
+ helmreleasev1 "helm.sh/helm/v4/pkg/release/v1"
+
+ 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/inventory"
+ "github.com/fluxcd/helm-controller/internal/postrender"
+ "github.com/fluxcd/helm-controller/internal/release"
+ "github.com/fluxcd/helm-controller/internal/storage"
+)
+
+var (
+ // ErrNoLatest is returned when the HelmRelease has no latest release
+ // but this is required by the ActionReconciler.
+ ErrNoLatest = errors.New("no latest release")
+ // ErrReleaseMismatch is returned when the resulting release after running
+ // an action does not match the expected latest and/or previous release.
+ // This can happen for actions where targeting a release by version is not
+ // possible, for example while running tests.
+ ErrReleaseMismatch = errors.New("release mismatch")
+)
+
+// mutateObservedRelease is a function that mutates the Observation with the
+// given HelmRelease object.
+type mutateObservedRelease func(*v2.HelmRelease, release.Observation) release.Observation
+
+// observedReleases is a map of Helm releases as observed to be written to the
+// Helm storage. The key is the version of the release.
+type observedReleases map[int]release.Observation
+
+// sortedVersions returns the versions of the observed releases in descending
+// order.
+func (r observedReleases) sortedVersions() (versions []int) {
+ for ver := range r {
+ versions = append(versions, ver)
+ }
+ sort.Sort(sort.Reverse(sort.IntSlice(versions)))
+ return
+}
+
+// recordOnObject records the observed releases on the HelmRelease object.
+func (r observedReleases) recordOnObject(obj *v2.HelmRelease, mutators ...mutateObservedRelease) {
+ switch len(r) {
+ case 0:
+ return
+ case 1:
+ var obs release.Observation
+ for _, o := range r {
+ obs = o
+ }
+ for _, mut := range mutators {
+ obs = mut(obj, obs)
+ }
+ obj.Status.History = append(v2.Snapshots{release.ObservedToSnapshot(obs)}, obj.Status.History...)
+ default:
+ versions := r.sortedVersions()
+ obs := r[versions[0]]
+ for _, mut := range mutators {
+ obs = mut(obj, obs)
+ }
+ obj.Status.History = append(v2.Snapshots{release.ObservedToSnapshot(obs)}, obj.Status.History...)
+
+ for _, ver := range versions[1:] {
+ for i := range obj.Status.History {
+ snap := obj.Status.History[i]
+ if snap.Targets(r[ver].Name, r[ver].Namespace, r[ver].Version) {
+ obs := r[ver]
+ obs.OCIDigest = snap.OCIDigest
+ newSnap := release.ObservedToSnapshot(obs)
+ newSnap.SetTestHooks(snap.GetTestHooks())
+ obj.Status.History[i] = newSnap
+ return
+ }
+ }
+ }
+ }
+}
+
+func mutateOCIDigest(obj *v2.HelmRelease, obs release.Observation) release.Observation {
+ obs.OCIDigest = obj.Status.LastAttemptedRevisionDigest
+ return obs
+}
+
+func mutateAction(action v2.ReleaseAction) func(obj *v2.HelmRelease, obs release.Observation) release.Observation {
+ return func(obj *v2.HelmRelease, obs release.Observation) release.Observation {
+ obs.Action = action
+ return obs
+ }
+}
+
+func releaseToObservation(rls *helmreleasev1.Release,
+ snapshot *v2.Snapshot, action v2.ReleaseAction) release.Observation {
+
+ obs := release.ObserveRelease(rls)
+ obs.OCIDigest = snapshot.OCIDigest
+ obs.Action = action
+ return obs
+}
+
+// observeRelease returns a storage.ObserveFunc that stores the observed
+// releases in the given observedReleases map.
+// It can be used for Helm actions that modify multiple releases in the
+// Helm storage, such as install and upgrade.
+func observeRelease(observed observedReleases) storage.ObserveFunc {
+ return func(rls helmrelease.Releaser) {
+ rlsTyped, ok := rls.(*helmreleasev1.Release)
+ if !ok {
+ return
+ }
+ obs := release.ObserveRelease(rlsTyped)
+ observed[obs.Version] = obs
+ }
+}
+
+// observeInventory returns a storage.ObserveFunc that builds an inventory
+// from the release manifest and chart CRDs, and stores it in the HelmRelease
+// status.
+func observeInventory(obj *v2.HelmRelease, chart *helmchart.Chart, getter genericclioptions.RESTClientGetter, recorder record.EventRecorder) storage.ObserveFunc {
+ return func(rls helmrelease.Releaser) {
+ rlsTyped, ok := rls.(*helmreleasev1.Release)
+ if !ok {
+ return
+ }
+
+ restCfg, err := getter.ToRESTConfig()
+ if err != nil {
+ recorder.Eventf(obj, corev1.EventTypeWarning, v2.InventoryBuildFailedReason,
+ "failed to build inventory for %s/%s: %s", obj.GetNamespace(), obj.GetName(), err.Error())
+ return
+ }
+ c, err := client.New(restCfg, client.Options{})
+ if err != nil {
+ recorder.Eventf(obj, corev1.EventTypeWarning, v2.InventoryBuildFailedReason,
+ "failed to build inventory for %s/%s: %s", obj.GetNamespace(), obj.GetName(), err.Error())
+ return
+ }
+
+ inv := inventory.New()
+ warnings, err := inventory.AddManifest(inv, rlsTyped.Manifest, rlsTyped.Namespace, c)
+ if err != nil {
+ recorder.Eventf(obj, corev1.EventTypeWarning, v2.InventoryBuildFailedReason,
+ "failed to build inventory for %s/%s: %s", obj.GetNamespace(), obj.GetName(), err.Error())
+ return
+ }
+ if len(warnings) > 0 {
+ recorder.Eventf(obj, corev1.EventTypeWarning, v2.NamespaceCheckSkippedReason,
+ strings.Join(warnings, "; "))
+ }
+ if chart != nil {
+ if err := inventory.AddCRDs(inv, chart); err != nil {
+ recorder.Eventf(obj, corev1.EventTypeWarning, v2.InventoryBuildFailedReason,
+ "failed to build inventory for %s/%s: %s", obj.GetNamespace(), obj.GetName(), err.Error())
+ return
+ }
+ }
+ obj.Status.Inventory = inv
+ }
+}
+
+// summarize composes a Ready condition out of the Remediated, TestSuccess and
+// Released conditions of the given Request.Object, and sets it on the object.
+//
+// The composition is made by sorting them by highest generation and priority
+// of the summary conditions, taking the first result.
+//
+// Not taking the generation of the object itself into account ensures that if
+// the change in generation of the resource does not result in a release, the
+// Ready condition is still reflected for the current generation based on a
+// release made for the previous generation.
+//
+// It takes the current specification of the object into account, and deals
+// with the conditional handling of TestSuccess. Deleting the condition when
+// tests are not enabled, and excluding it when failures must be ignored.
+//
+// If Ready=True, any Stalled condition is removed.
+//
+// The ObservedPostRenderersDigest and ObservedCommonMetadataDigest are
+// updated to reflect the current spec.
+func summarize(req *Request) {
+ var sumConds = []string{v2.RemediatedCondition, v2.ReleasedCondition}
+ if req.Object.GetTest().Enable && !req.Object.GetTest().IgnoreFailures {
+ sumConds = []string{v2.RemediatedCondition, v2.TestSuccessCondition, v2.ReleasedCondition}
+ }
+
+ // Remove any stale TestSuccess condition as soon as tests are disabled.
+ if !req.Object.GetTest().Enable {
+ conditions.Delete(req.Object, v2.TestSuccessCondition)
+ }
+
+ conds := req.Object.Status.Conditions
+ if len(conds) == 0 {
+ // Nothing to summarize if there are no conditions.
+ return
+ }
+
+ sort.SliceStable(conds, func(i, j int) bool {
+ iPos, ok := inStringSlice(sumConds, conds[i].Type)
+ if !ok {
+ return false
+ }
+
+ jPos, ok := inStringSlice(sumConds, conds[j].Type)
+ if !ok {
+ return true
+ }
+
+ return (conds[i].ObservedGeneration >= conds[j].ObservedGeneration) && (iPos < jPos)
+ })
+
+ status := conds[0].Status
+
+ // Any remediated state is considered an error.
+ if conds[0].Type == v2.RemediatedCondition {
+ status = metav1.ConditionFalse
+ }
+
+ if status == metav1.ConditionTrue {
+ conditions.Delete(req.Object, meta.StalledCondition)
+ }
+
+ conditions.Set(req.Object, &metav1.Condition{
+ Type: meta.ReadyCondition,
+ Status: status,
+ Reason: conds[0].Reason,
+ Message: conds[0].Message,
+ ObservedGeneration: req.Object.Generation,
+ })
+
+ // Update the observed post-renderers and common-metadata digests to
+ // reflect the current spec.
+ req.Object.Status.ObservedPostRenderersDigest = ""
+ if req.Object.Spec.PostRenderers != nil {
+ req.Object.Status.ObservedPostRenderersDigest = postrender.Digest(digest.Canonical, req.Object.Spec.PostRenderers).String()
+ }
+ req.Object.Status.ObservedCommonMetadataDigest = ""
+ if req.Object.Spec.CommonMetadata != nil {
+ req.Object.Status.ObservedCommonMetadataDigest = postrender.CommonMetadataDigest(digest.Canonical, req.Object.Spec.CommonMetadata).String()
+ }
+}
+
+// eventMessageWithLog returns an event message composed out of the given
+// message and any log messages by appending them to the message.
+func eventMessageWithLog(msg string, log *action.LogBuffer) string {
+ if !log.Empty() {
+ msg = msg + "\n\nLast Helm logs:\n\n" + log.String()
+ }
+ return msg
+}
+
+// addMeta is a function that adds metadata to an event map.
+type addMeta func(map[string]string)
+
+const (
+ // metaOCIDigestKey is the key for the chart OCI artifact digest.
+ metaOCIDigestKey = "oci-digest"
+
+ // metaAppVersionKey is the key for the app version found in chart metadata.
+ metaAppVersionKey = "app-version"
+)
+
+// eventMeta returns the event (annotation) metadata based on the given
+// parameters.
+func eventMeta(revision, token string, metas ...addMeta) map[string]string {
+ var metadata map[string]string
+ if revision != "" || token != "" {
+ metadata = make(map[string]string)
+ if revision != "" {
+ metadata[eventMetaGroupKey(eventv1.MetaRevisionKey)] = revision
+ }
+ if token != "" {
+ metadata[eventMetaGroupKey(eventv1.MetaTokenKey)] = token
+ }
+ }
+
+ for _, add := range metas {
+ add(metadata)
+ }
+
+ return metadata
+}
+
+func addOCIDigest(digest string) addMeta {
+ return func(m map[string]string) {
+ if digest != "" {
+ if m == nil {
+ m = make(map[string]string)
+ }
+ m[eventMetaGroupKey(metaOCIDigestKey)] = digest
+ }
+ }
+}
+
+func addAppVersion(appVersion string) addMeta {
+ return func(m map[string]string) {
+ if appVersion != "" {
+ if m == nil {
+ m = make(map[string]string)
+ }
+ m[eventMetaGroupKey(metaAppVersionKey)] = appVersion
+ }
+ }
+}
+
+// eventMetaGroupKey returns the event (annotation) metadata key prefixed with
+// the group.
+func eventMetaGroupKey(key string) string {
+ return v2.GroupVersion.Group + "/" + key
+}
diff --git a/internal/reconcile/release_test.go b/internal/reconcile/release_test.go
new file mode 100644
index 000000000..1f8b67525
--- /dev/null
+++ b/internal/reconcile/release_test.go
@@ -0,0 +1,800 @@
+/*
+Copyright 2022 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package reconcile
+
+import (
+ "context"
+ "fmt"
+ "testing"
+
+ "github.com/go-logr/logr"
+ . "github.com/onsi/gomega"
+ chart "helm.sh/helm/v4/pkg/chart/v2"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "sigs.k8s.io/controller-runtime/pkg/log"
+
+ "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/release"
+)
+
+const (
+ mockReleaseName = "mock-release"
+ mockReleaseNamespace = "mock-ns"
+)
+
+var (
+ commonMetadata = &v2.CommonMetadata{
+ Labels: map[string]string{
+ "common-label": "test-label-value",
+ },
+ Annotations: map[string]string{
+ "common-annotation": "test-annotation-value",
+ },
+ }
+ commonMetadata2 = &v2.CommonMetadata{
+ Labels: map[string]string{
+ "common-label": "test-label-value",
+ "new-label": "new-label-value",
+ },
+ }
+ 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
+ status v2.HelmReleaseStatus
+ expectedStatus *v2.HelmReleaseStatus
+ }{
+ {
+ name: "summarize conditions",
+ generation: 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,
+ 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{
+ Test: &v2.Test{
+ Enable: true,
+ },
+ },
+ 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,
+ 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{
+ Test: &v2.Test{
+ Enable: true,
+ },
+ },
+ 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",
+ 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{
+ Test: &v2.Test{
+ Enable: true,
+ },
+ },
+ 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,
+ 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{
+ Test: &v2.Test{
+ Enable: true,
+ },
+ },
+ 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,
+ 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,
+ 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,
+ },
+ },
+ },
+ },
+ {
+ name: "with stale observed generation",
+ generation: 5,
+ spec: &v2.HelmReleaseSpec{
+ Test: &v2.Test{
+ Enable: true,
+ },
+ },
+ 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,
+ },
+ },
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ g := NewWithT(t)
+
+ obj := &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Generation: tt.generation,
+ },
+ 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.expectedStatus.Conditions))
+ g.Expect(obj.Status.ObservedPostRenderersDigest).To(Equal(tt.expectedStatus.ObservedPostRenderersDigest))
+ })
+ }
+}
+
+func mockLogBuffer() *action.LogBuffer {
+ ctx := log.IntoContext(context.Background(), logr.Discard())
+ buf := action.NewDebugLogBuffer(ctx)
+ for i := range 10 {
+ buf.Appendf("line %d", i+1)
+ }
+ return buf
+}
+
+func Test_mutateAction(t *testing.T) {
+ tests := []struct {
+ name string
+ action v2.ReleaseAction
+ }{
+ {
+ name: "install action",
+ action: v2.ReleaseActionInstall,
+ },
+ {
+ name: "upgrade action",
+ action: v2.ReleaseActionUpgrade,
+ },
+ {
+ name: "rollback action",
+ action: v2.ReleaseActionRollback,
+ },
+ {
+ name: "uninstall action",
+ action: v2.ReleaseActionUninstall,
+ },
+ {
+ name: "uninstall-remediation action",
+ action: v2.ReleaseActionUninstallRemediation,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ g := NewWithT(t)
+
+ obs := release.Observation{
+ Name: mockReleaseName,
+ Version: 1,
+ }
+ mutator := mutateAction(tt.action)
+ result := mutator(&v2.HelmRelease{}, obs)
+ g.Expect(result.Action).To(Equal(tt.action))
+ })
+ }
+}
+
+func Test_RecordOnObject(t *testing.T) {
+ tests := []struct {
+ name string
+ obj *v2.HelmRelease
+ r observedReleases
+ mutate bool
+ testFunc func(*v2.HelmRelease) error
+ }{
+ {
+ name: "record observed releases",
+ obj: &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: mockReleaseName,
+ Namespace: mockReleaseNamespace,
+ },
+ },
+ r: observedReleases{
+ 1: {
+ Name: mockReleaseName,
+ Version: 1,
+ ChartMetadata: chart.Metadata{
+ Name: mockReleaseName,
+ Version: "1.0.0",
+ },
+ },
+ },
+ testFunc: func(obj *v2.HelmRelease) error {
+ if len(obj.Status.History) != 1 {
+ return fmt.Errorf("history length is not 1")
+ }
+ if obj.Status.History[0].Name != mockReleaseName {
+ return fmt.Errorf("release name is not %s", mockReleaseName)
+ }
+ return nil
+ },
+ },
+ {
+ name: "record observed releases with multiple versions",
+ obj: &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: mockReleaseName,
+ Namespace: mockReleaseNamespace,
+ },
+ },
+ r: observedReleases{
+ 1: {
+ Name: mockReleaseName,
+ Version: 1,
+ ChartMetadata: chart.Metadata{
+ Name: mockReleaseName,
+ Version: "1.0.0",
+ },
+ },
+ 2: {
+ Name: mockReleaseName,
+ Version: 2,
+ ChartMetadata: chart.Metadata{
+ Name: mockReleaseName,
+ Version: "2.0.0",
+ },
+ },
+ },
+ testFunc: func(obj *v2.HelmRelease) error {
+ if len(obj.Status.History) != 1 {
+ return fmt.Errorf("want history length 1, got %d", len(obj.Status.History))
+ }
+ if obj.Status.History[0].Name != mockReleaseName {
+ return fmt.Errorf("release name is not %s", mockReleaseName)
+ }
+ if obj.Status.History[0].ChartVersion != "2.0.0" {
+ return fmt.Errorf("want chart version %s, got %s", "2.0.0", obj.Status.History[0].ChartVersion)
+ }
+ return nil
+ },
+ },
+ {
+ name: "record observed releases with status digest",
+ obj: &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: mockReleaseName,
+ Namespace: mockReleaseNamespace,
+ },
+ Status: v2.HelmReleaseStatus{
+ LastAttemptedRevisionDigest: "sha256:123456",
+ },
+ },
+ r: observedReleases{
+ 1: {
+ Name: mockReleaseName,
+ Version: 1,
+ ChartMetadata: chart.Metadata{
+ Name: mockReleaseName,
+ Version: "1.0.0",
+ },
+ },
+ },
+ mutate: true,
+ testFunc: func(obj *v2.HelmRelease) error {
+ h := obj.Status.History.Latest()
+ if h.Name != mockReleaseName {
+ return fmt.Errorf("release name is not %s", mockReleaseName)
+ }
+ if h.ChartVersion != "1.0.0" {
+ return fmt.Errorf("want chart version %s, got %s", "1.0.0", h.ChartVersion)
+ }
+ if h.OCIDigest != obj.Status.LastAttemptedRevisionDigest {
+ return fmt.Errorf("want digest %s, got %s", obj.Status.LastAttemptedRevisionDigest, h.OCIDigest)
+ }
+ return nil
+ },
+ },
+ {
+ name: "record observed releases with multiple versions and status digest",
+ obj: &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: mockReleaseName,
+ Namespace: mockReleaseNamespace,
+ },
+ Status: v2.HelmReleaseStatus{
+ LastAttemptedRevisionDigest: "sha256:123456",
+ },
+ },
+ r: observedReleases{
+ 1: {
+ Name: mockReleaseName,
+ Version: 1,
+ ChartMetadata: chart.Metadata{
+ Name: mockReleaseName,
+ Version: "1.0.0",
+ },
+ },
+ 2: {
+ Name: mockReleaseName,
+ Version: 2,
+ ChartMetadata: chart.Metadata{
+ Name: mockReleaseName,
+ Version: "2.0.0",
+ },
+ },
+ },
+ mutate: true,
+ testFunc: func(obj *v2.HelmRelease) error {
+ if len(obj.Status.History) != 1 {
+ return fmt.Errorf("want history length 1, got %d", len(obj.Status.History))
+ }
+ h := obj.Status.History.Latest()
+ if h.Name != mockReleaseName {
+ return fmt.Errorf("release name is not %s", mockReleaseName)
+ }
+ if h.ChartVersion != "2.0.0" {
+ return fmt.Errorf("want chart version %s, got %s", "2.0.0", h.ChartVersion)
+ }
+ if h.OCIDigest != obj.Status.LastAttemptedRevisionDigest {
+ return fmt.Errorf("want digest %s, got %s", obj.Status.LastAttemptedRevisionDigest, h.OCIDigest)
+ }
+ return nil
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ g := NewWithT(t)
+
+ if tt.mutate {
+ tt.r.recordOnObject(tt.obj, mutateOCIDigest)
+ } else {
+ tt.r.recordOnObject(tt.obj)
+ }
+ err := tt.testFunc(tt.obj)
+ g.Expect(err).ToNot(HaveOccurred())
+ })
+ }
+}
+
+func Test_RecordOnObject_withAction(t *testing.T) {
+ g := NewWithT(t)
+
+ obj := &v2.HelmRelease{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: mockReleaseName,
+ Namespace: mockReleaseNamespace,
+ },
+ }
+ r := observedReleases{
+ 1: {
+ Name: mockReleaseName,
+ Version: 1,
+ ChartMetadata: chart.Metadata{
+ Name: mockReleaseName,
+ Version: "1.0.0",
+ },
+ },
+ }
+ r.recordOnObject(obj, mutateOCIDigest, mutateAction(v2.ReleaseActionInstall))
+
+ g.Expect(obj.Status.History).To(HaveLen(1))
+ g.Expect(obj.Status.History[0].Action).To(Equal(v2.ReleaseActionInstall))
+ g.Expect(obj.Status.History[0].Name).To(Equal(mockReleaseName))
+}
diff --git a/internal/reconcile/rollback_remediation.go b/internal/reconcile/rollback_remediation.go
new file mode 100644
index 000000000..a795e5e04
--- /dev/null
+++ b/internal/reconcile/rollback_remediation.go
@@ -0,0 +1,200 @@
+/*
+Copyright 2022 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package reconcile
+
+import (
+ "context"
+ "fmt"
+ "strings"
+
+ helmrelease "helm.sh/helm/v4/pkg/release"
+ helmreleasev1 "helm.sh/helm/v4/pkg/release/v1"
+ corev1 "k8s.io/api/core/v1"
+ "k8s.io/client-go/tools/record"
+
+ "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/release"
+ "github.com/fluxcd/helm-controller/internal/storage"
+ "github.com/fluxcd/pkg/chartutil"
+)
+
+// RollbackRemediation is an ActionReconciler which attempts to roll back
+// a Request.Object to a previous successful deployed release in the
+// Status.History.
+//
+// The writes to the Helm storage during the rollback are observed, and update
+// the Status.History field.
+//
+// After a successful rollback, the object is marked with Remediated=True and
+// an event is emitted. When the rollback fails, the object is marked with
+// Remediated=False and a warning event is emitted.
+//
+// When the Request.Object does not have a (successful) previous deployed
+// release, it returns an error of type ErrMissingRollbackTarget. In addition,
+// it returns ErrReleaseMismatch if the name and/or namespace of the latest and
+// previous release do not match. Any other returned error indicates the caller
+// should retry as it did not cause a change to the Helm storage.
+//
+// At the end of the reconciliation, the Status.Conditions are summarized and
+// propagated to the Ready condition on the Request.Object.
+//
+// The caller is assumed to have verified the integrity of Request.Object using
+// e.g. action.VerifySnapshot before calling Reconcile.
+type RollbackRemediation struct {
+ configFactory *action.ConfigFactory
+ eventRecorder record.EventRecorder
+}
+
+// NewRollbackRemediation returns a new RollbackRemediation reconciler
+// configured with the provided values.
+func NewRollbackRemediation(configFactory *action.ConfigFactory, eventRecorder record.EventRecorder) *RollbackRemediation {
+ return &RollbackRemediation{
+ configFactory: configFactory,
+ eventRecorder: eventRecorder,
+ }
+}
+
+func (r *RollbackRemediation) Reconcile(ctx context.Context, req *Request) error {
+ var (
+ cur = req.Object.Status.History.Latest().DeepCopy()
+ logBuf = action.NewDebugLogBuffer(ctx)
+ cfg = r.configFactory.Build(logBuf, observeRollback(req.Object), observeInventory(req.Object, req.Chart, r.configFactory.Getter, r.eventRecorder))
+ )
+
+ defer summarize(req)
+
+ // Previous is required to determine what version to roll back to.
+ prev := req.Object.Status.History.Previous(req.Object.GetUpgrade().GetRemediation().MustIgnoreTestFailures(req.Object.GetTest().IgnoreFailures))
+ if prev == nil {
+ return fmt.Errorf("%w: required to rollback", ErrMissingRollbackTarget)
+ }
+
+ // Confirm previous and current point to the same release.
+ if prev.Name != cur.Name || prev.Namespace != cur.Namespace {
+ return fmt.Errorf("%w: previous release name or namespace %s does not match current %s",
+ ErrReleaseMismatch, prev.FullReleaseName(), cur.FullReleaseName())
+ }
+
+ // Run the Helm rollback action.
+ if err := action.Rollback(cfg, req.Object, prev.Name, prev.Version); err != nil {
+ r.failure(req, prev, logBuf, err)
+
+ // Return error if we did not store a release, as this does not
+ // affect state and the caller should e.g. retry.
+ if newCur := req.Object.Status.History.Latest(); newCur == nil || (cur != nil && newCur.Digest == cur.Digest) {
+ return err
+ }
+
+ return nil
+ }
+
+ r.success(req, prev)
+ return nil
+}
+
+func (r *RollbackRemediation) Name() string {
+ return "rollback"
+}
+
+func (r *RollbackRemediation) Type() ReconcilerType {
+ return ReconcilerTypeRemediate
+}
+
+const (
+ // fmtRollbackRemediationFailure is the message format for a rollback
+ // remediation failure.
+ fmtRollbackRemediationFailure = "Helm rollback to previous release %s with chart %s failed: %s"
+ // fmtRollbackRemediationSuccess is the message format for a successful
+ // rollback remediation.
+ fmtRollbackRemediationSuccess = "Helm rollback to previous release %s with chart %s succeeded"
+)
+
+// failure records the failure of a Helm rollback action in the status of the
+// given Request.Object by marking Remediated=False and emitting a warning
+// event.
+func (r *RollbackRemediation) failure(req *Request, prev *v2.Snapshot, buffer *action.LogBuffer, err error) {
+ // Compose failure message.
+ msg := fmt.Sprintf(fmtRollbackRemediationFailure, prev.FullReleaseName(), prev.VersionedChartName(), strings.TrimSpace(err.Error()))
+
+ // Mark remediation failure on object.
+ req.Object.Status.Failures++
+ conditions.MarkFalse(req.Object, v2.RemediatedCondition, v2.RollbackFailedReason, "%s", msg)
+
+ // Record warning event, this message contains more data than the
+ // Condition summary.
+ r.eventRecorder.AnnotatedEventf(
+ req.Object,
+ eventMeta(prev.ChartVersion, chartutil.DigestValues(digest.Canonical, req.Values).String(),
+ addAppVersion(prev.AppVersion), addOCIDigest(prev.OCIDigest)),
+ corev1.EventTypeWarning,
+ v2.RollbackFailedReason,
+ eventMessageWithLog(msg, buffer),
+ )
+}
+
+// success records the success of a Helm rollback action in the status of the
+// given Request.Object by marking Remediated=True and emitting an event.
+func (r *RollbackRemediation) success(req *Request, prev *v2.Snapshot) {
+ // Compose success message.
+ msg := fmt.Sprintf(fmtRollbackRemediationSuccess, prev.FullReleaseName(), prev.VersionedChartName())
+
+ // Mark remediation success on object.
+ conditions.MarkTrue(req.Object, v2.RemediatedCondition, v2.RollbackSucceededReason, "%s", msg)
+
+ // Record event.
+ r.eventRecorder.AnnotatedEventf(
+ req.Object,
+ eventMeta(prev.ChartVersion, chartutil.DigestValues(digest.Canonical, req.Values).String(),
+ addAppVersion(prev.AppVersion), addOCIDigest(prev.OCIDigest)),
+ corev1.EventTypeNormal,
+ v2.RollbackSucceededReason,
+ msg,
+ )
+}
+
+// observeRollback returns a storage.ObserveFunc to track the rollback history
+// of a HelmRelease.
+// It observes the rollback action of a Helm release by comparing the release
+// history with the recorded snapshots.
+// If the rolled-back release matches a snapshot, it updates the snapshot with
+// the observed release data.
+// If no matching snapshot is found, it creates a new snapshot and prepends it
+// to the release history.
+func observeRollback(obj *v2.HelmRelease) storage.ObserveFunc {
+ return func(rlsr helmrelease.Releaser) {
+ rls, ok := rlsr.(*helmreleasev1.Release)
+ if !ok {
+ return
+ }
+ for i := range obj.Status.History {
+ snap := obj.Status.History[i]
+ if snap.Targets(rls.Name, rls.Namespace, rls.Version) {
+ newSnap := release.ObservedToSnapshot(releaseToObservation(rls, snap, v2.ReleaseActionRollback))
+ newSnap.SetTestHooks(snap.GetTestHooks())
+ obj.Status.History[i] = newSnap
+ return
+ }
+ }
+
+ obs := release.ObserveRelease(rls)
+ obj.Status.History = append(v2.Snapshots{release.ObservedToSnapshot(obs)}, obj.Status.History...)
+ }
+}
diff --git a/internal/reconcile/rollback_remediation_test.go b/internal/reconcile/rollback_remediation_test.go
new file mode 100644
index 000000000..43c85ce85
--- /dev/null
+++ b/internal/reconcile/rollback_remediation_test.go
@@ -0,0 +1,722 @@
+/*
+Copyright 2022 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package reconcile
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "strings"
+ "testing"
+ "time"
+
+ . "github.com/onsi/gomega"
+ helmreleasecommon "helm.sh/helm/v4/pkg/release/common"
+ helmrelease "helm.sh/helm/v4/pkg/release/v1"
+ helmreleaseutil "helm.sh/helm/v4/pkg/release/v1/util"
+ helmstorage "helm.sh/helm/v4/pkg/storage"
+ helmdriver "helm.sh/helm/v4/pkg/storage/driver"
+ corev1 "k8s.io/api/core/v1"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/client-go/tools/record"
+
+ eventv1 "github.com/fluxcd/pkg/apis/event/v1beta1"
+ "github.com/fluxcd/pkg/apis/meta"
+ "github.com/fluxcd/pkg/runtime/conditions"
+
+ "github.com/fluxcd/pkg/chartutil"
+
+ 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/release"
+ "github.com/fluxcd/helm-controller/internal/storage"
+ "github.com/fluxcd/helm-controller/internal/testutil"
+)
+
+func TestRollbackRemediation_Reconcile(t *testing.T) {
+ var (
+ mockCreateErr = fmt.Errorf("storage create error")
+ mockUpdateErr = fmt.Errorf("storage update error")
+ )
+
+ tests := []struct {
+ name string
+ // driver allows for modifying the Helm storage driver.
+ driver func(driver helmdriver.Driver) helmdriver.Driver
+ // releases is the list of releases that are stored in the driver
+ // before rollback.
+ releases func(namespace string) []*helmrelease.Release
+ // spec modifies the HelmRelease object's spec before rollback.
+ spec func(spec *v2.HelmReleaseSpec)
+ // status to configure on the HelmRelease before rollback.
+ status func(releases []*helmrelease.Release) v2.HelmReleaseStatus
+ // wantErr is the error that is expected to be returned.
+ wantErr error
+ // expectedConditions are the conditions that are expected to be set on
+ // the HelmRelease after rolling back.
+ expectConditions []metav1.Condition
+ // expectHistory is the expected History on the HelmRelease after
+ // rolling back.
+ expectHistory func(releases []*helmrelease.Release) v2.Snapshots
+ // expectFailures is the expected Failures count on the HelmRelease.
+ expectFailures int64
+ // expectInstallFailures is the expected InstallFailures count on the
+ // HelmRelease.
+ expectInstallFailures int64
+ // expectUpgradeFailures is the expected UpgradeFailures count on the
+ // HelmRelease.
+ expectUpgradeFailures int64
+ // statusReader is an optional StatusReader to configure on the
+ // ConfigFactory.
+ statusReader bool
+ }{
+ {
+ name: "rollback",
+ releases: func(namespace string) []*helmrelease.Release {
+ return []*helmrelease.Release{
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Version: 1,
+ Chart: testutil.BuildChart(),
+ Status: helmreleasecommon.StatusSuperseded,
+ Namespace: namespace,
+ }),
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Version: 2,
+ Chart: testutil.BuildChart(),
+ Status: helmreleasecommon.StatusFailed,
+ Namespace: namespace,
+ }),
+ }
+ },
+ status: func(releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ return v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(releases[1])),
+ release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
+ },
+ }
+ },
+ expectConditions: []metav1.Condition{
+ *conditions.FalseCondition(meta.ReadyCondition, v2.RollbackSucceededReason, "succeeded"),
+ *conditions.TrueCondition(v2.RemediatedCondition, v2.RollbackSucceededReason, "succeeded"),
+ },
+ expectHistory: func(releases []*helmrelease.Release) v2.Snapshots {
+ return v2.Snapshots{
+ observeReleaseWithAction(releases[2], v2.ReleaseActionRollback),
+ release.ObservedToSnapshot(release.ObserveRelease(releases[1])),
+ release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
+ }
+ },
+ },
+ {
+ name: "rollback without previous target release",
+ releases: func(namespace string) []*helmrelease.Release {
+ return []*helmrelease.Release{
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Version: 1,
+ Chart: testutil.BuildChart(),
+ Status: helmreleasecommon.StatusSuperseded,
+ Namespace: namespace,
+ }),
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Version: 2,
+ Chart: testutil.BuildChart(),
+ Status: helmreleasecommon.StatusFailed,
+ Namespace: namespace,
+ }),
+ }
+ },
+ status: func(releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ return v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(releases[1])),
+ },
+ }
+ },
+ wantErr: ErrMissingRollbackTarget,
+ expectHistory: func(releases []*helmrelease.Release) v2.Snapshots {
+ return v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(releases[1])),
+ }
+ },
+ },
+ {
+ name: "rollback failure",
+ releases: func(namespace string) []*helmrelease.Release {
+ return []*helmrelease.Release{
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Version: 1,
+ Chart: testutil.BuildChart(testutil.ChartWithFailingHook()),
+ Status: helmreleasecommon.StatusSuperseded,
+ Namespace: namespace,
+ }, testutil.ReleaseWithFailingHook()),
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Version: 2,
+ Chart: testutil.BuildChart(),
+ Status: helmreleasecommon.StatusFailed,
+ Namespace: namespace,
+ }),
+ }
+ },
+ status: func(releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ return v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(releases[1])),
+ release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
+ },
+ }
+ },
+ expectConditions: []metav1.Condition{
+ *conditions.FalseCondition(meta.ReadyCondition, v2.RollbackFailedReason,
+ "context deadline exceeded"),
+ *conditions.FalseCondition(v2.RemediatedCondition, v2.RollbackFailedReason,
+ "context deadline exceeded"),
+ },
+ expectHistory: func(releases []*helmrelease.Release) v2.Snapshots {
+ return v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(releases[2])),
+ release.ObservedToSnapshot(release.ObserveRelease(releases[1])),
+ release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
+ }
+ },
+ expectFailures: 1,
+ },
+ {
+ name: "rollback with storage create error",
+ driver: func(driver helmdriver.Driver) helmdriver.Driver {
+ return &storage.Failing{
+ Driver: driver,
+ CreateErr: mockCreateErr,
+ }
+ },
+ releases: func(namespace string) []*helmrelease.Release {
+ return []*helmrelease.Release{
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Version: 1,
+ Chart: testutil.BuildChart(),
+ Status: helmreleasecommon.StatusSuperseded,
+ Namespace: namespace,
+ }),
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Version: 2,
+ Chart: testutil.BuildChart(),
+ Status: helmreleasecommon.StatusFailed,
+ Namespace: namespace,
+ }),
+ }
+ },
+ status: func(releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ return v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(releases[1])),
+ release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
+ },
+ }
+ },
+ wantErr: mockCreateErr,
+ expectConditions: []metav1.Condition{
+ *conditions.FalseCondition(meta.ReadyCondition, v2.RollbackFailedReason,
+ "%s", mockCreateErr.Error()),
+ *conditions.FalseCondition(v2.RemediatedCondition, v2.RollbackFailedReason,
+ "%s", mockCreateErr.Error()),
+ },
+ expectHistory: func(releases []*helmrelease.Release) v2.Snapshots {
+ return v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(releases[1])),
+ release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
+ }
+ },
+ expectFailures: 1,
+ },
+ {
+ name: "rollback with storage update error",
+ driver: func(driver helmdriver.Driver) helmdriver.Driver {
+ return &storage.Failing{
+ Driver: driver,
+ UpdateErr: mockUpdateErr,
+ }
+ },
+ releases: func(namespace string) []*helmrelease.Release {
+ return []*helmrelease.Release{
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Version: 1,
+ Chart: testutil.BuildChart(),
+ Status: helmreleasecommon.StatusSuperseded,
+ Namespace: namespace,
+ }),
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Version: 2,
+ Chart: testutil.BuildChart(),
+ Status: helmreleasecommon.StatusFailed,
+ Namespace: namespace,
+ }),
+ }
+ },
+ status: func(releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ return v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(releases[1])),
+ release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
+ },
+ }
+ },
+ expectConditions: []metav1.Condition{
+ *conditions.FalseCondition(meta.ReadyCondition, v2.RollbackFailedReason,
+ "storage update error"),
+ *conditions.FalseCondition(v2.RemediatedCondition, v2.RollbackFailedReason,
+ "storage update error"),
+ },
+ expectHistory: func(releases []*helmrelease.Release) v2.Snapshots {
+ return v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(releases[2])),
+ release.ObservedToSnapshot(release.ObserveRelease(releases[1])),
+ release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
+ }
+ },
+ expectFailures: 1,
+ },
+ {
+ name: "rollback with status reader",
+ releases: func(namespace string) []*helmrelease.Release {
+ return []*helmrelease.Release{
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Version: 1,
+ Chart: testutil.BuildChart(),
+ Status: helmreleasecommon.StatusSuperseded,
+ Namespace: namespace,
+ }),
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Version: 2,
+ Chart: testutil.BuildChart(),
+ Status: helmreleasecommon.StatusFailed,
+ Namespace: namespace,
+ }),
+ }
+ },
+ status: func(releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ return v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(releases[1])),
+ release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
+ },
+ }
+ },
+ statusReader: true,
+ expectConditions: []metav1.Condition{
+ *conditions.FalseCondition(meta.ReadyCondition, v2.RollbackSucceededReason, "succeeded"),
+ *conditions.TrueCondition(v2.RemediatedCondition, v2.RollbackSucceededReason, "succeeded"),
+ },
+ expectHistory: func(releases []*helmrelease.Release) v2.Snapshots {
+ return v2.Snapshots{
+ observeReleaseWithAction(releases[2], v2.ReleaseActionRollback),
+ release.ObservedToSnapshot(release.ObserveRelease(releases[1])),
+ release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
+ }
+ },
+ },
+ }
+ 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
+
+ var releases []*helmrelease.Release
+ if tt.releases != nil {
+ releases = tt.releases(releaseNamespace)
+ helmreleaseutil.SortByRevision(releases)
+ }
+
+ obj := &v2.HelmRelease{
+ Spec: v2.HelmReleaseSpec{
+ ReleaseName: mockReleaseName,
+ TargetNamespace: releaseNamespace,
+ StorageNamespace: releaseNamespace,
+ Timeout: &metav1.Duration{Duration: 100 * time.Millisecond},
+ },
+ }
+ if tt.status != nil {
+ obj.Status = tt.status(releases)
+ }
+
+ getter, err := RESTClientGetterFromManager(testEnv.Manager, obj.GetReleaseNamespace())
+ g.Expect(err).ToNot(HaveOccurred())
+
+ cfgOpts := []action.ConfigFactoryOption{
+ action.WithStorage(action.DefaultStorageDriver, obj.GetStorageNamespace()),
+ }
+ var mockSR *testutil.MockStatusReader
+ if tt.statusReader {
+ mockSR = &testutil.MockStatusReader{}
+ cfgOpts = append(cfgOpts, action.WithResourceManager(mockSR.NewResourceManagerFuncWithClient(testEnv.Client, testEnv.Manager.GetRESTMapper())))
+ }
+ cfg, err := action.NewConfigFactory(getter, cfgOpts...)
+ g.Expect(err).ToNot(HaveOccurred())
+
+ store := helmstorage.Init(cfg.Driver)
+ for _, r := range releases {
+ g.Expect(store.Create(r)).To(Succeed())
+ }
+
+ if tt.driver != nil {
+ cfg.Driver = tt.driver(cfg.Driver)
+ }
+
+ recorder := new(record.FakeRecorder)
+ got := (NewRollbackRemediation(cfg, recorder)).Reconcile(context.TODO(), &Request{
+ Object: obj,
+ })
+ if tt.wantErr != nil {
+ g.Expect(errors.Is(got, tt.wantErr)).To(BeTrue())
+ } else {
+ g.Expect(got).ToNot(HaveOccurred())
+ }
+
+ g.Expect(obj.Status.Conditions).To(conditions.MatchConditions(tt.expectConditions))
+
+ releases, _ = storeHistory(store, mockReleaseName)
+ helmreleaseutil.SortByRevision(releases)
+
+ if tt.expectHistory != nil {
+ g.Expect(obj.Status.History).To(testutil.Equal(tt.expectHistory(releases)))
+ } else {
+ g.Expect(obj.Status.History).To(BeEmpty(), "expected history to be empty")
+ }
+
+ g.Expect(obj.Status.Failures).To(Equal(tt.expectFailures))
+ g.Expect(obj.Status.InstallFailures).To(Equal(tt.expectInstallFailures))
+ g.Expect(obj.Status.UpgradeFailures).To(Equal(tt.expectUpgradeFailures))
+
+ if mockSR != nil {
+ g.Expect(mockSR.SupportsCalled()).To(BeNumerically(">", 0), "expected StatusReader.Supports to be called")
+ }
+ })
+ }
+}
+
+func TestRollbackRemediation_failure(t *testing.T) {
+ var (
+ prev = testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Chart: testutil.BuildChart(),
+ Version: 4,
+ })
+ obj = &v2.HelmRelease{
+ Status: v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(prev)),
+ },
+ },
+ }
+ err = errors.New("rollback error")
+ )
+
+ t.Run("records failure", func(t *testing.T) {
+ g := NewWithT(t)
+
+ recorder := testutil.NewFakeRecorder(10, false)
+ r := &RollbackRemediation{
+ eventRecorder: recorder,
+ }
+ req := &Request{Object: obj.DeepCopy()}
+ r.failure(req, release.ObservedToSnapshot(release.ObserveRelease(prev)), nil, err)
+
+ expectMsg := fmt.Sprintf(fmtRollbackRemediationFailure,
+ fmt.Sprintf("%s/%s.v%d", prev.Namespace, prev.Name, prev.Version),
+ fmt.Sprintf("%s@%s", prev.Chart.Name(), prev.Chart.Metadata.Version),
+ strings.TrimSpace(err.Error()))
+
+ g.Expect(req.Object.Status.Conditions).To(conditions.MatchConditions([]metav1.Condition{
+ *conditions.FalseCondition(v2.RemediatedCondition, v2.RollbackFailedReason, "%s", expectMsg),
+ }))
+ g.Expect(req.Object.Status.Failures).To(Equal(int64(1)))
+ g.Expect(recorder.GetEvents()).To(ConsistOf([]corev1.Event{
+ {
+ Type: corev1.EventTypeWarning,
+ Reason: v2.RollbackFailedReason,
+ Message: expectMsg,
+ ObjectMeta: metav1.ObjectMeta{
+ Annotations: map[string]string{
+ eventMetaGroupKey(eventv1.MetaRevisionKey): prev.Chart.Metadata.Version,
+ eventMetaGroupKey(metaAppVersionKey): prev.Chart.Metadata.AppVersion,
+ eventMetaGroupKey(eventv1.MetaTokenKey): chartutil.DigestValues(digest.Canonical, req.Values).String(),
+ },
+ },
+ },
+ }))
+ })
+
+ t.Run("records failure with logs", func(t *testing.T) {
+ g := NewWithT(t)
+
+ recorder := testutil.NewFakeRecorder(10, false)
+ r := &RollbackRemediation{
+ eventRecorder: recorder,
+ }
+ req := &Request{Object: obj.DeepCopy()}
+ r.failure(req, release.ObservedToSnapshot(release.ObserveRelease(prev)), mockLogBuffer(), err)
+
+ expectSubStr := "Last Helm logs"
+ g.Expect(conditions.IsFalse(req.Object, v2.RemediatedCondition)).To(BeTrue())
+ g.Expect(conditions.GetMessage(req.Object, v2.RemediatedCondition)).ToNot(ContainSubstring(expectSubStr))
+
+ events := recorder.GetEvents()
+ g.Expect(events).To(HaveLen(1))
+ g.Expect(events[0].Message).To(ContainSubstring(expectSubStr))
+ })
+}
+
+func TestRollbackRemediation_success(t *testing.T) {
+ g := NewWithT(t)
+
+ var prev = testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Chart: testutil.BuildChart(),
+ Version: 4,
+ })
+
+ recorder := testutil.NewFakeRecorder(10, false)
+ r := &RollbackRemediation{
+ eventRecorder: recorder,
+ }
+ req := &Request{Object: &v2.HelmRelease{}, Values: map[string]any{"foo": "bar"}}
+ r.success(req, release.ObservedToSnapshot(release.ObserveRelease(prev)))
+
+ expectMsg := fmt.Sprintf(fmtRollbackRemediationSuccess,
+ fmt.Sprintf("%s/%s.v%d", prev.Namespace, prev.Name, prev.Version),
+ fmt.Sprintf("%s@%s", prev.Chart.Name(), prev.Chart.Metadata.Version))
+
+ g.Expect(req.Object.Status.Conditions).To(conditions.MatchConditions([]metav1.Condition{
+ *conditions.TrueCondition(v2.RemediatedCondition, v2.RollbackSucceededReason, "%s", expectMsg),
+ }))
+ g.Expect(req.Object.Status.Failures).To(Equal(int64(0)))
+ g.Expect(recorder.GetEvents()).To(ConsistOf([]corev1.Event{
+ {
+ Type: corev1.EventTypeNormal,
+ Reason: v2.RollbackSucceededReason,
+ Message: expectMsg,
+ ObjectMeta: metav1.ObjectMeta{
+ Annotations: map[string]string{
+ eventMetaGroupKey(eventv1.MetaRevisionKey): prev.Chart.Metadata.Version,
+ eventMetaGroupKey(metaAppVersionKey): prev.Chart.Metadata.AppVersion,
+ eventMetaGroupKey(eventv1.MetaTokenKey): chartutil.DigestValues(digest.Canonical, req.Values).String(),
+ },
+ },
+ },
+ }))
+}
+
+func Test_observeRollback(t *testing.T) {
+ t.Run("rollback", func(t *testing.T) {
+ g := NewWithT(t)
+
+ obj := &v2.HelmRelease{}
+ rls := helmrelease.Mock(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: mockReleaseNamespace,
+ Version: 2,
+ Status: helmreleasecommon.StatusPendingRollback,
+ })
+ observeRollback(obj)(rls)
+ expect := release.ObservedToSnapshot(release.ObserveRelease(rls))
+
+ g.Expect(obj.Status.History).To(testutil.Equal(v2.Snapshots{
+ expect,
+ }))
+ })
+
+ t.Run("rollback with latest", func(t *testing.T) {
+ g := NewWithT(t)
+
+ latest := &v2.Snapshot{
+ Name: mockReleaseName,
+ Namespace: mockReleaseNamespace,
+ Version: 2,
+ Status: helmreleasecommon.StatusFailed.String(),
+ }
+ obj := &v2.HelmRelease{
+ Status: v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ latest,
+ },
+ },
+ }
+ rls := helmrelease.Mock(&helmrelease.MockReleaseOptions{
+ Name: latest.Name,
+ Namespace: latest.Namespace,
+ Version: latest.Version + 1,
+ Status: helmreleasecommon.StatusPendingRollback,
+ })
+ expect := release.ObservedToSnapshot(release.ObserveRelease(rls))
+
+ observeRollback(obj)(rls)
+ g.Expect(obj.Status.History).To(testutil.Equal(v2.Snapshots{
+ expect,
+ latest,
+ }))
+ })
+
+ t.Run("rollback with update to previous deployed", func(t *testing.T) {
+ g := NewWithT(t)
+
+ previous := &v2.Snapshot{
+ Name: mockReleaseName,
+ Namespace: mockReleaseNamespace,
+ Version: 2,
+ Status: helmreleasecommon.StatusFailed.String(),
+ }
+ latest := &v2.Snapshot{
+ Name: mockReleaseName,
+ Namespace: mockReleaseNamespace,
+ Version: 3,
+ Status: helmreleasecommon.StatusDeployed.String(),
+ }
+
+ obj := &v2.HelmRelease{
+ Status: v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ latest,
+ previous,
+ },
+ },
+ }
+ rls := helmrelease.Mock(&helmrelease.MockReleaseOptions{
+ Name: previous.Name,
+ Namespace: previous.Namespace,
+ Version: previous.Version,
+ Status: helmreleasecommon.StatusSuperseded,
+ })
+ obs := release.ObserveRelease(rls)
+ obs.Action = v2.ReleaseActionRollback
+ expect := release.ObservedToSnapshot(obs)
+
+ observeRollback(obj)(rls)
+ g.Expect(obj.Status.History).To(testutil.Equal(v2.Snapshots{
+ latest,
+ expect,
+ }))
+ })
+
+ t.Run("rollback with update to previous deployed copies existing test hooks", func(t *testing.T) {
+ g := NewWithT(t)
+
+ previous := &v2.Snapshot{
+ Name: mockReleaseName,
+ Namespace: mockReleaseNamespace,
+ Version: 2,
+ Status: helmreleasecommon.StatusFailed.String(),
+ TestHooks: &map[string]*v2.TestHookStatus{
+ "test-hook": {
+ Phase: helmrelease.HookPhaseSucceeded.String(),
+ },
+ },
+ }
+ latest := &v2.Snapshot{
+ Name: mockReleaseName,
+ Namespace: mockReleaseNamespace,
+ Version: 3,
+ Status: helmreleasecommon.StatusDeployed.String(),
+ }
+
+ obj := &v2.HelmRelease{
+ Status: v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ latest,
+ previous,
+ },
+ },
+ }
+ rls := helmrelease.Mock(&helmrelease.MockReleaseOptions{
+ Name: previous.Name,
+ Namespace: previous.Namespace,
+ Version: previous.Version,
+ Status: helmreleasecommon.StatusSuperseded,
+ })
+ obs := release.ObserveRelease(rls)
+ obs.Action = v2.ReleaseActionRollback
+ expect := release.ObservedToSnapshot(obs)
+ expect.SetTestHooks(previous.GetTestHooks())
+
+ observeRollback(obj)(rls)
+ g.Expect(obj.Status.History).To(testutil.Equal(v2.Snapshots{
+ latest,
+ expect,
+ }))
+ })
+
+ t.Run("rollback with update to previous deployed with OCI Digest", func(t *testing.T) {
+ g := NewWithT(t)
+
+ previous := &v2.Snapshot{
+ Name: mockReleaseName,
+ Namespace: mockReleaseNamespace,
+ Version: 2,
+ Status: helmreleasecommon.StatusFailed.String(),
+ OCIDigest: "sha256:fcdc2b0de1581a3633ada4afee3f918f6eaa5b5ab38c3fef03d5b48d3f85d9f6",
+ }
+ latest := &v2.Snapshot{
+ Name: mockReleaseName,
+ Namespace: mockReleaseNamespace,
+ Version: 3,
+ Status: helmreleasecommon.StatusDeployed.String(),
+ OCIDigest: "sha256:aedc2b0de1576a3633ada4afee3f918f6eaa5b5ab38c3fef03d5b48d3f85d9f6",
+ }
+
+ obj := &v2.HelmRelease{
+ Status: v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ latest,
+ previous,
+ },
+ },
+ }
+ rls := helmrelease.Mock(&helmrelease.MockReleaseOptions{
+ Name: previous.Name,
+ Namespace: previous.Namespace,
+ Version: previous.Version,
+ Status: helmreleasecommon.StatusSuperseded,
+ })
+ obs := release.ObserveRelease(rls)
+ obs.OCIDigest = "sha256:fcdc2b0de1581a3633ada4afee3f918f6eaa5b5ab38c3fef03d5b48d3f85d9f6"
+ obs.Action = v2.ReleaseActionRollback
+ expect := release.ObservedToSnapshot(obs)
+
+ observeRollback(obj)(rls)
+ g.Expect(obj.Status.History).To(testutil.Equal(v2.Snapshots{
+ latest,
+ expect,
+ }))
+ })
+}
diff --git a/internal/reconcile/state.go b/internal/reconcile/state.go
new file mode 100644
index 000000000..e78b6e2af
--- /dev/null
+++ b/internal/reconcile/state.go
@@ -0,0 +1,210 @@
+/*
+Copyright 2022 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package reconcile
+
+import (
+ "context"
+ "errors"
+ "fmt"
+
+ "github.com/fluxcd/pkg/ssa/jsondiff"
+ "helm.sh/helm/v4/pkg/kube"
+ helmreleasecommon "helm.sh/helm/v4/pkg/release/common"
+ helmrelease "helm.sh/helm/v4/pkg/release/v1"
+ 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
+// comparing the Helm storage with the v2.HelmRelease object.
+type ReleaseStatus string
+
+// String returns the string representation of the release status.
+func (s ReleaseStatus) String() string {
+ return string(s)
+}
+
+const (
+ // ReleaseStatusUnknown indicates that the status of the release could not
+ // be determined.
+ ReleaseStatusUnknown ReleaseStatus = "Unknown"
+ // ReleaseStatusAbsent indicates that the release is not present in the
+ // Helm storage.
+ ReleaseStatusAbsent ReleaseStatus = "Absent"
+ // ReleaseStatusUnmanaged indicates that the release is present in the Helm
+ // storage, but is not managed by the v2.HelmRelease object.
+ ReleaseStatusUnmanaged ReleaseStatus = "Unmanaged"
+ // ReleaseStatusOutOfSync indicates that the release is present in the Helm
+ // storage, but is not in sync with the v2.HelmRelease object.
+ ReleaseStatusOutOfSync ReleaseStatus = "OutOfSync"
+ // ReleaseStatusDrifted indicates that the release is present in the Helm
+ // storage, but the cluster state has drifted from the manifest in the
+ // storage.
+ ReleaseStatusDrifted ReleaseStatus = "Drifted"
+ // ReleaseStatusLocked indicates that the release is present in the Helm
+ // storage, but is locked.
+ ReleaseStatusLocked ReleaseStatus = "Locked"
+ // ReleaseStatusUntested indicates that the release is present in the Helm
+ // storage, but has not been tested.
+ ReleaseStatusUntested ReleaseStatus = "Untested"
+ // ReleaseStatusInSync indicates that the release is present in the Helm
+ // storage, and is in sync with the v2.HelmRelease object.
+ ReleaseStatusInSync ReleaseStatus = "InSync"
+ // ReleaseStatusFailed indicates that the release is present in the Helm
+ // storage, but has failed.
+ ReleaseStatusFailed ReleaseStatus = "Failed"
+)
+
+// ReleaseState represents the state of a Helm release as determined by
+// comparing the Helm storage with the v2.HelmRelease object.
+type ReleaseState struct {
+ // Status is the status of the release.
+ Status ReleaseStatus
+ // Reason for the Status.
+ Reason string
+ // Diff contains any differences between the Helm storage manifest and the
+ // cluster state when Status equals ReleaseStatusDrifted.
+ Diff jsondiff.DiffSet
+}
+
+// DetermineReleaseState determines the state of the Helm release as compared
+// to the v2.HelmRelease object. It returns a ReleaseState that indicates
+// the status of the release, and an error if the state could not be determined.
+func DetermineReleaseState(ctx context.Context, cfg *action.ConfigFactory, req *Request, disallowedFieldManagers []string) (ReleaseState, error) {
+ rls, err := action.LastRelease(cfg.Build(nil), req.Object.GetReleaseName())
+ if err != nil {
+ if errors.Is(err, action.ErrReleaseNotFound) {
+ return ReleaseState{Status: ReleaseStatusAbsent, Reason: "no release in storage for object"}, nil
+ }
+ return ReleaseState{Status: ReleaseStatusUnknown}, fmt.Errorf("failed to retrieve last release from storage: %w", err)
+ }
+
+ // If the release is in a pending state, it must be unlocked before any
+ // further action can be taken.
+ if rls.Info.Status.IsPending() {
+ return ReleaseState{Status: ReleaseStatusLocked, Reason: fmt.Sprintf("release with status '%s'", rls.Info.Status)}, err
+ }
+
+ // Confirm we have a release object to compare against.
+ if req.Object.Status.History.Len() == 0 {
+ if rls.Info.Status == helmreleasecommon.StatusUninstalled {
+ return ReleaseState{Status: ReleaseStatusAbsent, Reason: "found uninstalled release in storage"}, nil
+ }
+ return ReleaseState{Status: ReleaseStatusUnmanaged, Reason: "found existing release in storage"}, err
+ }
+
+ // Verify the release object against the state we observed during our
+ // last reconciliation.
+ cur := req.Object.Status.History.Latest()
+ if err := action.VerifyReleaseObject(cur, rls); err != nil {
+ if interrors.IsOneOf(err, action.ErrReleaseDigest, action.ErrReleaseNotObserved) {
+ // The release object has been mutated in such a way that we are
+ // unable to determine the state of the release.
+ // Effectively, this means that the object no longer manages the
+ // release, and we should e.g. perform an upgrade to bring
+ // the release back in-sync and under management.
+ return ReleaseState{Status: ReleaseStatusUnmanaged, Reason: err.Error()}, nil
+ }
+ return ReleaseState{Status: ReleaseStatusUnknown}, fmt.Errorf("failed to verify release object: %w", err)
+ }
+
+ // Further determine the state of the release based on the Helm release
+ // status, which can now be considered reliable.
+ switch rls.Info.Status {
+ case helmreleasecommon.StatusFailed:
+ return ReleaseState{Status: ReleaseStatusFailed}, nil
+ case helmreleasecommon.StatusUninstalled:
+ return ReleaseState{Status: ReleaseStatusAbsent, Reason: "found uninstalled release in storage"}, nil
+ case helmreleasecommon.StatusDeployed:
+ // Verify the release is in sync with the desired configuration.
+ if err = action.VerifyRelease(rls, cur, req.Chart.Metadata, req.Values); err != nil {
+ switch err {
+ case action.ErrChartChanged, action.ErrConfigDigest:
+ return ReleaseState{Status: ReleaseStatusOutOfSync, Reason: err.Error()}, nil
+ default:
+ return ReleaseState{Status: ReleaseStatusUnknown}, err
+ }
+ }
+
+ // Verify if postrender digest or common metadata digest has changed
+ // for new generations only. The observed digests are updated after
+ // each successful release action, so comparing here will not cause
+ // an infinite loop within the same reconciliation.
+ //
+ // We use the top-level status.observedGeneration rather than the
+ // Ready condition's ObservedGeneration, because the latter can be
+ // inadvertently advanced by the patch helper's conflict resolution
+ // (patchStatusConditions re-fetches the object from the API server,
+ // and conditions.Set always sets ObservedGeneration to the latest
+ // metadata.generation).
+ if req.Object.Status.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
+ }
+ var commonMetadataDigest string
+ if req.Object.Spec.CommonMetadata != nil {
+ commonMetadataDigest = postrender.CommonMetadataDigest(digest.Canonical, req.Object.Spec.CommonMetadata).String()
+ }
+ if commonMetadataDigest != req.Object.Status.ObservedCommonMetadataDigest {
+ return ReleaseState{Status: ReleaseStatusOutOfSync, Reason: "common metadata 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`.
+ if testSpec := req.Object.GetTest(); testSpec.Enable {
+ // Confirm the release has been tested if enabled.
+ if !cur.HasBeenTested() {
+ return ReleaseState{Status: ReleaseStatusUntested}, nil
+ }
+
+ // Act on any observed test failure.
+ remediation := req.Object.GetActiveRemediation()
+ if remediation != nil && !remediation.MustIgnoreTestFailures(testSpec.IgnoreFailures) && cur.HasTestInPhase(helmrelease.HookPhaseFailed.String()) {
+ return ReleaseState{Status: ReleaseStatusFailed, Reason: "release has test in failed phase"}, nil
+ }
+ }
+
+ // Confirm the cluster state matches the desired config.
+ if diffOpts := req.Object.GetDriftDetection(); diffOpts.MustDetectChanges() {
+ diffSet, err := action.Diff(ctx, cfg.Build(nil), rls, kube.ManagedFieldsManager, disallowedFieldManagers, req.Object.GetDriftDetection().Ignore...)
+ hasChanges := diffSet.HasChanges()
+ if err != nil {
+ if !hasChanges {
+ return ReleaseState{Status: ReleaseStatusUnknown}, fmt.Errorf("unable to determine cluster state: %w", err)
+ }
+ ctrl.LoggerFrom(ctx).Error(err, "diff of release against cluster state completed with error")
+ }
+ if hasChanges {
+ return ReleaseState{Status: ReleaseStatusDrifted, Diff: diffSet}, nil
+ }
+ }
+
+ return ReleaseState{Status: ReleaseStatusInSync}, nil
+ default:
+ return ReleaseState{Status: ReleaseStatusUnknown}, fmt.Errorf("unable to determine state for release with status '%s'", rls.Info.Status)
+ }
+}
diff --git a/internal/reconcile/state_test.go b/internal/reconcile/state_test.go
new file mode 100644
index 000000000..9c3fed1ac
--- /dev/null
+++ b/internal/reconcile/state_test.go
@@ -0,0 +1,815 @@
+/*
+Copyright 2022 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package reconcile
+
+import (
+ "context"
+ "strings"
+ "testing"
+
+ . "github.com/onsi/gomega"
+ helmchartutil "helm.sh/helm/v4/pkg/chart/common"
+ helmchart "helm.sh/helm/v4/pkg/chart/v2"
+ helmreleasecommon "helm.sh/helm/v4/pkg/release/common"
+ helmrelease "helm.sh/helm/v4/pkg/release/v1"
+ helmstorage "helm.sh/helm/v4/pkg/storage"
+ helmdriver "helm.sh/helm/v4/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"
+
+ 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"
+)
+
+func Test_DetermineReleaseState(t *testing.T) {
+ tests := []struct {
+ name string
+ releases []*helmrelease.Release
+ spec func(spec *v2.HelmReleaseSpec)
+ status func(releases []*helmrelease.Release) v2.HelmReleaseStatus
+ chart *helmchart.Chart
+ values helmchartutil.Values
+ want ReleaseState
+ wantErr bool
+ }{
+ {
+ name: "in-sync release",
+ releases: []*helmrelease.Release{
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: mockReleaseNamespace,
+ Version: 1,
+ Status: helmreleasecommon.StatusDeployed,
+ Chart: testutil.BuildChart(),
+ }, testutil.ReleaseWithConfig(map[string]any{"foo": "bar"})),
+ },
+ status: func(releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ return v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
+ },
+ }
+ },
+ chart: testutil.BuildChart(),
+ values: map[string]any{"foo": "bar"},
+ want: ReleaseState{
+ Status: ReleaseStatusInSync,
+ },
+ },
+ {
+ name: "no release in storage",
+ releases: nil,
+ want: ReleaseState{
+ Status: ReleaseStatusAbsent,
+ },
+ },
+ {
+ name: "release disappeared from storage",
+ status: func(_ []*helmrelease.Release) v2.HelmReleaseStatus {
+ return v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: mockReleaseNamespace,
+ Version: 1,
+ Status: helmreleasecommon.StatusDeployed,
+ Chart: testutil.BuildChart(),
+ }))),
+ },
+ }
+ },
+ want: ReleaseState{
+ Status: ReleaseStatusAbsent,
+ },
+ },
+ {
+ name: "existing release without current",
+ releases: []*helmrelease.Release{
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: mockReleaseNamespace,
+ Version: 1,
+ Status: helmreleasecommon.StatusDeployed,
+ Chart: testutil.BuildChart(),
+ }),
+ },
+ want: ReleaseState{
+ Status: ReleaseStatusUnmanaged,
+ },
+ },
+ {
+ name: "release digest parse error",
+ releases: []*helmrelease.Release{
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: mockReleaseNamespace,
+ Version: 1,
+ Status: helmreleasecommon.StatusDeployed,
+ Chart: testutil.BuildChart(),
+ }, testutil.ReleaseWithConfig(map[string]any{"foo": "bar"})),
+ },
+ status: func(releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ cur := release.ObservedToSnapshot(release.ObserveRelease(releases[0]))
+ cur.Digest = "sha256:invalid"
+ return v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ cur,
+ },
+ }
+ },
+ chart: testutil.BuildChart(),
+ values: map[string]any{"foo": "bar"},
+ want: ReleaseState{
+ Status: ReleaseStatusUnmanaged,
+ },
+ },
+ {
+ name: "release digest mismatch",
+ releases: []*helmrelease.Release{
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: mockReleaseNamespace,
+ Version: 1,
+ Status: helmreleasecommon.StatusDeployed,
+ Chart: testutil.BuildChart(),
+ }, testutil.ReleaseWithConfig(map[string]any{"foo": "bar"})),
+ },
+ status: func(releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ cur := release.ObservedToSnapshot(release.ObserveRelease(releases[0]))
+ // Digest for empty string is always mismatch
+ cur.Digest = "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+ return v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ cur,
+ },
+ }
+ },
+ chart: testutil.BuildChart(),
+ values: map[string]any{"foo": "bar"},
+ want: ReleaseState{
+ Status: ReleaseStatusUnmanaged,
+ },
+ },
+ {
+ name: "release in pending state",
+ releases: []*helmrelease.Release{
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: mockReleaseNamespace,
+ Version: 1,
+ Status: helmreleasecommon.StatusPendingInstall,
+ Chart: testutil.BuildChart(),
+ }, testutil.ReleaseWithConfig(map[string]any{"foo": "bar"})),
+ },
+ chart: testutil.BuildChart(),
+ values: map[string]any{"foo": "bar"},
+ want: ReleaseState{
+ Status: ReleaseStatusLocked,
+ },
+ },
+ {
+ name: "untested release",
+ releases: []*helmrelease.Release{
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: mockReleaseNamespace,
+ Version: 1,
+ Status: helmreleasecommon.StatusDeployed,
+ Chart: testutil.BuildChart(),
+ }, testutil.ReleaseWithConfig(map[string]any{"foo": "bar"})),
+ },
+ spec: func(spec *v2.HelmReleaseSpec) {
+ spec.Test = &v2.Test{
+ Enable: true,
+ }
+ },
+ status: func(releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ return v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
+ },
+ }
+ },
+ chart: testutil.BuildChart(),
+ values: map[string]any{"foo": "bar"},
+ want: ReleaseState{
+ Status: ReleaseStatusUntested,
+ },
+ },
+ {
+ name: "failed test",
+ releases: []*helmrelease.Release{
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: mockReleaseNamespace,
+ Version: 1,
+ Status: helmreleasecommon.StatusSuperseded,
+ Chart: testutil.BuildChart(),
+ }),
+ testutil.BuildRelease(
+ &helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: mockReleaseNamespace,
+ Version: 2,
+ Status: helmreleasecommon.StatusDeployed,
+ Chart: testutil.BuildChart(),
+ },
+ testutil.ReleaseWithConfig(map[string]any{"foo": "bar"}),
+ testutil.ReleaseWithHookExecution("failure-tests", []helmrelease.HookEvent{helmrelease.HookTest},
+ helmrelease.HookPhaseFailed),
+ ),
+ },
+ spec: func(spec *v2.HelmReleaseSpec) {
+ spec.Test = &v2.Test{
+ Enable: true,
+ }
+ },
+ status: func(releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ cur := release.ObservedToSnapshot(release.ObserveRelease(releases[1]))
+ cur.SetTestHooks(release.TestHooksFromRelease(releases[1]))
+
+ return v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ cur,
+ },
+ LastAttemptedReleaseAction: v2.ReleaseActionUpgrade,
+ }
+ },
+ chart: testutil.BuildChart(),
+ values: map[string]any{"foo": "bar"},
+ want: ReleaseState{
+ Status: ReleaseStatusFailed,
+ },
+ },
+ {
+ name: "failed test with ignore failures set",
+ releases: []*helmrelease.Release{
+ testutil.BuildRelease(
+ &helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: mockReleaseNamespace,
+ Version: 2,
+ Status: helmreleasecommon.StatusDeployed,
+ Chart: testutil.BuildChart(),
+ },
+ testutil.ReleaseWithConfig(map[string]any{"foo": "bar"}),
+ testutil.ReleaseWithHookExecution("failure-tests", []helmrelease.HookEvent{helmrelease.HookTest},
+ helmrelease.HookPhaseFailed),
+ ),
+ },
+ chart: testutil.BuildChart(),
+ values: map[string]any{"foo": "bar"},
+ spec: func(spec *v2.HelmReleaseSpec) {
+ spec.Test = &v2.Test{
+ Enable: true,
+ IgnoreFailures: true,
+ }
+ },
+ status: func(releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ cur := release.ObservedToSnapshot(release.ObserveRelease(releases[0]))
+ cur.SetTestHooks(release.TestHooksFromRelease(releases[0]))
+
+ return v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ cur,
+ },
+ LastAttemptedReleaseAction: v2.ReleaseActionInstall,
+ }
+ },
+ want: ReleaseState{
+ Status: ReleaseStatusInSync,
+ },
+ },
+ {
+ name: "failed test is ignored when not made by controller",
+ releases: []*helmrelease.Release{
+ testutil.BuildRelease(
+ &helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: mockReleaseNamespace,
+ Version: 2,
+ Status: helmreleasecommon.StatusDeployed,
+ Chart: testutil.BuildChart(),
+ },
+ testutil.ReleaseWithConfig(map[string]any{"foo": "bar"}),
+ testutil.ReleaseWithHookExecution("failure-tests", []helmrelease.HookEvent{helmrelease.HookTest},
+ helmrelease.HookPhaseFailed),
+ ),
+ },
+ chart: testutil.BuildChart(),
+ values: map[string]any{"foo": "bar"},
+ spec: func(spec *v2.HelmReleaseSpec) {
+ spec.Test = &v2.Test{
+ Enable: true,
+ }
+ },
+ status: func(releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ return v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
+ },
+ }
+ },
+ want: ReleaseState{
+ Status: ReleaseStatusUntested,
+ },
+ },
+ {
+ name: "failed release",
+ releases: []*helmrelease.Release{
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: mockReleaseNamespace,
+ Version: 1,
+ Status: helmreleasecommon.StatusSuperseded,
+ Chart: testutil.BuildChart(),
+ }),
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: mockReleaseNamespace,
+ Version: 2,
+ Status: helmreleasecommon.StatusFailed,
+ Chart: testutil.BuildChart(),
+ }, testutil.ReleaseWithConfig(map[string]any{"foo": "bar"})),
+ },
+ chart: testutil.BuildChart(),
+ values: map[string]any{},
+ status: func(releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ return v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(releases[1])),
+ release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
+ },
+ }
+ },
+ want: ReleaseState{
+ Status: ReleaseStatusFailed,
+ },
+ },
+ {
+ name: "uninstalled release",
+ releases: []*helmrelease.Release{
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: mockReleaseNamespace,
+ Version: 1,
+ Status: helmreleasecommon.StatusUninstalled,
+ Chart: testutil.BuildChart(),
+ }, testutil.ReleaseWithConfig(map[string]any{"foo": "bar"})),
+ },
+ chart: testutil.BuildChart(),
+ values: map[string]any{},
+ status: func(releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ return v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
+ },
+ }
+ },
+ want: ReleaseState{
+ Status: ReleaseStatusAbsent,
+ },
+ },
+ {
+ name: "uninstalled release without current",
+ releases: []*helmrelease.Release{
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: mockReleaseNamespace,
+ Version: 1,
+ Status: helmreleasecommon.StatusUninstalled,
+ Chart: testutil.BuildChart(),
+ }, testutil.ReleaseWithConfig(map[string]any{"foo": "bar"})),
+ },
+ want: ReleaseState{
+ Status: ReleaseStatusAbsent,
+ },
+ },
+ {
+ name: "chart changed",
+ releases: []*helmrelease.Release{
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: mockReleaseNamespace,
+ Version: 1,
+ Status: helmreleasecommon.StatusDeployed,
+ Chart: testutil.BuildChart(),
+ }, testutil.ReleaseWithConfig(map[string]any{"foo": "bar"})),
+ },
+ status: func(releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ return v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
+ },
+ }
+ },
+ chart: testutil.BuildChart(testutil.ChartWithName("other-name")),
+ values: map[string]any{"foo": "bar"},
+ want: ReleaseState{
+ Status: ReleaseStatusOutOfSync,
+ },
+ },
+ {
+ name: "values changed",
+ releases: []*helmrelease.Release{
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: mockReleaseNamespace,
+ Version: 1,
+ Status: helmreleasecommon.StatusDeployed,
+ Chart: testutil.BuildChart(),
+ }, testutil.ReleaseWithConfig(map[string]any{"foo": "bar"})),
+ },
+ status: func(releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ return v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
+ },
+ }
+ },
+ chart: testutil.BuildChart(),
+ values: map[string]any{"bar": "foo"},
+ want: ReleaseState{
+ Status: ReleaseStatusOutOfSync,
+ },
+ },
+ {
+ name: "postRenderers changed",
+ releases: []*helmrelease.Release{
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: mockReleaseNamespace,
+ Version: 1,
+ Status: helmreleasecommon.StatusDeployed,
+ Chart: testutil.BuildChart(),
+ }, testutil.ReleaseWithConfig(map[string]any{"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: 1,
+ },
+ },
+ }
+ },
+ chart: testutil.BuildChart(),
+ values: map[string]any{"foo": "bar"},
+ want: ReleaseState{
+ Status: ReleaseStatusOutOfSync,
+ },
+ },
+ {
+ name: "postRenderers mismatch ignored for processed generation",
+ releases: []*helmrelease.Release{
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: mockReleaseNamespace,
+ Version: 1,
+ Status: helmreleasecommon.StatusDeployed,
+ Chart: testutil.BuildChart(),
+ }, testutil.ReleaseWithConfig(map[string]any{"foo": "bar"})),
+ },
+ spec: func(spec *v2.HelmReleaseSpec) {
+ spec.PostRenderers = postRenderers2
+ },
+ status: func(releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ return v2.HelmReleaseStatus{
+ ObservedGeneration: 2,
+ History: v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
+ },
+ ObservedPostRenderersDigest: postrender.Digest(digest.Canonical, postRenderers).String(),
+ }
+ },
+ chart: testutil.BuildChart(),
+ values: map[string]any{"foo": "bar"},
+ want: ReleaseState{
+ Status: ReleaseStatusInSync,
+ },
+ },
+ {
+ name: "commonMetadata changed",
+ releases: []*helmrelease.Release{
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: mockReleaseNamespace,
+ Version: 1,
+ Status: helmreleasecommon.StatusDeployed,
+ Chart: testutil.BuildChart(),
+ }, testutil.ReleaseWithConfig(map[string]any{"foo": "bar"})),
+ },
+ spec: func(spec *v2.HelmReleaseSpec) {
+ spec.CommonMetadata = commonMetadata2
+ },
+ status: func(releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ return v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
+ },
+ ObservedCommonMetadataDigest: postrender.CommonMetadataDigest(digest.Canonical, commonMetadata).String(),
+ Conditions: []metav1.Condition{
+ {
+ Type: meta.ReadyCondition,
+ Status: metav1.ConditionTrue,
+ ObservedGeneration: 1,
+ },
+ },
+ }
+ },
+ chart: testutil.BuildChart(),
+ values: map[string]any{"foo": "bar"},
+ want: ReleaseState{
+ Status: ReleaseStatusOutOfSync,
+ },
+ },
+ {
+ name: "commonMetadata mismatch ignored for processed generation",
+ releases: []*helmrelease.Release{
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: mockReleaseNamespace,
+ Version: 1,
+ Status: helmreleasecommon.StatusDeployed,
+ Chart: testutil.BuildChart(),
+ }, testutil.ReleaseWithConfig(map[string]any{"foo": "bar"})),
+ },
+ spec: func(spec *v2.HelmReleaseSpec) {
+ spec.CommonMetadata = commonMetadata2
+ },
+ status: func(releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ return v2.HelmReleaseStatus{
+ ObservedGeneration: 2,
+ History: v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
+ },
+ ObservedCommonMetadataDigest: postrender.CommonMetadataDigest(digest.Canonical, commonMetadata).String(),
+ }
+ },
+ chart: testutil.BuildChart(),
+ values: map[string]any{"foo": "bar"},
+ want: ReleaseState{
+ Status: ReleaseStatusInSync,
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ g := NewWithT(t)
+
+ obj := &v2.HelmRelease{
+ Spec: v2.HelmReleaseSpec{
+ ReleaseName: mockReleaseName,
+ TargetNamespace: mockReleaseNamespace,
+ 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)
+ }
+ if tt.status != nil {
+ obj.Status = tt.status(tt.releases)
+ }
+
+ cfg, err := action.NewConfigFactory(&kube.MemoryRESTClientGetter{},
+ action.WithStorage(helmdriver.MemoryDriverName, mockReleaseNamespace),
+ )
+ g.Expect(err).ToNot(HaveOccurred())
+
+ if len(tt.releases) > 0 {
+ store := helmstorage.Init(cfg.Driver)
+ for _, i := range tt.releases {
+ g.Expect(store.Create(i)).To(Succeed())
+ }
+ }
+
+ got, err := DetermineReleaseState(context.TODO(), cfg, &Request{
+ Object: obj,
+ Chart: tt.chart,
+ Values: tt.values,
+ }, nil)
+ if tt.wantErr {
+ g.Expect(got).To(BeNil())
+ g.Expect(err).To(HaveOccurred())
+ return
+ }
+
+ g.Expect(err).ToNot(HaveOccurred())
+ g.Expect(got.Status).To(Equal(tt.want.Status))
+ g.Expect(got.Reason).To(ContainSubstring(tt.want.Reason))
+ })
+ }
+}
+
+func TestDetermineReleaseState_DriftDetection(t *testing.T) {
+ tests := []struct {
+ name string
+ driftMode v2.DriftDetectionMode
+ applyManifest bool
+ want func(namespace string) ReleaseState
+ }{
+ {
+ name: "with drift and detection mode enabled",
+ driftMode: v2.DriftDetectionEnabled,
+ want: func(namespace string) ReleaseState {
+ return ReleaseState{
+ Status: ReleaseStatusDrifted,
+ Diff: jsondiff.DiffSet{
+ {
+ Type: jsondiff.DiffTypeCreate,
+ DesiredObject: &unstructured.Unstructured{
+ Object: map[string]any{
+ "apiVersion": "v1",
+ "kind": "Secret",
+ "metadata": map[string]any{
+ "name": "fixture",
+ "namespace": namespace,
+ "creationTimestamp": nil,
+ "labels": map[string]any{
+ "app.kubernetes.io/managed-by": "Helm",
+ },
+ "annotations": map[string]any{
+ "meta.helm.sh/release-name": mockReleaseName,
+ "meta.helm.sh/release-namespace": namespace,
+ },
+ },
+ },
+ },
+ },
+ },
+ }
+ },
+ },
+ {
+ name: "without drift and detection mode enabled",
+ driftMode: v2.DriftDetectionEnabled,
+ applyManifest: true,
+ want: func(_ string) ReleaseState {
+ return ReleaseState{Status: ReleaseStatusInSync}
+ },
+ },
+ {
+ name: "with drift and detection mode warn",
+ driftMode: v2.DriftDetectionWarn,
+ want: func(namespace string) ReleaseState {
+ return ReleaseState{
+ Status: ReleaseStatusDrifted,
+ Diff: jsondiff.DiffSet{
+ {
+ Type: jsondiff.DiffTypeCreate,
+ DesiredObject: &unstructured.Unstructured{
+ Object: map[string]any{
+ "apiVersion": "v1",
+ "kind": "Secret",
+ "metadata": map[string]any{
+ "name": "fixture",
+ "namespace": namespace,
+ "creationTimestamp": nil,
+ "labels": map[string]any{
+ "app.kubernetes.io/managed-by": "Helm",
+ },
+ "annotations": map[string]any{
+ "meta.helm.sh/release-name": mockReleaseName,
+ "meta.helm.sh/release-namespace": namespace,
+ },
+ },
+ },
+ },
+ },
+ },
+ }
+ },
+ },
+ {
+ name: "without drift and detection mode warn",
+ applyManifest: true,
+ driftMode: v2.DriftDetectionWarn,
+ want: func(_ string) ReleaseState {
+ return ReleaseState{Status: ReleaseStatusInSync}
+ },
+ },
+ {
+ name: "drift detection mode disabled",
+ driftMode: v2.DriftDetectionDisabled,
+ want: func(_ string) ReleaseState {
+ return ReleaseState{Status: ReleaseStatusInSync}
+ },
+ },
+ }
+
+ 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
+
+ chart := testutil.BuildChart()
+
+ rls := testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: releaseNamespace,
+ Version: 1,
+ Status: helmreleasecommon.StatusDeployed,
+ Chart: chart,
+ })
+
+ if tt.applyManifest {
+ objs, err := ssautil.ReadObjects(strings.NewReader(rls.Manifest))
+ g.Expect(err).ToNot(HaveOccurred())
+
+ for _, obj := range objs {
+ g.Expect(ssanormalize.Unstructured(obj)).To(Succeed())
+ obj.SetNamespace(releaseNamespace)
+ obj.SetLabels(map[string]string{
+ "app.kubernetes.io/managed-by": "Helm",
+ })
+ obj.SetAnnotations(map[string]string{
+ "meta.helm.sh/release-name": rls.Name,
+ "meta.helm.sh/release-namespace": rls.Namespace,
+ })
+ g.Expect(testEnv.Create(context.Background(), obj)).To(Succeed())
+ }
+ }
+
+ obj := &v2.HelmRelease{
+ Spec: v2.HelmReleaseSpec{
+ ReleaseName: mockReleaseName,
+ TargetNamespace: releaseNamespace,
+ StorageNamespace: releaseNamespace,
+ DriftDetection: &v2.DriftDetection{
+ Mode: tt.driftMode,
+ },
+ },
+ Status: v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(rls)),
+ },
+ },
+ }
+
+ 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)
+ g.Expect(store.Create(rls)).To(Succeed())
+
+ got, err := DetermineReleaseState(context.TODO(), cfg, &Request{
+ Object: obj,
+ Chart: testutil.BuildChart(),
+ Values: rls.Config,
+ }, nil)
+ g.Expect(err).ToNot(HaveOccurred())
+
+ want := tt.want(releaseNamespace)
+ g.Expect(got).To(Equal(want))
+ })
+ }
+}
diff --git a/internal/reconcile/suite_test.go b/internal/reconcile/suite_test.go
new file mode 100644
index 000000000..0868bc76c
--- /dev/null
+++ b/internal/reconcile/suite_test.go
@@ -0,0 +1,174 @@
+/*
+Copyright 2022 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package reconcile
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "testing"
+
+ "helm.sh/helm/v4/pkg/kube"
+ helmrelease "helm.sh/helm/v4/pkg/release/v1"
+ helmstorage "helm.sh/helm/v4/pkg/storage"
+ corev1 "k8s.io/api/core/v1"
+ apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
+ "k8s.io/apimachinery/pkg/api/meta"
+ "k8s.io/apimachinery/pkg/runtime"
+ utilruntime "k8s.io/apimachinery/pkg/util/runtime"
+ "k8s.io/cli-runtime/pkg/genericclioptions"
+ "k8s.io/client-go/discovery"
+ "k8s.io/client-go/discovery/cached/memory"
+ "k8s.io/client-go/rest"
+ "k8s.io/client-go/tools/clientcmd"
+ clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
+ ctrl "sigs.k8s.io/controller-runtime"
+ "sigs.k8s.io/controller-runtime/pkg/manager"
+
+ "github.com/fluxcd/pkg/runtime/testenv"
+
+ sourcev1 "github.com/fluxcd/source-controller/api/v1"
+
+ v2 "github.com/fluxcd/helm-controller/api/v2"
+ "github.com/fluxcd/helm-controller/internal/release"
+)
+
+const testFieldManager = "helm-controller"
+
+var (
+ ctx = ctrl.SetupSignalHandler()
+ testEnv *testenv.Environment
+)
+
+func NewTestScheme() *runtime.Scheme {
+ s := runtime.NewScheme()
+ utilruntime.Must(corev1.AddToScheme(s))
+ utilruntime.Must(apiextensionsv1.AddToScheme(s))
+ utilruntime.Must(sourcev1.AddToScheme(s))
+ utilruntime.Must(v2.AddToScheme(s))
+ return s
+}
+
+func TestMain(m *testing.M) {
+ testEnv = testenv.New(
+ testenv.WithCRDPath(
+ filepath.Join("..", "..", "build", "config", "crd", "bases"),
+ filepath.Join("..", "..", "config", "crd", "bases"),
+ ),
+ testenv.WithScheme(NewTestScheme()),
+ )
+
+ go func() {
+ fmt.Println("Starting the test environment")
+ if err := testEnv.Start(ctx); err != nil {
+ panic(fmt.Sprintf("Failed to start the test environment manager: %v", err))
+ }
+ }()
+ <-testEnv.Manager.Elected()
+
+ // Globally configure field manager for all tests.
+ kube.ManagedFieldsManager = "reconciler-tests"
+
+ code := m.Run()
+
+ fmt.Println("Stopping the test environment")
+ if err := testEnv.Stop(); err != nil {
+ panic(fmt.Sprintf("Failed to stop the test environment: %v", err))
+ }
+ os.Exit(code)
+}
+
+type managerRESTClientGetter struct {
+ restConfig *rest.Config
+ discoveryClient discovery.CachedDiscoveryInterface
+ restMapper meta.RESTMapper
+ namespaceConfig clientcmd.ClientConfig
+}
+
+func RESTClientGetterFromManager(mgr manager.Manager, ns string) (genericclioptions.RESTClientGetter, error) {
+ cfg := mgr.GetConfig()
+ dc, err := discovery.NewDiscoveryClientForConfig(cfg)
+ if err != nil {
+ return nil, err
+ }
+ cdc := memory.NewMemCacheClient(dc)
+ rm := mgr.GetRESTMapper()
+ return &managerRESTClientGetter{
+ restConfig: cfg,
+ discoveryClient: cdc,
+ restMapper: rm,
+ namespaceConfig: &namespaceClientConfig{ns},
+ }, nil
+}
+
+func (c *managerRESTClientGetter) ToRESTConfig() (*rest.Config, error) {
+ return c.restConfig, nil
+}
+
+func (c *managerRESTClientGetter) ToDiscoveryClient() (discovery.CachedDiscoveryInterface, error) {
+ return c.discoveryClient, nil
+}
+
+func (c *managerRESTClientGetter) ToRESTMapper() (meta.RESTMapper, error) {
+ return c.restMapper, nil
+}
+
+func (c *managerRESTClientGetter) ToRawKubeConfigLoader() clientcmd.ClientConfig {
+ return c.namespaceConfig
+}
+
+var _ clientcmd.ClientConfig = &namespaceClientConfig{}
+
+type namespaceClientConfig struct {
+ namespace string
+}
+
+func (c namespaceClientConfig) RawConfig() (clientcmdapi.Config, error) {
+ return clientcmdapi.Config{}, nil
+}
+
+func (c namespaceClientConfig) ClientConfig() (*rest.Config, error) {
+ return nil, nil
+}
+
+func (c namespaceClientConfig) Namespace() (string, bool, error) {
+ return c.namespace, false, nil
+}
+
+func (c namespaceClientConfig) ConfigAccess() clientcmd.ConfigAccess {
+ return nil
+}
+
+func storeHistory(store *helmstorage.Storage, releaseName string) ([]*helmrelease.Release, error) {
+ releasers, err := store.History(releaseName)
+ if err != nil {
+ return nil, err
+ }
+ history := make([]*helmrelease.Release, 0, len(releasers))
+ for _, r := range releasers {
+ history = append(history, r.(*helmrelease.Release))
+ }
+ return history, nil
+}
+
+// observeReleaseWithAction creates a Snapshot from a release with the given
+// action set. This is a test helper to simplify constructing expected snapshots.
+func observeReleaseWithAction(rls *helmrelease.Release, action v2.ReleaseAction) *v2.Snapshot {
+ obs := release.ObserveRelease(rls)
+ obs.Action = action
+ return release.ObservedToSnapshot(obs)
+}
diff --git a/internal/reconcile/test.go b/internal/reconcile/test.go
new file mode 100644
index 000000000..938433ceb
--- /dev/null
+++ b/internal/reconcile/test.go
@@ -0,0 +1,211 @@
+/*
+Copyright 2022 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package reconcile
+
+import (
+ "context"
+ "fmt"
+ "strings"
+
+ helmrelease "helm.sh/helm/v4/pkg/release"
+ helmreleasev1 "helm.sh/helm/v4/pkg/release/v1"
+ corev1 "k8s.io/api/core/v1"
+ "k8s.io/client-go/tools/record"
+
+ "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/release"
+ "github.com/fluxcd/helm-controller/internal/storage"
+)
+
+// Test is an ActionReconciler which attempts to perform a Helm test for
+// the latest release of the Request.Object.
+//
+// The writes to the Helm storage during testing are observed, which causes the
+// TestHooks field of the latest Snapshot in the Status.History to be updated
+// if it matches the target of the test.
+//
+// When all test hooks for the release succeed, the object is marked with
+// TestSuccess=True and an event is emitted. When one of the test hooks fails,
+// Helm stops running the remaining tests, and the object is marked with
+// TestSuccess=False and a warning event is emitted. If test failures are not
+// ignored, the failure count for the active remediation strategy is
+// incremented.
+//
+// When the Request.Object does not have a latest release, it returns an
+// error of type ErrNoLatest. In addition, it returns ErrReleaseMismatch
+// if the test ran for a different release target than the latest release.
+// Any other returned error indicates the caller should retry as it did not cause
+// a change to the Helm storage.
+//
+// At the end of the reconciliation, the Status.Conditions are summarized and
+// propagated to the Ready condition on the Request.Object.
+//
+// The caller is assumed to have verified the integrity of Request.Object using
+// e.g. action.VerifySnapshot before calling Reconcile.
+type Test struct {
+ configFactory *action.ConfigFactory
+ eventRecorder record.EventRecorder
+}
+
+// NewTest returns a new Test reconciler configured with the provided values.
+func NewTest(cfg *action.ConfigFactory, recorder record.EventRecorder) *Test {
+ return &Test{configFactory: cfg, eventRecorder: recorder}
+}
+
+func (r *Test) Reconcile(ctx context.Context, req *Request) error {
+ var (
+ cur = req.Object.Status.History.Latest().DeepCopy()
+ cfg = r.configFactory.Build(action.NewDebugLogBuffer(ctx), observeTest(req.Object))
+ )
+
+ defer summarize(req)
+
+ // We only accept test results for the current release.
+ if cur == nil {
+ return fmt.Errorf("%w: required for test", ErrNoLatest)
+ }
+
+ // Run the Helm test action.
+ rls, err := action.Test(ctx, cfg, req.Object)
+
+ // The Helm test action does always target the latest release. Before
+ // accepting results, we need to confirm this is actually the release we
+ // have recorded as latest.
+ if rls != nil && !release.ObserveRelease(rls).Targets(cur.Name, cur.Namespace, cur.Version) {
+ err = fmt.Errorf("%w: tested release %s/%s.v%d != current release %s/%s.v%d",
+ ErrReleaseMismatch, rls.Namespace, rls.Name, rls.Version, cur.Namespace, cur.Name, cur.Version)
+ }
+
+ // Something went wrong.
+ if err != nil {
+ r.failure(req, err)
+
+ // If we failed to observe anything happened at all, we want to retry
+ // and return the error to indicate this.
+ if !req.Object.Status.History.Latest().HasBeenTested() {
+ return err
+ }
+ return nil
+ }
+
+ r.success(req)
+ return nil
+}
+
+func (r *Test) Name() string {
+ return "test"
+}
+
+func (r *Test) Type() ReconcilerType {
+ return ReconcilerTypeTest
+}
+
+const (
+ // fmtTestPending is the message format used when awaiting tests to be run.
+ fmtTestPending = "Helm release %s with chart %s is awaiting tests"
+ // fmtTestFailure is the message format for a test failure.
+ fmtTestFailure = "Helm test failed for release %s with chart %s: %s"
+ // fmtTestSuccess is the message format for a successful test.
+ fmtTestSuccess = "Helm test succeeded for release %s with chart %s: %s"
+)
+
+// failure records the failure of a Helm test action in the status of the given
+// Request.Object by marking TestSuccess=False and increasing the failure
+// counter. In addition, it emits a warning event for the Request.Object.
+// The active remediation failure count is only incremented if test failures
+// are not ignored.
+func (r *Test) failure(req *Request, err error) {
+ // Compose failure message.
+ cur := req.Object.Status.History.Latest()
+ msg := fmt.Sprintf(fmtTestFailure, cur.FullReleaseName(), cur.VersionedChartName(), strings.TrimSpace(err.Error()))
+
+ // Mark test failure on object.
+ req.Object.Status.Failures++
+ conditions.MarkFalse(req.Object, v2.TestSuccessCondition, v2.TestFailedReason, "%s", msg)
+
+ // Record warning event, this message contains more data than the
+ // Condition summary.
+ r.eventRecorder.AnnotatedEventf(
+ req.Object,
+ eventMeta(cur.ChartVersion, cur.ConfigDigest, addAppVersion(cur.AppVersion), addOCIDigest(cur.OCIDigest)),
+ corev1.EventTypeWarning,
+ v2.TestFailedReason,
+ msg,
+ )
+
+ if req.Object.Status.History.Latest().HasBeenTested() {
+ // Count the failure of the test for the active remediation strategy if enabled.
+ remediation := req.Object.GetActiveRemediation()
+ if remediation != nil && !remediation.MustIgnoreTestFailures(req.Object.GetTest().IgnoreFailures) {
+ remediation.IncrementFailureCount(req.Object)
+ }
+ }
+}
+
+// success records the failure of a Helm test action in the status of the given
+// Request.Object by marking TestSuccess=True and emitting an event.
+func (r *Test) success(req *Request) {
+ // Compose success message.
+ cur := req.Object.Status.History.Latest()
+ var hookMsg = "no test hooks"
+ if l := len(cur.GetTestHooks()); l > 0 {
+ h := "hook"
+ if l > 1 {
+ h += "s"
+ }
+ hookMsg = fmt.Sprintf("%d test %s completed successfully", l, h)
+ }
+ msg := fmt.Sprintf(fmtTestSuccess, cur.FullReleaseName(), cur.VersionedChartName(), hookMsg)
+
+ // Mark test success on object.
+ conditions.MarkTrue(req.Object, v2.TestSuccessCondition, v2.TestSucceededReason, "%s", msg)
+
+ // Record event.
+ r.eventRecorder.AnnotatedEventf(
+ req.Object,
+ eventMeta(cur.ChartVersion, cur.ConfigDigest, addAppVersion(cur.AppVersion), addOCIDigest(cur.OCIDigest)),
+ corev1.EventTypeNormal,
+ v2.TestSucceededReason,
+ msg,
+ )
+}
+
+// observeTest returns a storage.ObserveFunc to track test results of a
+// HelmRelease.
+// It only accepts test results for the latest release and updates the
+// latest snapshot with the observed test results.
+func observeTest(obj *v2.HelmRelease) storage.ObserveFunc {
+ return func(rlsr helmrelease.Releaser) {
+ rls, ok := rlsr.(*helmreleasev1.Release)
+ if !ok {
+ return
+ }
+ // Only accept test results for the latest release.
+ if !obj.Status.History.Latest().Targets(rls.Name, rls.Namespace, rls.Version) {
+ return
+ }
+
+ // Update the latest snapshot with the test result.
+ latest := obj.Status.History.Latest()
+ tested := release.ObservedToSnapshot(releaseToObservation(rls, latest, latest.GetAction()))
+ tested.SetTestHooks(release.TestHooksFromRelease(rls))
+ obj.Status.History[0] = tested
+ }
+}
diff --git a/internal/reconcile/test_test.go b/internal/reconcile/test_test.go
new file mode 100644
index 000000000..6267d4a78
--- /dev/null
+++ b/internal/reconcile/test_test.go
@@ -0,0 +1,654 @@
+/*
+Copyright 2022 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package reconcile
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "testing"
+ "time"
+
+ . "github.com/onsi/gomega"
+ helmreleasecommon "helm.sh/helm/v4/pkg/release/common"
+ helmrelease "helm.sh/helm/v4/pkg/release/v1"
+ helmreleaseutil "helm.sh/helm/v4/pkg/release/v1/util"
+ helmstorage "helm.sh/helm/v4/pkg/storage"
+ helmdriver "helm.sh/helm/v4/pkg/storage/driver"
+ corev1 "k8s.io/api/core/v1"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/client-go/tools/record"
+
+ eventv1 "github.com/fluxcd/pkg/apis/event/v1beta1"
+ "github.com/fluxcd/pkg/apis/meta"
+ "github.com/fluxcd/pkg/runtime/conditions"
+
+ "github.com/fluxcd/pkg/chartutil"
+
+ 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/release"
+ "github.com/fluxcd/helm-controller/internal/testutil"
+)
+
+// testHookFixtures is a list of release.Hook in every possible LastRun state.
+var testHookFixtures = []*helmrelease.Hook{
+ {
+ Name: "never-run-test",
+ Events: []helmrelease.HookEvent{helmrelease.HookTest},
+ },
+ {
+ Name: "passing-test",
+ Events: []helmrelease.HookEvent{helmrelease.HookTest},
+ LastRun: helmrelease.HookExecution{
+ StartedAt: testutil.MustParseHelmTime("2006-01-02T15:04:05Z"),
+ CompletedAt: testutil.MustParseHelmTime("2006-01-02T15:04:07Z"),
+ Phase: helmrelease.HookPhaseSucceeded,
+ },
+ },
+ {
+ Name: "failing-test",
+ Events: []helmrelease.HookEvent{helmrelease.HookTest},
+ LastRun: helmrelease.HookExecution{
+ StartedAt: testutil.MustParseHelmTime("2006-01-02T15:10:05Z"),
+ CompletedAt: testutil.MustParseHelmTime("2006-01-02T15:10:07Z"),
+ Phase: helmrelease.HookPhaseFailed,
+ },
+ },
+ {
+ Name: "passing-pre-install",
+ Events: []helmrelease.HookEvent{helmrelease.HookPreInstall},
+ LastRun: helmrelease.HookExecution{
+ StartedAt: testutil.MustParseHelmTime("2006-01-02T15:00:05Z"),
+ CompletedAt: testutil.MustParseHelmTime("2006-01-02T15:00:07Z"),
+ Phase: helmrelease.HookPhaseSucceeded,
+ },
+ },
+}
+
+func TestTest_Reconcile(t *testing.T) {
+ tests := []struct {
+ name string
+ // driver allows for modifying the Helm storage driver.
+ driver func(driver helmdriver.Driver) helmdriver.Driver
+ // releases is the list of releases that are stored in the driver
+ // before test.
+ releases func(namespace string) []*helmrelease.Release
+ // spec modifies the HelmRelease Object spec before test.
+ spec func(spec *v2.HelmReleaseSpec)
+ // status to configure on the HelmRelease Object before test.
+ status func(releases []*helmrelease.Release) v2.HelmReleaseStatus
+ // wantErr is the error that is expected to be returned.
+ wantErr error
+ // expectedConditions are the conditions that are expected to be set on
+ // the HelmRelease after running test.
+ expectConditions []metav1.Condition
+ // expectHistory is the expected History on the HelmRelease after
+ // running test.
+ expectHistory func(releases []*helmrelease.Release) v2.Snapshots
+ // expectFailures is the expected Failures count of the HelmRelease.
+ expectFailures int64
+ // expectInstallFailures is the expected InstallFailures count of the
+ // HelmRelease.
+ expectInstallFailures int64
+ // expectUpgradeFailures is the expected UpgradeFailures count of the
+ // HelmRelease.
+ expectUpgradeFailures int64
+ }{
+ {
+ name: "test success",
+ releases: func(namespace string) []*helmrelease.Release {
+ return []*helmrelease.Release{
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: namespace,
+ Version: 1,
+ Chart: testutil.BuildChart(testutil.ChartWithTestHook()),
+ Status: helmreleasecommon.StatusDeployed,
+ }, testutil.ReleaseWithTestHook()),
+ }
+ },
+ status: func(releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ return v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
+ },
+ }
+ },
+ expectConditions: []metav1.Condition{
+ *conditions.TrueCondition(meta.ReadyCondition, v2.TestSucceededReason,
+ "1 test hook completed successfully"),
+ *conditions.TrueCondition(v2.TestSuccessCondition, v2.TestSucceededReason,
+ "1 test hook completed successfully"),
+ },
+ expectHistory: func(releases []*helmrelease.Release) v2.Snapshots {
+ withTests := release.ObservedToSnapshot(release.ObserveRelease(releases[0]))
+ withTests.SetTestHooks(release.TestHooksFromRelease(releases[0]))
+ return v2.Snapshots{withTests}
+ },
+ },
+ {
+ name: "test without hooks",
+ releases: func(namespace string) []*helmrelease.Release {
+ return []*helmrelease.Release{
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: namespace,
+ Version: 1,
+ Status: helmreleasecommon.StatusDeployed,
+ Chart: testutil.BuildChart(),
+ }),
+ }
+ },
+ status: func(releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ return v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
+ },
+ }
+ },
+ expectConditions: []metav1.Condition{
+ *conditions.TrueCondition(meta.ReadyCondition, v2.TestSucceededReason,
+ "no test hooks"),
+ *conditions.TrueCondition(v2.TestSuccessCondition, v2.TestSucceededReason,
+ "no test hooks"),
+ },
+ expectHistory: func(releases []*helmrelease.Release) v2.Snapshots {
+ withTests := release.ObservedToSnapshot(release.ObserveRelease(releases[0]))
+ withTests.SetTestHooks(release.TestHooksFromRelease(releases[0]))
+ return v2.Snapshots{withTests}
+ },
+ },
+ {
+ name: "test install failure",
+ releases: func(namespace string) []*helmrelease.Release {
+ return []*helmrelease.Release{
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: namespace,
+ Version: 1,
+ Status: helmreleasecommon.StatusDeployed,
+ Chart: testutil.BuildChart(testutil.ChartWithFailingTestHook()),
+ }, testutil.ReleaseWithFailingTestHook()),
+ }
+ },
+ status: func(releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ return v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
+ },
+ LastAttemptedReleaseAction: v2.ReleaseActionInstall,
+ InstallFailures: 0,
+ }
+ },
+ expectConditions: []metav1.Condition{
+ *conditions.FalseCondition(meta.ReadyCondition, v2.TestFailedReason,
+ "ontext deadline exceeded"),
+ *conditions.FalseCondition(v2.TestSuccessCondition, v2.TestFailedReason,
+ "ontext deadline exceeded"),
+ },
+ expectHistory: func(releases []*helmrelease.Release) v2.Snapshots {
+ withTests := release.ObservedToSnapshot(release.ObserveRelease(releases[0]))
+ withTests.SetTestHooks(release.TestHooksFromRelease(releases[0]))
+ return v2.Snapshots{withTests}
+ },
+ expectFailures: 1,
+ expectInstallFailures: 1,
+ },
+ {
+ name: "test without current",
+ releases: func(namespace string) []*helmrelease.Release {
+ return []*helmrelease.Release{
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: namespace,
+ Version: 1,
+ Chart: testutil.BuildChart(testutil.ChartWithTestHook()),
+ Status: helmreleasecommon.StatusDeployed,
+ }, testutil.ReleaseWithTestHook()),
+ }
+ },
+ expectConditions: []metav1.Condition{},
+ wantErr: ErrNoLatest,
+ },
+ {
+ name: "test with stale current",
+ releases: func(namespace string) []*helmrelease.Release {
+ return []*helmrelease.Release{
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: namespace,
+ Version: 1,
+ Chart: testutil.BuildChart(testutil.ChartWithTestHook()),
+ Status: helmreleasecommon.StatusSuperseded,
+ }, testutil.ReleaseWithTestHook()),
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: namespace,
+ Version: 2,
+ Chart: testutil.BuildChart(),
+ Status: helmreleasecommon.StatusDeployed,
+ }),
+ }
+ },
+ status: func(releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ return v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
+ },
+ }
+ },
+ expectConditions: []metav1.Condition{
+ *conditions.FalseCondition(meta.ReadyCondition, v2.TestFailedReason,
+ "%s", ErrReleaseMismatch.Error()),
+ *conditions.FalseCondition(v2.TestSuccessCondition, v2.TestFailedReason,
+ "%s", ErrReleaseMismatch.Error()),
+ },
+ expectHistory: func(releases []*helmrelease.Release) v2.Snapshots {
+ return v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
+ }
+ },
+ expectFailures: 1,
+ wantErr: ErrReleaseMismatch,
+ },
+ }
+ 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
+
+ var releases []*helmrelease.Release
+ if tt.releases != nil {
+ releases = tt.releases(releaseNamespace)
+ helmreleaseutil.SortByRevision(releases)
+ }
+
+ obj := &v2.HelmRelease{
+ Spec: v2.HelmReleaseSpec{
+ ReleaseName: mockReleaseName,
+ TargetNamespace: releaseNamespace,
+ StorageNamespace: releaseNamespace,
+ Timeout: &metav1.Duration{Duration: 200 * time.Millisecond},
+ Test: &v2.Test{
+ Enable: true,
+ },
+ },
+ }
+ 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())
+ }
+
+ if tt.driver != nil {
+ cfg.Driver = tt.driver(cfg.Driver)
+ }
+
+ recorder := new(record.FakeRecorder)
+ got := (NewTest(cfg, recorder)).Reconcile(context.TODO(), &Request{
+ Object: obj,
+ })
+ if tt.wantErr != nil {
+ g.Expect(errors.Is(got, tt.wantErr)).To(BeTrue())
+ } else {
+ g.Expect(got).ToNot(HaveOccurred())
+ }
+
+ g.Expect(obj.Status.Conditions).To(conditions.MatchConditions(tt.expectConditions))
+
+ releases, _ = storeHistory(store, mockReleaseName)
+ helmreleaseutil.SortByRevision(releases)
+
+ if tt.expectHistory != nil {
+ g.Expect(obj.Status.History).To(testutil.Equal(tt.expectHistory(releases)))
+ } else {
+ g.Expect(obj.Status.History).To(BeEmpty(), "expected history to be empty")
+ }
+
+ g.Expect(obj.Status.Failures).To(Equal(tt.expectFailures))
+ g.Expect(obj.Status.InstallFailures).To(Equal(tt.expectInstallFailures))
+ g.Expect(obj.Status.UpgradeFailures).To(Equal(tt.expectUpgradeFailures))
+ })
+ }
+}
+
+func Test_observeTest(t *testing.T) {
+ t.Run("test with current", func(t *testing.T) {
+ g := NewWithT(t)
+
+ obj := &v2.HelmRelease{
+ Status: v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ &v2.Snapshot{
+ Name: mockReleaseName,
+ Namespace: mockReleaseNamespace,
+ Version: 1,
+ },
+ },
+ },
+ }
+ rls := testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: mockReleaseNamespace,
+ Version: 1,
+ }, testutil.ReleaseWithHooks(testHookFixtures))
+
+ expect := release.ObservedToSnapshot(release.ObserveRelease(rls))
+ expect.SetTestHooks(release.TestHooksFromRelease(rls))
+
+ observeTest(obj)(rls)
+ g.Expect(obj.Status.History).To(testutil.Equal(v2.Snapshots{
+ expect,
+ }))
+ })
+
+ t.Run("test with current OCI Digest", func(t *testing.T) {
+ g := NewWithT(t)
+
+ obj := &v2.HelmRelease{
+ Status: v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ &v2.Snapshot{
+ Name: mockReleaseName,
+ Namespace: mockReleaseNamespace,
+ Version: 1,
+ OCIDigest: "sha256:fcdc2b0de1581a3633ada4afee3f918f6eaa5b5ab38c3fef03d5b48d3f85d9f6",
+ },
+ },
+ },
+ }
+ rls := testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: mockReleaseNamespace,
+ Version: 1,
+ }, testutil.ReleaseWithHooks(testHookFixtures))
+
+ obs := release.ObserveRelease(rls)
+ obs.OCIDigest = "sha256:fcdc2b0de1581a3633ada4afee3f918f6eaa5b5ab38c3fef03d5b48d3f85d9f6"
+ expect := release.ObservedToSnapshot(obs)
+ expect.SetTestHooks(release.TestHooksFromRelease(rls))
+
+ observeTest(obj)(rls)
+ g.Expect(obj.Status.History).To(testutil.Equal(v2.Snapshots{
+ expect,
+ }))
+ })
+
+ t.Run("test with current preserves action", func(t *testing.T) {
+ g := NewWithT(t)
+
+ obj := &v2.HelmRelease{
+ Status: v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ &v2.Snapshot{
+ Name: mockReleaseName,
+ Namespace: mockReleaseNamespace,
+ Version: 1,
+ Action: v2.ReleaseActionInstall,
+ },
+ },
+ },
+ }
+ rls := testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: mockReleaseNamespace,
+ Version: 1,
+ }, testutil.ReleaseWithHooks(testHookFixtures))
+
+ observeTest(obj)(rls)
+ g.Expect(obj.Status.History.Latest().Action).To(Equal(v2.ReleaseActionInstall))
+ })
+
+ t.Run("test targeting different version than latest", func(t *testing.T) {
+ g := NewWithT(t)
+
+ current := &v2.Snapshot{
+ Name: mockReleaseName,
+ Namespace: mockReleaseNamespace,
+ Version: 2,
+ }
+ previous := &v2.Snapshot{
+ Name: mockReleaseName,
+ Namespace: mockReleaseNamespace,
+ Version: 1,
+ }
+ obj := &v2.HelmRelease{
+ Status: v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ current,
+ previous,
+ },
+ },
+ }
+ rls := testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: mockReleaseNamespace,
+ Version: previous.Version,
+ }, testutil.ReleaseWithHooks(testHookFixtures))
+
+ observeTest(obj)(rls)
+ g.Expect(obj.Status.History).To(testutil.Equal(v2.Snapshots{
+ current,
+ previous,
+ }))
+ })
+
+ t.Run("test without current", func(t *testing.T) {
+ g := NewWithT(t)
+
+ obj := &v2.HelmRelease{}
+
+ rls := testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: mockReleaseNamespace,
+ Version: 2,
+ }, testutil.ReleaseWithHooks(testHookFixtures))
+
+ observeTest(obj)(rls)
+ g.Expect(obj.Status.History).To(BeEmpty())
+ })
+}
+
+func TestTest_failure(t *testing.T) {
+ var (
+ cur = testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: mockReleaseNamespace,
+ Chart: testutil.BuildChart(),
+ Version: 4,
+ })
+ obj = &v2.HelmRelease{
+ Status: v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(cur)),
+ },
+ },
+ }
+ err = errors.New("test error")
+ )
+
+ t.Run("records failure", func(t *testing.T) {
+ g := NewWithT(t)
+
+ recorder := testutil.NewFakeRecorder(10, false)
+ r := &Test{
+ eventRecorder: recorder,
+ }
+
+ req := &Request{Object: obj.DeepCopy()}
+ r.failure(req, err)
+
+ expectMsg := fmt.Sprintf(fmtTestFailure,
+ fmt.Sprintf("%s/%s.v%d", cur.Namespace, cur.Name, cur.Version),
+ fmt.Sprintf("%s@%s", cur.Chart.Name(), cur.Chart.Metadata.Version),
+ err.Error())
+
+ g.Expect(req.Object.Status.Conditions).To(conditions.MatchConditions([]metav1.Condition{
+ *conditions.FalseCondition(v2.TestSuccessCondition, v2.TestFailedReason, "%s", expectMsg),
+ }))
+ g.Expect(req.Object.Status.Failures).To(Equal(int64(1)))
+ g.Expect(req.Object.Status.InstallFailures).To(BeZero())
+ g.Expect(req.Object.Status.UpgradeFailures).To(BeZero())
+ g.Expect(recorder.GetEvents()).To(ConsistOf([]corev1.Event{
+ {
+ Type: corev1.EventTypeWarning,
+ Reason: v2.TestFailedReason,
+ Message: expectMsg,
+ ObjectMeta: metav1.ObjectMeta{
+ Annotations: map[string]string{
+ eventMetaGroupKey(eventv1.MetaRevisionKey): cur.Chart.Metadata.Version,
+ eventMetaGroupKey(metaAppVersionKey): cur.Chart.Metadata.AppVersion,
+ eventMetaGroupKey(eventv1.MetaTokenKey): chartutil.DigestValues(digest.Canonical, cur.Config).String(),
+ },
+ },
+ },
+ }))
+ })
+
+ t.Run("increases remediation failure count", func(t *testing.T) {
+ g := NewWithT(t)
+
+ recorder := testutil.NewFakeRecorder(10, false)
+ r := &Test{
+ eventRecorder: recorder,
+ }
+
+ obj := obj.DeepCopy()
+ obj.Status.LastAttemptedReleaseAction = v2.ReleaseActionInstall
+ obj.Status.History.Latest().SetTestHooks(map[string]*v2.TestHookStatus{})
+ req := &Request{Object: obj}
+ r.failure(req, err)
+
+ g.Expect(req.Object.Status.InstallFailures).To(Equal(int64(1)))
+ })
+
+ t.Run("follows ignore failure instructions", func(t *testing.T) {
+ g := NewWithT(t)
+
+ recorder := testutil.NewFakeRecorder(10, false)
+ r := &Test{
+ eventRecorder: recorder,
+ }
+
+ obj := obj.DeepCopy()
+ obj.Spec.Test = &v2.Test{IgnoreFailures: true}
+ obj.Status.History.Latest().SetTestHooks(map[string]*v2.TestHookStatus{})
+ req := &Request{Object: obj}
+ r.failure(req, err)
+
+ g.Expect(req.Object.Status.InstallFailures).To(BeZero())
+ })
+}
+
+func TestTest_success(t *testing.T) {
+ g := NewWithT(t)
+
+ var (
+ cur = testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: mockReleaseNamespace,
+ Chart: testutil.BuildChart(),
+ Version: 4,
+ })
+ obj = &v2.HelmRelease{
+ Status: v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(cur)),
+ },
+ },
+ }
+ )
+
+ t.Run("records success", func(t *testing.T) {
+ recorder := testutil.NewFakeRecorder(10, false)
+ r := &Test{
+ eventRecorder: recorder,
+ }
+
+ obj := obj.DeepCopy()
+ obj.Status.History.Latest().SetTestHooks(map[string]*v2.TestHookStatus{
+ "test": {
+ Phase: helmrelease.HookPhaseSucceeded.String(),
+ },
+ "test-2": {
+ Phase: helmrelease.HookPhaseSucceeded.String(),
+ },
+ })
+ req := &Request{Object: obj}
+ r.success(req)
+
+ expectMsg := fmt.Sprintf(fmtTestSuccess,
+ fmt.Sprintf("%s/%s.v%d", cur.Namespace, cur.Name, cur.Version),
+ fmt.Sprintf("%s@%s", cur.Chart.Name(), cur.Chart.Metadata.Version),
+ "2 test hooks completed successfully")
+
+ g.Expect(req.Object.Status.Conditions).To(conditions.MatchConditions([]metav1.Condition{
+ *conditions.TrueCondition(v2.TestSuccessCondition, v2.TestSucceededReason, "%s", expectMsg),
+ }))
+ g.Expect(req.Object.Status.Failures).To(Equal(int64(0)))
+ g.Expect(recorder.GetEvents()).To(ConsistOf([]corev1.Event{
+ {
+ Type: corev1.EventTypeNormal,
+ Reason: v2.TestSucceededReason,
+ Message: expectMsg,
+ ObjectMeta: metav1.ObjectMeta{
+ Annotations: map[string]string{
+ eventMetaGroupKey(eventv1.MetaRevisionKey): cur.Chart.Metadata.Version,
+ eventMetaGroupKey(metaAppVersionKey): cur.Chart.Metadata.AppVersion,
+ eventMetaGroupKey(eventv1.MetaTokenKey): chartutil.DigestValues(digest.Canonical, cur.Config).String(),
+ },
+ },
+ },
+ }))
+ })
+
+ t.Run("records success without hooks", func(t *testing.T) {
+ r := &Test{
+ eventRecorder: new(testutil.FakeRecorder),
+ }
+
+ obj := obj.DeepCopy()
+ obj.Status.History.Latest().SetTestHooks(map[string]*v2.TestHookStatus{})
+ req := &Request{Object: obj}
+ r.success(req)
+
+ g.Expect(conditions.IsTrue(req.Object, v2.TestSuccessCondition)).To(BeTrue())
+ g.Expect(req.Object.Status.Conditions[0].Message).To(ContainSubstring("no test hooks"))
+ })
+}
diff --git a/internal/reconcile/uninstall.go b/internal/reconcile/uninstall.go
new file mode 100644
index 000000000..09d49dc57
--- /dev/null
+++ b/internal/reconcile/uninstall.go
@@ -0,0 +1,244 @@
+/*
+Copyright 2022 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package reconcile
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "strings"
+
+ helmrelease "helm.sh/helm/v4/pkg/release"
+ helmreleasecommon "helm.sh/helm/v4/pkg/release/common"
+ helmreleasev1 "helm.sh/helm/v4/pkg/release/v1"
+ helmdriver "helm.sh/helm/v4/pkg/storage/driver"
+ corev1 "k8s.io/api/core/v1"
+ "k8s.io/client-go/tools/record"
+
+ "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/release"
+ "github.com/fluxcd/helm-controller/internal/storage"
+)
+
+// Uninstall is an ActionReconciler which attempts to uninstall a Helm release
+// based on the given Request data.
+//
+// The writes to the Helm storage during the uninstallation are observed, and
+// update the Status.History field.
+//
+// After a successful uninstall, the object is marked with Released=False and
+// an event is emitted. When the uninstallation fails, the object is marked
+// with Released=False and a warning event is emitted.
+//
+// When the Request.Object does not have a latest release, it returns an
+// error of type ErrNoLatest. If the uninstallation targeted a different
+// release (version) than the latest release, it returns an error of type
+// ErrReleaseMismatch. In addition, it returns ErrNoStorageUpdate if the
+// uninstallation completed without updating the Helm storage. In which case
+// the resources for the release will be removed from the cluster, but the
+// storage object remains in the cluster. Any other returned error indicates
+// the caller should retry as it did not cause a change to the Helm storage or
+// the cluster resources.
+//
+// At the end of the reconciliation, the Status.Conditions are summarized and
+// propagated to the Ready condition on the Request.Object.
+//
+// This reconciler is different from UninstallRemediation, in that it makes
+// observations to the Released condition type instead of Remediated. Use this
+// reconciler to uninstall a release, and UninstallRemediation to remediate a
+// release.
+//
+// The caller is assumed to have verified the integrity of Request.Object using
+// e.g. action.VerifySnapshot before calling Reconcile.
+type Uninstall struct {
+ configFactory *action.ConfigFactory
+ eventRecorder record.EventRecorder
+}
+
+// NewUninstall returns a new Uninstall reconciler configured with the provided
+// values.
+func NewUninstall(cfg *action.ConfigFactory, recorder record.EventRecorder) *Uninstall {
+ return &Uninstall{configFactory: cfg, eventRecorder: recorder}
+}
+
+func (r *Uninstall) Reconcile(ctx context.Context, req *Request) error {
+ var (
+ cur = req.Object.Status.History.Latest().DeepCopy()
+ logBuf = action.NewDebugLogBuffer(ctx)
+ cfg = r.configFactory.Build(logBuf, observeUninstall(req.Object, v2.ReleaseActionUninstall))
+ )
+
+ defer summarize(req)
+
+ // Require current to run uninstall.
+ if cur == nil {
+ return fmt.Errorf("%w: required to uninstall", ErrNoLatest)
+ }
+
+ // Run the Helm uninstall action.
+ res, err := action.Uninstall(ctx, cfg, req.Object, cur.Name)
+
+ // When the release is not found, something else has already uninstalled
+ // the release. As such, we can assume the release is uninstalled while
+ // taking note that we did not do it.
+ if errors.Is(err, helmdriver.ErrReleaseNotFound) {
+ conditions.MarkFalse(req.Object, v2.ReleasedCondition, v2.UninstallSucceededReason,
+ "Release %s was not found, assuming it is uninstalled", cur.FullReleaseName())
+ return nil
+ }
+
+ // When the release is already uninstalled and the user requested to keep
+ // the history, we can assume the release is uninstalled while taking note
+ // that we did not do it.
+ // This can happen when the release was uninstalled as part of a
+ // remediation, with a subsequent uninstall request due to the object
+ // being deleted.
+ if err != nil && req.Object.GetUninstall().KeepHistory && strings.Contains(err.Error(), "is already deleted") {
+ conditions.MarkFalse(req.Object, v2.ReleasedCondition, v2.UninstallSucceededReason,
+ "Release %s was already uninstalled", cur.FullReleaseName())
+ return nil
+ }
+
+ // The Helm uninstall action does always target the latest release. Before
+ // accepting results, we need to confirm this is actually the release we
+ // have recorded as latest.
+ if res != nil {
+ rls, ok := res.Release.(*helmreleasev1.Release)
+ if !ok {
+ return fmt.Errorf("only the Chart API v2 is supported")
+ }
+ if !release.ObserveRelease(rls).Targets(cur.Name, cur.Namespace, cur.Version) {
+ err = fmt.Errorf("%w: uninstalled release %s/%s.v%d != current release %s",
+ ErrReleaseMismatch, rls.Namespace, rls.Name, rls.Version, cur.FullReleaseName())
+ }
+ }
+
+ // The Helm uninstall action may return without an error while the update
+ // to the storage failed. Detect this and return an error.
+ if err == nil && cur.Digest == req.Object.Status.History.Latest().Digest {
+ // An exception is made for the case where the release was already marked
+ // as uninstalled, which would only result in the release object getting
+ // removed from the storage.
+ if s := helmreleasecommon.Status(cur.Status); s != helmreleasecommon.StatusUninstalled {
+ err = fmt.Errorf("uninstall completed with error: %w", ErrNoStorageUpdate)
+ }
+ }
+
+ // Handle any error.
+ if err != nil {
+ r.failure(req, logBuf, err)
+ return err
+ }
+
+ // Mark success.
+ r.success(req)
+ return nil
+}
+
+func (r *Uninstall) Name() string {
+ return "uninstall"
+}
+
+func (r *Uninstall) Type() ReconcilerType {
+ return ReconcilerTypeRelease
+}
+
+const (
+ // fmtUninstallFailed is the message format for an uninstall failure.
+ fmtUninstallFailure = "Helm uninstall failed for release %s with chart %s: %s"
+ // fmtUninstallSuccess is the message format for a successful uninstall.
+ fmtUninstallSuccess = "Helm uninstall succeeded for release %s with chart %s"
+)
+
+// failure records the failure of a Helm uninstall action in the status of the
+// given Request.Object by marking Released=False and emitting a warning
+// event.
+func (r *Uninstall) failure(req *Request, buffer *action.LogBuffer, err error) {
+ // Compose success message.
+ cur := req.Object.Status.History.Latest()
+ msg := fmt.Sprintf(fmtUninstallFailure, cur.FullReleaseName(), cur.VersionedChartName(), strings.TrimSpace(err.Error()))
+
+ // Mark remediation failure on object.
+ req.Object.Status.Failures++
+ conditions.MarkFalse(req.Object, v2.ReleasedCondition, v2.UninstallFailedReason, "%s", msg)
+
+ // Record warning event, this message contains more data than the
+ // Condition summary.
+ r.eventRecorder.AnnotatedEventf(
+ req.Object,
+ eventMeta(cur.ChartVersion, cur.ConfigDigest, addAppVersion(cur.AppVersion), addOCIDigest(cur.OCIDigest)),
+ corev1.EventTypeWarning, v2.UninstallFailedReason,
+ eventMessageWithLog(msg, buffer),
+ )
+}
+
+// success records the success of a Helm uninstall action in the status of
+// the given Request.Object by marking Released=False and emitting an
+// event.
+func (r *Uninstall) success(req *Request) {
+ // Compose success message.
+ cur := req.Object.Status.History.Latest()
+ msg := fmt.Sprintf(fmtUninstallSuccess, cur.FullReleaseName(), cur.VersionedChartName())
+
+ // Clear inventory as the release has been uninstalled.
+ req.Object.Status.Inventory = nil
+
+ // Mark remediation success on object.
+ conditions.MarkFalse(req.Object, v2.ReleasedCondition, v2.UninstallSucceededReason, "%s", msg)
+
+ // Record warning event, this message contains more data than the
+ // Condition summary.
+ r.eventRecorder.AnnotatedEventf(
+ req.Object,
+ eventMeta(cur.ChartVersion, cur.ConfigDigest, addAppVersion(cur.AppVersion), addOCIDigest(cur.OCIDigest)),
+ corev1.EventTypeNormal,
+ v2.UninstallSucceededReason,
+ msg,
+ )
+}
+
+// observeUninstall returns a storage.ObserveFunc to track uninstallations of a
+// HelmRelease.
+// It compares the release history snapshots with the uninstalled release
+// information.
+// If a matching snapshot for the uninstalled release is found, it updates the
+// snapshot with the observed release data.
+func observeUninstall(obj *v2.HelmRelease, action v2.ReleaseAction) storage.ObserveFunc {
+ // NB: One could argue that we should only update the latest release in
+ // the history.
+ // But like during rollback, Helm may supersede any previous releases.
+ // As such, we need to update all releases we have in our history.
+ // xref: https://github.com/helm/helm/pull/12564
+ return func(rlsr helmrelease.Releaser) {
+ rls, ok := rlsr.(*helmreleasev1.Release)
+ if !ok {
+ return
+ }
+ for i := range obj.Status.History {
+ snap := obj.Status.History[i]
+ if snap.Targets(rls.Name, rls.Namespace, rls.Version) {
+ newSnap := release.ObservedToSnapshot(releaseToObservation(rls, snap, action))
+ newSnap.SetTestHooks(snap.GetTestHooks())
+ obj.Status.History[i] = newSnap
+ return
+ }
+ }
+ }
+}
diff --git a/internal/reconcile/uninstall_remediation.go b/internal/reconcile/uninstall_remediation.go
new file mode 100644
index 000000000..f1015608a
--- /dev/null
+++ b/internal/reconcile/uninstall_remediation.go
@@ -0,0 +1,188 @@
+/*
+Copyright 2022 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package reconcile
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "strings"
+
+ helmreleasev1 "helm.sh/helm/v4/pkg/release/v1"
+ corev1 "k8s.io/api/core/v1"
+ "k8s.io/client-go/tools/record"
+
+ "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/release"
+)
+
+var (
+ ErrNoStorageUpdate = errors.New("release not updated in Helm storage")
+)
+
+// UninstallRemediation is an ActionReconciler which attempts to remediate a
+// failed Helm release for the given Request data by uninstalling it.
+//
+// The writes to the Helm storage during the rollback are observed, and update
+// the Status.History field.
+//
+// After a successful uninstall, the object is marked with Remediated=True and
+// an event is emitted. When the uninstallation fails, the object is marked
+// with Remediated=False and a warning event is emitted.
+//
+// When the Request.Object does not have a latest release, it returns an
+// error of type ErrNoLatest. If the uninstallation targeted a different
+// release (version) than the latest release, it returns an error of type
+// ErrReleaseMismatch. In addition, it returns ErrNoStorageUpdate if the
+// uninstallation completed without updating the Helm storage. In which case
+// the resources for the release will be removed from the cluster, but the
+// storage object remains in the cluster. Any other returned error indicates
+// the caller should retry as it did not cause a change to the Helm storage or
+// the cluster resources.
+//
+// At the end of the reconciliation, the Status.Conditions are summarized and
+// propagated to the Ready condition on the Request.Object.
+//
+// This reconciler is different from Uninstall, in that it makes observations
+// to the Remediated condition type instead of Released. Use this reconciler
+// to remediate a failed release, and Uninstall to uninstall a release.
+//
+// The caller is assumed to have verified the integrity of Request.Object using
+// e.g. action.VerifySnapshot before calling Reconcile.
+type UninstallRemediation struct {
+ configFactory *action.ConfigFactory
+ eventRecorder record.EventRecorder
+}
+
+// NewUninstallRemediation returns a new UninstallRemediation reconciler
+// configured with the provided values.
+func NewUninstallRemediation(cfg *action.ConfigFactory, recorder record.EventRecorder) *UninstallRemediation {
+ return &UninstallRemediation{configFactory: cfg, eventRecorder: recorder}
+}
+
+func (r *UninstallRemediation) Reconcile(ctx context.Context, req *Request) error {
+ var (
+ cur = req.Object.Status.History.Latest().DeepCopy()
+ logBuf = action.NewDebugLogBuffer(ctx)
+ cfg = r.configFactory.Build(logBuf, observeUninstall(req.Object, v2.ReleaseActionUninstallRemediation))
+ )
+
+ // Require current to run uninstall.
+ if cur == nil {
+ return fmt.Errorf("%w: required to uninstall", ErrNoLatest)
+ }
+
+ // Run the Helm uninstall action.
+ res, err := action.Uninstall(ctx, cfg, req.Object, cur.Name)
+
+ // The Helm uninstall action does always target the latest release. Before
+ // accepting results, we need to confirm this is actually the release we
+ // have recorded as latest.
+ if res != nil {
+ rls, ok := res.Release.(*helmreleasev1.Release)
+ if !ok {
+ return fmt.Errorf("only the Chart API v2 is supported")
+ }
+ if !release.ObserveRelease(rls).Targets(cur.Name, cur.Namespace, cur.Version) {
+ err = fmt.Errorf("%w: uninstalled release %s/%s.v%d != current release %s",
+ ErrReleaseMismatch, rls.Namespace, rls.Name, rls.Version, cur.FullReleaseName())
+ }
+ }
+
+ // The Helm uninstall action may return without an error while the update
+ // to the storage failed. Detect this and return an error.
+ if err == nil && cur.Digest == req.Object.Status.History.Latest().Digest {
+ err = fmt.Errorf("uninstall completed with error: %w", ErrNoStorageUpdate)
+ }
+
+ // Handle any error.
+ if err != nil {
+ r.failure(req, logBuf, err)
+ if cur.Digest == req.Object.Status.History.Latest().Digest {
+ return err
+ }
+ return nil
+ }
+
+ // Mark success.
+ r.success(req)
+ return nil
+}
+
+func (r *UninstallRemediation) Name() string {
+ return "uninstall"
+}
+
+func (r *UninstallRemediation) Type() ReconcilerType {
+ return ReconcilerTypeRemediate
+}
+
+const (
+ // fmtUninstallRemediationFailure is the message format for an uninstall
+ // remediation failure.
+ fmtUninstallRemediationFailure = "Helm uninstall remediation for release %s with chart %s failed: %s"
+ // fmtUninstallRemediationSuccess is the message format for a successful
+ // uninstall remediation.
+ fmtUninstallRemediationSuccess = "Helm uninstall remediation for release %s with chart %s succeeded"
+)
+
+// success records the success of a Helm uninstall remediation action in the
+// status of the given Request.Object by marking Remediated=False and emitting
+// a warning event.
+func (r *UninstallRemediation) failure(req *Request, buffer *action.LogBuffer, err error) {
+ // Compose success message.
+ cur := req.Object.Status.History.Latest()
+ msg := fmt.Sprintf(fmtUninstallRemediationFailure, cur.FullReleaseName(), cur.VersionedChartName(), strings.TrimSpace(err.Error()))
+
+ // Mark uninstall failure on object.
+ req.Object.Status.Failures++
+ conditions.MarkFalse(req.Object, v2.RemediatedCondition, v2.UninstallFailedReason, "%s", msg)
+
+ // Record warning event, this message contains more data than the
+ // Condition summary.
+ r.eventRecorder.AnnotatedEventf(
+ req.Object,
+ eventMeta(cur.ChartVersion, cur.ConfigDigest, addAppVersion(cur.AppVersion), addOCIDigest(cur.OCIDigest)),
+ corev1.EventTypeWarning,
+ v2.UninstallFailedReason,
+ eventMessageWithLog(msg, buffer),
+ )
+}
+
+// success records the success of a Helm uninstall remediation action in the
+// status of the given Request.Object by marking Remediated=True and emitting
+// an event.
+func (r *UninstallRemediation) success(req *Request) {
+ // Compose success message.
+ cur := req.Object.Status.History.Latest()
+ msg := fmt.Sprintf(fmtUninstallRemediationSuccess, cur.FullReleaseName(), cur.VersionedChartName())
+
+ // Mark remediation success on object.
+ conditions.MarkTrue(req.Object, v2.RemediatedCondition, v2.UninstallSucceededReason, "%s", msg)
+
+ // Record event.
+ r.eventRecorder.AnnotatedEventf(
+ req.Object,
+ eventMeta(cur.ChartVersion, cur.ConfigDigest, addAppVersion(cur.AppVersion), addOCIDigest(cur.OCIDigest)),
+ corev1.EventTypeNormal,
+ v2.UninstallSucceededReason,
+ msg,
+ )
+}
diff --git a/internal/reconcile/uninstall_remediation_test.go b/internal/reconcile/uninstall_remediation_test.go
new file mode 100644
index 000000000..3179a1862
--- /dev/null
+++ b/internal/reconcile/uninstall_remediation_test.go
@@ -0,0 +1,546 @@
+/*
+Copyright 2022 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package reconcile
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "testing"
+ "time"
+
+ . "github.com/onsi/gomega"
+ helmreleasecommon "helm.sh/helm/v4/pkg/release/common"
+ helmrelease "helm.sh/helm/v4/pkg/release/v1"
+ releaseutil "helm.sh/helm/v4/pkg/release/v1/util"
+ helmstorage "helm.sh/helm/v4/pkg/storage"
+ helmdriver "helm.sh/helm/v4/pkg/storage/driver"
+ corev1 "k8s.io/api/core/v1"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/client-go/tools/record"
+
+ eventv1 "github.com/fluxcd/pkg/apis/event/v1beta1"
+ "github.com/fluxcd/pkg/runtime/conditions"
+
+ "github.com/fluxcd/pkg/chartutil"
+
+ 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/release"
+ "github.com/fluxcd/helm-controller/internal/storage"
+ "github.com/fluxcd/helm-controller/internal/testutil"
+)
+
+func TestUninstallRemediation_Reconcile(t *testing.T) {
+ var (
+ mockUpdateErr = fmt.Errorf("storage update error")
+ mockDeleteErr = fmt.Errorf("storage delete error")
+ )
+
+ tests := []struct {
+ name string
+ // driver allows for modifying the Helm storage driver.
+ driver func(helmdriver.Driver) helmdriver.Driver
+ // releases is the list of releases that are stored in the driver
+ // before uninstall.
+ releases func(namespace string) []*helmrelease.Release
+ // spec modifies the HelmRelease Object spec before uninstall.
+ spec func(spec *v2.HelmReleaseSpec)
+ // status to configure on the HelmRelease Object before uninstall.
+ status func(releases []*helmrelease.Release) v2.HelmReleaseStatus
+ // wantErr is the error that is expected to be returned.
+ wantErr error
+ // expectedConditions are the conditions that are expected to be set on
+ // the HelmRelease after running rollback.
+ expectConditions []metav1.Condition
+ // expectHistory is the expected History of the HelmRelease after
+ // uninstall.
+ expectHistory func(releases []*helmrelease.Release) v2.Snapshots
+ // expectFailures is the expected Failures count of the HelmRelease.
+ expectFailures int64
+ // expectInstallFailures is the expected InstallFailures count of the
+ // HelmRelease.
+ expectInstallFailures int64
+ // expectUpgradeFailures is the expected UpgradeFailures count of the
+ // HelmRelease.
+ expectUpgradeFailures int64
+ // statusReader is an optional StatusReader to configure on the
+ // ConfigFactory.
+ statusReader bool
+ }{
+ {
+ name: "uninstall success",
+ releases: func(namespace string) []*helmrelease.Release {
+ return []*helmrelease.Release{
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: namespace,
+ Version: 1,
+ Chart: testutil.BuildChart(testutil.ChartWithTestHook()),
+ Status: helmreleasecommon.StatusDeployed,
+ }),
+ }
+ },
+ spec: func(spec *v2.HelmReleaseSpec) {
+ spec.Uninstall = &v2.Uninstall{
+ KeepHistory: true,
+ }
+ },
+ status: func(releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ return v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
+ },
+ }
+ },
+ expectConditions: []metav1.Condition{
+ *conditions.TrueCondition(v2.RemediatedCondition, v2.UninstallSucceededReason,
+ "succeeded"),
+ },
+ expectHistory: func(releases []*helmrelease.Release) v2.Snapshots {
+ return v2.Snapshots{
+ observeReleaseWithAction(releases[0], v2.ReleaseActionUninstallRemediation),
+ }
+ },
+ },
+ {
+ name: "uninstall failure",
+ releases: func(namespace string) []*helmrelease.Release {
+ return []*helmrelease.Release{
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: namespace,
+ Version: 1,
+ Status: helmreleasecommon.StatusDeployed,
+ Chart: testutil.BuildChart(testutil.ChartWithFailingHook()),
+ }, testutil.ReleaseWithFailingHook()),
+ }
+ },
+ spec: func(spec *v2.HelmReleaseSpec) {
+ spec.Uninstall = &v2.Uninstall{
+ KeepHistory: true,
+ }
+ },
+ status: func(releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ return v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
+ },
+ }
+ },
+ expectConditions: []metav1.Condition{
+ *conditions.FalseCondition(v2.RemediatedCondition, v2.UninstallFailedReason,
+ "context deadline exceeded"),
+ },
+ expectHistory: func(releases []*helmrelease.Release) v2.Snapshots {
+ return v2.Snapshots{
+ observeReleaseWithAction(releases[0], v2.ReleaseActionUninstallRemediation),
+ }
+ },
+ expectFailures: 1,
+ },
+ {
+ name: "uninstall failure without storage update",
+ driver: func(driver helmdriver.Driver) helmdriver.Driver {
+ return &storage.Failing{
+ // Explicitly inherit the driver, as we want to rely on the
+ // Secret storage, as the memory storage does not detach
+ // objects from the release action. Causing writes post-persist
+ // to leak to the stored release object.
+ // xref: https://github.com/helm/helm/issues/11304
+ Driver: driver,
+ UpdateErr: mockUpdateErr,
+ }
+ },
+ releases: func(namespace string) []*helmrelease.Release {
+ return []*helmrelease.Release{
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: namespace,
+ Version: 1,
+ Chart: testutil.BuildChart(testutil.ChartWithTestHook()),
+ Status: helmreleasecommon.StatusDeployed,
+ }),
+ }
+ },
+ spec: func(spec *v2.HelmReleaseSpec) {
+ spec.Uninstall = &v2.Uninstall{
+ KeepHistory: true,
+ }
+ },
+ status: func(releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ return v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
+ },
+ }
+ },
+ expectConditions: []metav1.Condition{
+ *conditions.FalseCondition(v2.RemediatedCondition, v2.UninstallFailedReason,
+ "%s", ErrNoStorageUpdate.Error()),
+ },
+ expectHistory: func(releases []*helmrelease.Release) v2.Snapshots {
+ return v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
+ }
+ },
+ expectFailures: 1,
+ wantErr: ErrNoStorageUpdate,
+ },
+ {
+ name: "uninstall failure without storage delete",
+ driver: func(driver helmdriver.Driver) helmdriver.Driver {
+ return &storage.Failing{
+ // Explicitly inherit the driver, as we want to rely on the
+ // Secret storage, as the memory storage does not detach
+ // objects from the release action. Causing writes post-persist
+ // to leak to the stored release object.
+ // xref: https://github.com/helm/helm/issues/11304
+ Driver: driver,
+ DeleteErr: mockDeleteErr,
+ }
+ },
+ releases: func(namespace string) []*helmrelease.Release {
+ return []*helmrelease.Release{
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: namespace,
+ Version: 1,
+ Status: helmreleasecommon.StatusDeployed,
+ Chart: testutil.BuildChart(),
+ }),
+ }
+ },
+ status: func(releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ return v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
+ },
+ }
+ },
+ expectConditions: []metav1.Condition{
+ *conditions.FalseCondition(v2.RemediatedCondition, v2.UninstallFailedReason, "%s", mockDeleteErr.Error()),
+ },
+ expectHistory: func(releases []*helmrelease.Release) v2.Snapshots {
+ return v2.Snapshots{
+ observeReleaseWithAction(releases[0], v2.ReleaseActionUninstallRemediation),
+ }
+ },
+ expectFailures: 1,
+ },
+ {
+ name: "uninstall without current",
+ releases: func(namespace string) []*helmrelease.Release {
+ return []*helmrelease.Release{
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: namespace,
+ Version: 1,
+ Chart: testutil.BuildChart(),
+ Status: helmreleasecommon.StatusDeployed,
+ }),
+ }
+ },
+ expectConditions: []metav1.Condition{},
+ wantErr: ErrNoLatest,
+ },
+ {
+ name: "uninstall with stale current",
+ releases: func(namespace string) []*helmrelease.Release {
+ return []*helmrelease.Release{
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: namespace,
+ Version: 1,
+ Chart: testutil.BuildChart(testutil.ChartWithTestHook()),
+ Status: helmreleasecommon.StatusSuperseded,
+ }, testutil.ReleaseWithTestHook()),
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: namespace,
+ Version: 2,
+ Chart: testutil.BuildChart(),
+ Status: helmreleasecommon.StatusDeployed,
+ }),
+ }
+ },
+ spec: func(spec *v2.HelmReleaseSpec) {
+ spec.Uninstall = &v2.Uninstall{
+ KeepHistory: true,
+ }
+ },
+ status: func(releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ return v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
+ },
+ }
+ },
+ expectConditions: []metav1.Condition{
+ *conditions.FalseCondition(v2.RemediatedCondition, v2.UninstallFailedReason,
+ "%s", ErrReleaseMismatch.Error()),
+ },
+ expectHistory: func(releases []*helmrelease.Release) v2.Snapshots {
+ return v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
+ }
+ },
+ expectFailures: 1,
+ wantErr: ErrReleaseMismatch,
+ },
+ {
+ name: "uninstall success with status reader",
+ releases: func(namespace string) []*helmrelease.Release {
+ return []*helmrelease.Release{
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: namespace,
+ Version: 1,
+ Chart: testutil.BuildChart(testutil.ChartWithTestHook()),
+ Status: helmreleasecommon.StatusDeployed,
+ }),
+ }
+ },
+ spec: func(spec *v2.HelmReleaseSpec) {
+ spec.Uninstall = &v2.Uninstall{
+ KeepHistory: true,
+ }
+ },
+ status: func(releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ return v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
+ },
+ }
+ },
+ statusReader: true,
+ expectConditions: []metav1.Condition{
+ *conditions.TrueCondition(v2.RemediatedCondition, v2.UninstallSucceededReason,
+ "succeeded"),
+ },
+ expectHistory: func(releases []*helmrelease.Release) v2.Snapshots {
+ return v2.Snapshots{
+ observeReleaseWithAction(releases[0], v2.ReleaseActionUninstallRemediation),
+ }
+ },
+ },
+ }
+ 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
+
+ var releases []*helmrelease.Release
+ if tt.releases != nil {
+ releases = tt.releases(releaseNamespace)
+ releaseutil.SortByRevision(releases)
+ }
+
+ obj := &v2.HelmRelease{
+ Spec: v2.HelmReleaseSpec{
+ ReleaseName: mockReleaseName,
+ TargetNamespace: releaseNamespace,
+ StorageNamespace: releaseNamespace,
+ Timeout: &metav1.Duration{Duration: 200 * 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())
+
+ cfgOpts := []action.ConfigFactoryOption{
+ action.WithStorage(action.DefaultStorageDriver, obj.GetStorageNamespace()),
+ }
+ cfg, err := action.NewConfigFactory(getter, cfgOpts...)
+ g.Expect(err).ToNot(HaveOccurred())
+
+ store := helmstorage.Init(cfg.Driver)
+ for _, r := range releases {
+ g.Expect(store.Create(r)).To(Succeed())
+ }
+
+ if tt.driver != nil {
+ cfg.Driver = tt.driver(cfg.Driver)
+ }
+
+ recorder := new(record.FakeRecorder)
+ got := NewUninstallRemediation(cfg, recorder).Reconcile(context.TODO(), &Request{
+ Object: obj,
+ })
+ if tt.wantErr != nil {
+ g.Expect(errors.Is(got, tt.wantErr)).To(BeTrue())
+ } else {
+ g.Expect(got).ToNot(HaveOccurred())
+ }
+
+ g.Expect(obj.Status.Conditions).To(conditions.MatchConditions(tt.expectConditions))
+
+ releases, _ = storeHistory(store, mockReleaseName)
+ releaseutil.SortByRevision(releases)
+
+ if tt.expectHistory != nil {
+ g.Expect(obj.Status.History).To(testutil.Equal(tt.expectHistory(releases)))
+ } else {
+ g.Expect(obj.Status.History).To(BeEmpty(), "expected history to be empty")
+ }
+
+ g.Expect(obj.Status.Failures).To(Equal(tt.expectFailures))
+ g.Expect(obj.Status.InstallFailures).To(Equal(tt.expectInstallFailures))
+ g.Expect(obj.Status.UpgradeFailures).To(Equal(tt.expectUpgradeFailures))
+
+ // Note: For uninstall, StatusReader is configured but not actively used
+ // since uninstall waits for resource deletion, not health checks.
+ // The test verifies that configuring a StatusReader doesn't break uninstall.
+ })
+ }
+}
+
+func TestUninstallRemediation_failure(t *testing.T) {
+ var (
+ cur = testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Chart: testutil.BuildChart(),
+ Version: 4,
+ })
+ obj = &v2.HelmRelease{
+ Status: v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(cur)),
+ },
+ },
+ }
+ err = errors.New("uninstall error")
+ )
+
+ t.Run("records failure", func(t *testing.T) {
+ g := NewWithT(t)
+
+ recorder := testutil.NewFakeRecorder(10, false)
+ r := &UninstallRemediation{
+ eventRecorder: recorder,
+ }
+
+ req := &Request{Object: obj.DeepCopy()}
+ r.failure(req, nil, err)
+
+ expectMsg := fmt.Sprintf(fmtUninstallRemediationFailure,
+ fmt.Sprintf("%s/%s.v%d", cur.Namespace, cur.Name, cur.Version),
+ fmt.Sprintf("%s@%s", cur.Chart.Name(), cur.Chart.Metadata.Version),
+ err.Error())
+
+ g.Expect(req.Object.Status.Conditions).To(conditions.MatchConditions([]metav1.Condition{
+ *conditions.FalseCondition(v2.RemediatedCondition, v2.UninstallFailedReason, "%s", expectMsg),
+ }))
+ g.Expect(req.Object.Status.Failures).To(Equal(int64(1)))
+ g.Expect(recorder.GetEvents()).To(ConsistOf([]corev1.Event{
+ {
+ Type: corev1.EventTypeWarning,
+ Reason: v2.UninstallFailedReason,
+ Message: expectMsg,
+ ObjectMeta: metav1.ObjectMeta{
+ Annotations: map[string]string{
+ eventMetaGroupKey(eventv1.MetaRevisionKey): cur.Chart.Metadata.Version,
+ eventMetaGroupKey(metaAppVersionKey): cur.Chart.Metadata.AppVersion,
+ eventMetaGroupKey(eventv1.MetaTokenKey): chartutil.DigestValues(digest.Canonical, cur.Config).String(),
+ },
+ },
+ },
+ }))
+ })
+
+ t.Run("records failure with logs", func(t *testing.T) {
+ g := NewWithT(t)
+
+ recorder := testutil.NewFakeRecorder(10, false)
+ r := &UninstallRemediation{
+ eventRecorder: recorder,
+ }
+ req := &Request{Object: obj.DeepCopy()}
+ r.failure(req, mockLogBuffer(), err)
+
+ expectSubStr := "Last Helm logs"
+ g.Expect(conditions.IsFalse(req.Object, v2.RemediatedCondition)).To(BeTrue())
+ g.Expect(conditions.GetMessage(req.Object, v2.RemediatedCondition)).ToNot(ContainSubstring(expectSubStr))
+
+ events := recorder.GetEvents()
+ g.Expect(events).To(HaveLen(1))
+ g.Expect(events[0].Message).To(ContainSubstring(expectSubStr))
+ })
+}
+
+func TestUninstallRemediation_success(t *testing.T) {
+ g := NewWithT(t)
+
+ var cur = testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: mockReleaseNamespace,
+ Chart: testutil.BuildChart(),
+ Version: 4,
+ })
+
+ recorder := testutil.NewFakeRecorder(10, false)
+ r := &UninstallRemediation{
+ eventRecorder: recorder,
+ }
+
+ obj := &v2.HelmRelease{
+ Status: v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(cur)),
+ },
+ },
+ }
+
+ req := &Request{Object: obj}
+ r.success(req)
+
+ expectMsg := fmt.Sprintf(fmtUninstallRemediationSuccess,
+ fmt.Sprintf("%s/%s.v%d", cur.Namespace, cur.Name, cur.Version),
+ fmt.Sprintf("%s@%s", cur.Chart.Name(), cur.Chart.Metadata.Version))
+
+ g.Expect(req.Object.Status.Conditions).To(conditions.MatchConditions([]metav1.Condition{
+ *conditions.TrueCondition(v2.RemediatedCondition, v2.UninstallSucceededReason, "%s", expectMsg),
+ }))
+ g.Expect(req.Object.Status.Failures).To(Equal(int64(0)))
+ g.Expect(recorder.GetEvents()).To(ConsistOf([]corev1.Event{
+ {
+ Type: corev1.EventTypeNormal,
+ Reason: v2.UninstallSucceededReason,
+ Message: expectMsg,
+ ObjectMeta: metav1.ObjectMeta{
+ Annotations: map[string]string{
+ eventMetaGroupKey(eventv1.MetaRevisionKey): cur.Chart.Metadata.Version,
+ eventMetaGroupKey(metaAppVersionKey): cur.Chart.Metadata.AppVersion,
+ eventMetaGroupKey(eventv1.MetaTokenKey): chartutil.DigestValues(digest.Canonical, cur.Config).String(),
+ },
+ },
+ },
+ }))
+}
diff --git a/internal/reconcile/uninstall_test.go b/internal/reconcile/uninstall_test.go
new file mode 100644
index 000000000..29e8534e0
--- /dev/null
+++ b/internal/reconcile/uninstall_test.go
@@ -0,0 +1,843 @@
+/*
+Copyright 2022 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package reconcile
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "testing"
+ "time"
+
+ . "github.com/onsi/gomega"
+ helmreleasecommon "helm.sh/helm/v4/pkg/release/common"
+ helmrelease "helm.sh/helm/v4/pkg/release/v1"
+ releaseutil "helm.sh/helm/v4/pkg/release/v1/util"
+ helmstorage "helm.sh/helm/v4/pkg/storage"
+ helmdriver "helm.sh/helm/v4/pkg/storage/driver"
+ corev1 "k8s.io/api/core/v1"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/client-go/tools/record"
+
+ eventv1 "github.com/fluxcd/pkg/apis/event/v1beta1"
+ "github.com/fluxcd/pkg/apis/meta"
+ "github.com/fluxcd/pkg/runtime/conditions"
+
+ "github.com/fluxcd/pkg/chartutil"
+
+ 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/release"
+ "github.com/fluxcd/helm-controller/internal/storage"
+ "github.com/fluxcd/helm-controller/internal/testutil"
+)
+
+func TestUninstall_Reconcile(t *testing.T) {
+ mockUpdateErr := errors.New("mock update error")
+
+ tests := []struct {
+ name string
+ // driver allows for modifying the Helm storage driver.
+ driver func(helmdriver.Driver) helmdriver.Driver
+ // releases is the list of releases that are stored in the driver
+ // before uninstall.
+ releases func(namespace string) []*helmrelease.Release
+ // spec modifies the HelmRelease Object spec before uninstall.
+ spec func(spec *v2.HelmReleaseSpec)
+ // status to configure on the HelmRelease Object before uninstall.
+ status func(releases []*helmrelease.Release) v2.HelmReleaseStatus
+ // wantErr is the error that is expected to be returned.
+ wantErr error
+ // wantErrString is the error string that is expected to be in the
+ // returned error. This is used for scenarios that return
+ // untyped/unwrapped error that can't be asserted for their value.
+ wantErrString string
+ // expectedConditions are the conditions that are expected to be set on
+ // the HelmRelease after running rollback.
+ expectConditions []metav1.Condition
+ // expectHistory is the expected History of the HelmRelease after
+ // uninstall.
+ expectHistory func(namespace string, releases []*helmrelease.Release) v2.Snapshots
+ // expectInventory is the expected Inventory of the HelmRelease after
+ // uninstall. If nil, inventory is not checked.
+ expectInventory func() *v2.ResourceInventory
+ // expectFailures is the expected Failures count of the HelmRelease.
+ expectFailures int64
+ // expectInstallFailures is the expected InstallFailures count of the
+ // HelmRelease.
+ expectInstallFailures int64
+ // expectUpgradeFailures is the expected UpgradeFailures count of the
+ // HelmRelease.
+ expectUpgradeFailures int64
+ // statusReader is an optional StatusReader to configure on the
+ // ConfigFactory.
+ statusReader bool
+ }{
+ {
+ name: "uninstall success",
+ releases: func(namespace string) []*helmrelease.Release {
+ return []*helmrelease.Release{
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: namespace,
+ Version: 1,
+ Chart: testutil.BuildChart(testutil.ChartWithTestHook()),
+ Status: helmreleasecommon.StatusDeployed,
+ }),
+ }
+ },
+ spec: func(spec *v2.HelmReleaseSpec) {
+ spec.Uninstall = &v2.Uninstall{
+ KeepHistory: true,
+ }
+ },
+ status: func(releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ return v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
+ },
+ }
+ },
+ expectConditions: []metav1.Condition{
+ *conditions.FalseCondition(meta.ReadyCondition, v2.UninstallSucceededReason,
+ "succeeded"),
+ *conditions.FalseCondition(v2.ReleasedCondition, v2.UninstallSucceededReason,
+ "succeeded"),
+ },
+ expectHistory: func(namespace string, releases []*helmrelease.Release) v2.Snapshots {
+ return v2.Snapshots{
+ observeReleaseWithAction(releases[0], v2.ReleaseActionUninstall),
+ }
+ },
+ expectInventory: func() *v2.ResourceInventory {
+ return nil
+ },
+ },
+ {
+ name: "uninstall failure",
+ releases: func(namespace string) []*helmrelease.Release {
+ return []*helmrelease.Release{
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: namespace,
+ Version: 1,
+ Status: helmreleasecommon.StatusDeployed,
+ Chart: testutil.BuildChart(testutil.ChartWithFailingHook()),
+ }, testutil.ReleaseWithFailingHook()),
+ }
+ },
+ spec: func(spec *v2.HelmReleaseSpec) {
+ spec.Uninstall = &v2.Uninstall{
+ KeepHistory: true,
+ }
+ },
+ status: func(releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ return v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
+ },
+ }
+ },
+ expectConditions: []metav1.Condition{
+ *conditions.FalseCondition(meta.ReadyCondition, v2.UninstallFailedReason,
+ "context deadline exceeded"),
+ *conditions.FalseCondition(v2.ReleasedCondition, v2.UninstallFailedReason,
+ "context deadline exceeded"),
+ },
+ expectHistory: func(namespace string, releases []*helmrelease.Release) v2.Snapshots {
+ return v2.Snapshots{
+ observeReleaseWithAction(releases[0], v2.ReleaseActionUninstall),
+ }
+ },
+ expectFailures: 1,
+ wantErrString: "context deadline exceeded",
+ },
+ {
+ name: "uninstall failure without storage update",
+ driver: func(driver helmdriver.Driver) helmdriver.Driver {
+ return &storage.Failing{
+ // Explicitly inherit the driver, as we want to rely on the
+ // Secret storage, as the memory storage does not detach
+ // objects from the release action. Causing writes post-persist
+ // to leak to the stored release object.
+ // xref: https://github.com/helm/helm/issues/11304
+ Driver: driver,
+ UpdateErr: mockUpdateErr,
+ }
+ },
+ releases: func(namespace string) []*helmrelease.Release {
+ return []*helmrelease.Release{
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: namespace,
+ Version: 1,
+ Chart: testutil.BuildChart(testutil.ChartWithTestHook()),
+ Status: helmreleasecommon.StatusDeployed,
+ }),
+ }
+ },
+ spec: func(spec *v2.HelmReleaseSpec) {
+ spec.Uninstall = &v2.Uninstall{
+ KeepHistory: true,
+ }
+ },
+ status: func(releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ return v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
+ },
+ }
+ },
+ expectConditions: []metav1.Condition{
+ *conditions.FalseCondition(meta.ReadyCondition, v2.UninstallFailedReason,
+ "%s", ErrNoStorageUpdate.Error()),
+ *conditions.FalseCondition(v2.ReleasedCondition, v2.UninstallFailedReason,
+ "%s", ErrNoStorageUpdate.Error()),
+ },
+ expectHistory: func(namespace string, releases []*helmrelease.Release) v2.Snapshots {
+ return v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
+ }
+ },
+ expectFailures: 1,
+ wantErr: ErrNoStorageUpdate,
+ },
+ {
+ name: "uninstall failure without storage delete",
+ driver: func(driver helmdriver.Driver) helmdriver.Driver {
+ return &storage.Failing{
+ // Explicitly inherit the driver, as we want to rely on the
+ // Secret storage, as the memory storage does not detach
+ // objects from the release action. Causing writes post-persist
+ // to leak to the stored release object.
+ // xref: https://github.com/helm/helm/issues/11304
+ Driver: driver,
+ DeleteErr: fmt.Errorf("delete error"),
+ }
+ },
+ releases: func(namespace string) []*helmrelease.Release {
+ return []*helmrelease.Release{
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: namespace,
+ Version: 1,
+ Status: helmreleasecommon.StatusDeployed,
+ Chart: testutil.BuildChart(),
+ }),
+ }
+ },
+ status: func(releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ return v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
+ },
+ }
+ },
+ expectConditions: []metav1.Condition{
+ *conditions.FalseCondition(meta.ReadyCondition, v2.UninstallFailedReason,
+ "delete error"),
+ *conditions.FalseCondition(v2.ReleasedCondition, v2.UninstallFailedReason,
+ "delete error"),
+ },
+ expectHistory: func(namespace string, releases []*helmrelease.Release) v2.Snapshots {
+ return v2.Snapshots{
+ observeReleaseWithAction(releases[0], v2.ReleaseActionUninstall),
+ }
+ },
+ expectFailures: 1,
+ wantErrString: "Failed to purge the release",
+ },
+ {
+ name: "uninstall without current",
+ releases: func(namespace string) []*helmrelease.Release {
+ return []*helmrelease.Release{
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: namespace,
+ Version: 1,
+ Chart: testutil.BuildChart(),
+ Status: helmreleasecommon.StatusDeployed,
+ }),
+ }
+ },
+ expectConditions: []metav1.Condition{},
+ wantErr: ErrNoLatest,
+ },
+ {
+ name: "uninstall with stale current",
+ releases: func(namespace string) []*helmrelease.Release {
+ return []*helmrelease.Release{
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: namespace,
+ Version: 1,
+ Chart: testutil.BuildChart(testutil.ChartWithTestHook()),
+ Status: helmreleasecommon.StatusSuperseded,
+ }, testutil.ReleaseWithTestHook()),
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: namespace,
+ Version: 2,
+ Chart: testutil.BuildChart(),
+ Status: helmreleasecommon.StatusDeployed,
+ }),
+ }
+ },
+ spec: func(spec *v2.HelmReleaseSpec) {
+ spec.Uninstall = &v2.Uninstall{
+ KeepHistory: true,
+ }
+ },
+ status: func(releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ return v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
+ },
+ }
+ },
+ expectConditions: []metav1.Condition{
+ *conditions.FalseCondition(meta.ReadyCondition, v2.UninstallFailedReason,
+ "%s", ErrReleaseMismatch.Error()),
+ *conditions.FalseCondition(v2.ReleasedCondition, v2.UninstallFailedReason,
+ "%s", ErrReleaseMismatch.Error()),
+ },
+ expectHistory: func(namespace string, releases []*helmrelease.Release) v2.Snapshots {
+ return v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
+ }
+ },
+ expectFailures: 1,
+ wantErr: ErrReleaseMismatch,
+ },
+ {
+ name: "uninstall already deleted release",
+ driver: func(driver helmdriver.Driver) helmdriver.Driver {
+ return &storage.Failing{
+ // Explicitly inherit the driver, as we want to rely on the
+ // Secret storage, as the memory storage does not detach
+ // objects from the release action. Causing writes post-persist
+ // to leak to the stored release object.
+ // xref: https://github.com/helm/helm/issues/11304
+ Driver: driver,
+ QueryErr: helmdriver.ErrReleaseNotFound,
+ }
+ },
+ releases: func(namespace string) []*helmrelease.Release {
+ return []*helmrelease.Release{
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: namespace,
+ Version: 1,
+ Chart: testutil.BuildChart(testutil.ChartWithTestHook()),
+ Status: helmreleasecommon.StatusDeployed,
+ }),
+ }
+ },
+ status: func(releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ return v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
+ },
+ }
+ },
+ expectConditions: []metav1.Condition{
+ *conditions.FalseCondition(meta.ReadyCondition, v2.UninstallSucceededReason,
+ "assuming it is uninstalled"),
+ *conditions.FalseCondition(v2.ReleasedCondition, v2.UninstallSucceededReason,
+ "assuming it is uninstalled"),
+ },
+ expectHistory: func(namespace string, releases []*helmrelease.Release) v2.Snapshots {
+ return v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
+ }
+ },
+ },
+ {
+ name: "already uninstalled without keep history",
+ releases: func(namespace string) []*helmrelease.Release {
+ return []*helmrelease.Release{
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: namespace,
+ Version: 1,
+ Chart: testutil.BuildChart(testutil.ChartWithTestHook()),
+ Status: helmreleasecommon.StatusUninstalled,
+ }),
+ }
+ },
+ status: func(releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ return v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
+ },
+ }
+ },
+ expectConditions: []metav1.Condition{
+ *conditions.FalseCondition(meta.ReadyCondition, v2.UninstallSucceededReason,
+ "succeeded"),
+ *conditions.FalseCondition(v2.ReleasedCondition, v2.UninstallSucceededReason,
+ "succeeded"),
+ },
+ expectHistory: func(namespace string, releases []*helmrelease.Release) v2.Snapshots {
+ rls := testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: namespace,
+ Version: 1,
+ Chart: testutil.BuildChart(testutil.ChartWithTestHook()),
+ Status: helmreleasecommon.StatusUninstalled,
+ })
+ return v2.Snapshots{
+ observeReleaseWithAction(rls, v2.ReleaseActionUninstall),
+ }
+ },
+ },
+ {
+ name: "already uninstalled with keep history",
+ releases: func(namespace string) []*helmrelease.Release {
+ return []*helmrelease.Release{
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: namespace,
+ Version: 1,
+ Chart: testutil.BuildChart(testutil.ChartWithTestHook()),
+ Status: helmreleasecommon.StatusUninstalled,
+ }),
+ }
+ },
+ spec: func(spec *v2.HelmReleaseSpec) {
+ spec.Uninstall = &v2.Uninstall{
+ KeepHistory: true,
+ }
+ },
+ status: func(releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ return v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
+ },
+ }
+ },
+ expectConditions: []metav1.Condition{
+ *conditions.FalseCondition(meta.ReadyCondition, v2.UninstallSucceededReason,
+ "was already uninstalled"),
+ *conditions.FalseCondition(v2.ReleasedCondition, v2.UninstallSucceededReason,
+ "was already uninstalled"),
+ },
+ expectHistory: func(namespace string, releases []*helmrelease.Release) v2.Snapshots {
+ return v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
+ }
+ },
+ },
+ {
+ name: "uninstall success with status reader",
+ releases: func(namespace string) []*helmrelease.Release {
+ return []*helmrelease.Release{
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: namespace,
+ Version: 1,
+ Chart: testutil.BuildChart(testutil.ChartWithTestHook()),
+ Status: helmreleasecommon.StatusDeployed,
+ }),
+ }
+ },
+ spec: func(spec *v2.HelmReleaseSpec) {
+ spec.Uninstall = &v2.Uninstall{
+ KeepHistory: true,
+ }
+ },
+ status: func(releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ return v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
+ },
+ }
+ },
+ statusReader: true,
+ expectConditions: []metav1.Condition{
+ *conditions.FalseCondition(meta.ReadyCondition, v2.UninstallSucceededReason,
+ "succeeded"),
+ *conditions.FalseCondition(v2.ReleasedCondition, v2.UninstallSucceededReason,
+ "succeeded"),
+ },
+ expectHistory: func(namespace string, releases []*helmrelease.Release) v2.Snapshots {
+ return v2.Snapshots{
+ observeReleaseWithAction(releases[0], v2.ReleaseActionUninstall),
+ }
+ },
+ expectInventory: func() *v2.ResourceInventory {
+ return nil
+ },
+ },
+ }
+ 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
+
+ var releases []*helmrelease.Release
+ if tt.releases != nil {
+ releases = tt.releases(releaseNamespace)
+ releaseutil.SortByRevision(releases)
+ }
+
+ obj := &v2.HelmRelease{
+ Spec: v2.HelmReleaseSpec{
+ ReleaseName: mockReleaseName,
+ TargetNamespace: releaseNamespace,
+ StorageNamespace: releaseNamespace,
+ Timeout: &metav1.Duration{Duration: 200 * 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())
+
+ cfgOpts := []action.ConfigFactoryOption{
+ action.WithStorage(action.DefaultStorageDriver, obj.GetStorageNamespace()),
+ }
+ cfg, err := action.NewConfigFactory(getter, cfgOpts...)
+ g.Expect(err).ToNot(HaveOccurred())
+
+ store := helmstorage.Init(cfg.Driver)
+ for _, r := range releases {
+ g.Expect(store.Create(r)).To(Succeed())
+ }
+
+ if tt.driver != nil {
+ cfg.Driver = tt.driver(cfg.Driver)
+ }
+
+ recorder := new(record.FakeRecorder)
+ got := NewUninstall(cfg, recorder).Reconcile(context.TODO(), &Request{
+ Object: obj,
+ })
+ if tt.wantErr != nil {
+ g.Expect(errors.Is(got, tt.wantErr)).To(BeTrue())
+ } else if tt.wantErrString != "" {
+ g.Expect(got.Error()).To(ContainSubstring(tt.wantErrString))
+ } else {
+ g.Expect(got).ToNot(HaveOccurred())
+ }
+
+ g.Expect(obj.Status.Conditions).To(conditions.MatchConditions(tt.expectConditions))
+
+ releases, _ = storeHistory(store, mockReleaseName)
+ releaseutil.SortByRevision(releases)
+
+ if tt.expectHistory != nil {
+ g.Expect(obj.Status.History).To(testutil.Equal(tt.expectHistory(releaseNamespace, releases)))
+ } else {
+ g.Expect(obj.Status.History).To(BeEmpty(), "expected history to be empty")
+ }
+
+ g.Expect(obj.Status.Failures).To(Equal(tt.expectFailures))
+ g.Expect(obj.Status.InstallFailures).To(Equal(tt.expectInstallFailures))
+ g.Expect(obj.Status.UpgradeFailures).To(Equal(tt.expectUpgradeFailures))
+
+ if tt.expectInventory != nil {
+ g.Expect(obj.Status.Inventory).To(testutil.Equal(tt.expectInventory()))
+ }
+
+ // Note: For uninstall, StatusReader is configured but not actively used
+ // since uninstall waits for resource deletion, not health checks.
+ // The test verifies that configuring a StatusReader doesn't break uninstall.
+ })
+ }
+}
+
+func TestUninstall_failure(t *testing.T) {
+ var (
+ cur = testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Chart: testutil.BuildChart(),
+ Version: 4,
+ })
+ obj = &v2.HelmRelease{
+ Status: v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(cur)),
+ },
+ },
+ }
+ err = errors.New("uninstall error")
+ )
+
+ t.Run("records failure", func(t *testing.T) {
+ g := NewWithT(t)
+
+ recorder := testutil.NewFakeRecorder(10, false)
+ r := &Uninstall{
+ eventRecorder: recorder,
+ }
+
+ req := &Request{Object: obj.DeepCopy()}
+ r.failure(req, nil, err)
+
+ expectMsg := fmt.Sprintf(fmtUninstallFailure,
+ fmt.Sprintf("%s/%s.v%d", cur.Namespace, cur.Name, cur.Version),
+ fmt.Sprintf("%s@%s", cur.Chart.Name(), cur.Chart.Metadata.Version),
+ err.Error())
+
+ g.Expect(req.Object.Status.Conditions).To(conditions.MatchConditions([]metav1.Condition{
+ *conditions.FalseCondition(v2.ReleasedCondition, v2.UninstallFailedReason, "%s", expectMsg),
+ }))
+ g.Expect(req.Object.Status.Failures).To(Equal(int64(1)))
+ g.Expect(recorder.GetEvents()).To(ConsistOf([]corev1.Event{
+ {
+ Type: corev1.EventTypeWarning,
+ Reason: v2.UninstallFailedReason,
+ Message: expectMsg,
+ ObjectMeta: metav1.ObjectMeta{
+ Annotations: map[string]string{
+ eventMetaGroupKey(eventv1.MetaRevisionKey): cur.Chart.Metadata.Version,
+ eventMetaGroupKey(metaAppVersionKey): cur.Chart.Metadata.AppVersion,
+ eventMetaGroupKey(eventv1.MetaTokenKey): chartutil.DigestValues(digest.Canonical, cur.Config).String(),
+ },
+ },
+ },
+ }))
+ })
+
+ t.Run("records failure with logs", func(t *testing.T) {
+ g := NewWithT(t)
+
+ recorder := testutil.NewFakeRecorder(10, false)
+ r := &Uninstall{
+ eventRecorder: recorder,
+ }
+ req := &Request{Object: obj.DeepCopy()}
+ r.failure(req, mockLogBuffer(), err)
+
+ expectSubStr := "Last Helm logs"
+ g.Expect(conditions.IsFalse(req.Object, v2.ReleasedCondition)).To(BeTrue())
+ g.Expect(conditions.GetMessage(req.Object, v2.ReleasedCondition)).ToNot(ContainSubstring(expectSubStr))
+
+ events := recorder.GetEvents()
+ g.Expect(events).To(HaveLen(1))
+ g.Expect(events[0].Message).To(ContainSubstring(expectSubStr))
+ })
+}
+
+func TestUninstall_success(t *testing.T) {
+ g := NewWithT(t)
+
+ var cur = testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: mockReleaseNamespace,
+ Chart: testutil.BuildChart(),
+ Version: 4,
+ })
+
+ recorder := testutil.NewFakeRecorder(10, false)
+ r := &Uninstall{
+ eventRecorder: recorder,
+ }
+
+ obj := &v2.HelmRelease{
+ Status: v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(cur)),
+ },
+ },
+ }
+ req := &Request{Object: obj}
+ r.success(req)
+
+ expectMsg := fmt.Sprintf(fmtUninstallSuccess,
+ fmt.Sprintf("%s/%s.v%d", cur.Namespace, cur.Name, cur.Version),
+ fmt.Sprintf("%s@%s", cur.Chart.Name(), cur.Chart.Metadata.Version))
+
+ g.Expect(req.Object.Status.Conditions).To(conditions.MatchConditions([]metav1.Condition{
+ *conditions.FalseCondition(v2.ReleasedCondition, v2.UninstallSucceededReason, "%s", expectMsg),
+ }))
+ g.Expect(req.Object.Status.Failures).To(Equal(int64(0)))
+ g.Expect(recorder.GetEvents()).To(ConsistOf([]corev1.Event{
+ {
+ Type: corev1.EventTypeNormal,
+ Reason: v2.UninstallSucceededReason,
+ Message: expectMsg,
+ ObjectMeta: metav1.ObjectMeta{
+ Annotations: map[string]string{
+ eventMetaGroupKey(eventv1.MetaRevisionKey): cur.Chart.Metadata.Version,
+ eventMetaGroupKey(metaAppVersionKey): cur.Chart.Metadata.AppVersion,
+ eventMetaGroupKey(eventv1.MetaTokenKey): chartutil.DigestValues(digest.Canonical, cur.Config).String(),
+ },
+ },
+ },
+ }))
+}
+
+func Test_observeUninstall(t *testing.T) {
+ t.Run("uninstall of current", func(t *testing.T) {
+ g := NewWithT(t)
+
+ current := &v2.Snapshot{
+ Name: mockReleaseName,
+ Namespace: mockReleaseNamespace,
+ Version: 1,
+ Status: helmreleasecommon.StatusDeployed.String(),
+ }
+ obj := &v2.HelmRelease{
+ Status: v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ current,
+ },
+ },
+ }
+ rls := testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: current.Name,
+ Namespace: current.Namespace,
+ Version: current.Version,
+ Status: helmreleasecommon.StatusUninstalled,
+ })
+ obs := release.ObserveRelease(rls)
+ obs.Action = v2.ReleaseActionUninstall
+ expect := release.ObservedToSnapshot(obs)
+
+ observeUninstall(obj, v2.ReleaseActionUninstall)(rls)
+ g.Expect(obj.Status.History).To(testutil.Equal(v2.Snapshots{
+ expect,
+ }))
+ })
+
+ t.Run("uninstall without current", func(t *testing.T) {
+ g := NewWithT(t)
+
+ obj := &v2.HelmRelease{
+ Status: v2.HelmReleaseStatus{
+ History: nil,
+ },
+ }
+ rls := testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: mockReleaseNamespace,
+ Version: 1,
+ Status: helmreleasecommon.StatusUninstalling,
+ })
+
+ observeUninstall(obj, v2.ReleaseActionUninstall)(rls)
+ g.Expect(obj.Status.History).To(BeNil())
+ })
+
+ t.Run("uninstall of different version than current", func(t *testing.T) {
+ g := NewWithT(t)
+
+ current := &v2.Snapshot{
+ Name: mockReleaseName,
+ Namespace: mockReleaseNamespace,
+ Version: 1,
+ Status: helmreleasecommon.StatusDeployed.String(),
+ }
+ obj := &v2.HelmRelease{
+ Status: v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ current,
+ },
+ },
+ }
+ rls := testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: current.Name,
+ Namespace: current.Namespace,
+ Version: current.Version + 1,
+ Status: helmreleasecommon.StatusUninstalled,
+ })
+
+ observeUninstall(obj, v2.ReleaseActionUninstall)(rls)
+ g.Expect(obj.Status.History).To(testutil.Equal(v2.Snapshots{
+ current,
+ }))
+ })
+ t.Run("uninstall of current with OCI Digest", func(t *testing.T) {
+ g := NewWithT(t)
+
+ current := &v2.Snapshot{
+ Name: mockReleaseName,
+ Namespace: mockReleaseNamespace,
+ Version: 1,
+ Status: helmreleasecommon.StatusDeployed.String(),
+ OCIDigest: "sha256:fcdc2b0de1581a3633ada4afee3f918f6eaa5b5ab38c3fef03d5b48d3f85d9f6",
+ }
+ obj := &v2.HelmRelease{
+ Status: v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ current,
+ },
+ },
+ }
+ rls := testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: current.Name,
+ Namespace: current.Namespace,
+ Version: current.Version,
+ Status: helmreleasecommon.StatusUninstalled,
+ })
+ obs := release.ObserveRelease(rls)
+ obs.OCIDigest = "sha256:fcdc2b0de1581a3633ada4afee3f918f6eaa5b5ab38c3fef03d5b48d3f85d9f6"
+ obs.Action = v2.ReleaseActionUninstall
+ expect := release.ObservedToSnapshot(obs)
+
+ observeUninstall(obj, v2.ReleaseActionUninstall)(rls)
+ g.Expect(obj.Status.History).To(testutil.Equal(v2.Snapshots{
+ expect,
+ }))
+ })
+
+ t.Run("uninstall-remediation of current", func(t *testing.T) {
+ g := NewWithT(t)
+
+ current := &v2.Snapshot{
+ Name: mockReleaseName,
+ Namespace: mockReleaseNamespace,
+ Version: 1,
+ Status: helmreleasecommon.StatusDeployed.String(),
+ }
+ obj := &v2.HelmRelease{
+ Status: v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ current,
+ },
+ },
+ }
+ rls := testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: current.Name,
+ Namespace: current.Namespace,
+ Version: current.Version,
+ Status: helmreleasecommon.StatusUninstalled,
+ })
+ obs := release.ObserveRelease(rls)
+ obs.Action = v2.ReleaseActionUninstallRemediation
+ expect := release.ObservedToSnapshot(obs)
+
+ observeUninstall(obj, v2.ReleaseActionUninstallRemediation)(rls)
+ g.Expect(obj.Status.History).To(testutil.Equal(v2.Snapshots{
+ expect,
+ }))
+ })
+}
diff --git a/internal/reconcile/unlock.go b/internal/reconcile/unlock.go
new file mode 100644
index 000000000..395283c4b
--- /dev/null
+++ b/internal/reconcile/unlock.go
@@ -0,0 +1,182 @@
+/*
+Copyright 2022 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package reconcile
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "strings"
+
+ helmrelease "helm.sh/helm/v4/pkg/release"
+ helmreleasecommon "helm.sh/helm/v4/pkg/release/common"
+ helmreleasev1 "helm.sh/helm/v4/pkg/release/v1"
+ corev1 "k8s.io/api/core/v1"
+ "k8s.io/client-go/tools/record"
+
+ "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/release"
+ "github.com/fluxcd/helm-controller/internal/storage"
+)
+
+// Unlock is an ActionReconciler which attempts to unlock the latest release
+// for a Request.Object in the Helm storage if stuck in a pending state, by
+// setting the status to release.StatusFailed and persisting it.
+//
+// This write to the Helm storage is observed, and updates the Status.History
+// field if the persisted object targets the same release version.
+//
+// Any pending state marks the v2.HelmRelease object with
+// ReleasedCondition=False, even if persisting the object to the Helm storage
+// fails.
+//
+// At the end of the reconciliation, the Status.Conditions are summarized and
+// propagated to the Ready condition on the Request.Object.
+type Unlock struct {
+ configFactory *action.ConfigFactory
+ eventRecorder record.EventRecorder
+}
+
+// NewUnlock returns a new Unlock reconciler configured with the provided
+// values.
+func NewUnlock(cfg *action.ConfigFactory, recorder record.EventRecorder) *Unlock {
+ return &Unlock{configFactory: cfg, eventRecorder: recorder}
+}
+
+func (r *Unlock) Reconcile(_ context.Context, req *Request) error {
+ defer summarize(req)
+
+ // Build action configuration to gain access to Helm storage.
+ cfg := r.configFactory.Build(nil, observeUnlock(req.Object))
+
+ // Retrieve last release object.
+ rls, err := action.LastRelease(cfg, req.Object.GetReleaseName())
+ if err != nil {
+ // Ignore not found error. Assume caller will decide what to do
+ // when it re-assess state to determine the next action.
+ if errors.Is(err, action.ErrReleaseNotFound) {
+ return nil
+ }
+ // Return any other error to retry.
+ return err
+ }
+
+ // Ensure the release is in a pending state.
+ cur := processCurrentSnaphot(req.Object, rls)
+ if status := rls.Info.Status; status.IsPending() {
+ // Update pending status to failed and persist.
+ rls.SetStatus(helmreleasecommon.StatusFailed, fmt.Sprintf("Release unlocked from stale '%s' state", status.String()))
+ if err = cfg.Releases.Update(rls); err != nil {
+ r.failure(req, cur, status, err)
+ return err
+ }
+ r.success(req, cur, status)
+ }
+ return nil
+}
+
+func (r *Unlock) Name() string {
+ return "unlock"
+}
+
+func (r *Unlock) Type() ReconcilerType {
+ return ReconcilerTypeUnlock
+}
+
+const (
+ // fmtUnlockFailure is the message format for an unlock failure.
+ fmtUnlockFailure = "Unlock of Helm release %s with chart %s in %s state failed: %s"
+ // fmtUnlockSuccess is the message format for a successful unlock.
+ fmtUnlockSuccess = "Unlocked Helm release %s with chart %s in %s state"
+)
+
+// failure records the failure of an unlock action in the status of the given
+// Request.Object by marking ReleasedCondition=False and increasing the failure
+// counter. In addition, it emits a warning event for the Request.Object.
+func (r *Unlock) failure(req *Request, cur *v2.Snapshot, status helmreleasecommon.Status, err error) {
+ // Compose failure message.
+ msg := fmt.Sprintf(fmtUnlockFailure, cur.FullReleaseName(), cur.VersionedChartName(), status.String(), strings.TrimSpace(err.Error()))
+
+ // Mark unlock failure on object.
+ req.Object.Status.Failures++
+ conditions.MarkFalse(req.Object, v2.ReleasedCondition, "PendingRelease", "%s", msg)
+
+ // Record warning event.
+ r.eventRecorder.AnnotatedEventf(
+ req.Object,
+ eventMeta(cur.ChartVersion, cur.ConfigDigest, addAppVersion(cur.AppVersion), addOCIDigest(cur.OCIDigest)),
+ corev1.EventTypeWarning,
+ "PendingRelease",
+ msg,
+ )
+}
+
+// success records the success of an unlock action in the status of the given
+// Request.Object by marking ReleasedCondition=False and emitting an event.
+func (r *Unlock) success(req *Request, cur *v2.Snapshot, status helmreleasecommon.Status) {
+ // Compose success message.
+ msg := fmt.Sprintf(fmtUnlockSuccess, cur.FullReleaseName(), cur.VersionedChartName(), status.String())
+
+ // Mark unlock success on object.
+ conditions.MarkFalse(req.Object, v2.ReleasedCondition, "PendingRelease", "%s", msg)
+
+ // Record event.
+ r.eventRecorder.AnnotatedEventf(
+ req.Object,
+ eventMeta(cur.ChartVersion, cur.ConfigDigest, addAppVersion(cur.AppVersion), addOCIDigest(cur.OCIDigest)),
+ corev1.EventTypeNormal,
+ "PendingRelease",
+ msg,
+ )
+}
+
+// observeUnlock returns a storage.ObserveFunc to track unlocking actions on
+// a HelmRelease.
+// It updates the snapshot of a release when an unlock action is observed for
+// that release.
+func observeUnlock(obj *v2.HelmRelease) storage.ObserveFunc {
+ return func(rlsr helmrelease.Releaser) {
+ rls, ok := rlsr.(*helmreleasev1.Release)
+ if !ok {
+ return
+ }
+ for i := range obj.Status.History {
+ snap := obj.Status.History[i]
+ if snap.Targets(rls.Name, rls.Namespace, rls.Version) {
+ obj.Status.History[i] = release.ObservedToSnapshot(releaseToObservation(rls, snap, snap.GetAction()))
+ return
+ }
+ }
+ }
+}
+
+// processCurrentSnaphot processes the current snapshot based on a Helm release.
+// It also looks for the OCIDigest in the corresponding v2.HelmRelease history and
+// updates the current snapshot with the OCIDigest if found.
+func processCurrentSnaphot(obj *v2.HelmRelease, rls *helmreleasev1.Release) *v2.Snapshot {
+ cur := release.ObservedToSnapshot(release.ObserveRelease(rls))
+ for i := range obj.Status.History {
+ snap := obj.Status.History[i]
+ if snap.Targets(rls.Name, rls.Namespace, rls.Version) {
+ cur.OCIDigest = snap.OCIDigest
+ }
+ }
+ return cur
+}
diff --git a/internal/reconcile/unlock_test.go b/internal/reconcile/unlock_test.go
new file mode 100644
index 000000000..4a67b5a14
--- /dev/null
+++ b/internal/reconcile/unlock_test.go
@@ -0,0 +1,657 @@
+/*
+Copyright 2022 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package reconcile
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "testing"
+ "time"
+
+ . "github.com/onsi/gomega"
+ helmreleasecommon "helm.sh/helm/v4/pkg/release/common"
+ helmrelease "helm.sh/helm/v4/pkg/release/v1"
+ helmreleaseutil "helm.sh/helm/v4/pkg/release/v1/util"
+ helmstorage "helm.sh/helm/v4/pkg/storage"
+ helmdriver "helm.sh/helm/v4/pkg/storage/driver"
+ corev1 "k8s.io/api/core/v1"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/client-go/tools/record"
+
+ eventv1 "github.com/fluxcd/pkg/apis/event/v1beta1"
+ "github.com/fluxcd/pkg/apis/meta"
+ "github.com/fluxcd/pkg/runtime/conditions"
+
+ "github.com/fluxcd/pkg/chartutil"
+
+ 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/release"
+ "github.com/fluxcd/helm-controller/internal/storage"
+ "github.com/fluxcd/helm-controller/internal/testutil"
+)
+
+func TestUnlock_Reconcile(t *testing.T) {
+ var (
+ mockQueryErr = errors.New("storage query error")
+ mockUpdateErr = errors.New("storage update error")
+ )
+
+ tests := []struct {
+ name string
+ // driver allows for modifying the Helm storage driver.
+ driver func(helmdriver.Driver) helmdriver.Driver
+ // releases is the list of releases that are stored in the driver
+ // before unlock.
+ releases func(namespace string) []*helmrelease.Release
+ // spec modifies the HelmRelease Object spec before unlock.
+ spec func(spec *v2.HelmReleaseSpec)
+ // status to configure on the HelmRelease object before unlock.
+ status func(releases []*helmrelease.Release) v2.HelmReleaseStatus
+ // wantErr is the error that is expected to be returned.
+ wantErr error
+ // expectedConditions are the conditions that are expected to be set on
+ // the HelmRelease after running rollback.
+ expectConditions []metav1.Condition
+ // expectHistory is the expected History of the HelmRelease after
+ // unlock.
+ expectHistory func(releases []*helmrelease.Release) v2.Snapshots
+ // expectFailures is the expected Failures count of the HelmRelease.
+ expectFailures int64
+ // expectInstallFailures is the expected InstallFailures count of the
+ // HelmRelease.
+ expectInstallFailures int64
+ // expectUpgradeFailures is the expected UpgradeFailures count of the
+ // HelmRelease.
+ expectUpgradeFailures int64
+ }{
+ {
+ name: "unlock success",
+ releases: func(namespace string) []*helmrelease.Release {
+ return []*helmrelease.Release{
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: namespace,
+ Version: 1,
+ Chart: testutil.BuildChart(),
+ Status: helmreleasecommon.StatusPendingInstall,
+ }),
+ }
+ },
+ status: func(releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ return v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
+ },
+ }
+ },
+ expectConditions: []metav1.Condition{
+ *conditions.FalseCondition(meta.ReadyCondition, "PendingRelease", "Unlocked Helm release"),
+ *conditions.FalseCondition(v2.ReleasedCondition, "PendingRelease", "Unlocked Helm release"),
+ },
+ expectHistory: func(releases []*helmrelease.Release) v2.Snapshots {
+ return v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
+ }
+ },
+ },
+ {
+ name: "unlock failure",
+ driver: func(driver helmdriver.Driver) helmdriver.Driver {
+ return &storage.Failing{
+ Driver: driver,
+ UpdateErr: mockUpdateErr,
+ }
+ },
+ releases: func(namespace string) []*helmrelease.Release {
+ return []*helmrelease.Release{
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: namespace,
+ Version: 1,
+ Chart: testutil.BuildChart(),
+ Status: helmreleasecommon.StatusPendingRollback,
+ }),
+ }
+ },
+ status: func(releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ return v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
+ },
+ }
+ },
+ wantErr: mockUpdateErr,
+ expectConditions: []metav1.Condition{
+ *conditions.FalseCondition(meta.ReadyCondition, "PendingRelease", "in pending-rollback state failed: storage update error"),
+ *conditions.FalseCondition(v2.ReleasedCondition, "PendingRelease", "in pending-rollback state failed: storage update error"),
+ },
+ expectHistory: func(releases []*helmrelease.Release) v2.Snapshots {
+ return v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
+ }
+ },
+ expectFailures: 1,
+ },
+ {
+ name: "unlock without pending status",
+ releases: func(namespace string) []*helmrelease.Release {
+ return []*helmrelease.Release{
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: namespace,
+ Version: 1,
+ Chart: testutil.BuildChart(),
+ Status: helmreleasecommon.StatusFailed,
+ }),
+ }
+ },
+ status: func(releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ return v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ &v2.Snapshot{
+ Name: mockReleaseName,
+ Namespace: releases[0].Namespace,
+ Version: 1,
+ Status: helmreleasecommon.StatusFailed.String(),
+ },
+ },
+ }
+ },
+ expectConditions: []metav1.Condition{},
+ expectHistory: func(releases []*helmrelease.Release) v2.Snapshots {
+ return v2.Snapshots{
+ &v2.Snapshot{
+ Name: mockReleaseName,
+ Namespace: releases[0].Namespace,
+ Version: 1,
+ Status: helmreleasecommon.StatusFailed.String(),
+ },
+ }
+ },
+ },
+ {
+ name: "unlock with stale current",
+ releases: func(namespace string) []*helmrelease.Release {
+ return []*helmrelease.Release{
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: namespace,
+ Version: 2,
+ Chart: testutil.BuildChart(),
+ Status: helmreleasecommon.StatusDeployed,
+ }),
+ }
+ },
+ status: func(releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ return v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ &v2.Snapshot{
+ Name: mockReleaseName,
+ Namespace: releases[0].Namespace,
+ Version: releases[0].Version - 1,
+ Status: helmreleasecommon.StatusPendingInstall.String(),
+ },
+ },
+ }
+ },
+ expectHistory: func(releases []*helmrelease.Release) v2.Snapshots {
+ return v2.Snapshots{
+ &v2.Snapshot{
+ Name: mockReleaseName,
+ Namespace: releases[0].Namespace,
+ Version: releases[0].Version - 1,
+ Status: helmreleasecommon.StatusPendingInstall.String(),
+ },
+ }
+ },
+ },
+ {
+ name: "unlock without latest",
+ status: func(releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ return v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ &v2.Snapshot{
+ Name: mockReleaseName,
+ Version: 1,
+ Status: helmreleasecommon.StatusFailed.String(),
+ },
+ },
+ }
+ },
+ expectConditions: []metav1.Condition{},
+ expectHistory: func(releases []*helmrelease.Release) v2.Snapshots {
+ return v2.Snapshots{
+ &v2.Snapshot{
+ Name: mockReleaseName,
+ Version: 1,
+ Status: helmreleasecommon.StatusFailed.String(),
+ },
+ }
+ },
+ },
+ {
+ name: "unlock with storage query error",
+ driver: func(driver helmdriver.Driver) helmdriver.Driver {
+ return &storage.Failing{
+ Driver: driver,
+ QueryErr: mockQueryErr,
+ }
+ },
+ releases: func(namespace string) []*helmrelease.Release {
+ return []*helmrelease.Release{
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: namespace,
+ Version: 1,
+ Chart: testutil.BuildChart(),
+ Status: helmreleasecommon.StatusPendingInstall,
+ }),
+ }
+ },
+ status: func(releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ return v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ &v2.Snapshot{
+ Name: mockReleaseName,
+ Version: 1,
+ Status: helmreleasecommon.StatusFailed.String(),
+ },
+ },
+ }
+ },
+ wantErr: mockQueryErr,
+ expectConditions: []metav1.Condition{},
+ expectHistory: func(releases []*helmrelease.Release) v2.Snapshots {
+ return v2.Snapshots{
+ &v2.Snapshot{
+ Name: mockReleaseName,
+ Version: 1,
+ Status: helmreleasecommon.StatusFailed.String(),
+ },
+ }
+ },
+ },
+ }
+ 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
+
+ var releases []*helmrelease.Release
+ if tt.releases != nil {
+ releases = tt.releases(releaseNamespace)
+ helmreleaseutil.SortByRevision(releases)
+ }
+
+ obj := &v2.HelmRelease{
+ 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())
+ }
+
+ if tt.driver != nil {
+ cfg.Driver = tt.driver(cfg.Driver)
+ }
+
+ recorder := new(record.FakeRecorder)
+ got := NewUnlock(cfg, recorder).Reconcile(context.TODO(), &Request{
+ Object: obj,
+ })
+ if tt.wantErr != nil {
+ g.Expect(errors.Is(got, tt.wantErr)).To(BeTrue())
+ } else {
+ g.Expect(got).ToNot(HaveOccurred())
+ }
+
+ g.Expect(obj.Status.Conditions).To(conditions.MatchConditions(tt.expectConditions))
+
+ releases, _ = storeHistory(store, mockReleaseName)
+ helmreleaseutil.SortByRevision(releases)
+
+ if tt.expectHistory != nil {
+ g.Expect(obj.Status.History).To(testutil.Equal(tt.expectHistory(releases)))
+ } else {
+ g.Expect(obj.Status.History).To(BeEmpty(), "expected history to be empty")
+ }
+
+ g.Expect(obj.Status.Failures).To(Equal(tt.expectFailures))
+ g.Expect(obj.Status.InstallFailures).To(Equal(tt.expectInstallFailures))
+ g.Expect(obj.Status.UpgradeFailures).To(Equal(tt.expectUpgradeFailures))
+ })
+ }
+}
+
+func TestUnlock_failure(t *testing.T) {
+ g := NewWithT(t)
+
+ var (
+ cur = testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: mockReleaseNamespace,
+ Chart: testutil.BuildChart(),
+ Version: 4,
+ })
+ obj = &v2.HelmRelease{}
+ status = helmreleasecommon.StatusPendingInstall
+ err = fmt.Errorf("unlock error")
+ )
+
+ recorder := testutil.NewFakeRecorder(10, false)
+ r := &Unlock{
+ eventRecorder: recorder,
+ }
+
+ req := &Request{Object: obj}
+ r.failure(req, release.ObservedToSnapshot(release.ObserveRelease(cur)), status, err)
+
+ expectMsg := fmt.Sprintf(fmtUnlockFailure,
+ fmt.Sprintf("%s/%s.v%d", cur.Namespace, cur.Name, cur.Version),
+ fmt.Sprintf("%s@%s", cur.Chart.Name(), cur.Chart.Metadata.Version),
+ status, err.Error())
+
+ g.Expect(req.Object.Status.Conditions).To(conditions.MatchConditions([]metav1.Condition{
+ *conditions.FalseCondition(v2.ReleasedCondition, "PendingRelease", "%s", expectMsg),
+ }))
+ g.Expect(req.Object.Status.Failures).To(Equal(int64(1)))
+ g.Expect(recorder.GetEvents()).To(ConsistOf([]corev1.Event{
+ {
+ Type: corev1.EventTypeWarning,
+ Reason: "PendingRelease",
+ Message: expectMsg,
+ ObjectMeta: metav1.ObjectMeta{
+ Annotations: map[string]string{
+ eventMetaGroupKey(eventv1.MetaRevisionKey): cur.Chart.Metadata.Version,
+ eventMetaGroupKey(metaAppVersionKey): cur.Chart.Metadata.AppVersion,
+ eventMetaGroupKey(eventv1.MetaTokenKey): chartutil.DigestValues(digest.Canonical, cur.Config).String(),
+ },
+ },
+ },
+ }))
+}
+
+func TestUnlock_success(t *testing.T) {
+ g := NewWithT(t)
+
+ var (
+ cur = testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: mockReleaseNamespace,
+ Chart: testutil.BuildChart(),
+ Version: 4,
+ })
+ obj = &v2.HelmRelease{}
+ status = helmreleasecommon.StatusPendingInstall
+ )
+
+ recorder := testutil.NewFakeRecorder(10, false)
+ r := &Unlock{
+ eventRecorder: recorder,
+ }
+
+ req := &Request{Object: obj}
+ r.success(req, release.ObservedToSnapshot(release.ObserveRelease(cur)), status)
+
+ expectMsg := fmt.Sprintf(fmtUnlockSuccess,
+ fmt.Sprintf("%s/%s.v%d", cur.Namespace, cur.Name, cur.Version),
+ fmt.Sprintf("%s@%s", cur.Chart.Name(), cur.Chart.Metadata.Version),
+ status)
+
+ g.Expect(req.Object.Status.Conditions).To(conditions.MatchConditions([]metav1.Condition{
+ *conditions.FalseCondition(v2.ReleasedCondition, "PendingRelease", "%s", expectMsg),
+ }))
+ g.Expect(req.Object.Status.Failures).To(Equal(int64(0)))
+ g.Expect(recorder.GetEvents()).To(ConsistOf([]corev1.Event{
+ {
+ Type: corev1.EventTypeNormal,
+ Reason: "PendingRelease",
+ Message: expectMsg,
+ ObjectMeta: metav1.ObjectMeta{
+ Annotations: map[string]string{
+ eventMetaGroupKey(eventv1.MetaRevisionKey): cur.Chart.Metadata.Version,
+ eventMetaGroupKey(metaAppVersionKey): cur.Chart.Metadata.AppVersion,
+ eventMetaGroupKey(eventv1.MetaTokenKey): chartutil.DigestValues(digest.Canonical, cur.Config).String(),
+ },
+ },
+ },
+ }))
+}
+
+func TestUnlock_withOCIDigest(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
+
+ rls := testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: releaseNamespace,
+ Chart: testutil.BuildChart(),
+ Version: 4,
+ Status: helmreleasecommon.StatusPendingInstall,
+ })
+
+ obs := release.ObserveRelease(rls)
+ obs.OCIDigest = "sha256:fcdc2b0de1581a3633ada4afee3f918f6eaa5b5ab38c3fef03d5b48d3f85d9f6"
+ snap := release.ObservedToSnapshot(obs)
+
+ obj := &v2.HelmRelease{
+ Spec: v2.HelmReleaseSpec{
+ ReleaseName: mockReleaseName,
+ TargetNamespace: releaseNamespace,
+ StorageNamespace: releaseNamespace,
+ Timeout: &metav1.Duration{Duration: 100 * time.Millisecond},
+ },
+ Status: v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ snap,
+ },
+ },
+ }
+
+ 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)
+ g.Expect(store.Create(rls)).To(Succeed())
+
+ recorder := testutil.NewFakeRecorder(10, false)
+ got := NewUnlock(cfg, recorder).Reconcile(context.TODO(), &Request{
+ Object: obj,
+ })
+
+ g.Expect(got).ToNot(HaveOccurred())
+
+ g.Expect(obj.Status.Conditions).To(conditions.MatchConditions(
+ []metav1.Condition{
+ *conditions.FalseCondition(meta.ReadyCondition, "PendingRelease", "Unlocked Helm release"),
+ *conditions.FalseCondition(v2.ReleasedCondition, "PendingRelease", "Unlocked Helm release"),
+ }))
+
+ releases, _ := storeHistory(store, mockReleaseName)
+ helmreleaseutil.SortByRevision(releases)
+ expected := release.ObserveRelease(releases[0])
+ expected.OCIDigest = "sha256:fcdc2b0de1581a3633ada4afee3f918f6eaa5b5ab38c3fef03d5b48d3f85d9f6"
+ g.Expect(obj.Status.History).To(testutil.Equal(v2.Snapshots{
+ release.ObservedToSnapshot(expected),
+ }))
+
+ expectMsg := fmt.Sprintf(fmtUnlockSuccess,
+ fmt.Sprintf("%s/%s.v%d", rls.Namespace, snap.Name, snap.Version),
+ fmt.Sprintf("%s@%s", rls.Chart.Name(), rls.Chart.Metadata.Version),
+ rls.Info.Status.String())
+
+ g.Expect(recorder.GetEvents()).To(ConsistOf([]corev1.Event{
+ {
+ Type: corev1.EventTypeNormal,
+ Reason: "PendingRelease",
+ Message: expectMsg,
+ ObjectMeta: metav1.ObjectMeta{
+ Annotations: map[string]string{
+ eventMetaGroupKey(metaOCIDigestKey): expected.OCIDigest,
+ eventMetaGroupKey(eventv1.MetaRevisionKey): rls.Chart.Metadata.Version,
+ eventMetaGroupKey(metaAppVersionKey): rls.Chart.Metadata.AppVersion,
+ eventMetaGroupKey(eventv1.MetaTokenKey): chartutil.DigestValues(digest.Canonical, rls.Config).String(),
+ },
+ },
+ },
+ }))
+}
+
+func Test_observeUnlock(t *testing.T) {
+ t.Run("unlock", func(t *testing.T) {
+ g := NewWithT(t)
+
+ obj := &v2.HelmRelease{
+ Status: v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ {
+ Name: mockReleaseName,
+ Namespace: mockReleaseNamespace,
+ Version: 1,
+ Status: helmreleasecommon.StatusPendingRollback.String(),
+ },
+ },
+ },
+ }
+ rls := helmrelease.Mock(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: mockReleaseNamespace,
+ Version: 1,
+ Status: helmreleasecommon.StatusFailed,
+ })
+ expect := release.ObservedToSnapshot(release.ObserveRelease(rls))
+ observeUnlock(obj)(rls)
+
+ g.Expect(obj.Status.History).To(testutil.Equal(v2.Snapshots{
+ expect,
+ }))
+ })
+
+ t.Run("unlock with OCI Digest", func(t *testing.T) {
+ g := NewWithT(t)
+
+ obj := &v2.HelmRelease{
+ Status: v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ {
+ Name: mockReleaseName,
+ Namespace: mockReleaseNamespace,
+ Version: 1,
+ Status: helmreleasecommon.StatusPendingRollback.String(),
+ OCIDigest: "sha256:fcdc2b0de1581a3633ada4afee3f918f6eaa5b5ab38c3fef03d5b48d3f85d9f6",
+ },
+ },
+ },
+ }
+ rls := helmrelease.Mock(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: mockReleaseNamespace,
+ Version: 1,
+ Status: helmreleasecommon.StatusFailed,
+ })
+ obs := release.ObserveRelease(rls)
+ obs.OCIDigest = "sha256:fcdc2b0de1581a3633ada4afee3f918f6eaa5b5ab38c3fef03d5b48d3f85d9f6"
+ expect := release.ObservedToSnapshot(obs)
+ observeUnlock(obj)(rls)
+
+ g.Expect(obj.Status.History).To(testutil.Equal(v2.Snapshots{
+ expect,
+ }))
+ })
+
+ t.Run("unlock preserves action", func(t *testing.T) {
+ g := NewWithT(t)
+
+ obj := &v2.HelmRelease{
+ Status: v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ {
+ Name: mockReleaseName,
+ Namespace: mockReleaseNamespace,
+ Version: 1,
+ Status: helmreleasecommon.StatusPendingRollback.String(),
+ Action: v2.ReleaseActionUpgrade,
+ },
+ },
+ },
+ }
+ rls := helmrelease.Mock(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: mockReleaseNamespace,
+ Version: 1,
+ Status: helmreleasecommon.StatusFailed,
+ })
+ observeUnlock(obj)(rls)
+
+ g.Expect(obj.Status.History.Latest().Action).To(Equal(v2.ReleaseActionUpgrade))
+ })
+
+ t.Run("unlock without current", func(t *testing.T) {
+ g := NewWithT(t)
+
+ obj := &v2.HelmRelease{}
+ rls := helmrelease.Mock(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: mockReleaseNamespace,
+ Version: 1,
+ Status: helmreleasecommon.StatusFailed,
+ })
+ observeUnlock(obj)(rls)
+
+ g.Expect(obj.Status.History).To(BeEmpty())
+ })
+}
diff --git a/internal/reconcile/upgrade.go b/internal/reconcile/upgrade.go
new file mode 100644
index 000000000..1feb0d4a2
--- /dev/null
+++ b/internal/reconcile/upgrade.go
@@ -0,0 +1,193 @@
+/*
+Copyright 2022 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package reconcile
+
+import (
+ "context"
+ "fmt"
+ "strings"
+ "time"
+
+ corev1 "k8s.io/api/core/v1"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/client-go/tools/record"
+
+ "github.com/fluxcd/pkg/chartutil"
+ "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"
+)
+
+// Upgrade is an ActionReconciler which attempts to upgrade a Helm release
+// based on the given Request data.
+//
+// The writes to the Helm storage during the upgrade process are observed,
+// and update the Status.History field.
+//
+// On upgrade success, the object is marked with Released=True and emits an
+// event. In addition, the object is marked with TestSuccess=False if tests
+// are enabled to indicate we are awaiting the results.
+// On failure, the object is marked with Released=False and emits a warning
+// event. Only an error which resulted in a modification to the Helm storage
+// counts towards a failure for the active remediation strategy.
+//
+// At the end of the reconciliation, the Status.Conditions are summarized and
+// propagated to the Ready condition on the Request.Object.
+//
+// The caller is assumed to have verified the integrity of Request.Object using
+// e.g. action.VerifySnapshot before calling Reconcile.
+type Upgrade struct {
+ configFactory *action.ConfigFactory
+ eventRecorder record.EventRecorder
+ defaultToRetryOnFailure bool
+}
+
+// NewUpgrade returns a new Upgrade reconciler configured with the provided
+// values.
+func NewUpgrade(cfg *action.ConfigFactory, recorder record.EventRecorder, defaultToRetryOnFailure bool) *Upgrade {
+ return &Upgrade{configFactory: cfg, eventRecorder: recorder, defaultToRetryOnFailure: defaultToRetryOnFailure}
+}
+
+func (r *Upgrade) Reconcile(ctx context.Context, req *Request) error {
+ var (
+ logBuf = action.NewDebugLogBuffer(ctx)
+ obsReleases = make(observedReleases)
+ cfg = r.configFactory.Build(logBuf, observeRelease(obsReleases), observeInventory(req.Object, req.Chart, r.configFactory.Getter, r.eventRecorder))
+ startTime = time.Now()
+ )
+
+ defer summarize(req)
+
+ // Mark upgrade attempt on object.
+ req.Object.Status.LastAttemptedReleaseAction = v2.ReleaseActionUpgrade
+
+ // If we are upgrading, none of the previous conditions apply.
+ conditions.Delete(req.Object, v2.TestSuccessCondition)
+ conditions.Delete(req.Object, v2.RemediatedCondition)
+
+ // Run the Helm upgrade action.
+ _, err := action.Upgrade(ctx, cfg, req.Object, req.Chart, req.Values)
+
+ // Record the action duration in status.
+ req.Object.Status.LastAttemptedReleaseActionDuration = &metav1.Duration{Duration: time.Since(startTime)}
+
+ // Record the history of releases observed during the upgrade.
+ obsReleases.recordOnObject(req.Object,
+ mutateOCIDigest,
+ mutateAction(v2.ReleaseActionUpgrade))
+
+ if err != nil {
+ r.failure(req, logBuf, err)
+
+ // Return error if we did not store a release, as this does not
+ // affect state and the caller should e.g. retry.
+ if len(obsReleases) == 0 {
+ return err
+ }
+
+ // Count upgrade failure on object, this is used to determine if
+ // we should retry the upgrade and/or remediation. We only count
+ // attempts which did cause a modification to the storage, as
+ // without a new release in storage there is nothing to remediate,
+ // and the action can be retried immediately without causing
+ // storage drift.
+ req.Object.GetUpgrade().GetRemediation().IncrementFailureCount(req.Object)
+ return nil
+ }
+
+ r.success(req)
+ return nil
+}
+
+func (r *Upgrade) Name() string {
+ return "upgrade"
+}
+
+func (r *Upgrade) Type() ReconcilerType {
+ return ReconcilerTypeRelease
+}
+
+const (
+ // fmtUpgradeFailure is the message format for an upgrade failure.
+ fmtUpgradeFailure = "Helm upgrade failed for release %s/%s with chart %s@%s: %s"
+ // fmtUpgradeSuccess is the message format for a successful upgrade.
+ fmtUpgradeSuccess = "Helm upgrade succeeded for release %s with chart %s"
+)
+
+// failure records the failure of a Helm upgrade action in the status of the
+// given Request.Object by marking ReleasedCondition=False and increasing the
+// failure counter. In addition, it emits a warning event for the
+// Request.Object.
+//
+// Increase of the failure counter for the active remediation strategy should
+// be done conditionally by the caller after verifying the failed action has
+// modified the Helm storage. This to avoid counting failures which do not
+// result in Helm storage drift.
+func (r *Upgrade) failure(req *Request, buffer *action.LogBuffer, err error) {
+ // Compose failure message.
+ msg := fmt.Sprintf(fmtUpgradeFailure, req.Object.GetReleaseNamespace(), req.Object.GetReleaseName(), req.Chart.Name(), req.Chart.Metadata.Version, strings.TrimSpace(err.Error()))
+
+ // Mark upgrade failure on object.
+ req.Object.Status.Failures++
+ conditions.MarkFalse(req.Object, v2.ReleasedCondition, v2.UpgradeFailedReason, "%s", msg)
+
+ // Record warning event, this message contains more data than the
+ // Condition summary.
+ r.eventRecorder.AnnotatedEventf(
+ req.Object,
+ eventMeta(req.Chart.Metadata.Version, chartutil.DigestValues(digest.Canonical, req.Values).String(),
+ addAppVersion(req.Chart.AppVersion()), addOCIDigest(req.Object.Status.LastAttemptedRevisionDigest)),
+ corev1.EventTypeWarning,
+ v2.UpgradeFailedReason,
+ eventMessageWithLog(msg, buffer),
+ )
+}
+
+// success records the success of a Helm upgrade action in the status of the
+// given Request.Object by marking ReleasedCondition=True and emitting an
+// event. In addition, it marks TestSuccessCondition=False when tests are
+// enabled to indicate we are awaiting test results after having made the
+// release.
+func (r *Upgrade) success(req *Request) {
+ // Compose success message.
+ cur := req.Object.Status.History.Latest()
+ msg := fmt.Sprintf(fmtUpgradeSuccess, cur.FullReleaseName(), cur.VersionedChartName())
+
+ // Mark upgrade success on object.
+ conditions.MarkTrue(req.Object, v2.ReleasedCondition, v2.UpgradeSucceededReason, "%s", msg)
+ if req.Object.GetTest().Enable && !cur.HasBeenTested() {
+ conditions.MarkUnknown(req.Object, v2.TestSuccessCondition, "AwaitingTests", fmtTestPending,
+ cur.FullReleaseName(), cur.VersionedChartName())
+ }
+
+ // Failures are only relevant while the release is failed
+ // when a retry strategy is configured.
+ if req.Object.GetUpgrade().GetRetry(r.defaultToRetryOnFailure) != nil {
+ req.Object.Status.ClearFailures()
+ }
+
+ // Record event.
+ r.eventRecorder.AnnotatedEventf(
+ req.Object,
+ eventMeta(cur.ChartVersion, cur.ConfigDigest, addAppVersion(cur.AppVersion), addOCIDigest(cur.OCIDigest)),
+ corev1.EventTypeNormal,
+ v2.UpgradeSucceededReason,
+ msg,
+ )
+}
diff --git a/internal/reconcile/upgrade_test.go b/internal/reconcile/upgrade_test.go
new file mode 100644
index 000000000..674c1b16f
--- /dev/null
+++ b/internal/reconcile/upgrade_test.go
@@ -0,0 +1,881 @@
+/*
+Copyright 2022 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package reconcile
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "testing"
+ "time"
+
+ . "github.com/onsi/gomega"
+ helmchartutil "helm.sh/helm/v4/pkg/chart/common"
+ helmchart "helm.sh/helm/v4/pkg/chart/v2"
+ helmreleasecommon "helm.sh/helm/v4/pkg/release/common"
+ helmrelease "helm.sh/helm/v4/pkg/release/v1"
+ helmreleaseutil "helm.sh/helm/v4/pkg/release/v1/util"
+ helmstorage "helm.sh/helm/v4/pkg/storage"
+ helmdriver "helm.sh/helm/v4/pkg/storage/driver"
+ corev1 "k8s.io/api/core/v1"
+ apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/client-go/tools/record"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+
+ eventv1 "github.com/fluxcd/pkg/apis/event/v1beta1"
+ "github.com/fluxcd/pkg/apis/meta"
+ "github.com/fluxcd/pkg/chartutil"
+ "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/release"
+ "github.com/fluxcd/helm-controller/internal/storage"
+ "github.com/fluxcd/helm-controller/internal/testutil"
+)
+
+func TestUpgrade_Reconcile(t *testing.T) {
+ var (
+ mockCreateErr = fmt.Errorf("storage create error")
+ mockUpdateErr = fmt.Errorf("storage update error")
+ )
+
+ tests := []struct {
+ name string
+ // driver allows for modifying the Helm storage driver.
+ driver func(driver helmdriver.Driver) helmdriver.Driver
+ // releases is the list of releases that are stored in the driver
+ // before upgrade.
+ releases func(namespace string) []*helmrelease.Release
+ // chart to upgrade.
+ chart *helmchart.Chart
+ // values to use during upgrade.
+ values helmchartutil.Values
+ // spec modifies the HelmRelease object spec before upgrade.
+ spec func(spec *v2.HelmReleaseSpec)
+ // status to configure on the HelmRelease Object before upgrade.
+ status func(releases []*helmrelease.Release) v2.HelmReleaseStatus
+ // wantErr is the error that is expected to be returned.
+ wantErr error
+ // expectedConditions are the conditions that are expected to be set on
+ // the HelmRelease after upgrade.
+ expectConditions []metav1.Condition
+ // expectHistory returns the expected History of the HelmRelease after
+ // upgrade.
+ expectHistory func(releases []*helmrelease.Release) v2.Snapshots
+ // expectInventory is the expected Inventory of the HelmRelease after
+ // upgrade.
+ expectInventory func(namespace string) *v2.ResourceInventory
+ // expectFailures is the expected Failures count of the HelmRelease.
+ expectFailures int64
+ // expectInstallFailures is the expected InstallFailures count of the
+ // HelmRelease.
+ expectInstallFailures int64
+ // expectUpgradeFailures is the expected UpgradeFailures count of the
+ // HelmRelease.
+ expectUpgradeFailures int64
+ // statusReader is an optional StatusReader to configure on the
+ // ConfigFactory.
+ statusReader bool
+ }{
+ {
+ name: "upgrade success",
+ releases: func(namespace string) []*helmrelease.Release {
+ return []*helmrelease.Release{
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: namespace,
+ Chart: testutil.BuildChart(testutil.ChartWithTestHook()),
+ Version: 1,
+ Status: helmreleasecommon.StatusDeployed,
+ }),
+ }
+ },
+ chart: testutil.BuildChart(),
+ status: func(releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ return v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
+ },
+ }
+ },
+ expectConditions: []metav1.Condition{
+ *conditions.TrueCondition(meta.ReadyCondition, v2.UpgradeSucceededReason, "Helm upgrade succeeded"),
+ *conditions.TrueCondition(v2.ReleasedCondition, v2.UpgradeSucceededReason, "Helm upgrade succeeded"),
+ },
+ expectHistory: func(releases []*helmrelease.Release) v2.Snapshots {
+ return v2.Snapshots{
+ func() *v2.Snapshot {
+ obs := release.ObserveRelease(releases[1])
+ obs.Action = v2.ReleaseActionUpgrade
+ return release.ObservedToSnapshot(obs)
+ }(),
+ release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
+ }
+ },
+ expectInventory: func(namespace string) *v2.ResourceInventory {
+ return &v2.ResourceInventory{
+ Entries: []v2.ResourceRef{
+ {
+ ID: namespace + "_cm__ConfigMap",
+ Version: "v1",
+ },
+ },
+ }
+ },
+ },
+ {
+ name: "upgrade failure",
+ releases: func(namespace string) []*helmrelease.Release {
+ return []*helmrelease.Release{
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: namespace,
+ Chart: testutil.BuildChart(),
+ Version: 1,
+ Status: helmreleasecommon.StatusDeployed,
+ }),
+ }
+ },
+ chart: testutil.BuildChart(testutil.ChartWithFailingHook()),
+ status: func(releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ return v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
+ },
+ }
+ },
+ expectConditions: []metav1.Condition{
+ *conditions.FalseCondition(meta.ReadyCondition, v2.UpgradeFailedReason,
+ "context deadline exceeded"),
+ *conditions.FalseCondition(v2.ReleasedCondition, v2.UpgradeFailedReason,
+ "context deadline exceeded"),
+ },
+ expectHistory: func(releases []*helmrelease.Release) v2.Snapshots {
+ return v2.Snapshots{
+ func() *v2.Snapshot {
+ obs := release.ObserveRelease(releases[1])
+ obs.Action = v2.ReleaseActionUpgrade
+ return release.ObservedToSnapshot(obs)
+ }(),
+ release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
+ }
+ },
+ expectFailures: 1,
+ expectUpgradeFailures: 1,
+ },
+ {
+ name: "upgrade failure without storage create",
+ driver: func(driver helmdriver.Driver) helmdriver.Driver {
+ return &storage.Failing{
+ Driver: driver,
+ CreateErr: mockCreateErr,
+ }
+ },
+ releases: func(namespace string) []*helmrelease.Release {
+ return []*helmrelease.Release{
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: namespace,
+ Chart: testutil.BuildChart(),
+ Version: 1,
+ Status: helmreleasecommon.StatusDeployed,
+ }),
+ }
+ },
+ chart: testutil.BuildChart(),
+ status: func(releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ return v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
+ },
+ }
+ },
+ expectConditions: []metav1.Condition{
+ *conditions.FalseCondition(meta.ReadyCondition, v2.UpgradeFailedReason,
+ "%s", mockCreateErr.Error()),
+ *conditions.FalseCondition(v2.ReleasedCondition, v2.UpgradeFailedReason,
+ "%s", mockCreateErr.Error()),
+ },
+ expectHistory: func(releases []*helmrelease.Release) v2.Snapshots {
+ return v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
+ }
+ },
+ expectFailures: 1,
+ expectUpgradeFailures: 0,
+ wantErr: mockCreateErr,
+ },
+ {
+ name: "upgrade failure without storage update",
+ driver: func(driver helmdriver.Driver) helmdriver.Driver {
+ return &storage.Failing{
+ Driver: driver,
+ UpdateErr: mockUpdateErr,
+ }
+ },
+ releases: func(namespace string) []*helmrelease.Release {
+ return []*helmrelease.Release{
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: namespace,
+ Chart: testutil.BuildChart(),
+ Version: 1,
+ Status: helmreleasecommon.StatusDeployed,
+ }),
+ }
+ },
+ chart: testutil.BuildChart(),
+ status: func(releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ return v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
+ },
+ }
+ },
+ expectConditions: []metav1.Condition{
+ *conditions.FalseCondition(meta.ReadyCondition, v2.UpgradeFailedReason,
+ "%s", mockUpdateErr.Error()),
+ *conditions.FalseCondition(v2.ReleasedCondition, v2.UpgradeFailedReason,
+ "%s", mockUpdateErr.Error()),
+ },
+ expectHistory: func(releases []*helmrelease.Release) v2.Snapshots {
+ return v2.Snapshots{
+ func() *v2.Snapshot {
+ obs := release.ObserveRelease(releases[1])
+ obs.Action = v2.ReleaseActionUpgrade
+ return release.ObservedToSnapshot(obs)
+ }(),
+ release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
+ }
+ },
+ expectFailures: 1,
+ expectUpgradeFailures: 1,
+ },
+ {
+ name: "upgrade without current",
+ releases: func(namespace string) []*helmrelease.Release {
+ return []*helmrelease.Release{
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: namespace,
+ Chart: testutil.BuildChart(),
+ Version: 1,
+ Status: helmreleasecommon.StatusDeployed,
+ }),
+ }
+ },
+ chart: testutil.BuildChart(),
+ status: func(releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ return v2.HelmReleaseStatus{
+ History: nil,
+ }
+ },
+ expectConditions: []metav1.Condition{
+ *conditions.TrueCondition(meta.ReadyCondition, v2.UpgradeSucceededReason, "Helm upgrade succeeded"),
+ *conditions.TrueCondition(v2.ReleasedCondition, v2.UpgradeSucceededReason, "Helm upgrade succeeded"),
+ },
+ expectHistory: func(releases []*helmrelease.Release) v2.Snapshots {
+ return v2.Snapshots{
+ func() *v2.Snapshot {
+ obs := release.ObserveRelease(releases[1])
+ obs.Action = v2.ReleaseActionUpgrade
+ return release.ObservedToSnapshot(obs)
+ }(),
+ }
+ },
+ },
+ {
+ name: "upgrade with stale current",
+ releases: func(namespace string) []*helmrelease.Release {
+ return []*helmrelease.Release{
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: namespace,
+ Chart: testutil.BuildChart(),
+ Version: 1,
+ Status: helmreleasecommon.StatusSuperseded,
+ }),
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: namespace,
+ Chart: testutil.BuildChart(),
+ Version: 2,
+ Status: helmreleasecommon.StatusDeployed,
+ }),
+ }
+ },
+ chart: testutil.BuildChart(),
+ status: func(releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ return v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ {
+ Name: mockReleaseName,
+ Namespace: releases[0].Namespace,
+ Version: 1,
+ Status: helmreleasecommon.StatusDeployed.String(),
+ },
+ },
+ }
+ },
+ expectConditions: []metav1.Condition{
+ *conditions.TrueCondition(meta.ReadyCondition, v2.UpgradeSucceededReason,
+ "Helm upgrade succeeded"),
+ *conditions.TrueCondition(v2.ReleasedCondition, v2.UpgradeSucceededReason,
+ "Helm upgrade succeeded"),
+ },
+ expectHistory: func(releases []*helmrelease.Release) v2.Snapshots {
+ return v2.Snapshots{
+ func() *v2.Snapshot {
+ obs := release.ObserveRelease(releases[2])
+ obs.Action = v2.ReleaseActionUpgrade
+ return release.ObservedToSnapshot(obs)
+ }(),
+ {
+ Name: mockReleaseName,
+ Namespace: releases[0].Namespace,
+ Version: 1,
+ Status: helmreleasecommon.StatusDeployed.String(),
+ },
+ }
+ },
+ },
+ {
+ name: "upgrade with stale conditions",
+ releases: func(namespace string) []*helmrelease.Release {
+ return []*helmrelease.Release{
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: namespace,
+ Chart: testutil.BuildChart(),
+ Version: 2,
+ Status: helmreleasecommon.StatusDeployed,
+ }),
+ }
+ },
+ chart: testutil.BuildChart(),
+ status: func(releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ return v2.HelmReleaseStatus{
+ Conditions: []metav1.Condition{
+ *conditions.FalseCondition(v2.TestSuccessCondition, v2.TestFailedReason, ""),
+ *conditions.TrueCondition(v2.RemediatedCondition, v2.RollbackSucceededReason, ""),
+ },
+ }
+ },
+ expectConditions: []metav1.Condition{
+ *conditions.TrueCondition(meta.ReadyCondition, v2.UpgradeSucceededReason,
+ "Helm upgrade succeeded"),
+ *conditions.TrueCondition(v2.ReleasedCondition, v2.UpgradeSucceededReason,
+ "Helm upgrade succeeded"),
+ },
+ expectHistory: func(releases []*helmrelease.Release) v2.Snapshots {
+ return v2.Snapshots{
+ func() *v2.Snapshot {
+ obs := release.ObserveRelease(releases[1])
+ obs.Action = v2.ReleaseActionUpgrade
+ return release.ObservedToSnapshot(obs)
+ }(),
+ }
+ },
+ },
+ {
+ name: "upgrade success with status reader",
+ releases: func(namespace string) []*helmrelease.Release {
+ return []*helmrelease.Release{
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: namespace,
+ Chart: testutil.BuildChart(testutil.ChartWithTestHook()),
+ Version: 1,
+ Status: helmreleasecommon.StatusDeployed,
+ }),
+ }
+ },
+ chart: testutil.BuildChart(),
+ statusReader: true,
+ status: func(releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ return v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
+ },
+ }
+ },
+ expectConditions: []metav1.Condition{
+ *conditions.TrueCondition(meta.ReadyCondition, v2.UpgradeSucceededReason, "Helm upgrade succeeded"),
+ *conditions.TrueCondition(v2.ReleasedCondition, v2.UpgradeSucceededReason, "Helm upgrade succeeded"),
+ },
+ expectHistory: func(releases []*helmrelease.Release) v2.Snapshots {
+ return v2.Snapshots{
+ func() *v2.Snapshot {
+ obs := release.ObserveRelease(releases[1])
+ obs.Action = v2.ReleaseActionUpgrade
+ return release.ObservedToSnapshot(obs)
+ }(),
+ release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
+ }
+ },
+ expectInventory: func(namespace string) *v2.ResourceInventory {
+ return &v2.ResourceInventory{
+ Entries: []v2.ResourceRef{
+ {
+ ID: namespace + "_cm__ConfigMap",
+ Version: "v1",
+ },
+ },
+ }
+ },
+ },
+ }
+ 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
+
+ var releases []*helmrelease.Release
+ if tt.releases != nil {
+ releases = tt.releases(releaseNamespace)
+ helmreleaseutil.SortByRevision(releases)
+ }
+
+ obj := &v2.HelmRelease{
+ Spec: v2.HelmReleaseSpec{
+ ReleaseName: mockReleaseName,
+ TargetNamespace: releaseNamespace,
+ StorageNamespace: releaseNamespace,
+ Timeout: &metav1.Duration{Duration: 200 * 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())
+
+ cfgOpts := []action.ConfigFactoryOption{
+ action.WithStorage(action.DefaultStorageDriver, obj.GetStorageNamespace()),
+ }
+ var mockSR *testutil.MockStatusReader
+ if tt.statusReader {
+ mockSR = &testutil.MockStatusReader{}
+ cfgOpts = append(cfgOpts, action.WithResourceManager(mockSR.NewResourceManagerFuncWithClient(testEnv.Client, testEnv.Manager.GetRESTMapper())))
+ }
+ cfg, err := action.NewConfigFactory(getter, cfgOpts...)
+ g.Expect(err).ToNot(HaveOccurred())
+
+ store := helmstorage.Init(cfg.Driver)
+ for _, r := range releases {
+ g.Expect(store.Create(r)).To(Succeed())
+ }
+
+ if tt.driver != nil {
+ cfg.Driver = tt.driver(cfg.Driver)
+ }
+
+ recorder := new(record.FakeRecorder)
+ got := NewUpgrade(cfg, recorder, false).Reconcile(context.TODO(), &Request{
+ Object: obj,
+ Chart: tt.chart,
+ Values: tt.values,
+ })
+ if tt.wantErr != nil {
+ g.Expect(got).To(Equal(tt.wantErr))
+ } else {
+ g.Expect(got).ToNot(HaveOccurred())
+ }
+
+ g.Expect(obj.Status.Conditions).To(conditions.MatchConditions(tt.expectConditions))
+
+ releases, _ = storeHistory(store, mockReleaseName)
+ helmreleaseutil.SortByRevision(releases)
+
+ if tt.expectHistory != nil {
+ g.Expect(obj.Status.History).To(testutil.Equal(tt.expectHistory(releases)))
+ } else {
+ g.Expect(obj.Status.History).To(BeEmpty(), "expected history to be empty")
+ }
+
+ g.Expect(obj.Status.Failures).To(Equal(tt.expectFailures))
+ g.Expect(obj.Status.InstallFailures).To(Equal(tt.expectInstallFailures))
+ g.Expect(obj.Status.UpgradeFailures).To(Equal(tt.expectUpgradeFailures))
+ g.Expect(obj.Status.LastAttemptedReleaseAction).To(Equal(v2.ReleaseActionUpgrade))
+ g.Expect(obj.Status.LastAttemptedReleaseActionDuration).ToNot(BeNil())
+
+ if tt.expectInventory != nil {
+ g.Expect(obj.Status.Inventory).To(testutil.Equal(tt.expectInventory(releaseNamespace)))
+ }
+
+ if mockSR != nil {
+ g.Expect(mockSR.SupportsCalled()).To(BeNumerically(">", 0), "expected StatusReader.Supports to be called")
+ }
+ })
+ }
+}
+
+func TestUpgrade_Reconcile_withSubchartWithCRDs(t *testing.T) {
+ getValues := func(subchartValues map[string]any) helmchartutil.Values {
+ return helmchartutil.Values{"subchart": subchartValues}
+ }
+
+ releases := func(namespace string) []*helmrelease.Release {
+ return []*helmrelease.Release{
+ testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: namespace,
+ Chart: testutil.BuildChart(testutil.ChartWithTestHook()),
+ Version: 1,
+ Status: helmreleasecommon.StatusDeployed,
+ }),
+ }
+ }
+
+ status := func(releases []*helmrelease.Release) v2.HelmReleaseStatus {
+ return v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
+ },
+ }
+ }
+
+ expectConditions := []metav1.Condition{
+ *conditions.TrueCondition(meta.ReadyCondition, v2.UpgradeSucceededReason, "Helm upgrade succeeded"),
+ *conditions.TrueCondition(v2.ReleasedCondition, v2.UpgradeSucceededReason, "Helm upgrade succeeded"),
+ }
+
+ expectHistory := func(releases []*helmrelease.Release) v2.Snapshots {
+ obs := release.ObserveRelease(releases[1])
+ obs.Action = v2.ReleaseActionUpgrade
+ return v2.Snapshots{
+ release.ObservedToSnapshot(obs),
+ release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
+ }
+ }
+
+ for _, tt := range []struct {
+ name string
+ subchartValues map[string]any
+ subchartResourcesPresent bool
+ expectedMainChartValues map[string]any
+ }{
+ {
+ name: "subchart disabled should not deploy resources, including CRDs",
+ subchartValues: map[string]any{"enabled": false},
+ subchartResourcesPresent: false,
+ expectedMainChartValues: map[string]any{
+ "foo": "baz",
+ "myimports": map[string]any{"myint": 0},
+ },
+ },
+ {
+ name: "subchart enabled should deploy resources, including CRDs",
+ subchartValues: map[string]any{"enabled": true},
+ subchartResourcesPresent: true,
+ expectedMainChartValues: map[string]any{
+ "foo": "baz",
+ "myint": 123,
+ "myimports": map[string]any{"myint": 0}, // should be 456: https://github.com/helm/helm/issues/13223
+ "subchart": map[string]any{
+ "foo": "bar",
+ "global": map[string]any{},
+ "exports": map[string]any{"data": map[string]any{"myint": 123}},
+ "default": map[string]any{"data": map[string]any{"myint": 456}},
+ },
+ },
+ },
+ } {
+ 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 := releases(releaseNamespace)
+ helmreleaseutil.SortByRevision(releases)
+
+ obj := &v2.HelmRelease{
+ Spec: v2.HelmReleaseSpec{
+ ReleaseName: mockReleaseName,
+ TargetNamespace: releaseNamespace,
+ StorageNamespace: releaseNamespace,
+ Timeout: &metav1.Duration{Duration: 100 * time.Millisecond},
+ },
+ }
+ obj.Status = 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())
+ }
+
+ // Delete any prior CRD.
+ subChartCRD := &apiextensionsv1.CustomResourceDefinition{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "crontabs.stable.example.com",
+ },
+ }
+ _ = testEnv.Delete(context.TODO(), subChartCRD)
+
+ chart := testutil.BuildChartWithSubchartWithCRD()
+ recorder := new(record.FakeRecorder)
+ got := NewUpgrade(cfg, recorder, false).Reconcile(context.TODO(), &Request{
+ Object: obj,
+ Chart: chart,
+ Values: getValues(tt.subchartValues),
+ })
+ g.Expect(got).ToNot(HaveOccurred())
+
+ g.Expect(obj.Status.Conditions).To(conditions.MatchConditions(expectConditions))
+
+ releases, _ = storeHistory(store, mockReleaseName)
+ helmreleaseutil.SortByRevision(releases)
+
+ g.Expect(obj.Status.History).To(testutil.Equal(expectHistory(releases)))
+
+ // Assert main chart configmap is present.
+ mainChartCM := &corev1.ConfigMap{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "cm-main-chart",
+ Namespace: releaseNamespace,
+ },
+ }
+ err = testEnv.Get(context.TODO(), client.ObjectKeyFromObject(mainChartCM), mainChartCM)
+ g.Expect(err).NotTo(HaveOccurred())
+
+ // Assert subchart configmap is absent or present.
+ subChartCM := &corev1.ConfigMap{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "cm-sub-chart",
+ Namespace: releaseNamespace,
+ },
+ }
+ err = testEnv.Get(context.TODO(), client.ObjectKeyFromObject(subChartCM), subChartCM)
+ if tt.subchartResourcesPresent {
+ g.Expect(err).NotTo(HaveOccurred())
+ } else {
+ g.Expect(err).To(HaveOccurred())
+ }
+
+ // Assert subchart CRD is absent or present.
+ err = testEnv.Get(context.TODO(), client.ObjectKeyFromObject(subChartCRD), subChartCRD)
+ if tt.subchartResourcesPresent {
+ g.Expect(err).NotTo(HaveOccurred())
+ } else {
+ g.Expect(err).To(HaveOccurred())
+ }
+
+ // Assert main chart values.
+ g.Expect(chart.Values).To(testutil.Equal(tt.expectedMainChartValues))
+ })
+ }
+}
+
+func TestUpgrade_failure(t *testing.T) {
+ var (
+ obj = &v2.HelmRelease{
+ Spec: v2.HelmReleaseSpec{
+ ReleaseName: mockReleaseName,
+ TargetNamespace: mockReleaseNamespace,
+ },
+ Status: v2.HelmReleaseStatus{
+ LastAttemptedRevisionDigest: "sha256:1234567890",
+ },
+ }
+ chrt = testutil.BuildChart()
+ err = errors.New("upgrade error")
+ )
+
+ t.Run("records failure", func(t *testing.T) {
+ g := NewWithT(t)
+
+ recorder := testutil.NewFakeRecorder(10, false)
+ r := &Upgrade{
+ eventRecorder: recorder,
+ }
+
+ req := &Request{Object: obj.DeepCopy(), Chart: chrt, Values: map[string]any{"foo": "bar"}}
+ r.failure(req, nil, err)
+
+ expectMsg := fmt.Sprintf(fmtUpgradeFailure, mockReleaseNamespace, mockReleaseName, chrt.Name(),
+ chrt.Metadata.Version, err.Error())
+
+ g.Expect(req.Object.Status.Conditions).To(conditions.MatchConditions([]metav1.Condition{
+ *conditions.FalseCondition(v2.ReleasedCondition, v2.UpgradeFailedReason, "%s", expectMsg),
+ }))
+ g.Expect(req.Object.Status.Failures).To(Equal(int64(1)))
+ g.Expect(recorder.GetEvents()).To(ConsistOf([]corev1.Event{
+ {
+ Type: corev1.EventTypeWarning,
+ Reason: v2.UpgradeFailedReason,
+ Message: expectMsg,
+ ObjectMeta: metav1.ObjectMeta{
+ Annotations: map[string]string{
+ eventMetaGroupKey(metaOCIDigestKey): obj.Status.LastAttemptedRevisionDigest,
+ eventMetaGroupKey(eventv1.MetaRevisionKey): chrt.Metadata.Version,
+ eventMetaGroupKey(metaAppVersionKey): chrt.Metadata.AppVersion,
+ eventMetaGroupKey(eventv1.MetaTokenKey): chartutil.DigestValues(digest.Canonical, req.Values).String(),
+ },
+ },
+ },
+ }))
+ })
+
+ t.Run("records failure with logs", func(t *testing.T) {
+ g := NewWithT(t)
+
+ recorder := testutil.NewFakeRecorder(10, false)
+ r := &Upgrade{
+ eventRecorder: recorder,
+ }
+ req := &Request{Object: obj.DeepCopy(), Chart: chrt}
+ r.failure(req, mockLogBuffer(), err)
+
+ expectSubStr := "Last Helm logs"
+ g.Expect(conditions.IsFalse(req.Object, v2.ReleasedCondition)).To(BeTrue())
+ g.Expect(conditions.GetMessage(req.Object, v2.ReleasedCondition)).ToNot(ContainSubstring(expectSubStr))
+
+ events := recorder.GetEvents()
+ g.Expect(events).To(HaveLen(1))
+ g.Expect(events[0].Message).To(ContainSubstring(expectSubStr))
+ })
+}
+
+func TestUpgrade_success(t *testing.T) {
+ var (
+ cur = testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: mockReleaseName,
+ Namespace: mockReleaseNamespace,
+ Chart: testutil.BuildChart(),
+ })
+ obj = &v2.HelmRelease{
+ Status: v2.HelmReleaseStatus{
+ History: v2.Snapshots{
+ release.ObservedToSnapshot(release.ObserveRelease(cur)),
+ },
+ },
+ }
+ )
+
+ t.Run("records success", func(t *testing.T) {
+ g := NewWithT(t)
+
+ recorder := testutil.NewFakeRecorder(10, false)
+ r := &Upgrade{
+ eventRecorder: recorder,
+ }
+
+ req := &Request{
+ Object: obj.DeepCopy(),
+ }
+ r.success(req)
+
+ expectMsg := fmt.Sprintf(fmtUpgradeSuccess,
+ fmt.Sprintf("%s/%s.v%d", mockReleaseNamespace, mockReleaseName, obj.Status.History.Latest().Version),
+ fmt.Sprintf("%s@%s", obj.Status.History.Latest().ChartName, obj.Status.History.Latest().ChartVersion))
+
+ g.Expect(req.Object.Status.Conditions).To(conditions.MatchConditions([]metav1.Condition{
+ *conditions.TrueCondition(v2.ReleasedCondition, v2.UpgradeSucceededReason, "%s", expectMsg),
+ }))
+ g.Expect(recorder.GetEvents()).To(ConsistOf([]corev1.Event{
+ {
+ Type: corev1.EventTypeNormal,
+ Reason: v2.UpgradeSucceededReason,
+ Message: expectMsg,
+ ObjectMeta: metav1.ObjectMeta{
+ Annotations: map[string]string{
+ eventMetaGroupKey(eventv1.MetaRevisionKey): obj.Status.History.Latest().ChartVersion,
+ eventMetaGroupKey(metaAppVersionKey): obj.Status.History.Latest().AppVersion,
+ eventMetaGroupKey(eventv1.MetaTokenKey): obj.Status.History.Latest().ConfigDigest,
+ },
+ },
+ },
+ }))
+ })
+
+ t.Run("clears failures if retry strategy is configured", func(t *testing.T) {
+ g := NewWithT(t)
+
+ recorder := testutil.NewFakeRecorder(10, false)
+ r := &Upgrade{
+ eventRecorder: recorder,
+ }
+
+ req := &Request{
+ Object: obj.DeepCopy(),
+ }
+ req.Object.Spec.Upgrade = &v2.Upgrade{
+ Strategy: &v2.UpgradeStrategy{
+ Name: "RetryOnFailure",
+ },
+ }
+ req.Object.Status.Failures = 3
+ req.Object.Status.InstallFailures = 3
+ req.Object.Status.UpgradeFailures = 3
+ r.success(req)
+
+ g.Expect(req.Object.Status.Failures).To(BeZero())
+ g.Expect(req.Object.Status.InstallFailures).To(BeZero())
+ g.Expect(req.Object.Status.UpgradeFailures).To(BeZero())
+ })
+
+ t.Run("records success with TestSuccess=False", func(t *testing.T) {
+ g := NewWithT(t)
+
+ recorder := testutil.NewFakeRecorder(10, false)
+ r := &Upgrade{
+ eventRecorder: recorder,
+ }
+
+ obj := obj.DeepCopy()
+ obj.Spec.Test = &v2.Test{Enable: true}
+
+ req := &Request{Object: obj}
+ r.success(req)
+
+ g.Expect(conditions.IsTrue(req.Object, v2.ReleasedCondition)).To(BeTrue())
+
+ cond := conditions.Get(req.Object, v2.TestSuccessCondition)
+ g.Expect(cond).ToNot(BeNil())
+
+ expectMsg := fmt.Sprintf(fmtTestPending,
+ fmt.Sprintf("%s/%s.v%d", mockReleaseNamespace, mockReleaseName, obj.Status.History.Latest().Version),
+ fmt.Sprintf("%s@%s", obj.Status.History.Latest().ChartName, obj.Status.History.Latest().ChartVersion))
+ g.Expect(cond.Message).To(Equal(expectMsg))
+ })
+}
diff --git a/internal/release/decode_test.go b/internal/release/decode_test.go
new file mode 100644
index 000000000..c48267994
--- /dev/null
+++ b/internal/release/decode_test.go
@@ -0,0 +1,70 @@
+/*
+Copyright 2022 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package release
+
+import (
+ "bytes"
+ "compress/gzip"
+ "encoding/base64"
+ "encoding/json"
+ "io"
+
+ helmrelease "helm.sh/helm/v4/pkg/release/v1"
+)
+
+var (
+ b64 = base64.StdEncoding
+ magicGzip = []byte{0x1f, 0x8b, 0x08}
+)
+
+// decodeRelease decodes the bytes of data into a release
+// type. Data must contain a base64 encoded gzipped string of a
+// valid release, otherwise an error is returned.
+//
+// It is copied over from the Helm project to be able to deal
+// with encoded releases.
+// Ref: https://github.com/helm/helm/blob/v3.9.0/pkg/storage/driver/util.go#L56
+func decodeRelease(data string) (*helmrelease.Release, error) {
+ // base64 decode string
+ b, err := b64.DecodeString(data)
+ if err != nil {
+ return nil, err
+ }
+
+ // For backwards compatibility with releases that were stored before
+ // compression was introduced we skip decompression if the
+ // gzip magic header is not found
+ if bytes.Equal(b[0:3], magicGzip) {
+ r, err := gzip.NewReader(bytes.NewReader(b))
+ if err != nil {
+ return nil, err
+ }
+ defer r.Close()
+ b2, err := io.ReadAll(r)
+ if err != nil {
+ return nil, err
+ }
+ b = b2
+ }
+
+ var rls helmrelease.Release
+ // unmarshal release object bytes
+ if err := json.Unmarshal(b, &rls); err != nil {
+ return nil, err
+ }
+ return &rls, nil
+}
diff --git a/internal/release/digest.go b/internal/release/digest.go
new file mode 100644
index 000000000..90ae9966f
--- /dev/null
+++ b/internal/release/digest.go
@@ -0,0 +1,36 @@
+/*
+Copyright 2022 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package release
+
+import (
+ "encoding/json"
+
+ "github.com/opencontainers/go-digest"
+)
+
+// Digest calculates the digest of the given Observation by JSON encoding
+// it into a hash.Hash of the given digest.Algorithm. The algorithm is expected
+// to have been confirmed to be available by the caller, not doing this may
+// result in panics.
+func Digest(algo digest.Algorithm, rel Observation) digest.Digest {
+ digester := algo.Digester()
+ enc := json.NewEncoder(digester.Hash())
+ if err := enc.Encode(rel); err != nil {
+ return ""
+ }
+ return digester.Digest()
+}
diff --git a/internal/release/digest_test.go b/internal/release/digest_test.go
new file mode 100644
index 000000000..a721ace09
--- /dev/null
+++ b/internal/release/digest_test.go
@@ -0,0 +1,51 @@
+/*
+Copyright 2022 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package release
+
+import (
+ "testing"
+
+ . "github.com/onsi/gomega"
+
+ "github.com/opencontainers/go-digest"
+)
+
+func TestDigest(t *testing.T) {
+ tests := []struct {
+ name string
+ algo digest.Algorithm
+ rel Observation
+ exp digest.Digest
+ }{
+ {
+ name: "SHA256",
+ algo: digest.SHA256,
+ rel: Observation{
+ Name: "foo",
+ },
+ exp: "sha256:f1498f27a16a09cd7e1ee610d924df065c03d30035638babc95dd9a8d412ce4d",
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ g := NewWithT(t)
+
+ got := Digest(tt.algo, tt.rel)
+ g.Expect(got).To(Equal(tt.exp))
+ })
+ }
+}
diff --git a/internal/release/name.go b/internal/release/name.go
new file mode 100644
index 000000000..c01b68dfb
--- /dev/null
+++ b/internal/release/name.go
@@ -0,0 +1,46 @@
+/*
+Copyright 2022 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package release
+
+import (
+ "crypto/sha256"
+ "fmt"
+)
+
+// ShortenName returns a short release name in the format of
+// '-' for the given name
+// if it exceeds 53 characters in length.
+//
+// The shortening is done by hashing the given release name with
+// SHA256 and taking the first 12 characters of the resulting hash.
+// The hash is then appended to the release name shortened to 40
+// characters divided by a hyphen separator.
+//
+// For example: 'some-front-appended-namespace-release-wi-1234567890ab'
+// where '1234567890ab' are the first 12 characters of the SHA hash.
+func ShortenName(name string) string {
+ if len(name) <= 53 {
+ return name
+ }
+
+ const maxLength = 53
+ const shortHashLength = 12
+
+ sum := fmt.Sprintf("%x", sha256.Sum256([]byte(name)))
+ shortName := name[:maxLength-(shortHashLength+1)] + "-"
+ return shortName + sum[:shortHashLength]
+}
diff --git a/internal/release/name_test.go b/internal/release/name_test.go
new file mode 100644
index 000000000..416629d12
--- /dev/null
+++ b/internal/release/name_test.go
@@ -0,0 +1,55 @@
+/*
+Copyright 2022 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package release
+
+import (
+ "testing"
+
+ . "github.com/onsi/gomega"
+)
+
+func TestShortName(t *testing.T) {
+ g := NewWithT(t)
+
+ tests := []struct {
+ name string
+ expected string
+ }{
+ {
+ name: "release-name",
+ expected: "release-name",
+ },
+ {
+ name: "release-name-with-very-long-name-which-is-longer-than-53-characters",
+ expected: "release-name-with-very-long-name-which-i-788ca0d0d7b0",
+ },
+ {
+ name: "another-release-name-with-very-long-name-which-is-longer-than-53-characters",
+ expected: "another-release-name-with-very-long-name-7e72150d5a36",
+ },
+ {
+ name: "",
+ expected: "",
+ },
+ }
+
+ for _, tt := range tests {
+ got := ShortenName(tt.name)
+ g.Expect(got).To(Equal(tt.expected), got)
+ g.Expect(got).To(Satisfy(func(s string) bool { return len(s) <= 53 }))
+ }
+}
diff --git a/internal/release/observation.go b/internal/release/observation.go
new file mode 100644
index 000000000..fa3ac5e72
--- /dev/null
+++ b/internal/release/observation.go
@@ -0,0 +1,200 @@
+/*
+Copyright 2022 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package release
+
+import (
+ "encoding/json"
+ "io"
+
+ "github.com/mitchellh/copystructure"
+ chart "helm.sh/helm/v4/pkg/chart/v2"
+ helmrelease "helm.sh/helm/v4/pkg/release/v1"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+ v2 "github.com/fluxcd/helm-controller/api/v2"
+ "github.com/fluxcd/helm-controller/internal/digest"
+ "github.com/fluxcd/pkg/chartutil"
+)
+
+var (
+ DefaultDataFilters = []DataFilter{
+ IgnoreHookTestEvents,
+ }
+)
+
+// DataFilter allows for filtering data from the returned Observation while
+// making an observation.
+type DataFilter func(rel *Observation)
+
+// IgnoreHookTestEvents ignores test event hooks. For example, to exclude it
+// while generating a digest for the object. To prevent manual test triggers
+// from a user to interfere with the checksum.
+func IgnoreHookTestEvents(rel *Observation) {
+ if len(rel.Hooks) > 0 {
+ var hooks []helmrelease.Hook
+ for i := range rel.Hooks {
+ h := rel.Hooks[i]
+ if !IsHookForEvent(&h, helmrelease.HookTest) {
+ hooks = append(hooks, h)
+ }
+ }
+ rel.Hooks = hooks
+ }
+}
+
+// Observation is a copy of a Helm release object, as observed to be written
+// to the storage by a storage.Observer. The object is detached from the Helm
+// storage object, and mutations to it do not change the underlying release
+// object.
+type Observation struct {
+ // Name of the release.
+ Name string `json:"name"`
+ // Version of the release, at times also called revision.
+ Version int `json:"version"`
+ // Info provides information about the release.
+ Info helmrelease.Info `json:"info"`
+ // Action is the action that resulted in this observation.
+ // It's not serialized to JSON because it's not needed for
+ // the digest. The action is only tracked for visibility.
+ Action v2.ReleaseAction `json:"-"`
+ // ChartMetadata contains the current Chartfile data of the release.
+ ChartMetadata chart.Metadata `json:"chartMetadata"`
+ // Config is the set of extra Values added to the chart.
+ // These values override the default values inside the chart.
+ Config map[string]any `json:"config"`
+ // Manifest is the string representation of the rendered template.
+ Manifest string `json:"manifest"`
+ // Hooks are all the hooks declared for this release, and the current
+ // state they are in.
+ Hooks []helmrelease.Hook `json:"hooks"`
+ // 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,omitempty"`
+}
+
+// Targets returns if the release matches the given name, namespace and
+// version. If the version is 0, it matches any version.
+func (o Observation) Targets(name, namespace string, version int) bool {
+ return o.Name == name && o.Namespace == namespace && (version == 0 || o.Version == version)
+}
+
+// Encode JSON encodes the Observation and writes it into the given writer.
+func (o Observation) Encode(w io.Writer) error {
+ enc := json.NewEncoder(w)
+ if err := enc.Encode(o); err != nil {
+ return err
+ }
+ return nil
+}
+
+// ObserveRelease deep copies the values from the provided release.Release
+// into a new Observation while omitting all chart data except metadata.
+// If no filters are provided, it defaults to DefaultDataFilters. To not use
+// any filters, pass an explicit empty slice.
+func ObserveRelease(rel *helmrelease.Release, filter ...DataFilter) Observation {
+ if rel == nil {
+ return Observation{}
+ }
+
+ if filter == nil {
+ filter = DefaultDataFilters
+ }
+
+ obsRel := Observation{
+ Name: rel.Name,
+ Version: rel.Version,
+ Config: nil,
+ Manifest: rel.Manifest,
+ Hooks: nil,
+ Namespace: rel.Namespace,
+ }
+
+ if rel.Info != nil {
+ obsRel.Info = *rel.Info
+ }
+
+ if rel.Chart != nil && rel.Chart.Metadata != nil {
+ if v, err := copystructure.Copy(rel.Chart.Metadata); err == nil {
+ if vTyped, ok := v.(*chart.Metadata); ok {
+ obsRel.ChartMetadata = *vTyped
+ }
+ }
+ }
+
+ if len(rel.Config) > 0 {
+ if v, err := copystructure.Copy(rel.Config); err == nil {
+ obsRel.Config = v.(map[string]any)
+ }
+ }
+
+ if len(rel.Hooks) > 0 {
+ obsRel.Hooks = make([]helmrelease.Hook, len(rel.Hooks))
+ if v, err := copystructure.Copy(rel.Hooks); err == nil {
+ for i, h := range v.([]*helmrelease.Hook) {
+ obsRel.Hooks[i] = *h
+ }
+ }
+ }
+
+ for _, f := range filter {
+ f(&obsRel)
+ }
+
+ return obsRel
+}
+
+// ObservedToSnapshot returns a v2.Snapshot constructed from the
+// Observation data. Calculating the (config) digest using the
+// digest.Canonical algorithm.
+func ObservedToSnapshot(rls Observation) *v2.Snapshot {
+ return &v2.Snapshot{
+ APIVersion: v2.CurrentSnapshotAPIVersion,
+ Digest: Digest(digest.Canonical, rls).String(),
+ Name: rls.Name,
+ Namespace: rls.Namespace,
+ Version: rls.Version,
+ AppVersion: rls.ChartMetadata.AppVersion,
+ ChartName: rls.ChartMetadata.Name,
+ ChartVersion: rls.ChartMetadata.Version,
+ ConfigDigest: chartutil.DigestValues(digest.Canonical, rls.Config).String(),
+ FirstDeployed: metav1.NewTime(rls.Info.FirstDeployed),
+ LastDeployed: metav1.NewTime(rls.Info.LastDeployed),
+ Deleted: metav1.NewTime(rls.Info.Deleted),
+ Status: rls.Info.Status.String(),
+ Action: rls.Action,
+ OCIDigest: rls.OCIDigest,
+ }
+}
+
+// TestHooksFromRelease returns the list of v2.TestHookStatus for the
+// given release, indexed by name.
+func TestHooksFromRelease(rls *helmrelease.Release) map[string]*v2.TestHookStatus {
+ hooks := make(map[string]*v2.TestHookStatus)
+ for k, v := range GetTestHooks(rls) {
+ var h *v2.TestHookStatus
+ if v != nil {
+ h = &v2.TestHookStatus{
+ LastStarted: metav1.NewTime(v.LastRun.StartedAt),
+ LastCompleted: metav1.NewTime(v.LastRun.CompletedAt),
+ Phase: v.LastRun.Phase.String(),
+ }
+ }
+ hooks[k] = h
+ }
+ return hooks
+}
diff --git a/internal/release/observation_test.go b/internal/release/observation_test.go
new file mode 100644
index 000000000..a9a88399f
--- /dev/null
+++ b/internal/release/observation_test.go
@@ -0,0 +1,395 @@
+/*
+Copyright 2022 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package release
+
+import (
+ "bytes"
+ "testing"
+
+ . "github.com/onsi/gomega"
+ "github.com/opencontainers/go-digest"
+ helmrelease "helm.sh/helm/v4/pkg/release/v1"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+ v2 "github.com/fluxcd/helm-controller/api/v2"
+ "github.com/fluxcd/helm-controller/internal/testutil"
+)
+
+func TestIgnoreHookTestEvents(t *testing.T) {
+ // testHookFixtures is a list of release.Hook in every possible LastRun state.
+ var testHookFixtures = []helmrelease.Hook{
+ {
+ Name: "never-run-test",
+ Events: []helmrelease.HookEvent{helmrelease.HookTest},
+ },
+ {
+ Name: "passing-test",
+ Events: []helmrelease.HookEvent{helmrelease.HookTest},
+ LastRun: helmrelease.HookExecution{
+ Phase: helmrelease.HookPhaseSucceeded,
+ },
+ },
+ {
+ Name: "failing-test",
+ Events: []helmrelease.HookEvent{helmrelease.HookTest},
+ LastRun: helmrelease.HookExecution{
+ Phase: helmrelease.HookPhaseFailed,
+ },
+ },
+ {
+ Name: "passing-pre-install",
+ Events: []helmrelease.HookEvent{helmrelease.HookPreInstall},
+ LastRun: helmrelease.HookExecution{
+ Phase: helmrelease.HookPhaseSucceeded,
+ },
+ },
+ }
+
+ tests := []struct {
+ name string
+ hooks []helmrelease.Hook
+ want []helmrelease.Hook
+ }{
+ {
+ name: "ignores test hooks",
+ hooks: testHookFixtures,
+ want: []helmrelease.Hook{
+ testHookFixtures[3],
+ },
+ },
+ {
+ name: "no hooks",
+ hooks: []helmrelease.Hook{},
+ want: []helmrelease.Hook{},
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ g := NewWithT(t)
+
+ obs := Observation{
+ Hooks: tt.hooks,
+ }
+ IgnoreHookTestEvents(&obs)
+ g.Expect(obs.Hooks).To(Equal(tt.want))
+
+ })
+ }
+}
+
+func TestObservation_Targets(t *testing.T) {
+ tests := []struct {
+ name string
+ obs Observation
+ targetName string
+ targetNamespace string
+ targetVersion int
+ want bool
+ }{
+ {
+ name: "matching name, namespace and version",
+ obs: Observation{
+ Name: "foo",
+ Namespace: "bar",
+ Version: 2,
+ },
+ targetName: "foo",
+ targetNamespace: "bar",
+ targetVersion: 2,
+ want: true,
+ },
+ {
+ name: "matching name and namespace with version set to 0",
+ obs: Observation{
+ Name: "foo",
+ Namespace: "bar",
+ Version: 2,
+ },
+ targetName: "foo",
+ targetNamespace: "bar",
+ targetVersion: 0,
+ want: true,
+ },
+ {
+ name: "name mismatch",
+ obs: Observation{
+ Name: "baz",
+ Namespace: "bar",
+ Version: 2,
+ },
+ targetName: "foo",
+ targetNamespace: "bar",
+ targetVersion: 2,
+ },
+ {
+ name: "namespace mismatch",
+ obs: Observation{
+ Name: "foo",
+ Namespace: "baz",
+ Version: 2,
+ },
+ targetName: "foo",
+ targetNamespace: "bar",
+ targetVersion: 2,
+ },
+ {
+ name: "matching name, namespace and version",
+ obs: Observation{
+ Name: "foo",
+ Namespace: "bar",
+ Version: 2,
+ },
+ targetName: "foo",
+ targetNamespace: "bar",
+ targetVersion: 3,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ g := NewWithT(t)
+
+ g.Expect(tt.obs.Targets(tt.targetName, tt.targetNamespace, tt.targetVersion)).To(Equal(tt.want))
+ })
+ }
+}
+
+func TestObservation_Encode(t *testing.T) {
+ g := NewWithT(t)
+
+ o := Observation{
+ Name: "foo",
+ Namespace: "bar",
+ Version: 2,
+ }
+ w := &bytes.Buffer{}
+ g.Expect(o.Encode(w)).ToNot(HaveOccurred())
+ g.Expect(w.String()).ToNot(BeEmpty())
+}
+
+func TestObserveRelease(t *testing.T) {
+ var (
+ testReleaseWithConfig = testutil.BuildRelease(
+ &helmrelease.MockReleaseOptions{
+ Name: "foo",
+ Namespace: "namespace",
+ Version: 1,
+ Chart: testutil.BuildChart(),
+ },
+ testutil.ReleaseWithConfig(map[string]any{"foo": "bar"}),
+ )
+ testReleaseWithLabels = testutil.BuildRelease(
+ &helmrelease.MockReleaseOptions{
+ Name: "foo",
+ Namespace: "namespace",
+ Version: 1,
+ Chart: testutil.BuildChart(),
+ },
+ testutil.ReleaseWithLabels(map[string]string{"foo": "bar"}),
+ )
+ )
+
+ tests := []struct {
+ name string
+ release *helmrelease.Release
+ filters []DataFilter
+ want Observation
+ }{
+ {
+ name: "observes release",
+ release: smallRelease,
+ want: Observation{
+ Name: smallRelease.Name,
+ Namespace: smallRelease.Namespace,
+ Version: smallRelease.Version,
+ Info: *smallRelease.Info,
+ ChartMetadata: *smallRelease.Chart.Metadata,
+ Manifest: smallRelease.Manifest,
+ Hooks: nil,
+ Config: smallRelease.Config,
+ },
+ },
+ {
+ name: "observes with filters overwrite",
+ release: midRelease,
+ filters: []DataFilter{},
+ want: Observation{
+ Name: midRelease.Name,
+ Namespace: midRelease.Namespace,
+ Version: midRelease.Version,
+ Info: *midRelease.Info,
+ ChartMetadata: *midRelease.Chart.Metadata,
+ Manifest: midRelease.Manifest,
+ Hooks: func() []helmrelease.Hook {
+ var hooks []helmrelease.Hook
+ for _, h := range midRelease.Hooks {
+ hooks = append(hooks, *h)
+ }
+ return hooks
+ }(),
+ Config: midRelease.Config,
+ },
+ },
+ {
+ name: "observes config",
+ release: testReleaseWithConfig,
+ want: Observation{
+ Name: testReleaseWithConfig.Name,
+ Namespace: testReleaseWithConfig.Namespace,
+ Version: testReleaseWithConfig.Version,
+ Info: *testReleaseWithConfig.Info,
+ ChartMetadata: *testReleaseWithConfig.Chart.Metadata,
+ Config: testReleaseWithConfig.Config,
+ Manifest: testReleaseWithConfig.Manifest,
+ Hooks: []helmrelease.Hook{
+ *testReleaseWithConfig.Hooks[0],
+ },
+ },
+ },
+ {
+ name: "observes labels",
+ release: testReleaseWithLabels,
+ want: Observation{
+ Name: testReleaseWithLabels.Name,
+ Namespace: testReleaseWithLabels.Namespace,
+ Version: testReleaseWithLabels.Version,
+ Info: *testReleaseWithLabels.Info,
+ ChartMetadata: *testReleaseWithLabels.Chart.Metadata,
+ Config: testReleaseWithLabels.Config,
+ Manifest: testReleaseWithLabels.Manifest,
+ Hooks: []helmrelease.Hook{
+ *testReleaseWithLabels.Hooks[0],
+ },
+ },
+ },
+ {
+ name: "empty release",
+ release: &helmrelease.Release{},
+ want: Observation{},
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ g := NewWithT(t)
+
+ g.Expect(ObserveRelease(tt.release, tt.filters...)).To(testutil.Equal(tt.want))
+ })
+ }
+}
+
+func TestObservedToSnapshot(t *testing.T) {
+ g := NewWithT(t)
+
+ obs := ObserveRelease(testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: "foo",
+ Namespace: "namespace",
+ Version: 1,
+ Chart: testutil.BuildChart(),
+ }, testutil.ReleaseWithConfig(map[string]any{"foo": "bar"})))
+
+ got := ObservedToSnapshot(obs)
+
+ g.Expect(got.APIVersion).To(Equal(v2.CurrentSnapshotAPIVersion))
+ g.Expect(got.Name).To(Equal(obs.Name))
+ g.Expect(got.Namespace).To(Equal(obs.Namespace))
+ g.Expect(got.Version).To(Equal(obs.Version))
+ g.Expect(got.ChartName).To(Equal(obs.ChartMetadata.Name))
+ g.Expect(got.ChartVersion).To(Equal(obs.ChartMetadata.Version))
+ g.Expect(got.Status).To(BeEquivalentTo(obs.Info.Status))
+
+ g.Expect(obs.Info.FirstDeployed.Equal(got.FirstDeployed.Time)).To(BeTrue())
+ g.Expect(obs.Info.LastDeployed.Equal(got.LastDeployed.Time)).To(BeTrue())
+ g.Expect(obs.Info.Deleted.Equal(got.Deleted.Time)).To(BeTrue())
+
+ g.Expect(got.Action).To(BeEmpty())
+
+ g.Expect(got.Digest).ToNot(BeEmpty())
+ g.Expect(digest.Digest(got.Digest).Validate()).To(Succeed())
+
+ g.Expect(got.ConfigDigest).ToNot(BeEmpty())
+ g.Expect(digest.Digest(got.ConfigDigest).Validate()).To(Succeed())
+}
+
+func TestObservedToSnapshot_WithAction(t *testing.T) {
+ g := NewWithT(t)
+
+ obs := ObserveRelease(testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: "foo",
+ Namespace: "namespace",
+ Version: 1,
+ Chart: testutil.BuildChart(),
+ }))
+ obs.Action = v2.ReleaseActionInstall
+
+ got := ObservedToSnapshot(obs)
+
+ g.Expect(got.Action).To(Equal(v2.ReleaseActionInstall))
+ g.Expect(got.Name).To(Equal(obs.Name))
+ g.Expect(got.Namespace).To(Equal(obs.Namespace))
+ g.Expect(got.Version).To(Equal(obs.Version))
+}
+
+func TestTestHooksFromRelease(t *testing.T) {
+ g := NewWithT(t)
+
+ hooks := []*helmrelease.Hook{
+ {
+ Name: "never-run-test",
+ Events: []helmrelease.HookEvent{helmrelease.HookTest},
+ },
+ {
+ Name: "passing-test",
+ Events: []helmrelease.HookEvent{helmrelease.HookTest},
+ LastRun: helmrelease.HookExecution{
+ Phase: helmrelease.HookPhaseSucceeded,
+ },
+ },
+ {
+ Name: "failing-test",
+ Events: []helmrelease.HookEvent{helmrelease.HookTest},
+ LastRun: helmrelease.HookExecution{
+ Phase: helmrelease.HookPhaseFailed,
+ },
+ },
+ {
+ Name: "passing-pre-install",
+ Events: []helmrelease.HookEvent{helmrelease.HookPreInstall},
+ LastRun: helmrelease.HookExecution{
+ Phase: helmrelease.HookPhaseSucceeded,
+ },
+ },
+ }
+ rls := testutil.BuildRelease(&helmrelease.MockReleaseOptions{
+ Name: "foo",
+ Namespace: "namespace",
+ Version: 1,
+ Chart: testutil.BuildChart(),
+ }, testutil.ReleaseWithHooks(hooks))
+
+ g.Expect(TestHooksFromRelease(rls)).To(testutil.Equal(map[string]*v2.TestHookStatus{
+ hooks[0].Name: {},
+ hooks[1].Name: {
+ LastStarted: metav1.Time{Time: hooks[1].LastRun.StartedAt},
+ LastCompleted: metav1.Time{Time: hooks[1].LastRun.CompletedAt},
+ Phase: hooks[1].LastRun.Phase.String(),
+ },
+ hooks[2].Name: {
+ LastStarted: metav1.Time{Time: hooks[2].LastRun.StartedAt},
+ LastCompleted: metav1.Time{Time: hooks[2].LastRun.CompletedAt},
+ Phase: hooks[2].LastRun.Phase.String(),
+ },
+ }))
+}
diff --git a/internal/release/observed_bench_test.go b/internal/release/observed_bench_test.go
new file mode 100644
index 000000000..ce2e80e30
--- /dev/null
+++ b/internal/release/observed_bench_test.go
@@ -0,0 +1,49 @@
+/*
+Copyright 2022 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package release
+
+import (
+ "testing"
+
+ "github.com/opencontainers/go-digest"
+ release "helm.sh/helm/v4/pkg/release/v1"
+
+ intdigest "github.com/fluxcd/helm-controller/internal/digest"
+)
+
+func init() {
+ intdigest.Canonical = digest.SHA256
+}
+
+func benchmarkNewObservedRelease(rel release.Release, b *testing.B) {
+ b.ReportAllocs()
+ for n := 0; n < b.N; n++ {
+ ObservedToSnapshot(ObserveRelease(&rel))
+ }
+}
+
+func BenchmarkNewObservedReleaseSmall(b *testing.B) {
+ benchmarkNewObservedRelease(*smallRelease, b)
+}
+
+func BenchmarkNewObservedReleaseMid(b *testing.B) {
+ benchmarkNewObservedRelease(*midRelease, b)
+}
+
+func BenchmarkNewObservedReleaseBigger(b *testing.B) {
+ benchmarkNewObservedRelease(*biggerRelease, b)
+}
diff --git a/internal/release/suite_test.go b/internal/release/suite_test.go
new file mode 100644
index 000000000..1e8463384
--- /dev/null
+++ b/internal/release/suite_test.go
@@ -0,0 +1,62 @@
+/*
+Copyright 2022 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package release
+
+import (
+ "fmt"
+ "log"
+ "os"
+ "testing"
+
+ release "helm.sh/helm/v4/pkg/release/v1"
+)
+
+var (
+ // smallRelease is 125K while encoded.
+ smallRelease *release.Release
+ // midRelease is 17K while encoded, but heavier in metadata than smallRelease.
+ midRelease *release.Release
+ // biggerRelease is 862K while encoded.
+ biggerRelease *release.Release
+)
+
+func TestMain(m *testing.M) {
+ var err error
+ if smallRelease, err = decodeReleaseFromFile("testdata/istio-base-1"); err != nil {
+ log.Fatal(err)
+ }
+ if midRelease, err = decodeReleaseFromFile("testdata/podinfo-helm-1"); err != nil {
+ log.Fatal(err)
+ }
+ if biggerRelease, err = decodeReleaseFromFile("testdata/prom-stack-1"); err != nil {
+ log.Fatal(err)
+ }
+ r := m.Run()
+ os.Exit(r)
+}
+
+func decodeReleaseFromFile(path string) (*release.Release, error) {
+ b, err := os.ReadFile(path)
+ if err != nil {
+ return nil, fmt.Errorf("failed to load encoded release data: %w", err)
+ }
+ rel, err := decodeRelease(string(b))
+ if err != nil {
+ return nil, fmt.Errorf("failed to decode release data: %w", err)
+ }
+ return rel, nil
+}
diff --git a/internal/release/testdata/istio-base-1 b/internal/release/testdata/istio-base-1
new file mode 100644
index 000000000..a99ff1f77
--- /dev/null
+++ b/internal/release/testdata/istio-base-1
@@ -0,0 +1 @@
+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
\ No newline at end of file
diff --git a/internal/release/testdata/podinfo-helm-1 b/internal/release/testdata/podinfo-helm-1
new file mode 100644
index 000000000..e1e387187
--- /dev/null
+++ b/internal/release/testdata/podinfo-helm-1
@@ -0,0 +1 @@
+H4sIAAAAAAAC/+y9CXOrSNYg+lcUel9Ev/fG1xeQ8TWOqIgRsoWQJWxtbH0rKiDBgJQsJRYJ1Vf/fSIzWbV4qbrV3TNTHVF9LUhyOfs5efLkb93A8O3ufTcKLS94Db+4NvS3dhTGXhJu8+5VFz3t3v/WffW2cfKLZUcwzG2re99lKIb5QrFfqJslzdz3vt2zN9cUfXvDsewdo3evutD45AeWDe0EN8U/YrD1osQLg+59VwzixICwA0I/Qo26V904MZI07t53qyGuukGY2OgRfd0R7KSTuHbHiCLoAQP101nNJx0z72zTIPACB72ObdSlbwRWfP896HRs4Iad713Zi72k4yZJdP/1K818u6auqWv6/o66ozpJ2Elju5OH6bbZ+fcu+n6TmjZIYOdL0LHsVyOFSScKt8mX13C7M7ZWh8z163lod1D/99wdd/c96P5+1QWusU0Q7H07MSwjMdDfbXx1r7puiB+gycb3X786XuKm5jUI/a9xYr8aQbQNLSP4Wn8Qh+kWIDD988Mf/XzVzextTHBxc01d355g6IU07Yxs6HfwzDuv4bbzlJr2NrARWq66vuEFieEF9haNXq2lOWT3qmv7hgePHv/PNLa38XUQbu0I5tf1fLu//3zVNSJPrqaX0V30JJKPJvz7VReGYNO9D1IIr7qJ7UfQwORSz6R6+FV6Xj4urpN9ghaKQd+dLm6cOSNT4mge6gveBSM+NhTJtQSYmR4vrzY7Rws2Dgjk1PRhquc8ZQjyQV/wa5OhE01hN+AQPtm9OBEF6IsDVtGUPa2r09RQ2AD48gHkLDT9oWcK8kYc0dwgSL5NFnyuKWygL/qp7A9jS5EPE9R/bwxBb5oaAnewRlPnVaWexAEfWqP5zu7FjqFozvj8GJSJ29MH1E5XWDzWs3eXod8Tj+de1eSbOJidHQ8IQ8oY8NzrLPpm51T1fTFXaI4kiNcnSJkZzF1DYQ/iYPxsMnO4ErjcGojOBOquOZIhyNmDro4ZQ5HgJJizQFiV64D2iM9AMHOWG+5xLnOzJT1evSxmodGTPV2RKHPAB7o6cyYLOtUUGoIe72rMysFrgGNoCrILmFW69IeJrko7TZEgmqc4oDNRiA4mw+40dR6+LMbfJoG00xW0Ni63RpI5HVKp6XMbXUZzmHOix1fzBDn/zc55SlfonSkMKX3R94DAbQyF9c2c9S1lH5tozIVIYLnYnFvPePVAbZ6EhNZ8eW0JO0dHtCWwmS7IB3FAJWZr3ggf7EJX9lBTJTjZsK6pyAcgDNf6Ao3RT8yc/xUwHMHPA+XZORtZgpwAOt6pC/ZgCUPKUqeppsw3mA4Y+aAceGviDzf6COFW4kSPzFVj3EwUXMoa9W8n+d2mnnO4aeLjHZxPTWa4mfnDWFPYta6KXpPmASPnlg/X+oKlbJXHuBl4fUcc9BGMVvNl6CzUmWMqQ1YczV2DWTnagvd1FTimAlNLnSNc+GZPdCzBheLjPtOU+UBT9q7pSxB4/Hg14Ckz5z3Er5Y/jEwB8dYqLccp/1MUjhYFyTU9vqep87UxQLy7ckBv7loj+SAKnC8KY1YcjWnTZyPTB86YOcLdSGK0vJ9Yn6YP0KKPFSUv5A18uEgjxTgTpUUD3+y8n658OdaV4UFfsM9NnnhVKcwXmB6EvWsJK0cM+EwXYKr7d6keyLEpoP5Wnji4we0nyp1jBAifvGsJDjfuxWkJj4l/DGsEEyvXVamgKbh7XYAIwVlXpNDMiVxCMmY8lIYrqI9nlPy6kPu3hF/PyANfvrEEOTf9YUzkRlPm7JFcjXQPyVaW0hSYgrzvzZg9DXpzCDZwJnoNeRlIEAR6pDGr1BrBHeaZWeiIgrw2hDtHhHoEGEg1aH4/XYN0OrjZTdaPt88P/ZspoSUaIBmjcDTwjnWAiPC4tZQx1HrzWBzQ6ft8y+/M3piaKHoGAssF/szRBXlnChw76SXfxNEcmiofa+ocioPxzmTmkenr2cTXaVPYY7mH1onl2oOze374o/AsZLjz00/d36/OKMRfXBtGSAcnEax1ot2LsyePZwyFuhVHErQGvG8oe4hhvKQTS5UoTZFCfRk6Tx5XjHf362Aj3wBhmOoFnxF+4DPdI781xnVBMEsHXpghfUB05hzqSA+1+amEQdJuN6TN0cyZbKRQU8cUgfuqCRf83bKnQxCMIx3pnQFPId2t5X1mmvN34mieGwq9tBTdN1THET3KEwd0G2b4N9bp2ZMTPQBfxrylLfiNruiupewpUcC4YsXRkNaUfaT7EOoDRDv9Yt03TzKzciwsWyTyvTpzpPW0gMPUQfyvqdiWOJgMDcVHLBNy05cpXZ2W8PMNRY710dTR1DEUhX1kKpDSlZljCXdIRkYg74fIRiHycv68ynkEByzPQI+HWr5JBxvoi6NKf5Z9N/iMXxvCENEPgb0AKXFkRaawczR/5ViqRNaX8y5Z+65aZ00DFKJz31DYt+i6gdO2vVTLK/nF8uUc+HCjLxrt321L8Gsp7Fp80A7iaIfgH5myROu+HtmDvjdB8lk9smsa+B8Teek8r8laNFWOrUE/nRW0i+2Ols32xnxPdOecwOBEdqwaczqV98U6aNOfYvrBNO3DZNWTfd2HN+JATNpwlWOE48Y6d8CHqSVojujJh8lCPiD5cjJOOb/Pj5fqg4/9xvPJw6dZbwyx/BMIL4sF3DUssxD/SpHJ3DiaOi1pz7MXLTniIL2vK7uPyJOCtsV3YXKM5/K3jPDLYJ/gDvER0rdIB4te7ImD8avo/QHa82fkt1PKTynXFWRbnJeftW2F9L+UWQqL/JZMHMkHfRb+OulVfSM5FZl+2x6ovl8N1xrD0WYwf36PJ0+/maW6wrqav4clTrG9hXRiQ0Y/CTDVmD2tv22TRO+NVcDgyEa4PK7oV397n++7lP3Hvy/pT+L5+3aQXOeG31ChmspHFcmsQ0dT+R3IOWY6i7aGwm6ePf5RV/nY7MFEV1hq4NPQEoYbTZ27zw4yYTCIbvHyPmEuINOMsMX+gPsZ9PHYz94nzddZ8S0h/3fmUbBwYw7igM9LEUfGPifWqrahro4jSxgGyKQ4dvVWNdkj1ByAIK/x2kaY1JG5mJq9WdMMCIHw2CDTYj2EbSONGR7KOdWmZPXuwezJqVV9U7Nq4cIerNHYtQQ5sJdhQYLYxb0Vh+PMROrZt2QgzJHKL96fPL+tXBWBdu2hnGqq7hrKHs/32evv0XvAIHhJlNkTi/Z8gt2YR9ROjsGhGP/P4LiB05JeTEWmNAXN87Exz9MxRWLu/CEaJX0OkTtOaeo8Mhm2olcCMz5HJhEyaSwc5rjLACPlmsoj+HhIPekLsTmXHfC5RFfnoa7i0EY26WHX61YcjI9wXbsNxmhOgSFx0V5Vymu6kFgcjsau6VtINW51dROLgzmDRcqif/vSNAOQiHtsr6e1VtKfMxYSaCM6Luh8PCI0i9QFUOXMqkIl1brapjyhg5oHSBtKV8eJobDEZdlYuaZIcCXIuaFwmxUjIzNvAw6hMz30n1rr+5S4r+UCmkPpgswUaY155vEDMquCvcyXKmx5Ru6cXzeimcqcgiCY3jbhO1kQ07Xk7bYqb/ZN/jMU2tWZ1Sl9+DDRFAsSmcAdDHWegWCD8HLshkWmMgz0BTKhwQn9iAOevJd5xAMzk9lHWm9zW7iPjRAenkcKRnJsDvnMFODaPjNfjeESUxmm+kOTB8u191NE74WaPfc+maiYzrnLriRyk/kZcl0RPTbMX/b5YXNzTM+l+iUy+Dx/IVkCfLgGdMVjb80tqdof6Is8+5E+a7PzFI4lbRmKdgEOVg4EqYDDmTUPKGeyoAMQ8Ov3YfpOXyf65dw6LvDogsVzaOvH47X2E3FAJXpvvNNyup4XDjOt3pv3svz9h+deyc96/po/XBuMnOpVePsMPShjV2MS1HdiqeP4BYeub34Urtc4fPEGvDRmuDaEVUJgLedk/MYayPu35oNdsKNxc12ZRyfbAefpq5gjwZmuitwnQ3230nrae74AjxO52h471lUdmg9HPOjvM53aQ8uX4ze+zZELZzJUogtyrKmbYxojMmU0TYv3b8gERDvzzFzQEATjDKyP50NCMQie+L33YfxXc7AUNtSVYWwJLvvGmmjTd6Gm7CljtPkR9NccH7l8G/sNWqzafBaX52Sz7+Tax+gvnPrTT61VV1imZcNdkF+WsklxuJRBuvO0n4Yeny2p+Xi50V9UWh6rFD1c0RI/p1Yn87Lw+krbY0ibvTn8zJhvreviGgQuMPPPzH//Mqfu3p77B8f4I/PVGC42e+IZfqNIqGEZOqtH7nGxYoUlzckLmXtYUvuXlXNCd8QGrny1iq5pY8GuTWZ/lhc/O+dKTww+CGNqzM+oZLjczF9leTw95QcyzxPfz690zSls3tJxeGtnfnh2zvBt6b+TrYcTnIuDOvSqq2NsH5z6pBf8lEEjDP2wuXleOmdlaOETUSYjZeZD6MiP0uyvsd8qXODtoIlCQ2s0jrTe9E+v+yPjf2Kdf87OqrZX3sI3spv+/LqxvfZXrlnYR5Yvp7oqHVajcab5q9uT7wX5Rlemp8/f8UUwPEbINpFi4xRWWN5ojAs1Jj7/7UWeQe/2mcYMY5we8RD9GX7JDEF2zdE8tE/lW2T6kDKUYTzHNgpc6oqUmf78UMZm2nCeR6YiZ5Y6b7ajj3xBrMOx39qTZsDnPH15CjtddaF2CC/QD0k3OSNvMEyBwG00Zh+dw9dkwa8NQV4bu/PfYnj1z78zBW6tKfvQ7EnUc9DWUQ0ewbxf8MhdMyT7vHTYZ8Q3C47YMerp+gyFjSwBuubjHJrCkG3FLE7tHspQaGj2ZKrZTjpqZ/lcbCk0XCo41H0ipxtyC8ddT+mfxvGJFU69CJ0Jg2NiRzpgDAHD0cCXYJEG5FgCp2sKHbfg5MsHsyfnGiPjrQFDYTe6wlLiw2PeiOtge85iIGUMWltMmS7ISxxP7c2PdCuf4hScZqwwwPGXTFGGiTnop+3xGrrinHwgz3qGOg9buljRfYyjUdtebzz/S8a1BC7W1TGJawVtf/Ho3Z8bv0EvzRjnWzSiK/TOGsFHQxWR7fbGNrsbGUf7A2fTxUa8O/HlVFPGsa4UW0P+cGfI1XbYrSgMaUvgDlgGKmww6Wm55suUtnx8MhiI5LGzELjcUCMcJzOHOC5ZfaOr4tNxfLe0/T7pXz7hrWUMK570Lc9d4FvQGo6hvi5gKJzMfwdG08xaF3wkJJHpz27FxzkEwj6zEV8Hs+Ldn9gDUWC68uWdKcC1pk5PbGtQvluV22jld8Obt74jOKJde1jvJdTflrErDJNzfh/el9B6PF1+M1nwOE3q2eMXuiplljpe6wVv1zKjuV9Q2hnSzqplAKWp40BX57zly7mmWFBW55EpwFsSi765sA5pZy0u7nNcnL+pyInZG7N/fg00NBUut99axwXfqMAD+f4PrAH48h7J1jrFEtks5T4On+mVLuQRrbV1h1DYt1TTxudfG3024/efW1MwhkCVIejNK7pq+00fTe3xAmdrx/FH5M65NNVWSgTmMbJn9xBy4se2dpspDZtqDBqnneH0ilYcvUgzm5C0tAuyj4XWyMqAj2QGSI2DW+zzaPsTGViOt4sSXZ27ujCkNGxvtfYINnjOZN+ipJlYU8bQHJV270f2uNpysdinwHui7+zJ1WkJDZ0Oyn3SM3uhx3sZhiDnBlofU9FTnVoCK3vYqbb3R1aE4XsO9v75/StNYVOzN/+gnp057bSP5p5tdI4fG7ifxyCv9iXjphwF/jDVmdX73xGdjW3Vph4/ikWT9ydxFZJO+aF9qJEEtd4Ytva8BuxBV6RcV+et/e03+dfrOyCQY10l9HYpZt7g0ZO5l+st/cn2fhlJfWzaLTiVeHTi1yGfDfFesZZ5Bd9VlRp+Yod7miJt9Us+yaja63t+m+dOfJ1qHwL7Ew9h7Z+8IeMR7X4GZjh1tb3n7FqCe85PwCnl5fyPZdm5NVSxq7P+cb2+ZcP2asnZs3HZfp0CJBd725WvzJ9P2y/gdF5HQC/Y2FvrAzrCFGBqMHKuD9gd8DmcDvq+rbqPTD+BwJ/h/XtrPXTNER9qS7GS06t6PbnJ6JEprH6UfZq+d2RgEkiMlrNrcyQfLEHOcfozM4wH5NgAHhv4HG0JckUTE5IyuTEKu6qJIzRvQ+A2zx4/msuzVm4Cop+VLwe66tyKgzoGXK5tTsmr+jnlaAyX6gKkDIVLT2wPwc0w7B65pdw/od+FrljQxv7qcGcsOJz2f2J7DfmXFT071ybBe+z+PMLyft3K98D5FSaD7HVr+IGxsX48GRt9O+hnmspHE6a5X/2xdSNYncgz5HMw8s2z1880xqUR3ekDzlQ9znqq8yRK3Y/w40wYKbQUdqsrs8zuSRCPPZK4HwsHEs+Y0Ik9oalfL8Gi2Av7C+chp9ZFONTvfgx9c6GuDDe6Oj5cWq8hyK4uyDnY/fBxY0twb98atxnz+0HjIjzkhiJdXG/lG/7Y9e40hY203QW89njX9OH6h9JRbww1Zc7al2jp6P0H1zlb0tKbPA182dVH8HbCzCPA4OM+JzIUy8PBW21/kFwt1+hxhb/0kbkUunL2Q/FfHv/KFPomUxfhBb1St7MZCcmaz8kWoivemgfJVZid0ATG6/H7H7T2Ij/iXRnvaowLJ70k1FQpfJ39YLrszTPgv4H/1vsfo9+KPj+Acy7XF9w3QxgejBH1g3lgniH79qJd0X7/o+iMMpkEmh7H4CM3yJ++THMX2/4g+mOQfkMwvCTzrTL/+wfLQqs3zfDxu93FtfdAXth0bT/gQqxoa1te/BWEwavnfMAZAL68MdTpyTmA46R3a10HZGaI6HwYLJXh7kclu0/UMdQFeBj4tbNQzg0T+Tp0XvtlUvPwpgpcPmiUqRTJwwLt2mUgckHviiRNR1P2scHILMjpGARlIrfkWv7KET2xCm4D5NgLXGqONo7p3x0FPsI3AX7x9MCfAfq/6qRBBfwfdeJgQZPsuF0zw1+igD+kdMViy77JIcTQWfnyujiYh3cgqt2xaueBdi1BCpdHc/ujWevN9Ta80Cozvx1lLLj6ZOyCaB4+7c0mBf4bO29HUTmvuTvvQo1JDpZCZVrBeAU+ItOXYktB63XJAahBfzOBNRwmm7ELGHmm4Z04MSvGrfqZBNA1lZ33tCCRP8C47nS9YkBPTs5msY9qb/r9jJM6K7wZXWhGWC4dYnqH3i5lq3vHWaXns5Ra2RtHEaI6e+oYRwTfJJv82et7R9lSZTZorqt8BhhIlbsqJ7vsBe9bwjDAh4Kdc5ns8gzx1ErgYkOR2GePH+sbNrOGfK6rUr2rNmhk0iu0a/qz23MZgMWYVQbsuTaixxVroBNNlShdreiFCGBP/HzG1KjmszNZJwgGkenLeRkZPJ/lWpwg6c0R3cbPHr+a0f3PZ8CM5mswlDKNSaB1PpK3KzJ5Ctn1qUwS+j8zkwRbKiVe16awOZeJU2ScwFT/V6253FlUz2QkFYeiwYX1gtHqVnx43E0f+OQcXZWGwbPX30/XrnSatYTGH9K6Kr0/hnA2op3oCp2BYHMrPkzzpXIE05GemSM50Vck4+TcGJMFn2C5NSyi9AOO0VQxMwXoIY/0Es+UsgnrprOZa0dZLkVfE4V2cdGFczRS70Jnpq9HNQ3wjMnsaVORL8rIc/M4yaQ4f6oHyWDfUMCpHENzoGgXnOHRPxg9Ts7KciRrVaQrz2fFTRb8Vlc3lQyb+Ghu2nn+qXZb2m0/Y0DG9jbzgP0XmexVbH721h7qH7HwwtPzhH/SKmrkopS5CA9lXH8h9y9YiH/WEiPSv+bWy5YAOKut3sjhHJW5NMU+T6kVP7Sv8wm6qPP3jvf+XSBApfRxnz2+6VFU54L+zeeWz5wrLcpYvIPX4lxz2/qry2Pcni/x0rBqBRcCH1LEsjs539w+N9XYTyq9l4vnrtrlsk7oq6Sjz5WDOcmvKfOP8R5oTXOXLaaSrtt5ovXe6xMu+9IPdfXxzXWJG5zLSM7DeRvnSWAzC32HNcDmwprqb54Wm6i9v12/ezOPvCr7NXvX2v8jueCTRWUJfjqn2xKGuc7IVGnR4na7D8iIcznnZ86r/yC5d6SZLuUdFbLHACBMPxbY+ECFiQ+pqfr7P7SF/EZ1jP8LRNRxRZJ30OuHgZeE20+hl86QX2D2xHfRaypcik9d44KL7NrsjaHZwz5lMulp+4FfZI0Oq7iBZDJsZAlc/rdG+pxG0hV2AwQuambI1xkvpzuXDZ+3rT0EmCLNY/nD+LIUrPE08ev2VSbUX17x4qPZk4kdJ/FXYAD3I1ZU8zTwm6JK4DY/LP6qFDtb6hyC3ixBtIWz5B6HMRBcdxnIifiAC6DFZs+C1amBE8v7E3Tz+couLdo/LtyGrOnWc1gXfqrp+n1tdVLV5FJlEd+FpkClgHEyQ+AyI0f+JoZfQVfjUFf2ySSQwgnjZiYT41PMujqHE7WszCDeisIY6j6X6ws6NBluO1FI4SZkKU8E8h3oyWuNkaEuyJuyaoehzKHGDBHtHywBZjh7lYGp4ctrBCPRR9aPBMv4XiPrKjJzLjL9CGq92a0olDvbVd8uGPGJrkrhxI9vQM66Vm+aAQZudEVywaYYQ+BymZE9vPYD8sXhoYixNj2os7HOph9uqeO4wfdFfJM8L2KRhwkj0cDf3Ur5DfO8uNm1KniciTtOFvzB6LdjBhOl5X8n4mh37PujMZyJKm3EgYV5apz3NzbNv8wf4fN8w72uaP3hdVHtFlO6KlHiQPOP4kt47uKAPoiD+bfGKdilvJG4ehcZ0wriqQD48k7E1XwenbGnXZjX1JnI7uN8tR/KjytnPEoaJ4IlZdajskJ2ZJYgH6zasoZmoDXgc/aE7lLetDLNq1O5YvBJWRK8U9Byefmkna66lK6OU03ZV1UuMP3i4rNzFwTzuvLJIwtxLPlDXiyRwa+GBz8ggslhs/lhEiAUzQVNgUStHCcljprmA5/p/R8W3CiKpdCYxCYqQgEuYMDjpMcVS5uLPi2Odo4pcD1dFS8lnH+iGNbnE8ZbBb0G7ANJQK/F9kRtPa8CAA2R/gcS4nnX9NnMEoiIrNX4ibj1nj2+gJ9Ea4wEQXWI+G/R/AHRTGiwFiOkANEydDRlvzMUFj6vp+l0WR9ibBzkdZQcZJoP00lPCsd045CfOg7AIXTEYRxMlGkwGfQDXXUja9Dfk3Y4PE8hFd7YfnrWVR0C53Mml7ONwDu19P46iwr70QmWxAMasT+uZTJDoBOGz5ZCOdKCvxOFfWYxcn7JS/jMmYzPW/nNcyVnalwt6NbzRtnKWgT8AS/mwm6zZwhybC7YgzHgMLuJCD8EfgdLkda6WuVEnrSdKHO0bkqv0y4Q+3u6omfAXyXEQqOLXX60hj35rhANujKH5Tmtip08NgK9eWTmR2w5GPsaLh1Zpm60c9MNhf0VeR7PHr8xVIl4p/0aXkgbGgN2+zyapppqHSY9KdIFea2p4zEZY54BaEHNxyKmLQYa3lZzB/Nc3Lio01TksToJIDuTTyc1y0YSpSu6awZ8bjJz1/Q4HJdSmaJOzZDbkZ3BiJkObg6Tdf8Cy1vnv1tYVqNOX673prfigDfHOe3qwpzUQ/pXWRjtuFmgzt6xLt4RMetd8m+TMFHvbwPhbwPh32ggUCbDxY1YTmUg6Ax0zWB+sJhhbi44ylBYdqLOM5PZH/4S3204f1lQ8vPLYhZqPTk3B/0EMDNnTHys4KwfVmSV4hOHwaMzUUVnEpBnT4u+Pz7aq676pRxH3AxpS3Az4DdORT+OoaaOIfCQz0jm87oQz/pplsBtdeUms/xhbODQ8qr2A1sxhJNKWEkzO7bplzZhWFWBwtUoP+N4sO9dEvG5ywOwhnzLoAvflK7n9x7/ZRJWrfTG33bc33bcv82Ow5LnnBuGnhcRM5BzOOr0vAaptN6k0374ToYW5QDGaUvVAb0+ksR3n4ts1ef0cM3X3hiCAX8mt11OrVbe/sek2v9WUal37MbE8+0wTT4fjFqV1Yr+Dkj9bW/+HZD6awJSFuNGJr4kYw91te9MD9pu+hA7unDnPK/Rv1xaerXAlw8Woqey2vYydJa+zOiqeCb/7uerbmbA1I679791jddXL/CSvHv/2+9XXdMAGzuwyusHi59x9/6fP1918e4hufcSXxqJv7dsaOTd+1cDxvZV195uw231C4mZIb4msfFgSaRO9SwNXNuAiZs3nmxtwyp//37VdRmAhrIDw4S21Xge4csmQZSWE243uer6xn5u49sv4+49TV11fdsPt3nZfGv/miJRSH7/ftX1fMOxUZ9RCuFLCD2Qd++74qsUJi9bO7aDpIu+qm4ebd//2Li+MjGc5p2ORfEaDPAgCBN8GWdMYH48ZzfEM0IQj4zE7d53v/7/qEeIH/5+1S2qHOBpbsNXD9pnoIMvknQmdmbD7n23mFUQWvbChjZIEJbQ4FFo9Y8nhHMxT7s8WjhpVa709pq6prv4YwzuQZgGSfeeRg+qqzx/60LP90pwN8FfILFL+90aR136dup10UoKG7w1p2SbIoLbJ/Y2MOBLuE26+FbSq66zjUDxm+M48ntR9tC8kzRJouZ3vp1sPRAXj75x3wjAyO8eTaM2SR6hTgYwjRN7K75069n1SYbQOcARBVzQWNF8SjJOzjX3gsTeZgZCHM3GaIgkhPa2xBGijNTDQAsh6qH7//Ru2G/fQBfjPCQs6ttxjIm5S0AIXNs3SsgjomndKXrt2tD3nCDc2rUpIOb8TFPn2PAAOb5XLtIZFhnBjsW40PR4z1JgjLetc36nKdKWGII3T2LOrwwBHsSRRIPimIQ4kpCKiUXBik1GdExlSGlIzPkgJs7GkDJUHRapxO33wjDV++FaFFioM0TdkCM/sm+oLtQHZbG9x2ji8S+mv2dFLCJx6gdegzjiIfD4GInpiROm86H0uioPtOb8AzJSTebGkTfSEquZYPo08a3IGtyRfwUYmEj1zcJUC6J8siP/GoqVmr0xHHhsqOeorRs0nh0s/yYbeFNnhlUBl4oCLtBLg+IeOrALf50EUg/0w18n/tg18O95gn6/OuFaHOou8GFmqVNnsZoPwS5MwWicYbXWD1OsWhd3T08eS5kqn5tM2NqCnD/2H6aP177VRm1pTQycqPpbRCgTHh1LgKm94Hu6IhI/A9ePw1EGhJaNXtY2fLQysR9RhjCkEHrNnrVGti3IeQ/vTI+QrypRxZWkme7xuaWwuJaNKNCR1htnVUaVOnUM5cZZ9GQPoUvHV2fePA286Vp8hPhaR3LvDCiuhiM+58CJVog0zUCiNERe5Orb8vo3Mk/yrMr0qK7nEmh2opbWnOw8O+GTJvDYHwIMF+uzcCMKOMvCwTX2ct7VhZlTW6okawgcwmzSO45hswEa21JEct9L+c0ufBojK1TZJ+JI3um9sasLK2eibBxT3SSVtbnga4uvvgvDGfi8o/XDJxnBoI5gVVcygrz+DrFAsfaJpYwh8FmI/GBRkGKLnHDCMMfwbNQ4bV7rOnGilSGsHIUpTib05LzIRLGeBlJxisGigU/Y8mnBH3SMc0Tq+4jUfsNz2GnqGPk8iB3xM02dORozRCwBq5MPPTnXlZmjj+Sc0EmF+8LfLmhiKCM6QpZPbArlVcYrp4gxPA0gvkqsbjPgNgj/lrAq1ssnNs7hJzShDfhN82TpW7RQWsZii35WuP2ggFdl+eEMIzqzfAR3NA8yzzadYx95ZzIsNJGoFIYHwHBrQxmSqxFxDdX2dXH4ajmhiFWoU7KmOmaTDpDowGKtjZ9yfrrPxchTwjAezXFsAeR8bKi4tlQxFjklaalj/B6JZoBpYw5BMHVMRivGremzNT+EdwXmTdqySv/WCZ9WwjBHlrUlyDny3ObVHS38Ha6hyUi5ofJk3h6VTBb8nTho/DuLnOPalxqu19vfa1UfENOTGciJ5ss5viYXzXc0fdKE+g4I8h2P+YZ8u8/0nC/ujggdTZhDLZADbbBzNLxWzpkMeAdHmP1+LAo8ubeB/O0jdaYN+Ax4vAOEYWooEuLdqsZYMd4zwjkZj1zz2qgTL682uydNKOu5zw/FN6aq9J3XAc+DYOza5Eq94jrkuva7LI+nYBc5JCNHdhB8tUc2M33y94qc6XCqq5aRDC2u5kMeHlK/otC/I7Wpb25fyNW//0PrR87J+RsBz8fR/WGM5Cjqv7oScUjOeiAaaNRExWOIAs5EIld7juQcCNxBV6dP2kiO8AkigcsLfK6nhxktHaw1/j3g5QWuP7DPgBM5rfsQCGycpc+l5O++I682OBakq7gO3EEUaAh6kqsjnsVj7TOd4U5gJA7lsYhohLl70oTqfg4kR1xbOV3zyh+musAl4qO7kofEMwMCkh8rh9REQnAYQ2tkQV25cabYc585EpGZ+LTgwOfrO0XUcW72xmReAvGQ8fofHrMpueI0xTWY8dXPjyRj8tL4uC+xuY7GPSDwdIyhFaI5AkamJoXsqeoNLXhcg0Vf8BHI+RRH0vGpUUxrm+Yaqvs+zsBLJmbkQVdnca2j8f0hH+9fkA/WI75v7r01uKX8JrFg3Hd1ghybkAJcGwgnI8u11Smaa2RiOVHBDEdPZAHpCI62hHfWJDw6JL5Moi7EvirrjcobcWSFhiKFSC9YzJDFclehsa1kqbMnTWjfb1LwP4kZFzLAFPaZlfM0EKxcU+aFfXTnGA/j9cDnyzu9Wqeyi34OFpK3Cq6Bt9GUm9pGYXDfY/It3/q20SelKRaB+cNemcvj5WLFPb/4rW8pTQFPmlDeQda8O6yguUfoL32OWuHa43JawLT8fmdhm66IbCB5f+aMV7EeCgRyIUurq0fd42uI6wgfPu2fWcp+Q+yPOrKE8ILoxlIlDE+F0mNNsQJdHVtPpD7u4dm7y3R/7+qMBYHHumDUjwY+f3RGqJS7pb01Hq/I+lb4Onmsi87YZ9VJACSfLt0fMS9gv+k9LwkeVqNxRmqvyDTI+aqOF44AjfjMwLWhsRxv9VvfL1D1efO8dIkcb+wNVHJF4HKr38aFPZqj+eMa3ef7aVx7frmf+jxSwVvPyw37TGi9eQbS0Wl+MctJ9n4L7kd3fRW0UdnE5Lc1W22mzStocYSv3U99FqvQQYfp8pF9JvN6xnXqyS5j43p/sq76FALiYXenLd7k4WHx7nCWFh7d2WwWOedqdZe2zpTMSdJUNzJVORGFYVLc+4lsA1zzeeDzVb3uE9tjWJzPHPAPqyG+HhvpRgdguwxH8Z1WjeoTHcuvyFm8mVPX05YPiOcLt3tnMhUs6tPnzOV+FsP5atU8qZ7zO+SvgOKOhFa/o7cqbszP6wV8t6boaIsa56JQHbo6kg2FPFEujYX3ck7sB+KjEV+ztpXrvVatrN5RX9sfk2u22czCdVVLO7+SZ0hnYTlH9BnpG9lxOrIZ8f0BSH7Jh8KuR/TU2E1p7LjMIuftOqhn4FbL1amhsFtdHW+a6wGjcab7MNabcvrcSakLehPBB3i8Kw5b96U6yx4Pkf+C+KzBs0l5+obEHuRYH/Ae8v3J3iy2E+ratOUpLI+NzAD5L7prFvOYLmXCS0fjFne0Yh2AZCfqS1OQTQ5TvLfYe9sGnhfvQM6Py/rDWH9eqFFd8OW3V6XQgUW74xMgrT5wjXEiY7XB3a8ET9X8SL0txL/H9YML3lNoGn9b1RjP8XXcEIywTUTa7lrfU+ZIIuMNE+t4PPlxv2z68kXc6Elr3i2SN2RAzq7BqOCdh2FS4CHTC1lU1aEnfkd170iztvyxXJoudckgf89MpimTym9mx/OJDYWOkD15Xj7W8zEFmBjq5e/LsS70UVbOqPsRju5DIfaTY/cIXpY+t9EX1S4m8ZtGuC8X9KRIZ9iiEhZPmcweln7+MX6XAjwU+gDfsYJ8fkTTRWi3+hbxbn0PCjyRaWQ+HJK7dTsBQkQjyH6ctXIyCjoZJVwDBu3dyl34tOoh/xX69oKHGrKHm3EGD8nFOr6jDagE2fOikEB7SZP9eDnG5c1eVJIVhPxjpEeswtdE9rBW2P51zKifio868s+hLQwTIOzhpH855lPHkdpxo0bsI2vEBQG5S4JGMpx7UxY+0ESej94+VfiylA917O8O++34ZE5vnpmLI5jmPGv2Vo7py4iHEX4hYCQXVLGvpjy5cRDsSHUymOqH8AmPg+8mRj746k/FK9V+mExUCdkW6D3fnueNV+WxDHf4DgR1wLpWTwKTHtL9eqSrwHtRqupZZYyNNwt70xIgoyt7FuvDIeSXq52DddCCL+PSO12RIt2HsIxrlTEoUcD4J3SljDPLRzZPHZMCOb/WlBtH84lus3y4IfE4sjt7Lv5YxL/+QtrqJ7pX5KGo09RWhonZjxxN6D8N1jfOkxetDLX/69M6dBSFo0VBck3MQ6uTOK/Sq7LoWE2hY3XhtvsdbJ66v//8+1XXNwLv1Y6T7n3XiDzZ3sZeGNx3Mvp7sPEC675TbLF9D3w7MSwjMe6/B50ONEwbxvjPTseIovtOsQFHngDX2CbVsy94w5S8cW3oXydhCDdecv0K0z2wrr3wa2D4dt0eNap3Jd/9MI4MYN93LBvvX5fNt15iOPZ9Z2RDnzzb2tA24jfHeXsecWQDvOYo3CZk9V+KT9wkicgo6N19h7vj7orf2zAJQQjvO8vBC3mUGFvHTl5ww/LDsiNnG4FWRxzHfaij8sO42Ak+g5wzC07yyL7vVBuf34MvX758D5qkYERR/LWmh4eqGuRHSeIsOv9Pp5Biwzy+79BnkOIbCXAnTYC9C7I42RqJ7eRF+20IoRc4q8gyErvqwzf2q8DIDA8aJrSLsUskz5ufYNwXmWPlpJrYxDOqUwiqZ5gMfTtx7TRG0CVE+r2L6b17qVUMtgaawvdusk3tuh1sg+ADdFMBGBNRGCSGF9jbuosvHRD6vhFYjU6/dK6/tsQTefjlC5r8TzWnNh9/KdIGfuK+cd/arxGjFZ9WvNl6V6QCfEGk8tPZgaGd2fCn0+dbI7BC/wvOvvkJpw2cfY/zcY7f20HWWjMh1JfnB1EaPv+yEn8ZPE+e53WLTgenC913ghTC+jHOlbnvnEuAuW9wadX0pUqpue80M2rqZtDL7MCO45dtaNr3zQnY+wYyS5QeY4+sJgotAL3jp8C1web4YS2K62cwBAZ0wzjBSSFfSW7SodnKC7zEM+ADAv3CBmFgxRX/kP8ViZXVS7Z+2RIKDSao1ERNnAXFvhxpiWY/xws4I/jP99Yi1WZvJTX/oV5bRN45UVNv9ra1Dcv7D0M+TkP7cbivMqFa0yYpUcfMhVqT5KjjlUfpfYf22w9JxtR9h76dNhafhTD17WmYBke05aNHL0bi3ne+Ijl+irTm08Te+l6ApbuwNYD9Ym+90KoW2KPKhmS8poS1/SjJH7ztfee33495gAzRveq6Ybhp5SKdl+pEYiZ2nHxZM98srnvVRXZG9777ElrdKleu+PjrW+dw3zVlX0LryGZ5S5kfz+zExvmDBsRbts+xJX2qgb93sZkTu18RgL937zt4hnEKgB3H59ogbWIn9pcIC2n0hWm/hlv7C34Jtjbu/wr/wt3Ylm0VytazbGBsr7048UKkx71gbQOs74nyKRR5eUFTo4XlxTj7rVqXb8fu9eYuvjZ28deiYxE3DreKbaLhm19Vev5ExzcN5S9EkH+JkHQpSfGcBkONfyGNf8GN7zPqundN1ZYEkTydf/7jpOk/fq4Moq0T33c6//zHF8Oytj+dx/F1YWXiNEXy7daOE2OblGpSsjN7273q2pkd4JxQnEHb/fmqC404+WWbBt3737r4E9v6xUAEzVAM84Viv1A3S5q57327Z2+uGfq2d9u7oxm9e9UFoR8hNJ9vf3dP3Vyzt3f0HXvH3aD2kWvEiCsXJca7v191CaX8ginFw5mE3XO0UnB3TS3dnxtpaRe4ab1LCDPRu2xrfJLNG2dhfzSXt+f1N5P/ZzE5cufiI8Z2PCNI4p2x9b8mXpB/abU5Z/3HbsuKBs1f/93Uksvnp0fpp//6f0G6hZ0vsdX5B4L7Pzr/9VtpRi/kwe9fk3BjB53/7qx/7XzZdq7xz/+v8z2lKOa2+P9Gp0Vno873bj9N3HDrHTAy7ju8bWztbee/fsPj/v69e3agr5kBPeSydf6742ztCNNCOUDT6D82+RfyoGE1EFv/e/c9oVX5cT9caBEhdEdT3C13e/sRoUUz1yxHsTe33L9JaJWeHGa/X+Fua35ScB0dM/3Rwut0fn8LsP8sAYa4/0h+oUf4z/gr+vP+2/Utd2qJ/CEBVsiaYzliRN5XhKxSgmSE8M4JkXfEyH+IIKGZ61v6hmK/9e6+fUiQ3F7fUvTtHcOx7F8hSH6+6hYwxYdjqpBj975bwKP7+/8KAAD//6YAbxPvqAAA
\ No newline at end of file
diff --git a/internal/release/testdata/prom-stack-1 b/internal/release/testdata/prom-stack-1
new file mode 100644
index 000000000..ea3a7899a
--- /dev/null
+++ b/internal/release/testdata/prom-stack-1
@@ -0,0 +1 @@
+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
\ No newline at end of file
diff --git a/internal/release/util.go b/internal/release/util.go
new file mode 100644
index 000000000..8a396cd16
--- /dev/null
+++ b/internal/release/util.go
@@ -0,0 +1,45 @@
+/*
+Copyright 2022 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package release
+
+import (
+ helmrelease "helm.sh/helm/v4/pkg/release/v1"
+)
+
+// GetTestHooks returns the list of test hooks for the given release, indexed
+// by hook name.
+func GetTestHooks(rls *helmrelease.Release) map[string]*helmrelease.Hook {
+ th := make(map[string]*helmrelease.Hook)
+ for _, h := range rls.Hooks {
+ if IsHookForEvent(h, helmrelease.HookTest) {
+ th[h.Name] = h
+ }
+ }
+ return th
+}
+
+// IsHookForEvent returns if the given hook fires on the provided event.
+func IsHookForEvent(hook *helmrelease.Hook, event helmrelease.HookEvent) bool {
+ if hook != nil {
+ for _, e := range hook.Events {
+ if e == event {
+ return true
+ }
+ }
+ }
+ return false
+}
diff --git a/internal/release/util_test.go b/internal/release/util_test.go
new file mode 100644
index 000000000..2083e6ad9
--- /dev/null
+++ b/internal/release/util_test.go
@@ -0,0 +1,79 @@
+/*
+Copyright 2022 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package release
+
+import (
+ "testing"
+
+ . "github.com/onsi/gomega"
+ helmrelease "helm.sh/helm/v4/pkg/release/v1"
+
+ "github.com/fluxcd/helm-controller/internal/testutil"
+)
+
+func TestGetTestHooks(t *testing.T) {
+ g := NewWithT(t)
+
+ hooks := []*helmrelease.Hook{
+ {
+ Name: "pre-install",
+ Events: []helmrelease.HookEvent{
+ helmrelease.HookPreInstall,
+ },
+ },
+ {
+ Name: "test",
+ Events: []helmrelease.HookEvent{
+ helmrelease.HookTest,
+ },
+ },
+ {
+ Name: "post-install",
+ Events: []helmrelease.HookEvent{
+ helmrelease.HookPostInstall,
+ },
+ },
+ {
+ Name: "combined-test-hook",
+ Events: []helmrelease.HookEvent{
+ helmrelease.HookPostRollback,
+ helmrelease.HookTest,
+ },
+ },
+ }
+
+ g.Expect(GetTestHooks(&helmrelease.Release{
+ Hooks: hooks,
+ })).To(testutil.Equal(map[string]*helmrelease.Hook{
+ hooks[1].Name: hooks[1],
+ hooks[3].Name: hooks[3],
+ }))
+}
+
+func TestIsHookForEvent(t *testing.T) {
+ g := NewWithT(t)
+
+ hook := &helmrelease.Hook{
+ Events: []helmrelease.HookEvent{
+ helmrelease.HookPreInstall,
+ helmrelease.HookPostInstall,
+ },
+ }
+ g.Expect(IsHookForEvent(hook, helmrelease.HookPreInstall)).To(BeTrue())
+ g.Expect(IsHookForEvent(hook, helmrelease.HookPostInstall)).To(BeTrue())
+ g.Expect(IsHookForEvent(hook, helmrelease.HookTest)).To(BeFalse())
+}
diff --git a/internal/runner/log_buffer.go b/internal/runner/log_buffer.go
deleted file mode 100644
index 3241c013b..000000000
--- a/internal/runner/log_buffer.go
+++ /dev/null
@@ -1,85 +0,0 @@
-/*
-Copyright 2021 The Flux authors
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package runner
-
-import (
- "container/ring"
- "fmt"
- "strings"
- "sync"
-
- "github.com/go-logr/logr"
- "helm.sh/helm/v3/pkg/action"
-)
-
-const defaultBufferSize = 5
-
-func NewDebugLog(log logr.Logger) action.DebugLog {
- return func(format string, v ...interface{}) {
- log.Info(fmt.Sprintf(format, v...))
- }
-}
-
-type LogBuffer struct {
- mu sync.Mutex
- log action.DebugLog
- buffer *ring.Ring
-}
-
-func NewLogBuffer(log action.DebugLog, size int) *LogBuffer {
- if size <= 0 {
- size = defaultBufferSize
- }
- return &LogBuffer{
- log: log,
- buffer: ring.New(size),
- }
-}
-
-func (l *LogBuffer) Log(format string, v ...interface{}) {
- l.mu.Lock()
-
- // Filter out duplicate log lines, this happens for example when
- // Helm is waiting on workloads to become ready.
- msg := fmt.Sprintf(format, v...)
- if prev := l.buffer.Prev(); prev.Value != msg {
- l.buffer.Value = msg
- l.buffer = l.buffer.Next()
- }
-
- l.mu.Unlock()
- l.log(format, v...)
-}
-
-func (l *LogBuffer) Reset() {
- l.mu.Lock()
- l.buffer = ring.New(l.buffer.Len())
- l.mu.Unlock()
-}
-
-func (l *LogBuffer) String() string {
- var str string
- l.mu.Lock()
- l.buffer.Do(func(s interface{}) {
- if s == nil {
- return
- }
- str += s.(string) + "\n"
- })
- l.mu.Unlock()
- return strings.TrimSpace(str)
-}
diff --git a/internal/runner/log_buffer_test.go b/internal/runner/log_buffer_test.go
deleted file mode 100644
index 62035426e..000000000
--- a/internal/runner/log_buffer_test.go
+++ /dev/null
@@ -1,103 +0,0 @@
-/*
-Copyright 2021 The Flux authors
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package runner
-
-import (
- "testing"
-
- "github.com/go-logr/logr"
-)
-
-func TestLogBuffer_Log(t *testing.T) {
- tests := []struct {
- name string
- size int
- fill []string
- wantCount int
- want string
- }{
- {name: "log", size: 2, fill: []string{"a", "b", "c"}, wantCount: 3, want: "b\nc"},
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- var count int
- l := NewLogBuffer(func(format string, v ...interface{}) {
- count++
- return
- }, tt.size)
- for _, v := range tt.fill {
- l.Log("%s", v)
- }
- if count != tt.wantCount {
- t.Errorf("Inner Log() called %v times, want %v", count, tt.wantCount)
- }
- if got := l.String(); got != tt.want {
- t.Errorf("String() = %v, want %v", got, tt.want)
- }
- })
- }
-}
-
-func TestLogBuffer_Reset(t *testing.T) {
- bufferSize := 10
- l := NewLogBuffer(NewDebugLog(logr.Discard()), bufferSize)
-
- if got := l.buffer.Len(); got != bufferSize {
- t.Errorf("Len() = %v, want %v", got, bufferSize)
- }
-
- for _, v := range []string{"a", "b", "c"} {
- l.Log("%s", v)
- }
-
- if got := l.String(); got == "" {
- t.Errorf("String() = empty")
- }
-
- l.Reset()
-
- if got := l.buffer.Len(); got != bufferSize {
- t.Errorf("Len() = %v after Reset(), want %v", got, bufferSize)
- }
- if got := l.String(); got != "" {
- t.Errorf("String() != empty after Reset()")
- }
-}
-
-func TestLogBuffer_String(t *testing.T) {
- tests := []struct {
- name string
- size int
- fill []string
- want string
- }{
- {name: "empty buffer", fill: []string{}, want: ""},
- {name: "filled buffer", size: 2, fill: []string{"a", "b", "c"}, want: "b\nc"},
- {name: "duplicate buffer items", fill: []string{"b", "b", "b"}, want: "b"},
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- l := NewLogBuffer(NewDebugLog(logr.Discard()), tt.size)
- for _, v := range tt.fill {
- l.Log("%s", v)
- }
- if got := l.String(); got != tt.want {
- t.Errorf("String() = %v, want %v", got, tt.want)
- }
- })
- }
-}
diff --git a/internal/runner/post_renderer_origin_labels_test.go b/internal/runner/post_renderer_origin_labels_test.go
deleted file mode 100644
index 14a03c23a..000000000
--- a/internal/runner/post_renderer_origin_labels_test.go
+++ /dev/null
@@ -1,83 +0,0 @@
-/*
-Copyright 2021 The Flux authors
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package runner
-
-import (
- "bytes"
- "reflect"
- "testing"
-)
-
-const mixedResourceMock = `apiVersion: v1
-kind: Pod
-metadata:
- name: pod-without-labels
----
-apiVersion: v1
-kind: Service
-metadata:
- name: service-with-labels
- labels:
- existing: label
-`
-
-func Test_postRendererOriginLabels_Run(t *testing.T) {
- tests := []struct {
- name string
- renderedManifests string
- expectManifests string
- expectErr bool
- }{
- {
- name: "labels",
- renderedManifests: mixedResourceMock,
- expectManifests: `apiVersion: v1
-kind: Pod
-metadata:
- labels:
- helm.toolkit.fluxcd.io/name: name
- helm.toolkit.fluxcd.io/namespace: namespace
- name: pod-without-labels
----
-apiVersion: v1
-kind: Service
-metadata:
- labels:
- existing: label
- helm.toolkit.fluxcd.io/name: name
- helm.toolkit.fluxcd.io/namespace: namespace
- name: service-with-labels
-`,
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- k := &postRendererOriginLabels{
- name: "name",
- namespace: "namespace",
- }
- gotModifiedManifests, err := k.Run(bytes.NewBufferString(tt.renderedManifests))
- if (err != nil) != tt.expectErr {
- t.Errorf("Run() error = %v, expectErr %v", err, tt.expectErr)
- return
- }
- if !reflect.DeepEqual(gotModifiedManifests, bytes.NewBufferString(tt.expectManifests)) {
- t.Errorf("Run() gotModifiedManifests = %v, want %v", gotModifiedManifests, tt.expectManifests)
- }
- })
- }
-}
diff --git a/internal/runner/runner.go b/internal/runner/runner.go
deleted file mode 100644
index 0488054f1..000000000
--- a/internal/runner/runner.go
+++ /dev/null
@@ -1,479 +0,0 @@
-/*
-Copyright 2021 The Flux authors
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package runner
-
-import (
- "bytes"
- "context"
- "errors"
- "fmt"
- "sync"
- "time"
-
- "github.com/go-logr/logr"
- "helm.sh/helm/v3/pkg/action"
- "helm.sh/helm/v3/pkg/chart"
- "helm.sh/helm/v3/pkg/chartutil"
- "helm.sh/helm/v3/pkg/kube"
- "helm.sh/helm/v3/pkg/postrender"
- "helm.sh/helm/v3/pkg/release"
- "helm.sh/helm/v3/pkg/storage/driver"
- apiextension "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
- "k8s.io/cli-runtime/pkg/genericclioptions"
- "k8s.io/cli-runtime/pkg/resource"
-
- apierrors "k8s.io/apimachinery/pkg/api/errors"
- "k8s.io/apimachinery/pkg/api/meta"
- metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
- "k8s.io/apimachinery/pkg/runtime"
- "k8s.io/apimachinery/pkg/runtime/schema"
-
- runtimelogger "github.com/fluxcd/pkg/runtime/logger"
-
- v2 "github.com/fluxcd/helm-controller/api/v2beta1"
- "github.com/fluxcd/helm-controller/internal/features"
-)
-
-var accessor = meta.NewAccessor()
-
-type ActionError struct {
- Err error
- CapturedLogs string
-}
-
-func (e ActionError) Error() string {
- return e.Err.Error()
-}
-
-func (e ActionError) Unwrap() error {
- return e.Err
-}
-
-// Runner represents a Helm action runner capable of performing Helm
-// operations for a v2beta1.HelmRelease.
-type Runner struct {
- mu sync.Mutex
- config *action.Configuration
- logBuffer *LogBuffer
-}
-
-// NewRunner constructs a new Runner configured to run Helm actions with the
-// given genericclioptions.RESTClientGetter, and the release and storage
-// namespace configured to the provided values.
-func NewRunner(getter genericclioptions.RESTClientGetter, storageNamespace string, logger logr.Logger) (*Runner, error) {
- runner := &Runner{
- logBuffer: NewLogBuffer(NewDebugLog(logger.V(runtimelogger.DebugLevel)), defaultBufferSize),
- }
-
- // Default to the trace level logger for the Helm action configuration,
- // to ensure storage logs are captured.
- cfg := new(action.Configuration)
- if err := cfg.Init(getter, storageNamespace, "secret", NewDebugLog(logger.V(runtimelogger.TraceLevel))); err != nil {
- return nil, err
- }
-
- // Override the logger used by the Helm actions and Kube client with the log buffer,
- // which provides useful information in the event of an error.
- cfg.Log = runner.logBuffer.Log
- if kc, ok := cfg.KubeClient.(*kube.Client); ok {
- kc.Log = runner.logBuffer.Log
- }
- runner.config = cfg
-
- return runner, nil
-}
-
-// Create post renderer instances from HelmRelease and combine them into
-// a single combined post renderer.
-func postRenderers(hr v2.HelmRelease) (postrender.PostRenderer, error) {
- var combinedRenderer = newCombinedPostRenderer()
- for _, r := range hr.Spec.PostRenderers {
- if r.Kustomize != nil {
- combinedRenderer.addRenderer(newPostRendererKustomize(r.Kustomize))
- }
- }
- combinedRenderer.addRenderer(newPostRendererOriginLabels(&hr))
- if len(combinedRenderer.renderers) == 0 {
- return nil, nil
- }
- return &combinedRenderer, nil
-}
-
-// Install runs a Helm install action for the given v2beta1.HelmRelease.
-func (r *Runner) Install(ctx context.Context, hr v2.HelmRelease, chart *chart.Chart, values chartutil.Values) (*release.Release, error) {
- r.mu.Lock()
- defer r.mu.Unlock()
- defer r.logBuffer.Reset()
-
- install := action.NewInstall(r.config)
- install.ReleaseName = hr.GetReleaseName()
- install.Namespace = hr.GetReleaseNamespace()
- install.Timeout = hr.Spec.GetInstall().GetTimeout(hr.GetTimeout()).Duration
- install.Wait = !hr.Spec.GetInstall().DisableWait
- install.WaitForJobs = !hr.Spec.GetInstall().DisableWaitForJobs
- install.DisableHooks = hr.Spec.GetInstall().DisableHooks
- install.DisableOpenAPIValidation = hr.Spec.GetInstall().DisableOpenAPIValidation
- install.Replace = hr.Spec.GetInstall().Replace
- install.SkipCRDs = true
- install.Devel = true
-
- if hr.Spec.TargetNamespace != "" {
- install.CreateNamespace = hr.Spec.GetInstall().CreateNamespace
- }
-
- // If user opted-in to allow DNS lookups, enable it.
- if allowDNS, _ := features.Enabled(features.AllowDNSLookups); allowDNS {
- install.EnableDNS = allowDNS
- }
-
- renderer, err := postRenderers(hr)
- if err != nil {
- return nil, wrapActionErr(r.logBuffer, err)
- }
- install.PostRenderer = renderer
-
- // If user opted-in to install (or replace) CRDs, install them first.
- var legacyCRDsPolicy = v2.Create
- if hr.Spec.GetInstall().SkipCRDs {
- legacyCRDsPolicy = v2.Skip
- }
- cRDsPolicy, err := r.validateCRDsPolicy(hr.Spec.GetInstall().CRDs, legacyCRDsPolicy)
- if err != nil {
- return nil, wrapActionErr(r.logBuffer, err)
- }
- if cRDsPolicy != v2.Skip && len(chart.CRDObjects()) > 0 {
- if err := r.applyCRDs(cRDsPolicy, chart, setOriginVisitor(hr.Namespace, hr.Name)); err != nil {
- return nil, wrapActionErr(r.logBuffer, err)
- }
- }
-
- rel, err := install.RunWithContext(ctx, chart, values.AsMap())
- return rel, wrapActionErr(r.logBuffer, err)
-}
-
-// Upgrade runs an Helm upgrade action for the given v2beta1.HelmRelease.
-func (r *Runner) Upgrade(ctx context.Context, hr v2.HelmRelease, chart *chart.Chart, values chartutil.Values) (*release.Release, error) {
- r.mu.Lock()
- defer r.mu.Unlock()
- defer r.logBuffer.Reset()
-
- upgrade := action.NewUpgrade(r.config)
- upgrade.Namespace = hr.GetReleaseNamespace()
- upgrade.ResetValues = !hr.Spec.GetUpgrade().PreserveValues
- upgrade.ReuseValues = hr.Spec.GetUpgrade().PreserveValues
- upgrade.MaxHistory = hr.GetMaxHistory()
- upgrade.Timeout = hr.Spec.GetUpgrade().GetTimeout(hr.GetTimeout()).Duration
- upgrade.Wait = !hr.Spec.GetUpgrade().DisableWait
- upgrade.WaitForJobs = !hr.Spec.GetUpgrade().DisableWaitForJobs
- upgrade.DisableHooks = hr.Spec.GetUpgrade().DisableHooks
- upgrade.DisableOpenAPIValidation = hr.Spec.GetUpgrade().DisableOpenAPIValidation
- upgrade.Force = hr.Spec.GetUpgrade().Force
- upgrade.CleanupOnFail = hr.Spec.GetUpgrade().CleanupOnFail
- upgrade.Devel = true
-
- // If user opted-in to allow DNS lookups, enable it.
- if allowDNS, _ := features.Enabled(features.AllowDNSLookups); allowDNS {
- upgrade.EnableDNS = allowDNS
- }
-
- renderer, err := postRenderers(hr)
- if err != nil {
- return nil, wrapActionErr(r.logBuffer, err)
- }
- upgrade.PostRenderer = renderer
-
- // If user opted-in to upgrade CRDs, upgrade them first.
- cRDsPolicy, err := r.validateCRDsPolicy(hr.Spec.GetUpgrade().CRDs, v2.Skip)
- if err != nil {
- return nil, wrapActionErr(r.logBuffer, err)
- }
- if cRDsPolicy != v2.Skip && len(chart.CRDObjects()) > 0 {
- if err := r.applyCRDs(cRDsPolicy, chart, setOriginVisitor(hr.Namespace, hr.Name)); err != nil {
- return nil, wrapActionErr(r.logBuffer, err)
- }
- }
-
- rel, err := upgrade.RunWithContext(ctx, hr.GetReleaseName(), chart, values.AsMap())
- return rel, wrapActionErr(r.logBuffer, err)
-}
-
-func (r *Runner) validateCRDsPolicy(policy v2.CRDsPolicy, defaultValue v2.CRDsPolicy) (v2.CRDsPolicy, error) {
- switch policy {
- case "":
- return defaultValue, nil
- case v2.Skip:
- break
- case v2.Create:
- break
- case v2.CreateReplace:
- break
- default:
- return policy, fmt.Errorf("invalid CRD policy '%s' defined in field CRDsPolicy, valid values are '%s', '%s' or '%s'",
- policy, v2.Skip, v2.Create, v2.CreateReplace,
- )
- }
- return policy, nil
-}
-
-type rootScoped struct{}
-
-func (*rootScoped) Name() meta.RESTScopeName {
- return meta.RESTScopeNameRoot
-}
-
-// This has been adapted from https://github.com/helm/helm/blob/v3.5.4/pkg/action/install.go#L127
-func (r *Runner) applyCRDs(policy v2.CRDsPolicy, chart *chart.Chart, visitorFunc ...resource.VisitorFunc) error {
- r.config.Log("apply CRDs with policy %s", policy)
-
- // Collect all CRDs from all files in `crds` directory.
- allCrds := make(kube.ResourceList, 0)
- for _, obj := range chart.CRDObjects() {
- // Read in the resources
- res, err := r.config.KubeClient.Build(bytes.NewBuffer(obj.File.Data), false)
- if err != nil {
- r.config.Log("failed to parse CRDs from %s: %s", obj.Name, err)
- return errors.New(fmt.Sprintf("failed to parse CRDs from %s: %s", obj.Name, err))
- }
- allCrds = append(allCrds, res...)
- }
-
- // Visit CRDs with any provided visitor functions.
- for _, visitor := range visitorFunc {
- if err := allCrds.Visit(visitor); err != nil {
- return err
- }
- }
-
- var totalItems []*resource.Info
- switch policy {
- case v2.Skip:
- break
- case v2.Create:
- for i := range allCrds {
- if rr, err := r.config.KubeClient.Create(allCrds[i : i+1]); err != nil {
- crdName := allCrds[i].Name
- // If the error is CRD already exists, continue.
- if apierrors.IsAlreadyExists(err) {
- r.config.Log("CRD %s is already present. Skipping.", crdName)
- if rr != nil && rr.Created != nil {
- totalItems = append(totalItems, rr.Created...)
- }
- continue
- }
- r.config.Log("failed to create CRD %s: %s", crdName, err)
- return errors.New(fmt.Sprintf("failed to create CRD %s: %s", crdName, err))
- } else {
- if rr != nil && rr.Created != nil {
- totalItems = append(totalItems, rr.Created...)
- }
- }
- }
- case v2.CreateReplace:
- config, err := r.config.RESTClientGetter.ToRESTConfig()
- if err != nil {
- r.config.Log("Error while creating Kubernetes client config: %s", err)
- return err
- }
- clientset, err := apiextension.NewForConfig(config)
- if err != nil {
- r.config.Log("Error while creating Kubernetes clientset for apiextension: %s", err)
- return err
- }
- client := clientset.ApiextensionsV1().CustomResourceDefinitions()
- original := make(kube.ResourceList, 0)
- // Note, we build the originals from the current set of CRDs
- // and therefore this upgrade will never delete CRDs that existed in the former release
- // but no longer exist in the current release.
- for _, res := range allCrds {
- if o, err := client.Get(context.TODO(), res.Name, metav1.GetOptions{}); err == nil && o != nil {
- o.GetResourceVersion()
- original = append(original, &resource.Info{
- Client: clientset.ApiextensionsV1().RESTClient(),
- Mapping: &meta.RESTMapping{
- Resource: schema.GroupVersionResource{
- Group: "apiextensions.k8s.io",
- Version: res.Mapping.GroupVersionKind.Version,
- Resource: "customresourcedefinition",
- },
- GroupVersionKind: schema.GroupVersionKind{
- Kind: "CustomResourceDefinition",
- Group: "apiextensions.k8s.io",
- Version: res.Mapping.GroupVersionKind.Version,
- },
- Scope: &rootScoped{},
- },
- Namespace: o.ObjectMeta.Namespace,
- Name: o.ObjectMeta.Name,
- Object: o,
- ResourceVersion: o.ObjectMeta.ResourceVersion,
- })
- } else if !apierrors.IsNotFound(err) {
- r.config.Log("failed to get CRD %s: %s", res.Name, err)
- return err
- }
- }
- // Send them to Kube
- if rr, err := r.config.KubeClient.Update(original, allCrds, true); err != nil {
- r.config.Log("failed to apply CRD %s", err)
- return errors.New(fmt.Sprintf("failed to apply CRD %s", err))
- } else {
- if rr != nil {
- if rr.Created != nil {
- totalItems = append(totalItems, rr.Created...)
- }
- if rr.Updated != nil {
- totalItems = append(totalItems, rr.Updated...)
- }
- if rr.Deleted != nil {
- totalItems = append(totalItems, rr.Deleted...)
- }
- }
- }
- }
-
- if len(totalItems) > 0 {
- // Give time for the CRD to be recognized.
- if err := r.config.KubeClient.Wait(totalItems, 60*time.Second); err != nil {
- r.config.Log("Error waiting for items: %s", err)
- return err
- }
-
- // Clear the RESTMapper cache, since it will not have the new CRDs.
- // Further invalidation of the client is done at a later stage by Helm
- // when it gathers the server capabilities.
- if m, err := r.config.RESTClientGetter.ToRESTMapper(); err == nil {
- if rm, ok := m.(meta.ResettableRESTMapper); ok {
- r.config.Log("Clearing REST mapper cache")
- rm.Reset()
- }
- }
- }
- return nil
-}
-
-// Test runs an Helm test action for the given v2beta1.HelmRelease.
-func (r *Runner) Test(hr v2.HelmRelease) (*release.Release, error) {
- r.mu.Lock()
- defer r.mu.Unlock()
- defer r.logBuffer.Reset()
-
- test := action.NewReleaseTesting(r.config)
- test.Namespace = hr.GetReleaseNamespace()
- test.Timeout = hr.Spec.GetTest().GetTimeout(hr.GetTimeout()).Duration
-
- rel, err := test.Run(hr.GetReleaseName())
- return rel, wrapActionErr(r.logBuffer, err)
-}
-
-// Rollback runs an Helm rollback action for the given v2beta1.HelmRelease.
-func (r *Runner) Rollback(hr v2.HelmRelease) error {
- r.mu.Lock()
- defer r.mu.Unlock()
- defer r.logBuffer.Reset()
-
- rollback := action.NewRollback(r.config)
- rollback.Timeout = hr.Spec.GetRollback().GetTimeout(hr.GetTimeout()).Duration
- rollback.Wait = !hr.Spec.GetRollback().DisableWait
- rollback.WaitForJobs = !hr.Spec.GetRollback().DisableWaitForJobs
- rollback.DisableHooks = hr.Spec.GetRollback().DisableHooks
- rollback.Force = hr.Spec.GetRollback().Force
- rollback.Recreate = hr.Spec.GetRollback().Recreate
- rollback.CleanupOnFail = hr.Spec.GetRollback().CleanupOnFail
-
- err := rollback.Run(hr.GetReleaseName())
- return wrapActionErr(r.logBuffer, err)
-}
-
-// Uninstall runs an Helm uninstall action for the given v2beta1.HelmRelease.
-func (r *Runner) Uninstall(hr v2.HelmRelease) error {
- r.mu.Lock()
- defer r.mu.Unlock()
- defer r.logBuffer.Reset()
-
- uninstall := action.NewUninstall(r.config)
- uninstall.Timeout = hr.Spec.GetUninstall().GetTimeout(hr.GetTimeout()).Duration
- uninstall.DisableHooks = hr.Spec.GetUninstall().DisableHooks
- uninstall.KeepHistory = hr.Spec.GetUninstall().KeepHistory
- uninstall.DeletionPropagation = hr.Spec.GetUninstall().GetDeletionPropagation()
- uninstall.Wait = !hr.Spec.GetUninstall().DisableWait
-
- _, err := uninstall.Run(hr.GetReleaseName())
- return wrapActionErr(r.logBuffer, err)
-}
-
-// ObserveLastRelease observes the last revision, if there is one,
-// for the actual Helm release associated with the given v2beta1.HelmRelease.
-func (r *Runner) ObserveLastRelease(hr v2.HelmRelease) (*release.Release, error) {
- rel, err := r.config.Releases.Last(hr.GetReleaseName())
- if err != nil && errors.Is(err, driver.ErrReleaseNotFound) {
- err = nil
- }
- return rel, err
-}
-
-func wrapActionErr(log *LogBuffer, err error) error {
- if err == nil {
- return err
- }
- err = &ActionError{
- Err: err,
- CapturedLogs: log.String(),
- }
- return err
-}
-
-func setOriginVisitor(namespace, name string) resource.VisitorFunc {
- return func(info *resource.Info, err error) error {
- if err != nil {
- return err
- }
- if err = mergeLabels(info.Object, originLabels(namespace, name)); err != nil {
- return fmt.Errorf(
- "%s origin labels could not be updated: %s",
- resourceString(info), err,
- )
- }
- return nil
- }
-}
-
-func mergeLabels(obj runtime.Object, labels map[string]string) error {
- current, err := accessor.Labels(obj)
- if err != nil {
- return err
- }
- return accessor.SetLabels(obj, mergeStrStrMaps(current, labels))
-}
-
-func resourceString(info *resource.Info) string {
- _, k := info.Mapping.GroupVersionKind.ToAPIVersionAndKind()
- return fmt.Sprintf(
- "%s %q in namespace %q",
- k, info.Name, info.Namespace,
- )
-}
-
-func mergeStrStrMaps(current, desired map[string]string) map[string]string {
- result := make(map[string]string)
- for k, v := range current {
- result[k] = v
- }
- for k, desiredVal := range desired {
- result[k] = desiredVal
- }
- return result
-}
diff --git a/internal/storage/failing.go b/internal/storage/failing.go
new file mode 100644
index 000000000..c6454a74e
--- /dev/null
+++ b/internal/storage/failing.go
@@ -0,0 +1,104 @@
+/*
+Copyright 2022 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package storage
+
+import (
+ "helm.sh/helm/v4/pkg/release"
+ "helm.sh/helm/v4/pkg/storage/driver"
+)
+
+const (
+ // FailingDriverName is the name of the failing driver.
+ FailingDriverName = "failing"
+)
+
+// Failing is a failing Helm storage driver that returns the configured errors.
+type Failing struct {
+ driver.Driver
+
+ // GetErr is returned by Get if configured. If not set, the embedded driver
+ // result is returned.
+ GetErr error
+ // ListErr is returned by List if configured. If not set, the embedded
+ // driver result is returned.
+ ListErr error
+ // QueryErr is returned by Query if configured. If not set, the embedded
+ // driver result is returned.
+ QueryErr error
+ // CreateErr is returned by Create if configured. If not set, the embedded
+ // driver result is returned.
+ CreateErr error
+ // UpdateErr is returned by Update if configured. If not set, the embedded
+ // driver result is returned.
+ UpdateErr error
+ // DeleteErr is returned by Delete if configured. If not set, the embedded
+ // driver result is returned.
+ DeleteErr error
+}
+
+// Name returns the name of the driver.
+func (o *Failing) Name() string {
+ return FailingDriverName
+}
+
+// Get returns GetErr, or the embedded driver result.
+func (o *Failing) Get(key string) (release.Releaser, error) {
+ if o.GetErr != nil {
+ return nil, o.GetErr
+ }
+ return o.Driver.Get(key)
+}
+
+// List returns ListErr, or the embedded driver result.
+func (o *Failing) List(filter func(release.Releaser) bool) ([]release.Releaser, error) {
+ if o.ListErr != nil {
+ return nil, o.ListErr
+ }
+ return o.Driver.List(filter)
+}
+
+// Query returns QueryErr, or the embedded driver result.
+func (o *Failing) Query(keyvals map[string]string) ([]release.Releaser, error) {
+ if o.QueryErr != nil {
+ return nil, o.QueryErr
+ }
+ return o.Driver.Query(keyvals)
+}
+
+// Create returns CreateErr, or the embedded driver result.
+func (o *Failing) Create(key string, rls release.Releaser) error {
+ if o.CreateErr != nil {
+ return o.CreateErr
+ }
+ return o.Driver.Create(key, rls)
+}
+
+// Update returns UpdateErr, or the embedded driver result.
+func (o *Failing) Update(key string, rls release.Releaser) error {
+ if o.UpdateErr != nil {
+ return o.UpdateErr
+ }
+ return o.Driver.Update(key, rls)
+}
+
+// Delete returns DeleteErr, or the embedded driver result.
+func (o *Failing) Delete(key string) (release.Releaser, error) {
+ if o.DeleteErr != nil {
+ return nil, o.DeleteErr
+ }
+ return o.Driver.Delete(key)
+}
diff --git a/internal/storage/observer.go b/internal/storage/observer.go
new file mode 100644
index 000000000..95de0196f
--- /dev/null
+++ b/internal/storage/observer.go
@@ -0,0 +1,115 @@
+/*
+Copyright 2022 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package storage
+
+import (
+ helmrelease "helm.sh/helm/v4/pkg/release"
+ helmdriver "helm.sh/helm/v4/pkg/storage/driver"
+)
+
+// ObserverDriverName contains the string representation of Observer.
+const ObserverDriverName = "observer"
+
+// Observer is an observing Helm storage driver.
+//
+// It can be configured with a list of ObserveFunc functions that are called
+// after a successful persistence operation to the underlying driver.
+//
+// This allows for observations on persisted state as performed by the driver,
+// and works around the inconsistent behavior of some Helm actions that may
+// return an object that was not actually persisted to the Helm storage
+// (e.g. because a validation error occurred during a Helm upgrade).
+type Observer struct {
+ // driver holds the underlying driver.Driver implementation which is used
+ // to persist data to, and retrieve from.
+ driver helmdriver.Driver
+ // observers holds a slice of ObserveFunc which are called after a
+ // successful persistence of a release to storage driver.
+ observers []ObserveFunc
+}
+
+// ObserveFunc observes a release which has been successfully persisted to
+// storage.
+// NOTE: while it takes a pointer, the caller is expected to perform a
+// read-only operation.
+type ObserveFunc func(rel helmrelease.Releaser)
+
+// NewObserver creates a new Observer for the given Helm storage driver.
+func NewObserver(driver helmdriver.Driver, observers ...ObserveFunc) *Observer {
+ return &Observer{
+ driver: driver,
+ observers: observers,
+ }
+}
+
+// Name returns the name of the driver.
+func (o *Observer) Name() string {
+ return ObserverDriverName
+}
+
+// Get returns the release named by key or returns ErrReleaseNotFound.
+func (o *Observer) Get(key string) (helmrelease.Releaser, error) {
+ return o.driver.Get(key)
+}
+
+// List returns the list of all releases such that filter(release) == true.
+func (o *Observer) List(filter func(helmrelease.Releaser) bool) ([]helmrelease.Releaser, error) {
+ return o.driver.List(filter)
+}
+
+// Query returns the set of releases that match the provided set of labels.
+func (o *Observer) Query(keyvals map[string]string) ([]helmrelease.Releaser, error) {
+ return o.driver.Query(keyvals)
+}
+
+// Create creates a new release or returns driver.ErrReleaseExists.
+// It observes the release as provided after a successful creation.
+func (o *Observer) Create(key string, rls helmrelease.Releaser) error {
+ if err := o.driver.Create(key, rls); err != nil {
+ return err
+ }
+ for _, obs := range o.observers {
+ obs(rls)
+ }
+ return nil
+}
+
+// Update updates a release or returns driver.ErrReleaseNotFound.
+// After a successful update, it observes the release as provided.
+func (o *Observer) Update(key string, rls helmrelease.Releaser) error {
+ if err := o.driver.Update(key, rls); err != nil {
+ return err
+ }
+ for _, obs := range o.observers {
+ obs(rls)
+ }
+ return nil
+}
+
+// Delete deletes a release or returns driver.ErrReleaseNotFound.
+// After a successful deletion, it observes the release as returned by the
+// embedded driver.Deletor.
+func (o *Observer) Delete(key string) (helmrelease.Releaser, error) {
+ rls, err := o.driver.Delete(key)
+ if err != nil {
+ return nil, err
+ }
+ for _, obs := range o.observers {
+ obs(rls)
+ }
+ return rls, nil
+}
diff --git a/internal/storage/observer_test.go b/internal/storage/observer_test.go
new file mode 100644
index 000000000..5a42eb35e
--- /dev/null
+++ b/internal/storage/observer_test.go
@@ -0,0 +1,226 @@
+/*
+Copyright 2022 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package storage_test
+
+import (
+ "fmt"
+ "testing"
+
+ . "github.com/onsi/gomega"
+ helmrelease "helm.sh/helm/v4/pkg/release"
+ helmreleasecommon "helm.sh/helm/v4/pkg/release/common"
+ helmreleasev1 "helm.sh/helm/v4/pkg/release/v1"
+ helmdriver "helm.sh/helm/v4/pkg/storage/driver"
+
+ "github.com/fluxcd/helm-controller/internal/storage"
+)
+
+func TestObserver_Name(t *testing.T) {
+ g := NewWithT(t)
+
+ o := storage.NewObserver(helmdriver.NewMemory())
+ g.Expect(o.Name()).To(Equal(storage.ObserverDriverName))
+}
+
+func TestObserver_Get(t *testing.T) {
+ t.Run("ignores get", func(t *testing.T) {
+ g := NewWithT(t)
+
+ ms := helmdriver.NewMemory()
+ rel := releaseStub("success", 1, "ns1", helmreleasecommon.StatusDeployed).(*helmreleasev1.Release)
+ key := testKey(rel.Name, rel.Version)
+ g.Expect(ms.Create(key, rel)).To(Succeed())
+
+ var called bool
+ o := storage.NewObserver(ms, func(rls helmrelease.Releaser) {
+ called = true
+ })
+
+ got, err := o.Get(key)
+ g.Expect(err).ToNot(HaveOccurred())
+ g.Expect(got).To(Equal(rel))
+ g.Expect(called).To(BeFalse())
+ })
+}
+
+func TestObserver_List(t *testing.T) {
+ t.Run("ignores list", func(t *testing.T) {
+ g := NewWithT(t)
+
+ ms := helmdriver.NewMemory()
+ rel := releaseStub("success", 1, "ns1", helmreleasecommon.StatusDeployed).(*helmreleasev1.Release)
+ key := testKey(rel.Name, rel.Version)
+ g.Expect(ms.Create(key, rel)).To(Succeed())
+
+ var called bool
+ o := storage.NewObserver(ms, func(rls helmrelease.Releaser) {
+ called = true
+ })
+ got, err := o.List(func(r helmrelease.Releaser) bool {
+ // Include everything
+ return true
+ })
+ g.Expect(err).ToNot(HaveOccurred())
+ g.Expect(got).To(HaveLen(1))
+ g.Expect(got[0]).To(Equal(rel))
+ g.Expect(called).To(BeFalse())
+ })
+}
+
+func TestObserver_Query(t *testing.T) {
+ t.Run("ignores query", func(t *testing.T) {
+ g := NewWithT(t)
+
+ ms := helmdriver.NewMemory()
+ rel := releaseStub("success", 1, "ns1", helmreleasecommon.StatusDeployed).(*helmreleasev1.Release)
+ key := testKey(rel.Name, rel.Version)
+ g.Expect(ms.Create(key, rel)).To(Succeed())
+
+ var called bool
+ o := storage.NewObserver(ms, func(rls helmrelease.Releaser) {
+ called = true
+ })
+
+ rls, err := o.Query(map[string]string{"status": "deployed"})
+ g.Expect(err).ToNot(HaveOccurred())
+ g.Expect(rls).To(HaveLen(1))
+ g.Expect(rls[0]).To(Equal(rel))
+ g.Expect(called).To(BeFalse())
+ })
+}
+
+func TestObserver_Create(t *testing.T) {
+ t.Run("observes create success", func(t *testing.T) {
+ g := NewWithT(t)
+
+ ms := helmdriver.NewMemory()
+ rel := releaseStub("success", 1, "ns1", helmreleasecommon.StatusDeployed).(*helmreleasev1.Release)
+ key := testKey(rel.Name, rel.Version)
+
+ var called bool
+ o := storage.NewObserver(ms, func(rls helmrelease.Releaser) {
+ called = true
+ })
+
+ g.Expect(o.Create(key, rel)).To(Succeed())
+ g.Expect(called).To(BeTrue())
+ })
+
+ t.Run("ignores create error", func(t *testing.T) {
+ g := NewWithT(t)
+
+ ms := helmdriver.NewMemory()
+
+ rel := releaseStub("error", 1, "ns1", helmreleasecommon.StatusDeployed).(*helmreleasev1.Release)
+ key := testKey(rel.Name, rel.Version)
+ g.Expect(ms.Create(key, rel)).To(Succeed())
+
+ var called bool
+ o := storage.NewObserver(ms, func(rls helmrelease.Releaser) {
+ called = true
+ })
+
+ rel2 := releaseStub("error", 1, "ns1", helmreleasecommon.StatusFailed)
+ g.Expect(o.Create(key, rel2)).To(HaveOccurred())
+ g.Expect(called).To(BeFalse())
+ })
+}
+
+func TestObserver_Update(t *testing.T) {
+ t.Run("observes update success", func(t *testing.T) {
+ g := NewWithT(t)
+
+ ms := helmdriver.NewMemory()
+ rel := releaseStub("success", 1, "ns1", helmreleasecommon.StatusDeployed).(*helmreleasev1.Release)
+ key := testKey(rel.Name, rel.Version)
+ g.Expect(ms.Create(key, rel)).To(Succeed())
+
+ var called bool
+ o := storage.NewObserver(ms, func(rls helmrelease.Releaser) {
+ called = true
+ })
+
+ g.Expect(o.Update(key, rel)).To(Succeed())
+ g.Expect(called).To(BeTrue())
+ })
+
+ t.Run("ignores update error", func(t *testing.T) {
+ g := NewWithT(t)
+
+ var called bool
+ o := storage.NewObserver(helmdriver.NewMemory(), func(rls helmrelease.Releaser) {
+ called = true
+ })
+
+ rel := releaseStub("error", 1, "ns1", helmreleasecommon.StatusDeployed).(*helmreleasev1.Release)
+ key := testKey(rel.Name, rel.Version)
+ g.Expect(o.Update(key, rel)).To(HaveOccurred())
+ g.Expect(called).To(BeFalse())
+ })
+}
+
+func TestObserver_Delete(t *testing.T) {
+ t.Run("observes delete success", func(t *testing.T) {
+ g := NewWithT(t)
+
+ ms := helmdriver.NewMemory()
+ rel := releaseStub("success", 1, "ns1", helmreleasecommon.StatusDeployed).(*helmreleasev1.Release)
+ key := testKey(rel.Name, rel.Version)
+ g.Expect(ms.Create(key, rel)).To(Succeed())
+
+ var called bool
+ o := storage.NewObserver(ms, func(rls helmrelease.Releaser) {
+ called = true
+ })
+
+ got, err := o.Delete(key)
+ g.Expect(err).ToNot(HaveOccurred())
+ g.Expect(got).ToNot(BeNil())
+ g.Expect(called).To(BeTrue())
+
+ _, err = ms.Get(key)
+ g.Expect(err).To(Equal(helmdriver.ErrReleaseNotFound))
+ })
+
+ t.Run("delete release not found", func(t *testing.T) {
+ g := NewWithT(t)
+
+ var called bool
+ o := storage.NewObserver(helmdriver.NewMemory(), func(rls helmrelease.Releaser) {
+ called = true
+ })
+
+ key := testKey("error", 1)
+ got, err := o.Delete(key)
+ g.Expect(err).To(Equal(helmdriver.ErrReleaseNotFound))
+ g.Expect(got).To(BeNil())
+ g.Expect(called).To(BeFalse())
+ })
+}
+
+func releaseStub(name string, version int, namespace string, status helmreleasecommon.Status) helmrelease.Releaser {
+ return &helmreleasev1.Release{
+ Name: name,
+ Version: version,
+ Namespace: namespace,
+ Info: &helmreleasev1.Info{Status: status},
+ }
+}
+
+func testKey(name string, vers int) string {
+ return fmt.Sprintf("%s.v%d", name, vers)
+}
diff --git a/internal/strings/title.go b/internal/strings/title.go
new file mode 100644
index 000000000..78bb4a591
--- /dev/null
+++ b/internal/strings/title.go
@@ -0,0 +1,40 @@
+package strings
+
+import (
+ "strings"
+
+ "golang.org/x/text/cases"
+ "golang.org/x/text/language"
+)
+
+// Title returns a copy of the string s with all Unicode letters that begin
+// words mapped to their title case. It uses language.Und for word boundaries.
+// For a more general solution, see TitleWithLanguage.
+func Title(s string) string {
+ return TitleWithLanguage(s, language.Und)
+}
+
+// TitleWithLanguage returns a copy of the string s with all Unicode letters
+// that begin words mapped to their title case.
+func TitleWithLanguage(s string, lang language.Tag) string {
+ return cases.Title(lang, cases.NoLower).String(s)
+}
+
+// Normalize returns a copy of the string s with the first word mapped to its
+// title case. It uses language.Und for word boundaries.
+// For a more general solution, see NormalizeWithLanguage.
+func Normalize(s string) string {
+ return NormalizeWithLanguage(s, language.Und)
+}
+
+// NormalizeWithLanguage returns a copy of the string s with the first word
+// mapped to its title case. If lang is not nil, it is used to determine the
+// language for which the case transformation should be performed. If lang is
+// nil, language.Und is used.
+func NormalizeWithLanguage(s string, lang language.Tag) string {
+ words := strings.Fields(s)
+ if len(words) > 0 {
+ words[0] = TitleWithLanguage(words[0], lang)
+ }
+ return strings.Join(words, " ")
+}
diff --git a/internal/strings/title_test.go b/internal/strings/title_test.go
new file mode 100644
index 000000000..88af9c38e
--- /dev/null
+++ b/internal/strings/title_test.go
@@ -0,0 +1,153 @@
+package strings
+
+import (
+ "testing"
+
+ "golang.org/x/text/language"
+)
+
+func TestTitle(t *testing.T) {
+ tests := []struct {
+ name string
+ s string
+ want string
+ }{
+ {
+ name: "sentence",
+ s: "the quick brown fox jumps over the lazy dog",
+ want: "The Quick Brown Fox Jumps Over The Lazy Dog",
+ },
+ {
+ name: "sentence with uppercase word",
+ s: "the quick brown fox jumps over the LAZY dog",
+ want: "The Quick Brown Fox Jumps Over The LAZY Dog",
+ },
+ {
+ name: "empty string",
+ s: "",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if got := Title(tt.s); got != tt.want {
+ t.Errorf("Title() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
+
+func TestTitleWithLanguage(t *testing.T) {
+ tests := []struct {
+ name string
+ s string
+ lang language.Tag
+ want string
+ }{
+ {
+ name: "Dutch sentence",
+ s: "de snelle bruine vos springt over de luie hond in ijburg",
+ lang: language.Dutch,
+ want: "De Snelle Bruine Vos Springt Over De Luie Hond In IJburg",
+ },
+ {
+ name: "English sentence",
+ s: "the quick brown fox jumps over the lazy dog",
+ lang: language.English,
+ want: "The Quick Brown Fox Jumps Over The Lazy Dog",
+ },
+ {
+ name: "English sentence with uppercase word",
+ s: "the quick brown fox jumps over the LAZY dog",
+ lang: language.English,
+ want: "The Quick Brown Fox Jumps Over The LAZY Dog",
+ },
+ {
+ name: "empty",
+ s: "",
+ lang: language.English,
+ want: "",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if got := TitleWithLanguage(tt.s, tt.lang); got != tt.want {
+ t.Errorf("TitleWithLanguage() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
+
+func TestNormalize(t *testing.T) {
+ tests := []struct {
+ name string
+ s string
+ want string
+ }{
+ {
+ name: "sentence",
+ s: "the quick brown fox jumps over the lazy dog",
+ want: "The quick brown fox jumps over the lazy dog",
+ },
+ {
+ name: "sentence with uppercase word",
+ s: "MacDonald had a farm",
+ want: "MacDonald had a farm",
+ },
+ {
+ name: "empty string",
+ s: "",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if got := Normalize(tt.s); got != tt.want {
+ t.Errorf("Normalize() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
+
+func TestNormalizeWithLanguage(t *testing.T) {
+ tests := []struct {
+ name string
+ s string
+ lang language.Tag
+ want string
+ }{
+ {
+ name: "Dutch sentence",
+ s: "ijburg is een wijk in Amsterdam",
+ lang: language.Dutch,
+ want: "IJburg is een wijk in Amsterdam",
+ },
+ {
+ name: "English sentence",
+ s: "the quick brown fox jumps over the lazy dog",
+ lang: language.English,
+ want: "The quick brown fox jumps over the lazy dog",
+ },
+ {
+ name: "English sentence with uppercase word",
+ s: "MacDonald had a farm",
+ lang: language.English,
+ want: "MacDonald had a farm",
+ },
+ {
+ name: "empty",
+ s: "",
+ lang: language.Und,
+ want: "",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if got := NormalizeWithLanguage(tt.s, tt.lang); got != tt.want {
+ t.Errorf("NormalizeWithLanguage() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
diff --git a/internal/testutil/equal_cmp.go b/internal/testutil/equal_cmp.go
new file mode 100644
index 000000000..c9314b53b
--- /dev/null
+++ b/internal/testutil/equal_cmp.go
@@ -0,0 +1,67 @@
+/*
+Copyright 2022 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package testutil
+
+import (
+ "github.com/google/go-cmp/cmp"
+ "github.com/onsi/gomega/types"
+)
+
+// This file was adapted from https://github.com/KamikazeZirou/equal-cmp
+// Original license follows:
+//
+// MIT License
+//
+// Copyright (c) 2021 KamikazeZirou
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+
+// Equal uses go-cmp to compare actual with expected. Equal is strict about
+// types when performing comparisons.
+func Equal(expected any, options ...cmp.Option) types.GomegaMatcher {
+ return &equalCmpMatcher{
+ expected: expected,
+ options: options,
+ }
+}
+
+type equalCmpMatcher struct {
+ expected any
+ options cmp.Options
+}
+
+func (matcher *equalCmpMatcher) Match(actual any) (success bool, err error) {
+ return cmp.Equal(actual, matcher.expected, matcher.options), nil
+}
+
+func (matcher *equalCmpMatcher) FailureMessage(actual any) (message string) {
+ diff := cmp.Diff(matcher.expected, actual, matcher.options)
+ return "Mismatch (-want, +got):\n" + diff
+}
+
+func (matcher *equalCmpMatcher) NegatedFailureMessage(actual any) (message string) {
+ diff := cmp.Diff(matcher.expected, actual, matcher.options)
+ return "Mismatch (-want, +got):\n" + diff
+}
diff --git a/internal/testutil/fake_recorder.go b/internal/testutil/fake_recorder.go
new file mode 100644
index 000000000..d64b0bb3a
--- /dev/null
+++ b/internal/testutil/fake_recorder.go
@@ -0,0 +1,117 @@
+/*
+Copyright 2022 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package testutil
+
+import (
+ "fmt"
+
+ corev1 "k8s.io/api/core/v1"
+ "k8s.io/apimachinery/pkg/runtime"
+
+ _ "k8s.io/client-go/tools/record"
+)
+
+// FakeRecorder is used as a fake during tests.
+//
+// It was invented to be used in tests which require more precise control over
+// e.g. assertions of specific event fields like Reason. For which string
+// comparisons on the concentrated event message using record.FakeRecorder is
+// not sufficient.
+//
+// To empty the Events channel into a slice of the recorded events, use
+// GetEvents(). Not initializing Events will cause the recorder to not record
+// any messages.
+type FakeRecorder struct {
+ Events chan corev1.Event
+ IncludeObject bool
+}
+
+// NewFakeRecorder creates new fake event recorder with an Events channel with
+// the given size. Setting includeObject to true will cause the recorder to
+// include the object reference in the events.
+//
+// To initialize a recorder which does not record any events, simply use:
+//
+// recorder := new(FakeRecorder)
+func NewFakeRecorder(bufferSize int, includeObject bool) *FakeRecorder {
+ return &FakeRecorder{
+ Events: make(chan corev1.Event, bufferSize),
+ IncludeObject: includeObject,
+ }
+}
+
+// Event emits an event with the given message.
+func (f *FakeRecorder) Event(obj runtime.Object, eventType, reason, message string) {
+ f.Eventf(obj, eventType, reason, "%s", message)
+}
+
+// Eventf emits an event with the given message.
+func (f *FakeRecorder) Eventf(obj runtime.Object, eventType, reason, message string, args ...any) {
+ if f.Events != nil {
+ f.Events <- f.generateEvent(obj, nil, eventType, reason, message, args...)
+ }
+}
+
+// AnnotatedEventf emits an event with annotations.
+func (f *FakeRecorder) AnnotatedEventf(obj runtime.Object, annotations map[string]string, eventType, reason, message string, args ...any) {
+ if f.Events != nil {
+ f.Events <- f.generateEvent(obj, annotations, eventType, reason, message, args...)
+ }
+}
+
+// GetEvents empties the Events channel and returns a slice of recorded events.
+// If the Events channel is nil, it returns nil.
+func (f *FakeRecorder) GetEvents() (events []corev1.Event) {
+ if f.Events != nil {
+ for {
+ select {
+ case e := <-f.Events:
+ events = append(events, e)
+ default:
+ return events
+ }
+ }
+ }
+ return nil
+}
+
+// generateEvent generates a new mocked event with the given parameters.
+func (f *FakeRecorder) generateEvent(obj runtime.Object, annotations map[string]string, eventType, reason, message string, args ...any) corev1.Event {
+ event := corev1.Event{
+ InvolvedObject: objectReference(obj, f.IncludeObject),
+ Type: eventType,
+ Reason: reason,
+ Message: fmt.Sprintf(message, args...),
+ }
+ if annotations != nil {
+ event.ObjectMeta.Annotations = annotations
+ }
+ return event
+}
+
+// objectReference returns an object reference for the given object with the
+// kind and (group) API version set.
+func objectReference(obj runtime.Object, includeObject bool) corev1.ObjectReference {
+ if !includeObject {
+ return corev1.ObjectReference{}
+ }
+
+ return corev1.ObjectReference{
+ Kind: obj.GetObjectKind().GroupVersionKind().Kind,
+ APIVersion: obj.GetObjectKind().GroupVersionKind().GroupVersion().String(),
+ }
+}
diff --git a/internal/testutil/helm_time.go b/internal/testutil/helm_time.go
new file mode 100644
index 000000000..53cd79f30
--- /dev/null
+++ b/internal/testutil/helm_time.go
@@ -0,0 +1,31 @@
+/*
+Copyright 2022 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package testutil
+
+import (
+ "time"
+)
+
+// MustParseHelmTime parses a string into a Helm time.Time, panicking if it
+// fails.
+func MustParseHelmTime(t string) time.Time {
+ res, err := time.Parse(time.RFC3339, t)
+ if err != nil {
+ panic(err)
+ }
+ return res
+}
diff --git a/internal/testutil/mock_chart.go b/internal/testutil/mock_chart.go
new file mode 100644
index 000000000..f35a1f0dd
--- /dev/null
+++ b/internal/testutil/mock_chart.go
@@ -0,0 +1,315 @@
+/*
+Copyright 2022 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package testutil
+
+import (
+ "fmt"
+
+ helmchartutil "helm.sh/helm/v4/pkg/chart/common"
+ helmchart "helm.sh/helm/v4/pkg/chart/v2"
+)
+
+var manifestTmpl = `apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: cm
+ namespace: %[1]s
+data:
+ foo: bar
+`
+
+var manifestWithCustomNameTmpl = `apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: cm-%[1]s
+ namespace: %[2]s
+data:
+ foo: bar
+`
+
+var manifestWithHookTmpl = `apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: hook
+ namespace: %[1]s
+ annotations:
+ "helm.sh/hook": post-install,pre-delete,post-upgrade
+data:
+ name: value
+`
+
+var manifestWithFailingHookTmpl = `apiVersion: v1
+kind: Pod
+metadata:
+ name: failing-hook
+ namespace: %[1]s
+ annotations:
+ "helm.sh/hook": post-install,pre-delete,post-upgrade
+spec:
+ containers:
+ - name: test
+ image: alpine
+ command: ["/bin/sh", "-c", "exit 1"]
+`
+
+var manifestWithTestHookTmpl = `apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: test-hook
+ namespace: %[1]s
+ annotations:
+ "helm.sh/hook": test
+data:
+ test: data
+`
+
+var manifestWithFailingTestHookTmpl = `apiVersion: v1
+kind: Pod
+metadata:
+ name: failing-test-hook
+ namespace: %[1]s
+ annotations:
+ "helm.sh/hook": test
+spec:
+ containers:
+ - name: test
+ image: alpine
+ command: ["/bin/sh", "-c", "exit 1"]
+ restartPolicy: Never
+`
+
+var manifestWithFailingDeploymentTmpl = `apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: failing-deployment
+ namespace: %[1]s
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: failing-app
+ template:
+ metadata:
+ labels:
+ app: failing-app
+ spec:
+ containers:
+ - name: app
+ image: nonexistent.registry/badimage:v999.999.999
+ ports:
+ - containerPort: 8080
+`
+
+var crdManifest = `apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+ name: crontabs.stable.example.com
+spec:
+ group: stable.example.com
+ versions:
+ - name: v1
+ served: true
+ storage: true
+ schema:
+ openAPIV3Schema:
+ type: object
+ properties:
+ spec:
+ type: object
+ properties:
+ cronSpec:
+ type: string
+ image:
+ type: string
+ replicas:
+ type: integer
+ scope: Namespaced
+ names:
+ plural: crontabs
+ singular: crontab
+ kind: CronTab
+ shortNames:
+ - ct
+`
+
+// ChartOptions is a helper to build a Helm chart object.
+type ChartOptions struct {
+ *helmchart.Chart
+}
+
+// ChartOption is a function that can be used to modify a chart.
+type ChartOption func(*ChartOptions)
+
+// BuildChart returns a Helm chart object built with basic data
+// and any provided chart options.
+func BuildChart(opts ...ChartOption) *helmchart.Chart {
+ c := &ChartOptions{
+ Chart: &helmchart.Chart{
+ // TODO: This should be more complete.
+ Metadata: &helmchart.Metadata{
+ APIVersion: "v1",
+ Name: "hello",
+ Version: "0.1.0",
+ AppVersion: "1.2.3",
+ },
+ // This adds a basic template and hooks.
+ Templates: []*helmchartutil.File{
+ {
+ Name: "templates/manifest",
+ Data: fmt.Appendf(nil, manifestTmpl, "{{ default .Release.Namespace }}"),
+ },
+ {
+ Name: "templates/hooks",
+ Data: fmt.Appendf(nil, manifestWithHookTmpl, "{{ default .Release.Namespace }}"),
+ },
+ },
+ },
+ }
+
+ for _, opt := range opts {
+ opt(c)
+ }
+
+ return c.Chart
+}
+
+// ChartWithName sets the name of the chart.
+func ChartWithName(name string) ChartOption {
+ return func(opts *ChartOptions) {
+ opts.Metadata.Name = name
+ }
+}
+
+// ChartWithVersion sets the version of the chart.
+func ChartWithVersion(version string) ChartOption {
+ return func(opts *ChartOptions) {
+ opts.Metadata.Version = version
+ }
+}
+
+// ChartWithFailingHook appends a failing hook to the chart.
+func ChartWithFailingHook() ChartOption {
+ return func(opts *ChartOptions) {
+ opts.Templates = append(opts.Templates, &helmchartutil.File{
+ Name: "templates/failing-hook",
+ Data: fmt.Appendf(nil, manifestWithFailingHookTmpl, "{{ default .Release.Namespace }}"),
+ })
+ }
+}
+
+// ChartWithTestHook appends a test hook to the chart.
+func ChartWithTestHook() ChartOption {
+ return func(opts *ChartOptions) {
+ opts.Templates = append(opts.Templates, &helmchartutil.File{
+ Name: "templates/test-hooks",
+ Data: fmt.Appendf(nil, manifestWithTestHookTmpl, "{{ default .Release.Namespace }}"),
+ })
+ }
+}
+
+// ChartWithFailingTestHook appends a failing test hook to the chart.
+func ChartWithFailingTestHook() ChartOption {
+ return func(options *ChartOptions) {
+ options.Templates = append(options.Templates, &helmchartutil.File{
+ Name: "templates/test-hooks",
+ Data: fmt.Appendf(nil, manifestWithFailingTestHookTmpl, "{{ default .Release.Namespace }}"),
+ })
+ }
+}
+
+// ChartWithFailingDeployment appends a deployment with a non-existent image to the chart.
+// This is useful for testing health check timeout and cancellation scenarios.
+func ChartWithFailingDeployment() ChartOption {
+ return func(opts *ChartOptions) {
+ opts.Templates = append(opts.Templates, &helmchartutil.File{
+ Name: "templates/failing-deployment",
+ Data: fmt.Appendf(nil, manifestWithFailingDeploymentTmpl, "{{ default .Release.Namespace }}"),
+ })
+ }
+}
+
+// ChartWithManifestWithCustomName sets the name of the manifest.
+func ChartWithManifestWithCustomName(name string) ChartOption {
+ return func(opts *ChartOptions) {
+ opts.Templates = []*helmchartutil.File{
+ {
+ Name: "templates/manifest",
+ Data: fmt.Appendf(nil, manifestWithCustomNameTmpl, name, "{{ default .Release.Namespace }}"),
+ },
+ }
+ }
+}
+
+// ChartWithCRD appends a CRD to the chart.
+func ChartWithCRD() ChartOption {
+ return func(opts *ChartOptions) {
+ opts.Files = []*helmchartutil.File{
+ {
+ Name: "crds/crd.yaml",
+ Data: []byte(crdManifest),
+ },
+ }
+ }
+}
+
+// ChartWithDependency appends a dependency to the chart.
+func ChartWithDependency(md *helmchart.Dependency, chrt *helmchart.Chart) ChartOption {
+ return func(opts *ChartOptions) {
+ opts.Metadata.Dependencies = append(opts.Metadata.Dependencies, md)
+ opts.AddDependency(chrt)
+ }
+}
+
+// ChartWithValues sets the values.yaml file of the chart.
+func ChartWithValues(values map[string]any) ChartOption {
+ return func(opts *ChartOptions) {
+ opts.Values = values
+ }
+}
+
+// BuildChartWithSubchartWithCRD returns a Helm chart object with a subchart
+// that contains a CRD. Useful for testing helm-controller's staged CRDs-first
+// deployment logic.
+func BuildChartWithSubchartWithCRD() *helmchart.Chart {
+ subChart := BuildChart(
+ ChartWithName("subchart"),
+ ChartWithManifestWithCustomName("sub-chart"),
+ ChartWithCRD(),
+ ChartWithValues(helmchartutil.Values{
+ "foo": "bar",
+ "exports": map[string]any{"data": map[string]any{"myint": 123}},
+ "default": map[string]any{"data": map[string]any{"myint": 456}},
+ }))
+ mainChart := BuildChart(
+ ChartWithManifestWithCustomName("main-chart"),
+ ChartWithValues(helmchartutil.Values{
+ "foo": "baz",
+ "myimports": map[string]any{"myint": 0},
+ }),
+ ChartWithDependency(&helmchart.Dependency{
+ Name: "subchart",
+ Condition: "subchart.enabled",
+ ImportValues: []any{
+ "data",
+ map[string]any{
+ "child": "default.data",
+ "parent": "myimports",
+ },
+ },
+ }, subChart))
+ return mainChart
+}
diff --git a/internal/testutil/mock_release.go b/internal/testutil/mock_release.go
new file mode 100644
index 000000000..2a73a41aa
--- /dev/null
+++ b/internal/testutil/mock_release.go
@@ -0,0 +1,126 @@
+/*
+Copyright 2022 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package testutil
+
+import (
+ "fmt"
+
+ helmrelease "helm.sh/helm/v4/pkg/release/v1"
+)
+
+// ReleaseOptions is a helper to build a Helm release mock.
+type ReleaseOptions struct {
+ *helmrelease.Release
+}
+
+// ReleaseOption is a function that can be used to modify a release.
+type ReleaseOption func(*ReleaseOptions)
+
+// BuildRelease builds a release with release.Mock using the given options,
+// and applies any provided options to the release before returning it.
+func BuildRelease(mockOpts *helmrelease.MockReleaseOptions, opts ...ReleaseOption) *helmrelease.Release {
+ mock := helmrelease.Mock(mockOpts)
+ r := &ReleaseOptions{Release: mock}
+
+ for _, opt := range opts {
+ opt(r)
+ }
+
+ return r.Release
+}
+
+// ReleaseWithConfig sets the config on the release.
+func ReleaseWithConfig(config map[string]any) ReleaseOption {
+ return func(options *ReleaseOptions) {
+ options.Config = config
+ }
+}
+
+// ReleaseWithLabels sets the labels on the release.
+func ReleaseWithLabels(labels map[string]string) ReleaseOption {
+ return func(options *ReleaseOptions) {
+ options.Release.Labels = labels
+ }
+}
+
+// ReleaseWithFailingHook appends a failing hook to the release.
+func ReleaseWithFailingHook() ReleaseOption {
+ return func(options *ReleaseOptions) {
+ options.Release.Hooks = append(options.Release.Hooks, &helmrelease.Hook{
+ Name: "failing-hook",
+ Kind: "Pod",
+ Manifest: fmt.Sprintf(manifestWithFailingTestHookTmpl, options.Release.Namespace),
+ Events: []helmrelease.HookEvent{
+ helmrelease.HookPostInstall,
+ helmrelease.HookPostUpgrade,
+ helmrelease.HookPostRollback,
+ helmrelease.HookPostDelete,
+ },
+ })
+ }
+}
+
+// ReleaseWithHookExecution appends a hook with a last run with the given
+// execution phase on the release.
+func ReleaseWithHookExecution(name string, events []helmrelease.HookEvent, phase helmrelease.HookPhase) ReleaseOption {
+ return func(options *ReleaseOptions) {
+ options.Release.Hooks = append(options.Release.Hooks, &helmrelease.Hook{
+ Name: name,
+ Events: events,
+ LastRun: helmrelease.HookExecution{
+ StartedAt: MustParseHelmTime("2006-01-02T15:10:05Z"),
+ CompletedAt: MustParseHelmTime("2006-01-02T15:10:07Z"),
+ Phase: phase,
+ },
+ })
+ }
+}
+
+// ReleaseWithTestHook appends a test hook to the release.
+func ReleaseWithTestHook() ReleaseOption {
+ return func(options *ReleaseOptions) {
+ options.Release.Hooks = append(options.Release.Hooks, &helmrelease.Hook{
+ Name: "test-hook",
+ Kind: "ConfigMap",
+ Manifest: fmt.Sprintf(manifestWithTestHookTmpl, options.Release.Namespace),
+ Events: []helmrelease.HookEvent{
+ helmrelease.HookTest,
+ },
+ })
+ }
+}
+
+// ReleaseWithFailingTestHook appends a failing test hook to the release.
+func ReleaseWithFailingTestHook() ReleaseOption {
+ return func(options *ReleaseOptions) {
+ options.Release.Hooks = append(options.Release.Hooks, &helmrelease.Hook{
+ Name: "failing-test-hook",
+ Kind: "Pod",
+ Manifest: fmt.Sprintf(manifestWithFailingTestHookTmpl, options.Release.Namespace),
+ Events: []helmrelease.HookEvent{
+ helmrelease.HookTest,
+ },
+ })
+ }
+}
+
+// ReleaseWithHooks sets the hooks on the release.
+func ReleaseWithHooks(hooks []*helmrelease.Hook) ReleaseOption {
+ return func(options *ReleaseOptions) {
+ options.Release.Hooks = append(options.Release.Hooks, hooks...)
+ }
+}
diff --git a/internal/testutil/mock_slog_handler.go b/internal/testutil/mock_slog_handler.go
new file mode 100644
index 000000000..d6149ab8a
--- /dev/null
+++ b/internal/testutil/mock_slog_handler.go
@@ -0,0 +1,48 @@
+/*
+Copyright 2026 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package testutil
+
+import (
+ "context"
+ "log/slog"
+)
+
+// MockSLogHandler lets callers know if Handle was called.
+type MockSLogHandler struct {
+ Called bool
+}
+
+// Enabled implements slog.Handler.
+func (m *MockSLogHandler) Enabled(context.Context, slog.Level) bool {
+ return true
+}
+
+// Handle implements slog.Handler.
+func (m *MockSLogHandler) Handle(context.Context, slog.Record) error {
+ m.Called = true
+ return nil
+}
+
+// WithAttrs implements slog.Handler.
+func (m *MockSLogHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
+ return m
+}
+
+// WithGroup implements slog.Handler.
+func (m *MockSLogHandler) WithGroup(name string) slog.Handler {
+ return m
+}
diff --git a/internal/testutil/mock_status_reader.go b/internal/testutil/mock_status_reader.go
new file mode 100644
index 000000000..188cb9c0c
--- /dev/null
+++ b/internal/testutil/mock_status_reader.go
@@ -0,0 +1,112 @@
+/*
+Copyright 2026 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package testutil
+
+import (
+ "context"
+ "sync/atomic"
+
+ "github.com/fluxcd/cli-utils/pkg/kstatus/polling"
+ "github.com/fluxcd/cli-utils/pkg/kstatus/polling/engine"
+ "github.com/fluxcd/cli-utils/pkg/kstatus/polling/event"
+ "github.com/fluxcd/cli-utils/pkg/kstatus/status"
+ "github.com/fluxcd/cli-utils/pkg/object"
+ apimeta "k8s.io/apimachinery/pkg/api/meta"
+ "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+ "k8s.io/apimachinery/pkg/runtime/schema"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+
+ "github.com/fluxcd/pkg/ssa"
+)
+
+// newStatusReaderFunc matches the action.NewStatusReaderFunc type alias
+// without importing the action package (to avoid import cycles).
+type newStatusReaderFunc = func(apimeta.RESTMapper) engine.StatusReader
+
+// MockStatusReader is a mock implementation of engine.StatusReader that
+// returns a healthy status for all resources and tracks method calls.
+type MockStatusReader struct {
+ supportsCalled atomic.Int32
+ readStatusForObjectCalled atomic.Int32
+}
+
+var _ engine.StatusReader = (*MockStatusReader)(nil)
+
+// Supports returns true for all GroupKinds and records the call.
+func (m *MockStatusReader) Supports(gk schema.GroupKind) bool {
+ m.supportsCalled.Add(1)
+ return true
+}
+
+// ReadStatus returns a Current status for any resource.
+func (m *MockStatusReader) ReadStatus(ctx context.Context, reader engine.ClusterReader, resource object.ObjMetadata) (*event.ResourceStatus, error) {
+ return &event.ResourceStatus{
+ Identifier: resource,
+ Status: status.CurrentStatus,
+ Message: "Resource is current",
+ }, nil
+}
+
+// ReadStatusForObject returns a Current status for any object and records the call.
+func (m *MockStatusReader) ReadStatusForObject(ctx context.Context, reader engine.ClusterReader, obj *unstructured.Unstructured) (*event.ResourceStatus, error) {
+ m.readStatusForObjectCalled.Add(1)
+ return &event.ResourceStatus{
+ Identifier: object.UnstructuredToObjMetadata(obj),
+ Status: status.CurrentStatus,
+ Resource: obj,
+ Message: "Resource is current",
+ }, nil
+}
+
+// SupportsCalled returns the number of times Supports was called.
+func (m *MockStatusReader) SupportsCalled() int {
+ return int(m.supportsCalled.Load())
+}
+
+// ReadStatusForObjectCalled returns the number of times ReadStatusForObject was called.
+func (m *MockStatusReader) ReadStatusForObjectCalled() int {
+ return int(m.readStatusForObjectCalled.Load())
+}
+
+// NewResourceManagerFunc returns a function compatible with
+// action.WithResourceManager that uses this MockStatusReader as a custom
+// status reader in the returned ResourceManager.
+func (m *MockStatusReader) NewResourceManagerFunc() func(sr ...newStatusReaderFunc) *ssa.ResourceManager {
+ return m.NewResourceManagerFuncWithClient(nil, nil)
+}
+
+// NewResourceManagerFuncWithClient returns a function compatible with
+// action.WithResourceManager that uses this MockStatusReader with a real
+// client and REST mapper for the poller engine.
+func (m *MockStatusReader) NewResourceManagerFuncWithClient(c client.Client, mapper apimeta.RESTMapper) func(sr ...newStatusReaderFunc) *ssa.ResourceManager {
+ return func(sr ...newStatusReaderFunc) *ssa.ResourceManager {
+ readers := []engine.StatusReader{m}
+ for _, f := range sr {
+ readers = append(readers, f(mapper))
+ }
+ poller := polling.NewStatusPoller(c, mapper, polling.Options{
+ CustomStatusReaders: readers,
+ })
+ return ssa.NewResourceManager(c, poller, ssa.Owner{})
+ }
+}
+
+// NewMockResourceManagerFunc returns a function compatible with
+// action.WithResourceManager using a new MockStatusReader.
+func NewMockResourceManagerFunc() func(sr ...newStatusReaderFunc) *ssa.ResourceManager {
+ return (&MockStatusReader{}).NewResourceManagerFunc()
+}
diff --git a/internal/testutil/save_chart.go b/internal/testutil/save_chart.go
new file mode 100644
index 000000000..0c812586f
--- /dev/null
+++ b/internal/testutil/save_chart.go
@@ -0,0 +1,108 @@
+/*
+Copyright 2023 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package testutil
+
+import (
+ "io"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "github.com/opencontainers/go-digest"
+ chart "helm.sh/helm/v4/pkg/chart/v2"
+ chartutil "helm.sh/helm/v4/pkg/chart/v2/util"
+ v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/util/rand"
+
+ "github.com/fluxcd/pkg/apis/meta"
+)
+
+// SaveChart saves the given chart to the given directory, and returns the
+// path to the saved chart. The chart is saved with a random suffix to avoid
+// name collisions.
+func SaveChart(c *chart.Chart, outDir string) (string, error) {
+ tmpDir, err := os.MkdirTemp("", "chart-")
+ if err != nil {
+ return "", err
+ }
+ defer os.RemoveAll(tmpDir)
+
+ tmpChart, err := chartutil.Save(c, tmpDir)
+ if err != nil {
+ return "", err
+ }
+
+ var (
+ tmpChartFileName = filepath.Base(tmpChart)
+ tmpChartExt = filepath.Ext(tmpChartFileName)
+ newChartFileName = strings.TrimSuffix(tmpChartFileName, tmpChartExt) + "-" + rand.String(5) + tmpChartExt
+ targetPath = filepath.Join(outDir, newChartFileName)
+ )
+
+ if err = os.Rename(tmpChart, targetPath); err != nil {
+ return "", err
+ }
+ return targetPath, nil
+}
+
+// SaveChartAsArtifact saves the given chart to the given directory, and
+// returns an artifact with the chart's metadata. The chart is saved with a
+// random suffix to avoid name collisions.
+func SaveChartAsArtifact(c *chart.Chart, algo digest.Algorithm, baseURL, outDir string) (*meta.Artifact, error) {
+ abs, err := SaveChart(c, outDir)
+ if err != nil {
+ return nil, err
+ }
+
+ f, err := os.Open(abs)
+ if err != nil {
+ return nil, err
+ }
+ defer f.Close()
+
+ bc := &byteCountReader{Reader: f}
+ dig, err := algo.FromReader(bc)
+ if err != nil {
+ return nil, err
+ }
+
+ rel, err := filepath.Rel(outDir, abs)
+ if err != nil {
+ return nil, err
+ }
+ fileURL := strings.TrimSuffix(baseURL, "/") + "/" + rel
+
+ return &meta.Artifact{
+ Path: abs,
+ URL: fileURL,
+ Revision: c.Metadata.Version,
+ Digest: dig.String(),
+ LastUpdateTime: v1.Now(),
+ Size: &bc.Count,
+ }, nil
+}
+
+type byteCountReader struct {
+ Reader io.Reader
+ Count int64
+}
+
+func (b *byteCountReader) Read(p []byte) (n int, err error) {
+ n, err = b.Reader.Read(p)
+ b.Count += int64(n)
+ return n, err
+}
diff --git a/internal/util/object.go b/internal/util/object.go
deleted file mode 100644
index f6476fcdc..000000000
--- a/internal/util/object.go
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
-Copyright 2023 The Flux authors
-Copyright 2018 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-// TODO: Remove this when
-// https://github.com/kubernetes-sigs/controller-runtime/blob/c783d2527a7da76332a2d8d563a6ca0b80c12122/pkg/client/apiutil/apimachinery.go#L76-L104
-// is included in a semver release.
-
-package util
-
-import (
- "errors"
- "fmt"
-
- apimeta "k8s.io/apimachinery/pkg/api/meta"
- "k8s.io/apimachinery/pkg/runtime"
- "k8s.io/apimachinery/pkg/runtime/schema"
- "sigs.k8s.io/controller-runtime/pkg/client/apiutil"
-)
-
-// IsAPINamespaced returns true if the object is namespace scoped.
-// For unstructured objects the gvk is found from the object itself.
-func IsAPINamespaced(obj runtime.Object, scheme *runtime.Scheme, restmapper apimeta.RESTMapper) (bool, error) {
- gvk, err := apiutil.GVKForObject(obj, scheme)
- if err != nil {
- return false, err
- }
- return IsAPINamespacedWithGVK(gvk, restmapper)
-}
-
-// IsAPINamespacedWithGVK returns true if the object having the provided
-// GVK is namespace scoped.
-func IsAPINamespacedWithGVK(gk schema.GroupVersionKind, restmapper apimeta.RESTMapper) (bool, error) {
- restmapping, err := restmapper.RESTMapping(schema.GroupKind{Group: gk.Group, Kind: gk.Kind})
- if err != nil {
- return false, fmt.Errorf("failed to get restmapping: %w", err)
- }
-
- scope := restmapping.Scope.Name()
-
- if scope == "" {
- return false, errors.New("scope cannot be identified, empty scope returned")
- }
-
- if scope != apimeta.RESTScopeNameRoot {
- return true, nil
- }
- return false, nil
-}
diff --git a/internal/util/util.go b/internal/util/util.go
deleted file mode 100644
index 8c43d53b0..000000000
--- a/internal/util/util.go
+++ /dev/null
@@ -1,121 +0,0 @@
-/*
-Copyright 2020 The Flux authors
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package util
-
-import (
- "crypto/sha1"
- "fmt"
- "sort"
-
- goyaml "gopkg.in/yaml.v2"
- "helm.sh/helm/v3/pkg/chartutil"
- "helm.sh/helm/v3/pkg/release"
- "sigs.k8s.io/yaml"
-)
-
-// ValuesChecksum calculates and returns the SHA1 checksum for the
-// given chartutil.Values.
-func ValuesChecksum(values chartutil.Values) string {
- var s string
- if len(values) != 0 {
- s, _ = values.YAML()
- }
- return fmt.Sprintf("%x", sha1.Sum([]byte(s)))
-}
-
-// OrderedValuesChecksum sort the chartutil.Values then calculates
-// and returns the SHA1 checksum for the sorted values.
-func OrderedValuesChecksum(values chartutil.Values) string {
- var s []byte
- if len(values) != 0 {
- msValues := yaml.JSONObjectToYAMLObject(copyValues(values))
- SortMapSlice(msValues)
- s, _ = goyaml.Marshal(msValues)
- }
- return fmt.Sprintf("%x", sha1.Sum(s))
-}
-
-// SortMapSlice recursively sorts the given goyaml.MapSlice by key.
-// This is used to ensure that the values checksum is the same regardless
-// of the order of the keys in the values file.
-func SortMapSlice(ms goyaml.MapSlice) {
- sort.Slice(ms, func(i, j int) bool {
- return fmt.Sprint(ms[i].Key) < fmt.Sprint(ms[j].Key)
- })
- for _, item := range ms {
- if nestedMS, ok := item.Value.(goyaml.MapSlice); ok {
- SortMapSlice(nestedMS)
- } else if _, ok := item.Value.([]interface{}); ok {
- for _, vItem := range item.Value.([]interface{}) {
- if itemMS, ok := vItem.(goyaml.MapSlice); ok {
- SortMapSlice(itemMS)
- }
- }
- }
- }
-}
-
-// cleanUpMapValue changes all instances of
-// map[interface{}]interface{} to map[string]interface{}.
-// This is for handling the mismatch when unmarshaling
-// reference to the issue: https://github.com/go-yaml/yaml/issues/139
-func cleanUpMapValue(v interface{}) interface{} {
- switch v := v.(type) {
- case []interface{}:
- return cleanUpInterfaceArray(v)
- case map[interface{}]interface{}:
- return cleanUpInterfaceMap(v)
- default:
- return v
- }
-}
-
-func cleanUpInterfaceMap(in map[interface{}]interface{}) map[string]interface{} {
- result := make(map[string]interface{})
- for k, v := range in {
- result[fmt.Sprintf("%v", k)] = cleanUpMapValue(v)
- }
- return result
-}
-
-func cleanUpInterfaceArray(in []interface{}) []interface{} {
- result := make([]interface{}, len(in))
- for i, v := range in {
- result[i] = cleanUpMapValue(v)
- }
- return result
-}
-
-func copyValues(in map[string]interface{}) map[string]interface{} {
- copiedValues, _ := goyaml.Marshal(in)
- newValues := make(map[string]interface{})
-
- _ = goyaml.Unmarshal(copiedValues, newValues)
- for i, value := range newValues {
- newValues[i] = cleanUpMapValue(value)
- }
-
- return newValues
-}
-
-// ReleaseRevision returns the revision of the given release.Release.
-func ReleaseRevision(rel *release.Release) int {
- if rel == nil {
- return 0
- }
- return rel.Version
-}
diff --git a/internal/util/util_test.go b/internal/util/util_test.go
deleted file mode 100644
index 04826f642..000000000
--- a/internal/util/util_test.go
+++ /dev/null
@@ -1,230 +0,0 @@
-/*
-Copyright 2020 The Flux authors
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package util
-
-import (
- "reflect"
- "testing"
-
- goyaml "gopkg.in/yaml.v2"
- "helm.sh/helm/v3/pkg/chartutil"
- "helm.sh/helm/v3/pkg/release"
-)
-
-func TestValuesChecksum(t *testing.T) {
- tests := []struct {
- name string
- values chartutil.Values
- want string
- }{
- {
- name: "empty",
- values: chartutil.Values{},
- want: "da39a3ee5e6b4b0d3255bfef95601890afd80709",
- },
- {
- name: "value map",
- values: chartutil.Values{
- "foo": "bar",
- "baz": map[string]string{
- "cool": "stuff",
- },
- },
- want: "7d487b668ca37fe68c42adfc06fa4d0e74443afd",
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- if got := ValuesChecksum(tt.values); got != tt.want {
- t.Errorf("ValuesChecksum() = %v, want %v", got, tt.want)
- }
- })
- }
-}
-
-func TestOrderedValuesChecksum(t *testing.T) {
- tests := []struct {
- name string
- values chartutil.Values
- want string
- }{
- {
- name: "empty",
- values: chartutil.Values{},
- want: "da39a3ee5e6b4b0d3255bfef95601890afd80709",
- },
- {
- name: "value map",
- values: chartutil.Values{
- "foo": "bar",
- "baz": map[string]string{
- "fruit": "apple",
- "cool": "stuff",
- },
- },
- want: "dfd6589332e4d2da5df7bcbf5885f406f08b58ee",
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- if got := OrderedValuesChecksum(tt.values); got != tt.want {
- t.Errorf("OrderedValuesChecksum() = %v, want %v", got, tt.want)
- }
- })
- }
-}
-
-func TestReleaseRevision(t *testing.T) {
- var rel *release.Release
- if rev := ReleaseRevision(rel); rev != 0 {
- t.Fatalf("ReleaseRevision() = %v, want %v", rev, 0)
- }
- rel = &release.Release{Version: 1}
- if rev := ReleaseRevision(rel); rev != 1 {
- t.Fatalf("ReleaseRevision() = %v, want %v", rev, 1)
- }
-}
-
-func TestSortMapSlice(t *testing.T) {
- tests := []struct {
- name string
- input goyaml.MapSlice
- expected goyaml.MapSlice
- }{
- {
- name: "Simple case",
- input: goyaml.MapSlice{
- {Key: "b", Value: 2},
- {Key: "a", Value: 1},
- },
- expected: goyaml.MapSlice{
- {Key: "a", Value: 1},
- {Key: "b", Value: 2},
- },
- },
- {
- name: "Nested MapSlice",
- input: goyaml.MapSlice{
- {Key: "b", Value: 2},
- {Key: "a", Value: 1},
- {Key: "c", Value: goyaml.MapSlice{
- {Key: "d", Value: 4},
- {Key: "e", Value: 5},
- }},
- },
- expected: goyaml.MapSlice{
- {Key: "a", Value: 1},
- {Key: "b", Value: 2},
- {Key: "c", Value: goyaml.MapSlice{
- {Key: "d", Value: 4},
- {Key: "e", Value: 5},
- }},
- },
- },
- {
- name: "Empty MapSlice",
- input: goyaml.MapSlice{},
- expected: goyaml.MapSlice{},
- },
- {
- name: "Single element",
- input: goyaml.MapSlice{
- {Key: "a", Value: 1},
- },
- expected: goyaml.MapSlice{
- {Key: "a", Value: 1},
- },
- },
- {
- name: "Already sorted",
- input: goyaml.MapSlice{
- {Key: "a", Value: 1},
- {Key: "b", Value: 2},
- {Key: "c", Value: 3},
- },
- expected: goyaml.MapSlice{
- {Key: "a", Value: 1},
- {Key: "b", Value: 2},
- {Key: "c", Value: 3},
- },
- },
-
- {
- name: "Complex Case",
- input: goyaml.MapSlice{
- {Key: "b", Value: 2},
- {Key: "a", Value: map[interface{}]interface{}{
- "d": []interface{}{4, 5},
- "c": 3,
- }},
- {Key: "c", Value: goyaml.MapSlice{
- {Key: "f", Value: 6},
- {Key: "e", Value: goyaml.MapSlice{
- {Key: "h", Value: 8},
- {Key: "g", Value: 7},
- }},
- }},
- },
- expected: goyaml.MapSlice{
- {Key: "a", Value: map[interface{}]interface{}{
- "c": 3,
- "d": []interface{}{4, 5},
- }},
- {Key: "b", Value: 2},
- {Key: "c", Value: goyaml.MapSlice{
- {Key: "e", Value: goyaml.MapSlice{
- {Key: "g", Value: 7},
- {Key: "h", Value: 8},
- }},
- {Key: "f", Value: 6},
- }},
- },
- },
- {
- name: "Map slice in slice",
- input: goyaml.MapSlice{
- {Key: "b", Value: 2},
- {Key: "a", Value: []interface{}{
- map[interface{}]interface{}{
- "d": 4,
- "c": 3,
- },
- 1,
- }},
- },
- expected: goyaml.MapSlice{
- {Key: "a", Value: []interface{}{
- map[interface{}]interface{}{
- "c": 3,
- "d": 4,
- },
- 1,
- }},
- {Key: "b", Value: 2},
- },
- },
- }
-
- for _, test := range tests {
- t.Run(test.name, func(t *testing.T) {
- SortMapSlice(test.input)
- if !reflect.DeepEqual(test.input, test.expected) {
- t.Errorf("Expected %v, got %v", test.expected, test.input)
- }
- })
- }
-}
diff --git a/main.go b/main.go
index a9ea78c13..6f5668327 100644
--- a/main.go
+++ b/main.go
@@ -22,19 +22,22 @@ import (
"time"
flag "github.com/spf13/pflag"
- "helm.sh/helm/v3/pkg/kube"
+ "helm.sh/helm/v4/pkg/kube"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
_ "k8s.io/client-go/plugin/pkg/client/auth/gcp"
- "k8s.io/utils/pointer"
- "sigs.k8s.io/cli-utils/pkg/kstatus/polling"
+ "k8s.io/utils/ptr"
ctrl "sigs.k8s.io/controller-runtime"
ctrlcache "sigs.k8s.io/controller-runtime/pkg/cache"
ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
ctrlcfg "sigs.k8s.io/controller-runtime/pkg/config"
+ ctrlmetrics "sigs.k8s.io/controller-runtime/pkg/metrics"
+ metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
+ "github.com/fluxcd/pkg/auth"
+ "github.com/fluxcd/pkg/cache"
"github.com/fluxcd/pkg/runtime/acl"
"github.com/fluxcd/pkg/runtime/client"
helper "github.com/fluxcd/pkg/runtime/controller"
@@ -46,12 +49,15 @@ import (
"github.com/fluxcd/pkg/runtime/metrics"
"github.com/fluxcd/pkg/runtime/pprof"
"github.com/fluxcd/pkg/runtime/probes"
- sourcev1 "github.com/fluxcd/source-controller/api/v1beta2"
+ sourcev1 "github.com/fluxcd/source-controller/api/v1"
- v2 "github.com/fluxcd/helm-controller/api/v2beta1"
// +kubebuilder:scaffold:imports
+ v2 "github.com/fluxcd/helm-controller/api/v2"
+ intacl "github.com/fluxcd/helm-controller/internal/acl"
+ "github.com/fluxcd/helm-controller/internal/action"
"github.com/fluxcd/helm-controller/internal/controller"
+ intdigest "github.com/fluxcd/helm-controller/internal/digest"
"github.com/fluxcd/helm-controller/internal/features"
intkube "github.com/fluxcd/helm-controller/internal/kube"
"github.com/fluxcd/helm-controller/internal/oomwatch"
@@ -73,27 +79,35 @@ func init() {
}
func main() {
+ const (
+ tokenCacheDefaultMaxSize = 100
+ )
+
var (
- metricsAddr string
- eventsAddr string
- healthAddr string
- concurrent int
- requeueDependency time.Duration
- gracefulShutdownTimeout time.Duration
- httpRetry int
- clientOptions client.Options
- kubeConfigOpts client.KubeConfigOptions
- featureGates feathelper.FeatureGates
- logOptions logger.Options
- aclOptions acl.Options
- leaderElectionOptions leaderelection.Options
- rateLimiterOptions helper.RateLimiterOptions
- watchOptions helper.WatchOptions
- intervalJitterOptions jitter.IntervalOptions
- oomWatchInterval time.Duration
- oomWatchMemoryThreshold uint8
- oomWatchMaxMemoryPath string
- oomWatchCurrentMemoryPath string
+ metricsAddr string
+ eventsAddr string
+ healthAddr string
+ concurrent int
+ requeueDependency time.Duration
+ gracefulShutdownTimeout time.Duration
+ httpRetry int
+ clientOptions client.Options
+ kubeConfigOpts client.KubeConfigOptions
+ featureGates feathelper.FeatureGates
+ logOptions logger.Options
+ aclOptions acl.Options
+ leaderElectionOptions leaderelection.Options
+ rateLimiterOptions helper.RateLimiterOptions
+ watchOptions helper.WatchOptions
+ intervalJitterOptions jitter.IntervalOptions
+ oomWatchInterval time.Duration
+ oomWatchMemoryThreshold uint8
+ oomWatchMaxMemoryPath string
+ oomWatchCurrentMemoryPath string
+ snapshotDigestAlgo string
+ disallowedFieldManagers []string
+ tokenCacheOptions cache.TokenFlags
+ defaultKubeConfigServiceAccount string
)
flag.StringVar(&metricsAddr, "metrics-addr", ":8080",
@@ -110,8 +124,10 @@ func main() {
"The duration given to the reconciler to finish before forcibly stopping.")
flag.IntVar(&httpRetry, "http-retry", 9,
"The maximum number of retries when failing to fetch artifacts over HTTP.")
- flag.StringVar(&intkube.DefaultServiceAccountName, "default-service-account", "",
+ flag.StringVar(&intkube.DefaultServiceAccountName, auth.ControllerFlagDefaultServiceAccount, "",
"Default service account used for impersonation.")
+ flag.StringVar(&defaultKubeConfigServiceAccount, auth.ControllerFlagDefaultKubeConfigServiceAccount, "",
+ "Default service account used for kubeconfig.")
flag.Uint8Var(&oomWatchMemoryThreshold, "oom-watch-memory-threshold", 95,
"The memory threshold in percentage at which the OOM watcher will trigger a graceful shutdown. Requires feature gate 'OOMWatch' to be enabled.")
flag.DurationVar(&oomWatchInterval, "oom-watch-interval", 500*time.Millisecond,
@@ -120,6 +136,10 @@ func main() {
"The path to the cgroup memory limit file. Requires feature gate 'OOMWatch' to be enabled. If not set, the path will be automatically detected.")
flag.StringVar(&oomWatchCurrentMemoryPath, "oom-watch-current-memory-path", "",
"The path to the cgroup current memory usage file. Requires feature gate 'OOMWatch' to be enabled. If not set, the path will be automatically detected.")
+ flag.StringVar(&snapshotDigestAlgo, "snapshot-digest-algo", intdigest.Canonical.String(),
+ "The algorithm to use to calculate the digest of Helm release storage snapshots.")
+ flag.StringArrayVar(&disallowedFieldManagers, "override-manager", []string{},
+ "List of field managers to override during drift detection.")
clientOptions.BindFlags(flag.CommandLine)
logOptions.BindFlags(flag.CommandLine)
@@ -130,6 +150,7 @@ func main() {
featureGates.BindFlags(flag.CommandLine)
watchOptions.BindFlags(flag.CommandLine)
intervalJitterOptions.BindFlags(flag.CommandLine)
+ tokenCacheOptions.BindFlags(flag.CommandLine, tokenCacheDefaultMaxSize)
flag.Parse()
@@ -142,6 +163,43 @@ func main() {
os.Exit(1)
}
+ switch enabled, err := features.Enabled(auth.FeatureGateObjectLevelWorkloadIdentity); {
+ case err != nil:
+ setupLog.Error(err, "unable to check feature gate "+auth.FeatureGateObjectLevelWorkloadIdentity)
+ os.Exit(1)
+ case enabled:
+ auth.EnableObjectLevelWorkloadIdentity()
+ }
+
+ switch enabled, err := features.Enabled(features.UseHelm3Defaults); {
+ case err != nil:
+ setupLog.Error(err, "unable to check feature gate "+features.UseHelm3Defaults)
+ os.Exit(1)
+ case enabled:
+ action.UseHelm3Defaults = enabled
+ }
+
+ defaultToRetryOnFailure, err := features.Enabled(features.DefaultToRetryOnFailure)
+ if err != nil {
+ setupLog.Error(err, "unable to check feature gate "+features.DefaultToRetryOnFailure)
+ os.Exit(1)
+ }
+
+ cancelHealthCheckOnNewRevision, err := features.Enabled(features.CancelHealthCheckOnNewRevision)
+ if err != nil {
+ setupLog.Error(err, "unable to check feature gate "+features.CancelHealthCheckOnNewRevision)
+ os.Exit(1)
+ }
+
+ if defaultKubeConfigServiceAccount != "" {
+ auth.SetDefaultKubeConfigServiceAccount(defaultKubeConfigServiceAccount)
+ }
+
+ if auth.InconsistentObjectLevelConfiguration() {
+ setupLog.Error(auth.ErrInconsistentObjectLevelConfiguration, "invalid configuration")
+ os.Exit(1)
+ }
+
if err := intervalJitterOptions.SetGlobalJitter(nil); err != nil {
setupLog.Error(err, "unable to set global jitter")
os.Exit(1)
@@ -158,10 +216,16 @@ func main() {
os.Exit(1)
}
+ watchConfigsPredicate, err := helper.GetWatchConfigsPredicate(watchOptions)
+ if err != nil {
+ setupLog.Error(err, "unable to configure watch configs label selector for controller")
+ os.Exit(1)
+ }
+
var disableCacheFor []ctrlclient.Object
- shouldCache, err := features.Enabled(features.CacheSecretsAndConfigMaps)
+ shouldCache, err := features.Enabled(helper.FeatureGateCacheSecretsAndConfigMaps)
if err != nil {
- setupLog.Error(err, "unable to check feature gate CacheSecretsAndConfigMaps")
+ setupLog.Error(err, "unable to check feature gate "+helper.FeatureGateCacheSecretsAndConfigMaps)
os.Exit(1)
}
if !shouldCache {
@@ -173,13 +237,43 @@ func main() {
leaderElectionId = leaderelection.GenerateID(leaderElectionId, watchOptions.LabelSelector)
}
- // set the managedFields owner for resources reconciled from Helm charts
+ disableChartDigestTracking, err := features.Enabled(features.DisableChartDigestTracking)
+ if err != nil {
+ setupLog.Error(err, "unable to check feature gate "+features.DisableChartDigestTracking)
+ os.Exit(1)
+ }
+
+ additiveCELDependencyCheck, err := features.Enabled(helper.FeatureGateAdditiveCELDependencyCheck)
+ if err != nil {
+ setupLog.Error(err, "unable to check feature gate "+helper.FeatureGateAdditiveCELDependencyCheck)
+ os.Exit(1)
+ }
+
+ if ok, _ := features.Enabled(features.AdoptLegacyReleases); ok {
+ setupLog.Info("warning: the 'AdoptLegacyReleases' feature gate is ignored and has no effect since v1.5.0, " +
+ "adoption of HelmRelease resources in legacy API versions is no longer supported")
+ }
+
+ // Set the managedFields owner for resources reconciled from Helm charts.
kube.ManagedFieldsManager = controllerName
+ // Configure the ACL policy.
+ intacl.AllowCrossNamespaceRef = !aclOptions.NoCrossNamespaceRefs
+
+ // Configure the digest algorithm.
+ if snapshotDigestAlgo != intdigest.Canonical.String() {
+ algo, err := intdigest.AlgorithmForName(snapshotDigestAlgo)
+ if err != nil {
+ setupLog.Error(err, "unable to configure canonical digest algorithm")
+ os.Exit(1)
+ }
+ intdigest.Canonical = algo
+ }
+
restConfig := client.GetConfigOrDie(clientOptions)
- mgr, err := ctrl.NewManager(restConfig, ctrl.Options{
+
+ mgrConfig := ctrl.Options{
Scheme: scheme,
- MetricsBindAddress: metricsAddr,
HealthProbeBindAddress: healthAddr,
LeaderElection: leaderElectionOptions.Enable,
LeaderElectionReleaseOnCancel: leaderElectionOptions.ReleaseOnCancel,
@@ -198,20 +292,30 @@ func main() {
ByObject: map[ctrlclient.Object]ctrlcache.ByObject{
&v2.HelmRelease{}: {Label: watchSelector},
},
- Namespaces: []string{watchNamespace},
},
Controller: ctrlcfg.Controller{
- RecoverPanic: pointer.Bool(true),
+ RecoverPanic: ptr.To(true),
MaxConcurrentReconciles: concurrent,
},
- })
+ Metrics: metricsserver.Options{
+ BindAddress: metricsAddr,
+ ExtraHandlers: pprof.GetHandlers(),
+ },
+ }
+
+ if watchNamespace != "" {
+ mgrConfig.Cache.DefaultNamespaces = map[string]ctrlcache.Config{
+ watchNamespace: {},
+ }
+ }
+
+ mgr, err := ctrl.NewManager(restConfig, mgrConfig)
if err != nil {
setupLog.Error(err, "unable to start manager")
os.Exit(1)
}
probes.SetupChecks(mgr, setupLog)
- pprof.SetupHandlers(mgr, setupLog)
metricsH := helper.NewMetrics(mgr, metrics.MustMakeRecorder(), v2.HelmReleaseFinalizer)
var eventRecorder *events.Recorder
@@ -237,23 +341,65 @@ func main() {
ctx = ow.Watch(ctx)
}
- pollingOpts := polling.Options{}
+ var tokenCache *cache.TokenCache
+ if tokenCacheOptions.MaxSize > 0 {
+ var err error
+ tokenCache, err = cache.NewTokenCache(tokenCacheOptions.MaxSize,
+ cache.WithMaxDuration(tokenCacheOptions.MaxDuration),
+ cache.WithMetricsRegisterer(ctrlmetrics.Registry),
+ cache.WithMetricsPrefix("gotk_token_"))
+ if err != nil {
+ setupLog.Error(err, "unable to create token cache")
+ os.Exit(1)
+ }
+ }
+
+ allowExternalArtifact, err := features.Enabled(helper.FeatureGateExternalArtifact)
+ if err != nil {
+ setupLog.Error(err, "unable to check feature gate "+helper.FeatureGateExternalArtifact)
+ os.Exit(1)
+ }
+
+ directSourceFetch, err := features.Enabled(helper.FeatureGateDirectSourceFetch)
+ if err != nil {
+ setupLog.Error(err, "unable to check feature gate "+helper.FeatureGateDirectSourceFetch)
+ os.Exit(1)
+ }
+ if directSourceFetch {
+ setupLog.Info("DirectSourceFetch feature gate is enabled, source objects will be fetched directly from the API server")
+ }
+
+ disableConfigWatchers, err := features.Enabled(helper.FeatureGateDisableConfigWatchers)
+ if err != nil {
+ setupLog.Error(err, "unable to check feature gate "+helper.FeatureGateDisableConfigWatchers)
+ os.Exit(1)
+ }
+ watchConfigs := !disableConfigWatchers
+
if err = (&controller.HelmReleaseReconciler{
- Client: mgr.GetClient(),
- Config: mgr.GetConfig(),
- Scheme: mgr.GetScheme(),
- EventRecorder: eventRecorder,
- Metrics: metricsH,
- NoCrossNamespaceRef: aclOptions.NoCrossNamespaceRefs,
- ClientOpts: clientOptions,
- KubeConfigOpts: kubeConfigOpts,
- PollingOpts: pollingOpts,
- StatusPoller: polling.NewStatusPoller(mgr.GetClient(), mgr.GetRESTMapper(), pollingOpts),
- ControllerName: controllerName,
+ Client: mgr.GetClient(),
+ APIReader: mgr.GetAPIReader(),
+ EventRecorder: eventRecorder,
+ Metrics: metricsH,
+ GetClusterConfig: ctrl.GetConfig,
+ ClientOpts: clientOptions,
+ KubeConfigOpts: kubeConfigOpts,
+ FieldManager: controllerName,
+ DefaultToRetryOnFailure: defaultToRetryOnFailure,
+ DisableChartDigestTracking: disableChartDigestTracking,
+ AdditiveCELDependencyCheck: additiveCELDependencyCheck,
+ DirectSourceFetch: directSourceFetch,
+ TokenCache: tokenCache,
+ DependencyRequeueInterval: requeueDependency,
+ ArtifactFetchRetries: httpRetry,
+ AllowExternalArtifact: allowExternalArtifact,
+ DisallowedFieldManagers: disallowedFieldManagers,
}).SetupWithManager(ctx, mgr, controller.HelmReleaseReconcilerOptions{
- DependencyRequeueInterval: requeueDependency,
- HTTPRetry: httpRetry,
- RateLimiter: helper.GetRateLimiter(rateLimiterOptions),
+ RateLimiter: helper.GetRateLimiter(rateLimiterOptions),
+ WatchConfigs: watchConfigs,
+ WatchConfigsPredicate: watchConfigsPredicate,
+ WatchExternalArtifacts: allowExternalArtifact,
+ CancelHealthCheckOnRequeue: cancelHealthCheckOnNewRevision,
}); err != nil {
setupLog.Error(err, "unable to create controller", "controller", v2.HelmReleaseKind)
os.Exit(1)
diff --git a/tests/fuzz/Dockerfile.builder b/tests/fuzz/Dockerfile.builder
index 1eb6c7aba..6d5ca908e 100644
--- a/tests/fuzz/Dockerfile.builder
+++ b/tests/fuzz/Dockerfile.builder
@@ -1,9 +1,9 @@
FROM gcr.io/oss-fuzz-base/base-builder-go
-RUN wget https://go.dev/dl/go1.20.5.linux-amd64.tar.gz \
+RUN wget https://go.dev/dl/go1.26.0.linux-amd64.tar.gz \
&& mkdir temp-go \
&& rm -rf /root/.go/* \
- && tar -C temp-go/ -xzf go1.20.5.linux-amd64.tar.gz \
+ && tar -C temp-go/ -xzf go1.26.0.linux-amd64.tar.gz \
&& mv temp-go/go/* /root/.go/
ENV SRC=$GOPATH/src/github.com/fluxcd/helm-controller
|