Merge branch 'main' into fix-include-needs-transitive-1003

This commit is contained in:
copilot-swe-agent[bot] 2026-03-24 23:15:46 +00:00
commit fd9c2179fc
33 changed files with 1127 additions and 227 deletions

View File

@ -57,7 +57,7 @@ jobs:
go-version-file: go.mod
- name: check disk usage
run: df -h
- uses: azure/setup-helm@v4.3.1
- uses: azure/setup-helm@v5.0.0
with:
version: ${{ matrix.helm-version }}
- name: Build
@ -96,35 +96,35 @@ jobs:
- helm-version: v3.18.6
kustomize-version: v5.8.0
plugin-secrets-version: 4.7.4
plugin-diff-version: 3.15.1
plugin-diff-version: 3.15.3
extra-helmfile-flags: ''
# In case you need to test some optional helmfile features,
# enable it via extra-helmfile-flags below.
- helm-version: v3.18.6
kustomize-version: v5.8.0
plugin-secrets-version: 4.7.4
plugin-diff-version: 3.15.1
plugin-diff-version: 3.15.3
extra-helmfile-flags: '--enable-live-output'
- helm-version: v3.20.1
kustomize-version: v5.8.0
plugin-secrets-version: 4.7.4
plugin-diff-version: 3.15.1
plugin-diff-version: 3.15.3
extra-helmfile-flags: ''
- helm-version: v3.20.1
kustomize-version: v5.8.0
plugin-secrets-version: 4.7.4
plugin-diff-version: 3.15.1
plugin-diff-version: 3.15.3
extra-helmfile-flags: '--enable-live-output'
# Helmfile now supports both Helm 3.x and Helm 4.x
- helm-version: v4.1.3
kustomize-version: v5.8.0
plugin-secrets-version: 4.7.4
plugin-diff-version: 3.15.1
plugin-diff-version: 3.15.3
extra-helmfile-flags: ''
- helm-version: v4.1.3
kustomize-version: v5.8.0
plugin-secrets-version: 4.7.4
plugin-diff-version: 3.15.1
plugin-diff-version: 3.15.3
extra-helmfile-flags: '--enable-live-output'
steps:
- uses: actions/checkout@v6

View File

@ -95,7 +95,7 @@ RUN set -x && \
[ "$(age-keygen --version)" = "${AGE_VERSION}" ]
ARG HELM_SECRETS_VERSION="4.7.4"
RUN helm plugin install https://github.com/databus23/helm-diff --version v3.15.1 --verify=false && \
RUN helm plugin install https://github.com/databus23/helm-diff --version v3.15.3 --verify=false && \
helm plugin install https://github.com/jkroepke/helm-secrets/releases/download/v${HELM_SECRETS_VERSION}/secrets-${HELM_SECRETS_VERSION}.tgz --verify=false && \
helm plugin install https://github.com/jkroepke/helm-secrets/releases/download/v${HELM_SECRETS_VERSION}/secrets-getter-${HELM_SECRETS_VERSION}.tgz --verify=false && \
helm plugin install https://github.com/jkroepke/helm-secrets/releases/download/v${HELM_SECRETS_VERSION}/secrets-post-renderer-${HELM_SECRETS_VERSION}.tgz --verify=false && \

View File

@ -104,7 +104,7 @@ RUN set -x && \
[ "$(age-keygen --version)" = "${AGE_VERSION}" ]
ARG HELM_SECRETS_VERSION="4.7.4"
RUN helm plugin install https://github.com/databus23/helm-diff --version v3.15.1 --verify=false && \
RUN helm plugin install https://github.com/databus23/helm-diff --version v3.15.3 --verify=false && \
helm plugin install https://github.com/jkroepke/helm-secrets/releases/download/v${HELM_SECRETS_VERSION}/secrets-${HELM_SECRETS_VERSION}.tgz --verify=false && \
helm plugin install https://github.com/jkroepke/helm-secrets/releases/download/v${HELM_SECRETS_VERSION}/secrets-getter-${HELM_SECRETS_VERSION}.tgz --verify=false && \
helm plugin install https://github.com/jkroepke/helm-secrets/releases/download/v${HELM_SECRETS_VERSION}/secrets-post-renderer-${HELM_SECRETS_VERSION}.tgz --verify=false && \

View File

@ -104,7 +104,7 @@ RUN set -x && \
[ "$(age-keygen --version)" = "${AGE_VERSION}" ]
ARG HELM_SECRETS_VERSION="4.7.4"
RUN helm plugin install https://github.com/databus23/helm-diff --version v3.15.1 --verify=false && \
RUN helm plugin install https://github.com/databus23/helm-diff --version v3.15.3 --verify=false && \
helm plugin install https://github.com/jkroepke/helm-secrets/releases/download/v${HELM_SECRETS_VERSION}/secrets-${HELM_SECRETS_VERSION}.tgz --verify=false && \
helm plugin install https://github.com/jkroepke/helm-secrets/releases/download/v${HELM_SECRETS_VERSION}/secrets-getter-${HELM_SECRETS_VERSION}.tgz --verify=false && \
helm plugin install https://github.com/jkroepke/helm-secrets/releases/download/v${HELM_SECRETS_VERSION}/secrets-post-renderer-${HELM_SECRETS_VERSION}.tgz --verify=false && \

View File

@ -61,6 +61,7 @@ func NewApplyCmd(globalCfg *config.GlobalImpl) *cobra.Command {
f.BoolVar(&applyOptions.SuppressDiff, "suppress-diff", false, "suppress diff in the output. Usable in new installs")
f.BoolVar(&applyOptions.Wait, "wait", false, `Override helmDefaults.wait setting "helm upgrade --install --wait"`)
f.BoolVar(&applyOptions.WaitForJobs, "wait-for-jobs", false, `Override helmDefaults.waitForJobs setting "helm upgrade --install --wait-for-jobs"`)
f.IntVar(&applyOptions.Timeout, "timeout", 0, `Override helmDefaults.timeout in seconds for "helm upgrade --install --timeout" (default 0, which uses helmDefaults.timeout or helm's default if not set)`)
f.BoolVar(&applyOptions.ReuseValues, "reuse-values", false, `Override helmDefaults.reuseValues "helm upgrade --install --reuse-values"`)
f.BoolVar(&applyOptions.ResetValues, "reset-values", false, `Override helmDefaults.reuseValues "helm upgrade --install --reset-values"`)
f.StringVar(&applyOptions.PostRenderer, "post-renderer", "", `pass --post-renderer to "helm template" or "helm upgrade --install"`)
@ -71,6 +72,7 @@ func NewApplyCmd(globalCfg *config.GlobalImpl) *cobra.Command {
f.StringVar(&applyOptions.TrackMode, "track-mode", "", "Track mode for releases: 'helm' (default), 'helm-legacy' (Helm v4 only), or 'kubedog'")
f.IntVar(&applyOptions.TrackTimeout, "track-timeout", 0, `Timeout in seconds for kubedog tracking (0 to use default 300s timeout)`)
f.BoolVar(&applyOptions.TrackLogs, "track-logs", false, "Enable log streaming with kubedog tracking")
f.StringVar(&applyOptions.Description, "description", "", `Set description for all releases. If set, overridesdescriptions in helmfile.yaml. Will be passed to "helm upgrade --description"`)
return cmd
}

View File

@ -47,7 +47,7 @@ func NewSyncCmd(globalCfg *config.GlobalImpl) *cobra.Command {
f.BoolVar(&syncOptions.SyncReleaseLabels, "sync-release-labels", false, "sync release labels to the target release")
f.BoolVar(&syncOptions.Wait, "wait", false, `Override helmDefaults.wait setting "helm upgrade --install --wait"`)
f.BoolVar(&syncOptions.WaitForJobs, "wait-for-jobs", false, `Override helmDefaults.waitForJobs setting "helm upgrade --install --wait-for-jobs"`)
f.IntVar(&syncOptions.Timeout, "timeout", 0, `Override helmDefaults.timeout setting "helm upgrade --install --timeout" (default 0, which means no timeout)`)
f.IntVar(&syncOptions.Timeout, "timeout", 0, `Override helmDefaults.timeout in seconds for "helm upgrade --install --timeout" (default 0, which uses helmDefaults.timeout or helm's default if not set)`)
f.BoolVar(&syncOptions.ReuseValues, "reuse-values", false, `Override helmDefaults.reuseValues "helm upgrade --install --reuse-values"`)
f.BoolVar(&syncOptions.ResetValues, "reset-values", false, `Override helmDefaults.reuseValues "helm upgrade --install --reset-values"`)
f.StringVar(&syncOptions.PostRenderer, "post-renderer", "", `pass --post-renderer to "helm template" or "helm upgrade --install"`)
@ -57,6 +57,7 @@ func NewSyncCmd(globalCfg *config.GlobalImpl) *cobra.Command {
f.StringVar(&syncOptions.TrackMode, "track-mode", "", "Track mode for releases: 'helm' (default), 'helm-legacy' (Helm v4 only), or 'kubedog'")
f.IntVar(&syncOptions.TrackTimeout, "track-timeout", 0, `Timeout in seconds for kubedog tracking (0 to use default 300s timeout)`)
f.BoolVar(&syncOptions.TrackLogs, "track-logs", false, "Enable log streaming with kubedog tracking")
f.StringVar(&syncOptions.Description, "description", "", `Set description for all releases. If set, overrides descriptions in helmfile.yaml. Will be passed to "helm upgrade --description"`)
return cmd
}

View File

@ -593,6 +593,8 @@ Helmfile uses some OS environment variables to override default behaviour:
* `HELMFILE_FILE_PATH` - specify the path to the helmfile.yaml file
* `HELMFILE_INTERACTIVE` - enable interactive mode, expecting `true` lower case. The same as `--interactive` CLI flag
* `HELMFILE_RENDER_YAML` - force helmfile.yaml to be rendered as a Go template regardless of file extension, expecting `true` lower case. Useful for migrating from v0 to v1 without renaming files to `.gotmpl`
* `HELMFILE_AWS_SDK_LOG_LEVEL` - configure AWS SDK logging level for vals library. Valid values: `off` (default, secure, case-insensitive), `minimal`, `standard`, `verbose`, or custom comma-separated values like `request,response`. See issue #2270 for details
* `HELMFILE_VALS_FAIL_ON_MISSING_KEY_IN_MAP` - enable strict mode for vals secret references. When set to `true` (or any value accepted by Go's `strconv.ParseBool` like `TRUE`, `1`), vals will fail when a referenced key does not exist in the secret map. Invalid values will cause an error when vals is initialized (when secret refs are first evaluated). Default is `false` (when unset or empty) for backward compatibility. See issue #1563 for details
## CLI Reference
@ -1588,7 +1590,7 @@ Hooks associated to `presync` events are triggered before each release is synced
This is the ideal event to execute any commands that may mutate the cluster state as it will not be run for read-only operations like `lint`, `diff` or `template`.
`preapply` hooks are triggered before a release is uninstalled, installed, or upgraded as part of `helmfile apply`.
This is the ideal event to hook into when you are going to use `helmfile apply` for every kind of change and you want the hook to be triggered regardless of whether the releases have changed or not. Be sure to make each `preapply` hook command idempotent. Otherwise, rerunning helmfile-apply on a transient failure may end up either breaking your cluster, or the hook that runs for the second time will never succeed.
This is the ideal event to hook into when you are going to use `helmfile apply` for every kind of change. Note that preapply hooks will only run if at least one release has changes to apply. Be sure to make each `preapply` hook command idempotent. Otherwise, rerunning `helmfile apply` on a transient failure may end up either breaking your cluster, or the hook that runs for the second time will never succeed.
`preuninstall` hooks are triggered immediately before a release is uninstalled as part of `helmfile apply`, `helmfile sync`, `helmfile delete`, and `helmfile destroy`.

14
go.mod
View File

@ -7,7 +7,7 @@ require (
github.com/Masterminds/semver/v3 v3.4.0
github.com/Masterminds/sprig/v3 v3.3.0
github.com/aws/aws-sdk-go-v2/config v1.32.12
github.com/aws/aws-sdk-go-v2/service/s3 v1.97.1
github.com/aws/aws-sdk-go-v2/service/s3 v1.97.2
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc
github.com/go-test/deep v1.1.1
github.com/gofrs/flock v0.13.0
@ -37,8 +37,8 @@ require (
gopkg.in/yaml.v3 v3.0.1
helm.sh/helm/v3 v3.20.1
helm.sh/helm/v4 v4.1.3
k8s.io/apimachinery v0.35.2
k8s.io/client-go v0.35.2
k8s.io/apimachinery v0.35.3
k8s.io/client-go v0.35.3
)
require (
@ -57,7 +57,7 @@ require (
github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect
github.com/blang/semver v3.5.1+incompatible // indirect
github.com/dimchansky/utfbom v1.1.1 // indirect
github.com/fatih/color v1.18.0
github.com/fatih/color v1.19.0
github.com/fujiwara/tfstate-lookup v1.10.0 // indirect
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
@ -104,7 +104,7 @@ require (
golang.org/x/time v0.15.0 // indirect
google.golang.org/api v0.271.0 // indirect
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 // indirect
google.golang.org/grpc v1.79.2 // indirect
google.golang.org/grpc v1.79.3 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/ini.v1 v1.67.1 // indirect
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
@ -156,7 +156,7 @@ require (
github.com/atotto/clipboard v0.1.4 // indirect
github.com/avelino/slugify v0.0.0-20180501145920-855f152bd774 // indirect
github.com/aws/aws-sdk-go-v2 v1.41.4 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.7 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.19.12 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20 // indirect
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.0 // indirect
@ -340,7 +340,7 @@ require (
gopkg.in/gookit/color.v1 v1.1.6 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
k8s.io/api v0.35.2 // indirect
k8s.io/api v0.35.3 // indirect
k8s.io/apiextensions-apiserver v0.35.1 // indirect
k8s.io/apiserver v0.35.1 // indirect
k8s.io/cli-runtime v0.35.1 // indirect

28
go.sum
View File

@ -155,8 +155,8 @@ github.com/aws/aws-sdk-go v1.55.8 h1:JRmEUbU52aJQZ2AjX4q4Wu7t4uZjOu71uyNmaWlUkJQ
github.com/aws/aws-sdk-go v1.55.8/go.mod h1:ZkViS9AqA6otK+JBBNH2++sx1sgxrPKcSzPPvQkUtXk=
github.com/aws/aws-sdk-go-v2 v1.41.4 h1:10f50G7WyU02T56ox1wWXq+zTX9I1zxG46HYuG1hH/k=
github.com/aws/aws-sdk-go-v2 v1.41.4/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.7 h1:3kGOqnh1pPeddVa/E37XNTaWJ8W6vrbYV9lJEkCnhuY=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.7/go.mod h1:lyw7GFp3qENLh7kwzf7iMzAxDn+NzjXEAGjKS2UOKqI=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 h1:eBMB84YGghSocM7PsjmmPffTa+1FBUeNvGvFou6V/4o=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8/go.mod h1:lyw7GFp3qENLh7kwzf7iMzAxDn+NzjXEAGjKS2UOKqI=
github.com/aws/aws-sdk-go-v2/config v1.32.12 h1:O3csC7HUGn2895eNrLytOJQdoL2xyJy0iYXhoZ1OmP0=
github.com/aws/aws-sdk-go-v2/config v1.32.12/go.mod h1:96zTvoOFR4FURjI+/5wY1vc1ABceROO4lWgWJuxgy0g=
github.com/aws/aws-sdk-go-v2/credentials v1.19.12 h1:oqtA6v+y5fZg//tcTWahyN9PEn5eDU/Wpvc2+kJ4aY8=
@ -183,8 +183,8 @@ github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.20 h1:siU1A6xjUZ2N8
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.20/go.mod h1:4TLZCmVJDM3FOu5P5TJP0zOlu9zWgDWU7aUxWbr+rcw=
github.com/aws/aws-sdk-go-v2/service/kms v1.50.2 h1:UOHOXigIzDRaEU03CBQcZ5uW7FNC7E+vwfhsQWXl5RQ=
github.com/aws/aws-sdk-go-v2/service/kms v1.50.2/go.mod h1:nAa5gmcmAmjXN3tGuhPSHLXFeWv+7nzKhjZzh8F7MH0=
github.com/aws/aws-sdk-go-v2/service/s3 v1.97.1 h1:csi9NLpFZXb9fxY7rS1xVzgPRGMt7MSNWeQ6eo247kE=
github.com/aws/aws-sdk-go-v2/service/s3 v1.97.1/go.mod h1:qXVal5H0ChqXP63t6jze5LmFalc7+ZE7wOdLtZ0LCP0=
github.com/aws/aws-sdk-go-v2/service/s3 v1.97.2 h1:MRNiP6nqa20aEl8fQ6PJpEq11b2d40b16sm4WD7QgMU=
github.com/aws/aws-sdk-go-v2/service/s3 v1.97.2/go.mod h1:FrNA56srbsr3WShiaelyWYEo70x80mXnVZ17ZZfbeqg=
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.41.3 h1:9bb0dEq1WzA0ZxIGG2EmwEgxfMAJpHyusxwbVN7f6iM=
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.41.3/go.mod h1:2z9eg35jfuRtdPE4Ci0ousrOU9PBhDBilXA1cwq9Ptk=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.8 h1:0GFOLzEbOyZABS3PhYfBIx2rNBACYcKty+XGkTgw1ow=
@ -307,8 +307,8 @@ github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f/go.mod h1:OSY
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.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
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/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
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=
@ -1025,8 +1025,8 @@ google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZi
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.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU=
google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=
google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
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=
@ -1060,18 +1060,18 @@ helm.sh/helm/v4 v4.1.3 h1:Abfmb+oJUtxoaXDyB2Jhw1zRk3hT6aFfHta+AXb8Lno=
helm.sh/helm/v4 v4.1.3/go.mod h1:5dSo8rRgn3OTkDAc/k0Ipw5/Q+BlqKIKZwa0XwSiINI=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
k8s.io/api v0.35.2 h1:tW7mWc2RpxW7HS4CoRXhtYHSzme1PN1UjGHJ1bdrtdw=
k8s.io/api v0.35.2/go.mod h1:7AJfqGoAZcwSFhOjcGM7WV05QxMMgUaChNfLTXDRE60=
k8s.io/api v0.35.3 h1:pA2fiBc6+N9PDf7SAiluKGEBuScsTzd2uYBkA5RzNWQ=
k8s.io/api v0.35.3/go.mod h1:9Y9tkBcFwKNq2sxwZTQh1Njh9qHl81D0As56tu42GA4=
k8s.io/apiextensions-apiserver v0.35.1 h1:p5vvALkknlOcAqARwjS20kJffgzHqwyQRM8vHLwgU7w=
k8s.io/apiextensions-apiserver v0.35.1/go.mod h1:2CN4fe1GZ3HMe4wBr25qXyJnJyZaquy4nNlNmb3R7AQ=
k8s.io/apimachinery v0.35.2 h1:NqsM/mmZA7sHW02JZ9RTtk3wInRgbVxL8MPfzSANAK8=
k8s.io/apimachinery v0.35.2/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns=
k8s.io/apimachinery v0.35.3 h1:MeaUwQCV3tjKP4bcwWGgZ/cp/vpsRnQzqO6J6tJyoF8=
k8s.io/apimachinery v0.35.3/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns=
k8s.io/apiserver v0.35.1 h1:potxdhhTL4i6AYAa2QCwtlhtB1eCdWQFvJV6fXgJzxs=
k8s.io/apiserver v0.35.1/go.mod h1:BiL6Dd3A2I/0lBnteXfWmCFobHM39vt5+hJQd7Lbpi4=
k8s.io/cli-runtime v0.35.1 h1:uKcXFe8J7AMAM4Gm2JDK4mp198dBEq2nyeYtO+JfGJE=
k8s.io/cli-runtime v0.35.1/go.mod h1:55/hiXIq1C8qIJ3WBrWxEwDLdHQYhBNRdZOz9f7yvTw=
k8s.io/client-go v0.35.2 h1:YUfPefdGJA4aljDdayAXkc98DnPkIetMl4PrKX97W9o=
k8s.io/client-go v0.35.2/go.mod h1:4QqEwh4oQpeK8AaefZ0jwTFJw/9kIjdQi0jpKeYvz7g=
k8s.io/client-go v0.35.3 h1:s1lZbpN4uI6IxeTM2cpdtrwHcSOBML1ODNTCCfsP1pg=
k8s.io/client-go v0.35.3/go.mod h1:RzoXkc0mzpWIDvBrRnD+VlfXP+lRzqQjCmKtiwZ8Q9c=
k8s.io/component-base v0.35.1 h1:XgvpRf4srp037QWfGBLFsYMUQJkE5yMa94UsJU7pmcE=
k8s.io/component-base v0.35.1/go.mod h1:HI/6jXlwkiOL5zL9bqA3en1Ygv60F03oEpnuU1G56Bs=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=

View File

@ -171,8 +171,9 @@ func (a *App) Diff(c DiffConfigProvider) error {
Concurrency: c.Concurrency(),
IncludeNeeds: c.IncludeNeeds(),
IncludeTransitiveNeeds: c.IncludeTransitiveNeeds(),
}, func() {
}, func() []error {
msg, matched, affected, errs = a.diff(run, c)
return errs
})
if msg != nil {
@ -247,8 +248,9 @@ func (a *App) Template(c TemplateConfigProvider) error {
Values: c.Values(),
KubeVersion: c.KubeVersion(),
HelmOCIPlainHTTP: a.HelmOCIPlainHTTP,
}, func() {
}, func() []error {
ok, errs = a.template(run, c)
return errs
})
if prepErr != nil {
@ -269,8 +271,9 @@ func (a *App) WriteValues(c WriteValuesConfigProvider) error {
IncludeNeeds: c.IncludeNeeds(),
IncludeTransitiveNeeds: c.IncludeTransitiveNeeds(),
Concurrency: c.Concurrency(),
}, func() {
}, func() []error {
ok, errs = a.writeValues(run, c)
return errs
})
if prepErr != nil {
@ -323,8 +326,9 @@ func (a *App) Lint(c LintConfigProvider) error {
Concurrency: c.Concurrency(),
IncludeNeeds: c.IncludeNeeds(),
IncludeTransitiveNeeds: c.IncludeTransitiveNeeds(),
}, func() {
}, func() []error {
ok, lintErrs, errs = a.lint(run, c)
return append(errs, lintErrs...)
})
if prepErr != nil {
@ -365,8 +369,9 @@ func (a *App) Unittest(c UnittestConfigProvider) error {
Concurrency: c.Concurrency(),
IncludeNeeds: c.IncludeNeeds(),
IncludeTransitiveNeeds: c.IncludeTransitiveNeeds(),
}, func() {
}, func() []error {
ok, unittestErrs, errs = a.unittest(run, c)
return append(errs, unittestErrs...)
})
if prepErr != nil {
@ -401,7 +406,9 @@ func (a *App) Fetch(c FetchConfigProvider) error {
OutputDir: c.OutputDir(),
OutputDirTemplate: c.OutputDirTemplate(),
Concurrency: c.Concurrency(),
}, func() {})
}, func() []error {
return nil
})
if prepErr != nil {
errs = append(errs, prepErr)
@ -427,8 +434,9 @@ func (a *App) Sync(c SyncConfigProvider) error {
IncludeTransitiveNeeds: c.IncludeTransitiveNeeds(),
Validate: c.Validate(),
Concurrency: c.Concurrency(),
}, func() {
}, func() []error {
ok, errs = a.sync(run, c)
return errs
})
if prepErr != nil {
@ -464,7 +472,7 @@ func (a *App) Apply(c ApplyConfigProvider) error {
Concurrency: c.Concurrency(),
IncludeNeeds: c.IncludeNeeds(),
IncludeTransitiveNeeds: c.IncludeTransitiveNeeds(),
}, func() {
}, func() []error {
matched, updated, es := a.apply(run, c)
mut.Lock()
@ -472,6 +480,7 @@ func (a *App) Apply(c ApplyConfigProvider) error {
mut.Unlock()
ok, errs = matched, es
return errs
})
if prepErr != nil {
@ -500,8 +509,9 @@ func (a *App) Status(c StatusesConfigProvider) error {
SkipRepos: true,
SkipDeps: true,
Concurrency: c.Concurrency(),
}, func() {
}, func() []error {
ok, errs = a.status(run, c)
return errs
})
if err != nil {
@ -522,8 +532,9 @@ func (a *App) Destroy(c DestroyConfigProvider) error {
Concurrency: c.Concurrency(),
DeleteWait: c.DeleteWait(),
DeleteTimeout: c.DeleteTimeout(),
}, func() {
}, func() []error {
ok, errs = a.delete(run, true, c)
return errs
})
if err != nil {
errs = append(errs, err)
@ -548,8 +559,9 @@ func (a *App) Test(c TestConfigProvider) error {
SkipRefresh: c.SkipRefresh(),
SkipDeps: c.SkipDeps(),
Concurrency: c.Concurrency(),
}, func() {
}, func() []error {
errs = a.test(run, c)
return errs
})
if err != nil {
@ -567,11 +579,12 @@ func (a *App) PrintDAGState(c DAGConfigProvider) error {
SkipRepos: true,
SkipDeps: true,
Concurrency: 2,
}, func() {
}, func() []error {
err = a.dag(run)
if err != nil {
errs = append(errs, err)
}
return errs
})
return ok, errs
}, false, false, SetFilter(true))
@ -583,7 +596,7 @@ func (a *App) PrintState(c StateConfigProvider) error {
SkipRepos: true,
SkipDeps: true,
Concurrency: 2,
}, func() {
}, func() []error {
if c.EmbedValues() {
for i := range run.state.Releases {
r := run.state.Releases[i]
@ -591,7 +604,7 @@ func (a *App) PrintState(c StateConfigProvider) error {
values, err := run.state.LoadYAMLForEmbedding(&r, r.Values, r.MissingFileHandler, r.ValuesPathPrefix)
if err != nil {
errs = []error{err}
return
return errs
}
run.state.Releases[i].Values = values
@ -599,7 +612,7 @@ func (a *App) PrintState(c StateConfigProvider) error {
secrets, err := run.state.LoadYAMLForEmbedding(&r, r.Secrets, r.MissingFileHandler, r.ValuesPathPrefix)
if err != nil {
errs = []error{err}
return
return errs
}
run.state.Releases[i].Secrets = secrets
@ -609,17 +622,18 @@ func (a *App) PrintState(c StateConfigProvider) error {
stateYaml, err := run.state.ToYaml()
if err != nil {
errs = []error{err}
return
return errs
}
sourceFile, err := run.state.FullFilePath()
if err != nil {
errs = []error{err}
return
return errs
}
fmt.Printf("---\n# Source: %s\n\n%+v", sourceFile, stateYaml)
errs = []error{}
return errs
})
if err != nil {
@ -648,26 +662,30 @@ func (a *App) ListReleases(c ListConfigProvider) error {
err := a.ForEachState(func(run *Run) (_ bool, errs []error) {
var stateReleases []*HelmRelease
var err error
var listErr error
if !c.SkipCharts() {
err = run.withPreparedCharts("list", state.ChartPrepareOptions{
prepErr := run.withPreparedCharts("list", state.ChartPrepareOptions{
SkipRepos: true,
SkipDeps: true,
Concurrency: 2,
}, func() {
}, func() []error {
rel, err := a.list(run)
if err != nil {
panic(err)
errs = append(errs, err)
return []error{err}
}
stateReleases = rel
return nil
})
if prepErr != nil {
errs = append(errs, prepErr)
}
} else {
stateReleases, err = a.list(run)
}
if err != nil {
errs = append(errs, err)
stateReleases, listErr = a.list(run)
if listErr != nil {
errs = append(errs, listErr)
}
}
if len(stateReleases) > 0 {
@ -709,14 +727,16 @@ func (a *App) ListReleases(c ListConfigProvider) error {
func (a *App) list(run *Run) ([]*HelmRelease, error) {
var releases []*HelmRelease
for _, r := range run.state.Releases {
resolvedState, err := run.state.ResolveDeps()
if err != nil {
return nil, fmt.Errorf("unable to resolve dependencies for %s: %w", run.state.FilePath, err)
}
for _, r := range resolvedState.Releases {
labels := ""
if r.Labels == nil {
r.Labels = map[string]string{}
}
for k, v := range run.state.CommonLabels {
r.Labels[k] = v
}
var keys []string
for k := range r.Labels {
@ -730,7 +750,7 @@ func (a *App) list(run *Run) ([]*HelmRelease, error) {
}
labels = strings.Trim(labels, ",")
enabled, err := state.ConditionEnabled(r, run.state.Values())
enabled, err := state.ConditionEnabled(r, resolvedState.Values())
if err != nil {
return nil, err
}
@ -1716,6 +1736,10 @@ Do you really want to apply?
// Traverse DAG of all the releases so that we don't suffer from false-positive missing dependencies
st.Releases = selectedAndNeededReleases
if len(releasesToBeUpdated) == 0 && len(releasesToBeDeleted) == 0 {
return true, false, nil
}
if !interactive || interactive && r.askForConfirmation(confMsg) {
if _, preapplyErrors := withDAG(st, helm, a.Logger, state.PlanOptions{Purpose: "invoking preapply hooks for", Reverse: true, SelectedReleases: toApplyWithNeeds, SkipNeeds: true}, a.WrapWithoutSelector(func(subst *state.HelmState, helm helmexec.Interface) []error {
for _, r := range subst.Releases {
@ -1773,6 +1797,7 @@ Do you really want to apply?
Wait: c.Wait(),
WaitRetries: c.WaitRetries(),
WaitForJobs: c.WaitForJobs(),
Timeout: c.Timeout(),
ReuseValues: c.ReuseValues(),
ResetValues: c.ResetValues(),
PostRenderer: c.PostRenderer(),
@ -1785,6 +1810,7 @@ Do you really want to apply?
TrackMode: c.TrackMode(),
TrackTimeout: c.TrackTimeout(),
TrackLogs: c.TrackLogs(),
Description: c.Description(),
}
return subst.SyncReleases(&affectedReleases, helm, c.Values(), c.Concurrency(), syncOpts)
}))
@ -2241,6 +2267,7 @@ Do you really want to sync?
Wait: c.Wait(),
WaitRetries: c.WaitRetries(),
WaitForJobs: c.WaitForJobs(),
Timeout: c.Timeout(),
ReuseValues: c.ReuseValues(),
ResetValues: c.ResetValues(),
PostRenderer: c.PostRenderer(),
@ -2253,6 +2280,7 @@ Do you really want to sync?
TrackMode: c.TrackMode(),
TrackTimeout: c.TrackTimeout(),
TrackLogs: c.TrackLogs(),
Description: c.Description(),
}
return subst.SyncReleases(&affectedReleases, helm, c.Values(), c.Concurrency(), opts)
}))

View File

@ -25,6 +25,7 @@ func TestApply_2(t *testing.T) {
fields fields
ns string
concurrency int
timeout int
skipDiffOnInstall bool
error string
files map[string]string
@ -84,6 +85,7 @@ func TestApply_2(t *testing.T) {
syncErr := app.Apply(applyConfig{
// if we check log output, concurrency must be 1. otherwise the test becomes non-deterministic.
concurrency: tc.concurrency,
timeout: tc.timeout,
logger: logger,
skipDiffOnInstall: tc.skipDiffOnInstall,
skipNeeds: tc.fields.skipNeeds,
@ -653,4 +655,30 @@ foo 4 Fri Nov 1 08:40:07 2019 DEPLOYED raw-3.1.0 3.1.0 default
concurrency: 1,
})
})
t.Run("timeout flag is passed to helm", func(t *testing.T) {
check(t, testcase{
files: map[string]string{
"/path/to/helmfile.yaml": `
releases:
- name: my-release
chart: incubator/raw
namespace: default
`,
},
timeout: 300,
concurrency: 1,
upgraded: []exectest.Release{
{Name: "my-release", Flags: []string{"--timeout", "300s", "--kube-context", "default", "--namespace", "default"}},
},
diffs: map[exectest.DiffKey]error{
{Name: "my-release", Chart: "incubator/raw", Flags: "--kube-context default --namespace default --reset-values --detailed-exitcode"}: helmexec.ExitError{Code: 2},
},
lists: map[exectest.ListKey]string{
{Filter: "^my-release$", Flags: listFlags("default", "default")}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
my-release 4 Fri Nov 1 08:40:07 2019 DEPLOYED raw-3.1.0 3.1.0 default
`,
},
})
})
}

View File

@ -2,6 +2,7 @@ package app
import (
"bytes"
"encoding/json"
"os"
"testing"
@ -301,3 +302,160 @@ func TestListWithJSONOutput(t *testing.T) {
testListWithJSONOutput(t, configImpl{skipCharts: true})
})
}
func TestListWithLockFileVersion(t *testing.T) {
files := map[string]string{
"/path/to/helmfile.yaml": `
repositories:
- name: bitnami
url: https://charts.bitnami.com/bitnami
releases:
- name: redis
namespace: default
chart: bitnami/redis
version: ">=1.0.0"
`,
"/path/to/helmfile.lock": `version: v0.0.0
digest: sha256:abc123
generated: "2024-01-01T00:00:00Z"
dependencies:
- name: redis
repository: https://charts.bitnami.com/bitnami
version: 17.0.7
`,
}
stdout := os.Stdout
defer func() { os.Stdout = stdout }()
var buffer bytes.Buffer
syncWriter := testhelper.NewSyncWriter(&buffer)
logger := helmexec.NewLogger(syncWriter, "debug")
valsRuntime, err := vals.New(vals.Options{CacheSize: 32})
if err != nil {
t.Fatalf("unexpected error creating vals runtime: %v", err)
}
app := appWithFs(&App{
OverrideHelmBinary: DefaultHelmBinary,
fs: ffs.DefaultFileSystem(),
OverrideKubeContext: "default",
DisableKubeVersionAutoDetection: true,
Env: "default",
Logger: logger,
valsRuntime: valsRuntime,
}, files)
expectNoCallsToHelm(app)
out, err := testutil.CaptureStdout(func() {
err := app.ListReleases(configImpl{skipCharts: true, output: "json"})
assert.Nil(t, err)
})
assert.NoError(t, err)
var releases []HelmRelease
if err := json.Unmarshal([]byte(out), &releases); err != nil {
t.Fatalf("failed to parse JSON output: %v", err)
}
assert.Len(t, releases, 1, "expected 1 release")
assert.Equal(t, "redis", releases[0].Name)
assert.Equal(t, "bitnami/redis", releases[0].Chart)
assert.Equal(t, "17.0.7", releases[0].Version, "expected version from helmfile.lock")
}
func TestListWithLockFileVersion_MultiFile(t *testing.T) {
files := map[string]string{
"/path/to/helmfile.d/first.yaml": `
repositories:
- name: bitnami
url: https://charts.bitnami.com/bitnami
releases:
- name: redis
namespace: default
chart: bitnami/redis
version: ">=1.0.0"
`,
"/path/to/helmfile.d/first.lock": `version: v0.0.0
digest: sha256:abc123
generated: "2024-01-01T00:00:00Z"
dependencies:
- name: redis
repository: https://charts.bitnami.com/bitnami
version: 17.0.7
`,
"/path/to/helmfile.d/second.yaml": `
repositories:
- name: bitnami
url: https://charts.bitnami.com/bitnami
releases:
- name: nginx
namespace: default
chart: bitnami/nginx
version: ">=1.0.0"
`,
"/path/to/helmfile.d/second.lock": `version: v0.0.0
digest: sha256:def456
generated: "2024-01-01T00:00:00Z"
dependencies:
- name: nginx
repository: https://charts.bitnami.com/bitnami
version: 15.0.0
`,
}
stdout := os.Stdout
defer func() { os.Stdout = stdout }()
var buffer bytes.Buffer
syncWriter := testhelper.NewSyncWriter(&buffer)
logger := helmexec.NewLogger(syncWriter, "debug")
valsRuntime, err := vals.New(vals.Options{CacheSize: 32})
if err != nil {
t.Fatalf("unexpected error creating vals runtime: %v", err)
}
app := appWithFs(&App{
OverrideHelmBinary: DefaultHelmBinary,
fs: ffs.DefaultFileSystem(),
OverrideKubeContext: "default",
DisableKubeVersionAutoDetection: true,
Env: "default",
Logger: logger,
valsRuntime: valsRuntime,
}, files)
expectNoCallsToHelm(app)
out, err := testutil.CaptureStdout(func() {
err := app.ListReleases(configImpl{skipCharts: true, output: "json"})
assert.Nil(t, err)
})
assert.NoError(t, err)
var releases []HelmRelease
if err := json.Unmarshal([]byte(out), &releases); err != nil {
t.Fatalf("failed to parse JSON output: %v", err)
}
assert.Len(t, releases, 2, "expected 2 releases")
releaseMap := make(map[string]HelmRelease)
for _, r := range releases {
releaseMap[r.Name] = r
}
redis := releaseMap["redis"]
assert.Equal(t, "bitnami/redis", redis.Chart)
assert.Equal(t, "17.0.7", redis.Version, "expected redis version from first.lock")
nginx := releaseMap["nginx"]
assert.Equal(t, "bitnami/nginx", nginx.Chart)
assert.Equal(t, "15.0.0", nginx.Version, "expected nginx version from second.lock")
}

View File

@ -25,6 +25,7 @@ func TestSync(t *testing.T) {
fields fields
ns string
concurrency int
timeout int
skipDiffOnInstall bool
error string
files map[string]string
@ -81,6 +82,7 @@ func TestSync(t *testing.T) {
syncErr := app.Sync(applyConfig{
concurrency: tc.concurrency,
timeout: tc.timeout,
logger: logger,
skipDiffOnInstall: tc.skipDiffOnInstall,
skipNeeds: tc.fields.skipNeeds,
@ -478,4 +480,27 @@ releases:
concurrency: 1,
})
})
t.Run("timeout flag is passed to helm", func(t *testing.T) {
check(t, testcase{
files: map[string]string{
"/path/to/helmfile.yaml": `
releases:
- name: my-release
chart: incubator/raw
namespace: default
`,
},
timeout: 600,
concurrency: 1,
upgraded: []exectest.Release{
{Name: "my-release", Flags: []string{"--timeout", "600s", "--kube-context", "default", "--namespace", "default"}},
},
lists: map[exectest.ListKey]string{
{Filter: "^my-release$", Flags: listFlags("default", "default")}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
my-release 4 Fri Nov 1 08:40:07 2019 DEPLOYED raw-3.1.0 3.1.0 default
`,
},
})
})
}

View File

@ -2388,6 +2388,7 @@ type applyConfig struct {
wait bool
waitRetries int
waitForJobs bool
timeout int
reuseValues bool
postRenderer string
postRendererArgs []string
@ -2428,6 +2429,10 @@ func (a applyConfig) WaitForJobs() bool {
return a.waitForJobs
}
func (a applyConfig) Timeout() int {
return a.timeout
}
func (a applyConfig) Values() []string {
return a.values
}
@ -2624,6 +2629,10 @@ func (a applyConfig) TrackLogs() bool {
return a.trackLogs
}
func (a applyConfig) Description() string {
return ""
}
type depsConfig struct {
skipRepos bool
includeNeeds bool

View File

@ -0,0 +1,121 @@
package app
import (
"sync"
"testing"
"github.com/helmfile/vals"
"github.com/stretchr/testify/assert"
"go.uber.org/zap"
"github.com/helmfile/helmfile/pkg/exectest"
ffs "github.com/helmfile/helmfile/pkg/filesystem"
"github.com/helmfile/helmfile/pkg/helmexec"
)
func TestCleanupHooksErrorPropagation(t *testing.T) {
type testcase struct {
files map[string]string
releaseName string
expectedError bool
expectedInLogs string
}
check := func(t *testing.T, tc testcase) {
t.Helper()
var helm = &exectest.Helm{
FailOnUnexpectedList: true,
FailOnUnexpectedDiff: true,
DiffMutex: &sync.Mutex{},
ChartsMutex: &sync.Mutex{},
ReleasesMutex: &sync.Mutex{},
}
valsRuntime, err := vals.New(vals.Options{CacheSize: 32})
if err != nil {
t.Fatalf("unexpected error creating vals runtime: %v", err)
}
bs := runWithLogCapture(t, "info", func(t *testing.T, logger *zap.SugaredLogger) {
t.Helper()
app := appWithFs(&App{
OverrideHelmBinary: DefaultHelmBinary,
fs: ffs.DefaultFileSystem(),
OverrideKubeContext: "default",
DisableKubeVersionAutoDetection: true,
Env: "default",
Logger: logger,
helms: map[helmKey]helmexec.Interface{
createHelmKey("helm", "default"): helm,
},
valsRuntime: valsRuntime,
}, tc.files)
syncErr := app.Sync(applyConfig{
concurrency: 1,
logger: logger,
})
if tc.expectedError {
assert.Error(t, syncErr, "expected error for release %s", tc.releaseName)
} else {
assert.NoError(t, syncErr, "unexpected error for release %s", tc.releaseName)
}
})
logOutput := bs.String()
assert.Contains(t, logOutput, tc.expectedInLogs, "unexpected log output")
}
t.Run("cleanup hook receives error when sync fails", func(t *testing.T) {
check(t, testcase{
releaseName: "error-release",
files: map[string]string{
"/path/to/helmfile.yaml": `
hooks:
- name: global-cleanup
events:
- cleanup
showlogs: true
command: echo
args:
- "error is '{{ .Event.Error }}'"
releases:
- name: error-release
chart: incubator/raw
namespace: default
`,
},
expectedError: true,
expectedInLogs: "error is 'failed processing release error-release: error'",
})
})
t.Run("cleanup hook receives nil when sync succeeds", func(t *testing.T) {
check(t, testcase{
releaseName: "success-release",
files: map[string]string{
"/path/to/helmfile.yaml": `
hooks:
- name: global-cleanup
events:
- cleanup
showlogs: true
command: echo
args:
- "error is '{{ .Event.Error }}'"
releases:
- name: success-release
chart: incubator/raw
namespace: default
`,
},
expectedError: false,
expectedInLogs: "error is '<nil>'",
})
})
}

View File

@ -61,6 +61,7 @@ type ApplyConfigProvider interface {
Wait() bool
WaitRetries() int
WaitForJobs() bool
Timeout() int
IncludeTests() bool
@ -93,6 +94,8 @@ type ApplyConfigProvider interface {
TrackTimeout() int
TrackLogs() bool
Description() string
concurrencyConfig
interactive
loggingConfig
@ -116,6 +119,7 @@ type SyncConfigProvider interface {
Wait() bool
WaitRetries() int
WaitForJobs() bool
Timeout() int
SyncArgs() string
Validate() bool
@ -129,6 +133,8 @@ type SyncConfigProvider interface {
TrackTimeout() int
TrackLogs() bool
Description() string
DAGConfig
concurrencyConfig

View File

@ -19,7 +19,7 @@ import (
const (
HelmRequiredVersion = "v3.18.6" // Minimum required version (supports Helm 3.x and 4.x)
HelmDiffRecommendedVersion = "v3.15.1"
HelmDiffRecommendedVersion = "v3.15.3"
HelmRecommendedVersion = "v4.1.0" // Recommended to use latest Helm 4
HelmSecretsRecommendedVersion = "v4.7.4" // v4.7.0+ works with both Helm 3 (single plugin) and Helm 4 (split plugin architecture)
HelmGitRecommendedVersion = "v1.3.0"

View File

@ -57,7 +57,7 @@ func (r *Run) prepareChartsIfNeeded(helmfileCommand string, dir string, concurre
return releaseToChart, nil
}
func (r *Run) withPreparedCharts(helmfileCommand string, opts state.ChartPrepareOptions, f func()) error {
func (r *Run) withPreparedCharts(helmfileCommand string, opts state.ChartPrepareOptions, f func() []error) error {
if r.ReleaseToChart != nil {
panic("Run.PrepareCharts can be called only once")
}
@ -119,9 +119,16 @@ func (r *Run) withPreparedCharts(helmfileCommand string, opts state.ChartPrepare
r.ReleaseToChart = releaseToChart
f()
errs := f()
var firstErr error
for _, e := range errs {
if e != nil {
firstErr = e
break
}
}
_, err = r.state.TriggerGlobalCleanupEvent(helmfileCommand)
_, err = r.state.TriggerGlobalCleanupEvent(helmfileCommand, firstErr)
return err
}

View File

@ -2,8 +2,3 @@ merged environment: &{default map[] map[] map[]}
2 release(s) found in helmfile.yaml
Checking release existence using `helm status` for release foo_notFound
invoking preapply hooks for 1 groups of releases in this order:
GROUP RELEASES
1 default//bar, default//foo_notFound
invoking preapply hooks for releases in group 1/1: default//bar, default//foo_notFound

View File

@ -1,10 +1,3 @@
merged environment: &{default map[] map[] map[]}
2 release(s) found in helmfile.yaml
invoking preapply hooks for 2 groups of releases in this order:
GROUP RELEASES
1 default//foo
2 default//bar
invoking preapply hooks for releases in group 1/2: default//foo
invoking preapply hooks for releases in group 2/2: default//bar

View File

@ -0,0 +1,21 @@
merged environment: &{default map[] map[] map[]}
1 release(s) found in helmfile.yaml
Affected releases are:
my-release (incubator/raw) UPDATED
invoking preapply hooks for 1 groups of releases in this order:
GROUP RELEASES
1 default/default/my-release
invoking preapply hooks for releases in group 1/1: default/default/my-release
processing 1 groups of releases in this order:
GROUP RELEASES
1 default/default/my-release
processing releases in group 1/1: default/default/my-release
UPDATED RELEASES:
NAME NAMESPACE CHART VERSION DURATION
my-release default incubator/raw 3.1.0 0s

View File

@ -1,9 +1,3 @@
hook[prepare] logs | foo
hook[prepare] logs |
hook[preapply] logs | foo
hook[preapply] logs |
hook[cleanup] logs | foo
hook[cleanup] logs |

View File

@ -57,6 +57,8 @@ type ApplyOptions struct {
WaitRetries int
// WaitForJobs is true if the helm command should wait for the jobs to be completed
WaitForJobs bool
// Timeout is the timeout for helm operations in seconds
Timeout int
// Propagate '--skip-schema-validation' to helmv3 template and helm install
SkipSchemaValidation bool
// ReuseValues is true if the helm command should reuse the values
@ -86,6 +88,8 @@ type ApplyOptions struct {
TrackTimeout int
// TrackLogs enables log streaming with kubedog
TrackLogs bool
// Description is the description that will be passed to helm upgrade --description
Description string
}
// NewApply creates a new Apply
@ -235,6 +239,11 @@ func (a *ApplyImpl) WaitForJobs() bool {
return a.ApplyOptions.WaitForJobs
}
// Timeout returns the timeout.
func (a *ApplyImpl) Timeout() int {
return a.ApplyOptions.Timeout
}
// ReuseValues returns the ReuseValues.
func (a *ApplyImpl) ReuseValues() bool {
if !a.ResetValues() {
@ -307,6 +316,11 @@ func (a *ApplyImpl) TrackLogs() bool {
return a.ApplyOptions.TrackLogs
}
// Description returns the description.
func (a *ApplyImpl) Description() string {
return a.ApplyOptions.Description
}
func (a *ApplyImpl) ValidateConfig() error {
validTrackModes := []string{"helm", "helm-legacy", "kubedog"}
if a.ApplyOptions.TrackMode != "" && !slices.Contains(validTrackModes, a.ApplyOptions.TrackMode) {

View File

@ -59,6 +59,8 @@ type SyncOptions struct {
TrackTimeout int
// TrackLogs enables log streaming with kubedog
TrackLogs bool
// Description is the description that will be passed to helm upgrade --description
Description string
}
// NewSyncOptions creates a new Apply
@ -214,6 +216,11 @@ func (t *SyncImpl) TrackLogs() bool {
return t.SyncOptions.TrackLogs
}
// Description returns the description.
func (t *SyncImpl) Description() string {
return t.SyncOptions.Description
}
func (t *SyncImpl) ValidateConfig() error {
validTrackModes := []string{"helm", "helm-legacy", "kubedog"}
if t.SyncOptions.TrackMode != "" && !slices.Contains(validTrackModes, t.SyncOptions.TrackMode) {

View File

@ -28,4 +28,10 @@ const (
// Can be overridden by AWS_SDK_GO_LOG_LEVEL environment variable
// See issue #2270 and vals PR #893
AWSSDKLogLevel = "HELMFILE_AWS_SDK_LOG_LEVEL"
// ValsFailOnMissingKeyInMap controls whether vals should fail when a key is missing in a map.
// When set to "true", vals returns an error if a referenced key does not exist in the secret map.
// Default is false for backward compatibility (returns empty string for missing keys).
// See issue #1563
ValsFailOnMissingKeyInMap = "HELMFILE_VALS_FAIL_ON_MISSING_KEY_IN_MAP"
)

View File

@ -1,8 +1,10 @@
package event
import (
"errors"
"fmt"
"io"
"strings"
"testing"
"go.uber.org/zap"
@ -13,6 +15,11 @@ import (
)
type runner struct {
executeCalls []struct {
cmd string
args []string
env map[string]string
}
}
func (r *runner) ExecuteStdIn(cmd string, args []string, env map[string]string, stdin io.Reader) ([]byte, error) {
@ -20,6 +27,11 @@ func (r *runner) ExecuteStdIn(cmd string, args []string, env map[string]string,
}
func (r *runner) Execute(cmd string, args []string, env map[string]string, enableLiveOutput bool) ([]byte, error) {
r.executeCalls = append(r.executeCalls, struct {
cmd string
args []string
env map[string]string
}{cmd: cmd, args: args, env: env})
if cmd == "ng" {
return nil, fmt.Errorf("cmd failed due to invalid cmd: %s", cmd)
}
@ -188,3 +200,119 @@ func TestTrigger(t *testing.T) {
}
}
}
func TestTriggerCleanupEventWithError(t *testing.T) {
runner := &runner{}
core, _ := observer.New(zap.InfoLevel)
logger := zap.New(core).Sugar()
testError := errors.New("sync failed: release error")
hooks := []Hook{
{
Name: "cleanup-with-error",
Events: []string{"cleanup"},
Command: "echo",
Args: []string{"error is '{{ .Event.Error }}'"},
ShowLogs: true,
},
}
bus := &Bus{
Hooks: hooks,
StateFilePath: "/path/to/helmfile.yaml",
BasePath: ".",
Namespace: "default",
Env: environment.Environment{Name: "default"},
Logger: logger,
Fs: ffs.DefaultFileSystem(),
Runner: runner,
}
data := map[string]any{
"HelmfileCommand": "sync",
}
executed, err := bus.Trigger("cleanup", testError, data)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !executed {
t.Fatal("expected cleanup hook to be executed")
}
if len(runner.executeCalls) != 1 {
t.Fatalf("expected 1 execute call, got %d", len(runner.executeCalls))
}
call := runner.executeCalls[0]
if call.cmd != "echo" {
t.Errorf("expected command 'echo', got %q", call.cmd)
}
if len(call.args) != 1 {
t.Fatalf("expected 1 arg, got %d", len(call.args))
}
expectedArg := "error is 'sync failed: release error'"
if !strings.Contains(call.args[0], "error is") {
t.Errorf("expected arg to contain 'error is', got %q", call.args[0])
}
if call.args[0] != expectedArg {
t.Errorf("expected arg %q, got %q", expectedArg, call.args[0])
}
}
func TestTriggerCleanupEventWithNilError(t *testing.T) {
runner := &runner{}
core, _ := observer.New(zap.InfoLevel)
logger := zap.New(core).Sugar()
hooks := []Hook{
{
Name: "cleanup-nil-error",
Events: []string{"cleanup"},
Command: "echo",
Args: []string{"error is '{{ .Event.Error }}'"},
ShowLogs: true,
},
}
bus := &Bus{
Hooks: hooks,
StateFilePath: "/path/to/helmfile.yaml",
BasePath: ".",
Namespace: "default",
Env: environment.Environment{Name: "default"},
Logger: logger,
Fs: ffs.DefaultFileSystem(),
Runner: runner,
}
data := map[string]any{
"HelmfileCommand": "sync",
}
executed, err := bus.Trigger("cleanup", nil, data)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !executed {
t.Fatal("expected cleanup hook to be executed")
}
if len(runner.executeCalls) != 1 {
t.Fatalf("expected 1 execute call, got %d", len(runner.executeCalls))
}
call := runner.executeCalls[0]
expectedArg := "error is '<nil>'"
if call.args[0] != expectedArg {
t.Errorf("expected arg %q, got %q", expectedArg, call.args[0])
}
}

View File

@ -1,8 +1,10 @@
package plugins
import (
"fmt"
"io"
"os"
"strconv"
"strings"
"sync"
@ -17,46 +19,82 @@ const (
)
var instance *vals.Runtime
var once sync.Once
var mu sync.Mutex
func buildValsOptions() (vals.Options, error) {
// Configure AWS SDK logging via HELMFILE_AWS_SDK_LOG_LEVEL environment variable
// Default: "off" to prevent sensitive information (tokens, auth headers) from being exposed
// See issue #2270 and vals PR helmfile/vals#893
//
// Valid values:
// - "off" (default): No AWS SDK logging - secure, prevents credential leakage
// - "minimal": Log retries only - minimal debugging info
// - "standard": Log retries + requests - moderate debugging (previous default)
// - "verbose": Log everything - full debugging (requests, responses, bodies, signing)
// - Custom: Comma-separated values like "request,response"
//
// Note: AWS_SDK_GO_LOG_LEVEL environment variable always takes precedence over this setting
// Note: Case-insensitive for known values like "off", "OFF", "Off"
logLevel := strings.TrimSpace(os.Getenv(envvar.AWSSDKLogLevel))
// Configure fail on missing key behavior
// Default to false for backward compatibility
// Set HELMFILE_VALS_FAIL_ON_MISSING_KEY_IN_MAP=true to enable strict mode
// Supports common boolean values: "true", "TRUE", "1", etc.
// See issue #1563
envVal := strings.TrimSpace(os.Getenv(envvar.ValsFailOnMissingKeyInMap))
var failOnMissingKey bool
if envVal != "" {
var err error
failOnMissingKey, err = strconv.ParseBool(envVal)
if err != nil {
return vals.Options{}, fmt.Errorf("invalid value for %s: %q (must be a valid boolean)", envvar.ValsFailOnMissingKeyInMap, envVal)
}
}
// Default to "off" for security if not specified
if logLevel == "" {
logLevel = "off"
}
// Normalize known values to lowercase for case-insensitive handling
if strings.EqualFold(logLevel, "off") {
logLevel = "off"
}
opts := vals.Options{
CacheSize: valsCacheSize,
FailOnMissingKeyInMap: failOnMissingKey,
AWSLogLevel: logLevel,
}
// Also suppress vals' own internal logging unless user wants verbose output
// This prevents vals' log messages (separate from AWS SDK logs) from exposing credentials
if logLevel == "off" {
opts.LogOutput = io.Discard
}
// For other levels, allow vals to log to default output for debugging
return opts, nil
}
func ValsInstance() (*vals.Runtime, error) {
var err error
once.Do(func() {
// Configure AWS SDK logging via HELMFILE_AWS_SDK_LOG_LEVEL environment variable
// Default: "off" to prevent sensitive information (tokens, auth headers) from being exposed
// See issue #2270 and vals PR helmfile/vals#893
//
// Valid values:
// - "off" (default): No AWS SDK logging - secure, prevents credential leakage
// - "minimal": Log retries only - minimal debugging info
// - "standard": Log retries + requests - moderate debugging (previous default)
// - "verbose": Log everything - full debugging (requests, responses, bodies, signing)
// - Custom: Comma-separated values like "request,response"
//
// Note: AWS_SDK_GO_LOG_LEVEL environment variable always takes precedence over this setting
logLevel := strings.TrimSpace(os.Getenv(envvar.AWSSDKLogLevel))
mu.Lock()
defer mu.Unlock()
opts := vals.Options{
CacheSize: valsCacheSize,
}
if instance != nil {
return instance, nil
}
// Default to "off" for security if not specified
if logLevel == "" {
logLevel = "off"
}
opts, err := buildValsOptions()
if err != nil {
return nil, err
}
// Set AWS SDK log level for vals library
opts.AWSLogLevel = logLevel
instance, err = vals.New(opts)
if err != nil {
return nil, err
}
// Also suppress vals' own internal logging unless user wants verbose output
// This prevents vals' log messages (separate from AWS SDK logs) from exposing credentials
if logLevel == "off" {
opts.LogOutput = io.Discard
}
// For other levels, allow vals to log to default output for debugging
instance, err = vals.New(opts)
})
return instance, err
return instance, nil
}

View File

@ -2,11 +2,11 @@ package plugins
import (
"io"
"os"
"strings"
"testing"
"github.com/helmfile/vals"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/helmfile/helmfile/pkg/envvar"
)
@ -25,19 +25,165 @@ func TestValsInstance(t *testing.T) {
}
}
// TestAWSSDKLogLevelConfiguration tests the AWS SDK log level configuration logic
func TestBuildValsOptions(t *testing.T) {
tests := []struct {
name string
awsLogLevel string
failOnMissingKey string
expectedLogLevel string
expectedFailOnMissingKey bool
expectedLogOutputDiscarded bool
expectError bool
}{
{
name: "defaults",
awsLogLevel: "",
failOnMissingKey: "",
expectedLogLevel: "off",
expectedFailOnMissingKey: false,
expectedLogOutputDiscarded: true,
},
{
name: "explicit failOnMissingKey true",
awsLogLevel: "",
failOnMissingKey: "true",
expectedLogLevel: "off",
expectedFailOnMissingKey: true,
expectedLogOutputDiscarded: true,
},
{
name: "failOnMissingKey false",
awsLogLevel: "",
failOnMissingKey: "false",
expectedLogLevel: "off",
expectedFailOnMissingKey: false,
expectedLogOutputDiscarded: true,
},
{
name: "failOnMissingKey with whitespace",
awsLogLevel: "",
failOnMissingKey: " true ",
expectedLogLevel: "off",
expectedFailOnMissingKey: true,
expectedLogOutputDiscarded: true,
},
{
name: "failOnMissingKey uppercase TRUE",
awsLogLevel: "",
failOnMissingKey: "TRUE",
expectedLogLevel: "off",
expectedFailOnMissingKey: true,
expectedLogOutputDiscarded: true,
},
{
name: "failOnMissingKey numeric 1",
awsLogLevel: "",
failOnMissingKey: "1",
expectedLogLevel: "off",
expectedFailOnMissingKey: true,
expectedLogOutputDiscarded: true,
},
{
name: "failOnMissingKey numeric 0",
awsLogLevel: "",
failOnMissingKey: "0",
expectedLogLevel: "off",
expectedFailOnMissingKey: false,
expectedLogOutputDiscarded: true,
},
{
name: "failOnMissingKey invalid value",
awsLogLevel: "",
failOnMissingKey: "invalid",
expectError: true,
},
{
name: "aws log level verbose",
awsLogLevel: "verbose",
failOnMissingKey: "",
expectedLogLevel: "verbose",
expectedFailOnMissingKey: false,
expectedLogOutputDiscarded: false,
},
{
name: "aws log level with whitespace",
awsLogLevel: " minimal ",
failOnMissingKey: "",
expectedLogLevel: "minimal",
expectedFailOnMissingKey: false,
expectedLogOutputDiscarded: false,
},
{
name: "aws log level OFF uppercase",
awsLogLevel: "OFF",
failOnMissingKey: "",
expectedLogLevel: "off",
expectedFailOnMissingKey: false,
expectedLogOutputDiscarded: true,
},
{
name: "aws log level Off mixed case",
awsLogLevel: "Off",
failOnMissingKey: "",
expectedLogLevel: "off",
expectedFailOnMissingKey: false,
expectedLogOutputDiscarded: true,
},
{
name: "aws log level Off mixed case",
awsLogLevel: "Off",
failOnMissingKey: "",
expectedLogLevel: "off",
expectedFailOnMissingKey: false,
expectedLogOutputDiscarded: true,
},
{
name: "both options set",
awsLogLevel: "standard",
failOnMissingKey: "true",
expectedLogLevel: "standard",
expectedFailOnMissingKey: true,
expectedLogOutputDiscarded: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Setenv(envvar.AWSSDKLogLevel, tt.awsLogLevel)
t.Setenv(envvar.ValsFailOnMissingKeyInMap, tt.failOnMissingKey)
opts, err := buildValsOptions()
if tt.expectError {
require.Error(t, err)
assert.Contains(t, err.Error(), envvar.ValsFailOnMissingKeyInMap)
return
}
require.NoError(t, err)
assert.Equal(t, tt.expectedLogLevel, opts.AWSLogLevel)
assert.Equal(t, tt.expectedFailOnMissingKey, opts.FailOnMissingKeyInMap)
assert.Equal(t, valsCacheSize, opts.CacheSize)
isDiscarded := opts.LogOutput == io.Discard
assert.Equal(t, tt.expectedLogOutputDiscarded, isDiscarded)
})
}
}
func TestAWSSDKLogLevelConfiguration(t *testing.T) {
tests := []struct {
name string
envValue string
expectedLogLevel string
expectedLogOutput bool // true if LogOutput should be io.Discard
expectedLogOutput bool
}{
{
name: "no env var defaults to off",
envValue: "",
expectedLogLevel: "off",
expectedLogOutput: true, // LogOutput should be io.Discard
expectedLogOutput: true,
},
{
name: "explicit off",
@ -45,11 +191,17 @@ func TestAWSSDKLogLevelConfiguration(t *testing.T) {
expectedLogLevel: "off",
expectedLogOutput: true,
},
{
name: "OFF uppercase",
envValue: "OFF",
expectedLogLevel: "off",
expectedLogOutput: true,
},
{
name: "minimal logging",
envValue: "minimal",
expectedLogLevel: "minimal",
expectedLogOutput: false, // LogOutput should NOT be io.Discard
expectedLogOutput: false,
},
{
name: "standard logging",
@ -73,94 +225,34 @@ func TestAWSSDKLogLevelConfiguration(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Note: This test verifies the configuration logic, not the actual vals.New() call
// since ValsInstance() uses sync.Once and can only be initialized once per test run.
t.Setenv(envvar.AWSSDKLogLevel, tt.envValue)
// Simulate the logic from ValsInstance()
var logLevel string
if tt.envValue != "" {
logLevel = strings.TrimSpace(tt.envValue)
}
opts, err := buildValsOptions()
require.NoError(t, err)
// Default to "off" for security if not specified
if logLevel == "" {
logLevel = "off"
}
assert.Equal(t, tt.expectedLogLevel, opts.AWSLogLevel)
// Verify expected log level
if logLevel != tt.expectedLogLevel {
t.Errorf("Expected log level %q, got %q", tt.expectedLogLevel, logLevel)
}
// Verify LogOutput configuration logic
opts := vals.Options{
CacheSize: valsCacheSize,
}
opts.AWSLogLevel = logLevel
// Verify LogOutput is set to io.Discard only when level is "off"
if tt.expectedLogOutput {
opts.LogOutput = io.Discard
if opts.LogOutput != io.Discard {
t.Error("Expected LogOutput to be io.Discard for 'off' level")
}
}
isDiscarded := opts.LogOutput == io.Discard
assert.Equal(t, tt.expectedLogOutput, isDiscarded)
})
}
}
// TestEnvironmentVariableReading verifies that the HELMFILE_AWS_SDK_LOG_LEVEL env var is read correctly
func TestEnvironmentVariableReading(t *testing.T) {
tests := []struct {
name string
envValue string
expectedValue string
}{
{
name: "empty defaults to off",
envValue: "",
expectedValue: "off",
},
{
name: "whitespace trimmed",
envValue: " minimal ",
expectedValue: "minimal",
},
{
name: "standard value preserved",
envValue: "standard",
expectedValue: "standard",
},
}
func TestBuildValsOptionsIntegration(t *testing.T) {
t.Run("valid configuration produces working vals options", func(t *testing.T) {
t.Setenv(envvar.AWSSDKLogLevel, "off")
t.Setenv(envvar.ValsFailOnMissingKeyInMap, "true")
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Save and restore env var
original := os.Getenv(envvar.AWSSDKLogLevel)
defer func() {
if original == "" {
os.Unsetenv(envvar.AWSSDKLogLevel)
} else {
os.Setenv(envvar.AWSSDKLogLevel, original)
}
}()
opts, err := buildValsOptions()
require.NoError(t, err)
// Set test env var
if tt.envValue == "" {
os.Unsetenv(envvar.AWSSDKLogLevel)
} else {
os.Setenv(envvar.AWSSDKLogLevel, tt.envValue)
}
assert.Equal(t, valsCacheSize, opts.CacheSize)
assert.Equal(t, "off", opts.AWSLogLevel)
assert.True(t, opts.FailOnMissingKeyInMap)
assert.Equal(t, io.Discard, opts.LogOutput)
// Read and process like ValsInstance() does
logLevel := strings.TrimSpace(os.Getenv(envvar.AWSSDKLogLevel))
if logLevel == "" {
logLevel = "off"
}
if logLevel != tt.expectedValue {
t.Errorf("Expected %q, got %q", tt.expectedValue, logLevel)
}
})
}
rt, err := vals.New(opts)
require.NoError(t, err)
assert.NotNil(t, rt)
})
}

View File

@ -140,8 +140,15 @@ func (st *HelmState) mergeLockedDependencies() (*HelmState, error) {
// When basePath is set (e.g. when loaded with baseDir instead of os.Chdir),
// resolve the lock file path relative to basePath so it can be found
// without changing the working directory.
if lockFile != "" && st.basePath != "" && !filepath.IsAbs(lockFile) {
lockFile = filepath.Join(st.basePath, lockFile)
switch {
case lockFile != "":
if st.basePath != "" && !filepath.IsAbs(lockFile) {
lockFile = filepath.Join(st.basePath, lockFile)
}
case st.basePath != "":
// When no custom lockfile is specified, use the default lockfile name
// joined with basePath to ensure it's found when not changing CWD.
lockFile = filepath.Join(st.basePath, filename+".lock")
}
depMan := NewChartDependencyManager(filename, st.logger, lockFile)
@ -258,8 +265,13 @@ func getUnresolvedDependenciess(st *HelmState) (string, *UnresolvedDependencies)
func updateDependencies(st *HelmState, shell helmexec.DependencyUpdater, unresolved *UnresolvedDependencies, filename, wd string) (*HelmState, error) {
lockFile := st.LockFile
if lockFile != "" && st.basePath != "" && !filepath.IsAbs(lockFile) {
lockFile = filepath.Join(st.basePath, lockFile)
switch {
case lockFile != "":
if st.basePath != "" && !filepath.IsAbs(lockFile) {
lockFile = filepath.Join(st.basePath, lockFile)
}
case st.basePath != "":
lockFile = filepath.Join(st.basePath, filename+".lock")
}
depMan := NewChartDependencyManager(filename, st.logger, lockFile)

View File

@ -353,7 +353,10 @@ type ReleaseSpec struct {
UnitTests []string `yaml:"unitTests,omitempty"`
// Name is the name of this release
Name string `yaml:"name,omitempty"`
Name string `yaml:"name,omitempty"`
// Description is the description for this release that will be passed to helm upgrade with --description flag
Description string `yaml:"description,omitempty"`
Namespace string `yaml:"namespace,omitempty"`
Labels map[string]string `yaml:"labels,omitempty"`
Values []any `yaml:"values,omitempty"`
@ -917,6 +920,7 @@ type SyncOpts struct {
TrackMode string
TrackTimeout int
TrackLogs bool
Description string
}
type SyncOpt interface{ Apply(*SyncOpts) }
@ -3158,8 +3162,8 @@ func (st *HelmState) TriggerGlobalPrepareEvent(helmfileCommand string) (bool, er
return st.triggerGlobalReleaseEvent("prepare", nil, helmfileCommand)
}
func (st *HelmState) TriggerGlobalCleanupEvent(helmfileCommand string) (bool, error) {
return st.triggerGlobalReleaseEvent("cleanup", nil, helmfileCommand)
func (st *HelmState) TriggerGlobalCleanupEvent(helmfileCommand string, evtErr error) (bool, error) {
return st.triggerGlobalReleaseEvent("cleanup", evtErr, helmfileCommand)
}
func (st *HelmState) triggerGlobalReleaseEvent(evt string, evtErr error, helmfileCmd string) (bool, error) {
@ -3447,6 +3451,28 @@ func (st *HelmState) appendChartDownloadFlags(flags []string, release *ReleaseSp
return flags
}
// appendDescriptionFlags appends the helm command-line flag for release description
// Command line takes precedence over config file
func (st *HelmState) appendDescriptionFlags(flags []string, release *ReleaseSpec, opt *SyncOpts, helm helmexec.Interface) ([]string, error) {
description := release.Description
if opt != nil && opt.Description != "" {
description = opt.Description
}
if description != "" {
if !helm.IsVersionAtLeast("3.3.0") {
// Determine error message based on source
if opt != nil && opt.Description != "" {
return nil, fmt.Errorf("--description flag requires Helm 3.3.0 or greater")
}
return nil, fmt.Errorf("releases[].description requires Helm 3.3.0 or greater")
}
flags = append(flags, "--description", description)
}
return flags, nil
}
func (st *HelmState) needsPlainHttp(release *ReleaseSpec, repo *RepositorySpec) bool {
var repoPlainHttp, relPlainHttp bool
if repo != nil {
@ -3576,6 +3602,11 @@ func (st *HelmState) flagsForUpgrade(helm helmexec.Interface, release *ReleaseSp
flags = st.appendPostRenderFlags(flags, release, postRenderer, helm)
flags, err := st.appendDescriptionFlags(flags, release, opt, helm)
if err != nil {
return nil, nil, err
}
var postRendererArgs []string
if opt != nil {
postRendererArgs = opt.PostRendererArgs

View File

@ -908,6 +908,188 @@ func TestHelmState_flagsForUpgrade(t *testing.T) {
"--namespace", "test-namespace",
},
},
{
name: "description-from-release",
defaults: HelmSpec{
Verify: false,
CreateNamespace: &disable,
},
version: semver.MustParse("3.10.0"),
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Name: "test-charts",
Namespace: "test-namespace",
Description: "Release description from config",
},
syncOpts: &SyncOpts{},
want: []string{
"--version", "0.1",
"--description", "Release description from config",
"--namespace", "test-namespace",
},
},
{
name: "description-from-cli",
defaults: HelmSpec{
Verify: false,
CreateNamespace: &disable,
},
version: semver.MustParse("3.10.0"),
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Name: "test-charts",
Namespace: "test-namespace",
},
syncOpts: &SyncOpts{
Description: "CLI description from --description flag",
},
want: []string{
"--version", "0.1",
"--description", "CLI description from --description flag",
"--namespace", "test-namespace",
},
},
{
name: "description-cli-overrides-release",
defaults: HelmSpec{
Verify: false,
CreateNamespace: &disable,
},
version: semver.MustParse("3.10.0"),
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Name: "test-charts",
Namespace: "test-namespace",
Description: "Release description from config",
},
syncOpts: &SyncOpts{
Description: "CLI description overrides config",
},
want: []string{
"--version", "0.1",
"--description", "CLI description overrides config",
"--namespace", "test-namespace",
},
},
{
name: "description-empty-string-not-passed",
defaults: HelmSpec{
Verify: false,
CreateNamespace: &disable,
},
version: semver.MustParse("3.10.0"),
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Name: "test-charts",
Namespace: "test-namespace",
Description: "",
},
syncOpts: &SyncOpts{
Description: "",
},
want: []string{
"--version", "0.1",
"--namespace", "test-namespace",
},
},
{
name: "description-from-config-unsupported-version-3.1.0",
defaults: HelmSpec{
Verify: false,
CreateNamespace: &disable,
},
version: semver.MustParse("3.1.0"),
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Name: "test-charts",
Namespace: "test-namespace",
Description: "Release description from config",
},
syncOpts: &SyncOpts{},
wantErr: "releases[].description requires Helm 3.3.0 or greater",
},
{
name: "description-from-config-unsupported-version-3.2.4",
defaults: HelmSpec{
Verify: false,
CreateNamespace: &disable,
},
version: semver.MustParse("3.2.4"),
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Name: "test-charts",
Namespace: "test-namespace",
Description: "Release description from config",
},
syncOpts: &SyncOpts{},
wantErr: "releases[].description requires Helm 3.3.0 or greater",
},
{
name: "description-from-cli-unsupported-version",
defaults: HelmSpec{
Verify: false,
CreateNamespace: &disable,
},
version: semver.MustParse("3.2.0"),
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Name: "test-charts",
Namespace: "test-namespace",
},
syncOpts: &SyncOpts{
Description: "CLI description from --description flag",
},
wantErr: "--description flag requires Helm 3.3.0 or greater",
},
{
name: "description-empty-on-old-version",
defaults: HelmSpec{
Verify: false,
CreateNamespace: &disable,
},
version: semver.MustParse("3.1.0"),
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Name: "test-charts",
Namespace: "test-namespace",
// No description set
},
syncOpts: &SyncOpts{},
want: []string{
"--version", "0.1",
"--namespace", "test-namespace",
// No --description flag should appear
},
},
{
name: "description-from-config-supported-version-3.3.0",
defaults: HelmSpec{
Verify: false,
CreateNamespace: &disable,
},
version: semver.MustParse("3.3.0"),
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Name: "test-charts",
Namespace: "test-namespace",
Description: "Release description from config",
},
syncOpts: &SyncOpts{},
want: []string{
"--version", "0.1",
"--description", "Release description from config",
"--namespace", "test-namespace",
},
},
}
for i := range tests {
tt := tests[i]
@ -2548,10 +2730,10 @@ generated: 2019-05-16T15:42:45.50486+09:00
}
logger := helmexec.NewLogger(io.Discard, "debug")
basePath := "/src"
basePath := filepath.ToSlash(t.TempDir())
state := &HelmState{
basePath: basePath,
FilePath: "/src/helmfile.yaml",
FilePath: filepath.Join(basePath, "helmfile.yaml"),
ReleaseSetSpec: ReleaseSetSpec{
Releases: []ReleaseSpec{
{
@ -2584,8 +2766,8 @@ generated: 2019-05-16T15:42:45.50486+09:00
}
fs := testhelper.NewTestFs(map[string]string{
"/example/Chart.yaml": `foo: FOO`,
"/src/example/Chart.yaml": `foo: FOO`,
"/example/Chart.yaml": `foo: FOO`,
filepath.Join(basePath, "example/Chart.yaml"): `foo: FOO`,
})
fs.Cwd = basePath
state = injectFs(state, fs)
@ -2648,7 +2830,7 @@ func TestHelmState_ResolveDeps_NoLockFile(t *testing.T) {
logger: logger,
fs: &filesystem.FileSystem{
ReadFile: func(f string) ([]byte, error) {
if f != "helmfile.lock" {
if f != filepath.Join("/src", "helmfile.lock") {
return nil, fmt.Errorf("stub: unexpected file: %s", f)
}
return nil, os.ErrNotExist

View File

@ -38,39 +38,39 @@ func TestGenerateID(t *testing.T) {
run(testcase{
subject: "baseline",
release: ReleaseSpec{Name: "foo", Chart: "incubator/raw"},
want: "foo-values-5bc9c89c6b",
want: "foo-values-6ccb848dcd",
})
run(testcase{
subject: "different bytes content",
release: ReleaseSpec{Name: "foo", Chart: "incubator/raw"},
data: []byte(`{"k":"v"}`),
want: "foo-values-7bf9c8bcdf",
want: "foo-values-5bcbbc4c85",
})
run(testcase{
subject: "different map content",
release: ReleaseSpec{Name: "foo", Chart: "incubator/raw"},
data: map[string]any{"k": "v"},
want: "foo-values-65694d8947",
want: "foo-values-7c6468f955",
})
run(testcase{
subject: "different chart",
release: ReleaseSpec{Name: "foo", Chart: "stable/envoy"},
want: "foo-values-856c5f7dd5",
want: "foo-values-8645f5847f",
})
run(testcase{
subject: "different name",
release: ReleaseSpec{Name: "bar", Chart: "incubator/raw"},
want: "bar-values-fff55fbf5",
want: "bar-values-54bd8c865",
})
run(testcase{
subject: "specific ns",
release: ReleaseSpec{Name: "foo", Chart: "incubator/raw", Namespace: "myns"},
want: "myns-foo-values-6bfbb74765",
want: "myns-foo-values-b4849b445",
})
for id, n := range ids {

View File

@ -27,7 +27,7 @@ export HELM_DATA_HOME="${helm_dir}/data"
export HELM_HOME="${HELM_DATA_HOME}"
export HELM_PLUGINS="${HELM_DATA_HOME}/plugins"
export HELM_CONFIG_HOME="${helm_dir}/config"
HELM_DIFF_VERSION="${HELM_DIFF_VERSION:-3.15.1}"
HELM_DIFF_VERSION="${HELM_DIFF_VERSION:-3.15.3}"
HELM_GIT_VERSION="${HELM_GIT_VERSION:-1.4.1}"
HELM_SECRETS_VERSION="${HELM_SECRETS_VERSION:-4.7.4}"
export GNUPGHOME="${PWD}/${dir}/.gnupg"