Merge branch 'main' into feat/print-env

This commit is contained in:
yxxhero 2025-11-23 17:33:13 +08:00 committed by GitHub
commit c7b063cc86
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
97 changed files with 2763 additions and 523 deletions

View File

@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Free Disk Space
uses: jlumbroso/free-disk-space@main
with:
@ -39,7 +39,7 @@ jobs:
matrix:
helm-version: [v3.18.6, v3.19.2, v4.0.0]
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Free Disk Space
@ -96,38 +96,38 @@ jobs:
- helm-version: v3.18.6
kustomize-version: v5.8.0
plugin-secrets-version: 4.7.0
plugin-diff-version: 3.14.0
plugin-diff-version: 3.14.1
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.0
plugin-diff-version: 3.14.0
plugin-diff-version: 3.14.1
extra-helmfile-flags: '--enable-live-output'
- helm-version: v3.19.2
kustomize-version: v5.8.0
plugin-secrets-version: 4.7.0
plugin-diff-version: 3.14.0
plugin-diff-version: 3.14.1
extra-helmfile-flags: ''
- helm-version: v3.19.2
kustomize-version: v5.8.0
plugin-secrets-version: 4.7.0
plugin-diff-version: 3.14.0
plugin-diff-version: 3.14.1
extra-helmfile-flags: '--enable-live-output'
# Helmfile now supports both Helm 3.x and Helm 4.x
- helm-version: v4.0.0
kustomize-version: v5.8.0
plugin-secrets-version: 4.7.0
plugin-diff-version: 3.14.0
plugin-diff-version: 3.14.1
extra-helmfile-flags: ''
- helm-version: v4.0.0
kustomize-version: v5.8.0
plugin-secrets-version: 4.7.0
plugin-diff-version: 3.14.0
plugin-diff-version: 3.14.1
extra-helmfile-flags: '--enable-live-output'
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Free Disk Space
uses: jlumbroso/free-disk-space@main
with:
@ -175,7 +175,7 @@ jobs:
needs: tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
fetch-depth: 0
- uses: actions/download-artifact@v6

View File

@ -39,7 +39,7 @@ jobs:
suffix: "-ubuntu"
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
fetch-depth: 0

View File

@ -22,7 +22,7 @@ jobs:
goreleaser:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: check disk usage

View File

@ -93,7 +93,7 @@ RUN set -x && \
[ "$(age --version)" = "${AGE_VERSION}" ] && \
[ "$(age-keygen --version)" = "${AGE_VERSION}" ]
RUN helm plugin install https://github.com/databus23/helm-diff --version v3.14.0 --verify=false && \
RUN helm plugin install https://github.com/databus23/helm-diff --version v3.14.1 --verify=false && \
helm plugin install https://github.com/jkroepke/helm-secrets/releases/download/v4.7.0/helm-secrets.tgz --verify=false && \
helm plugin install https://github.com/jkroepke/helm-secrets/releases/download/v4.7.0/helm-secrets-getter.tgz --verify=false && \
helm plugin install https://github.com/jkroepke/helm-secrets/releases/download/v4.7.0/helm-secrets-post-renderer.tgz --verify=false && \

View File

@ -102,7 +102,7 @@ RUN set -x && \
[ "$(age --version)" = "${AGE_VERSION}" ] && \
[ "$(age-keygen --version)" = "${AGE_VERSION}" ]
RUN helm plugin install https://github.com/databus23/helm-diff --version v3.14.0 --verify=false && \
RUN helm plugin install https://github.com/databus23/helm-diff --version v3.14.1 --verify=false && \
helm plugin install https://github.com/jkroepke/helm-secrets/releases/download/v4.7.0/helm-secrets.tgz --verify=false && \
helm plugin install https://github.com/jkroepke/helm-secrets/releases/download/v4.7.0/helm-secrets-getter.tgz --verify=false && \
helm plugin install https://github.com/jkroepke/helm-secrets/releases/download/v4.7.0/helm-secrets-post-renderer.tgz --verify=false && \

View File

@ -102,7 +102,7 @@ RUN set -x && \
[ "$(age --version)" = "${AGE_VERSION}" ] && \
[ "$(age-keygen --version)" = "${AGE_VERSION}" ]
RUN helm plugin install https://github.com/databus23/helm-diff --version v3.14.0 --verify=false && \
RUN helm plugin install https://github.com/databus23/helm-diff --version v3.14.1 --verify=false && \
helm plugin install https://github.com/jkroepke/helm-secrets/releases/download/v4.7.0/helm-secrets.tgz --verify=false && \
helm plugin install https://github.com/jkroepke/helm-secrets/releases/download/v4.7.0/helm-secrets-getter.tgz --verify=false && \
helm plugin install https://github.com/jkroepke/helm-secrets/releases/download/v4.7.0/helm-secrets-post-renderer.tgz --verify=false && \

35
go.mod
View File

@ -6,8 +6,8 @@ require (
dario.cat/mergo v1.0.2
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.31.20
github.com/aws/aws-sdk-go-v2/service/s3 v1.90.2
github.com/aws/aws-sdk-go-v2/config v1.32.0
github.com/aws/aws-sdk-go-v2/service/s3 v1.92.0
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc
github.com/go-test/deep v1.1.1
github.com/golang/mock v1.6.0
@ -26,7 +26,7 @@ require (
github.com/zclconf/go-cty v1.17.0
github.com/zclconf/go-cty-yaml v1.1.0
go.szostok.io/version v1.2.0
go.uber.org/zap v1.27.0
go.uber.org/zap v1.27.1
go.yaml.in/yaml/v2 v2.4.3
go.yaml.in/yaml/v3 v3.0.4
golang.org/x/sync v0.18.0
@ -34,6 +34,7 @@ require (
helm.sh/helm/v3 v3.19.2
helm.sh/helm/v4 v4.0.0
k8s.io/apimachinery v0.34.2
k8s.io/client-go v0.34.2
)
require (
@ -145,25 +146,26 @@ require (
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de // indirect
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
github.com/atotto/clipboard v0.1.4 // indirect
github.com/aws/aws-sdk-go-v2 v1.39.6 // indirect
github.com/aws/aws-sdk-go-v2 v1.40.0 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.3 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.18.24 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.19.0 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.14 // indirect
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.19.9 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.13 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.13 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.13 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.14 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.4 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.13 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.5 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.14 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.14 // indirect
github.com/aws/aws-sdk-go-v2/service/kms v1.46.1 // indirect
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.39.7 // indirect
github.com/aws/aws-sdk-go-v2/service/signin v1.0.1 // indirect
github.com/aws/aws-sdk-go-v2/service/ssm v1.66.2 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.30.3 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.7 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.40.2 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.30.4 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.8 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.41.1 // indirect
github.com/aws/smithy-go v1.23.2 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/blang/semver/v4 v4.0.0 // indirect
@ -322,11 +324,10 @@ require (
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/api v0.34.1 // indirect
k8s.io/api v0.34.2 // indirect
k8s.io/apiextensions-apiserver v0.34.1 // indirect
k8s.io/apiserver v0.34.1 // indirect
k8s.io/cli-runtime v0.34.1 // indirect
k8s.io/client-go v0.34.1 // indirect
k8s.io/component-base v0.34.1 // indirect
k8s.io/klog/v2 v2.130.1 // indirect
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b // indirect

70
go.sum
View File

@ -141,48 +141,50 @@ github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
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.39.6 h1:2JrPCVgWJm7bm83BDwY5z8ietmeJUbh3O2ACnn+Xsqk=
github.com/aws/aws-sdk-go-v2 v1.39.6/go.mod h1:c9pm7VwuW0UPxAEYGyTmyurVcNrbF6Rt/wixFqDhcjE=
github.com/aws/aws-sdk-go-v2 v1.40.0 h1:/WMUA0kjhZExjOQN2z3oLALDREea1A7TobfuiBrKlwc=
github.com/aws/aws-sdk-go-v2 v1.40.0/go.mod h1:c9pm7VwuW0UPxAEYGyTmyurVcNrbF6Rt/wixFqDhcjE=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.3 h1:DHctwEM8P8iTXFxC/QK0MRjwEpWQeM9yzidCRjldUz0=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.3/go.mod h1:xdCzcZEtnSTKVDOmUZs4l/j3pSV6rpo1WXl5ugNsL8Y=
github.com/aws/aws-sdk-go-v2/config v1.31.20 h1:/jWF4Wu90EhKCgjTdy1DGxcbcbNrjfBHvksEL79tfQc=
github.com/aws/aws-sdk-go-v2/config v1.31.20/go.mod h1:95Hh1Tc5VYKL9NJ7tAkDcqeKt+MCXQB1hQZaRdJIZE0=
github.com/aws/aws-sdk-go-v2/credentials v1.18.24 h1:iJ2FmPT35EaIB0+kMa6TnQ+PwG5A1prEdAw+PsMzfHg=
github.com/aws/aws-sdk-go-v2/credentials v1.18.24/go.mod h1:U91+DrfjAiXPDEGYhh/x29o4p0qHX5HDqG7y5VViv64=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13 h1:T1brd5dR3/fzNFAQch/iBKeX07/ffu/cLu+q+RuzEWk=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13/go.mod h1:Peg/GBAQ6JDt+RoBf4meB1wylmAipb7Kg2ZFakZTlwk=
github.com/aws/aws-sdk-go-v2/config v1.32.0 h1:T5WWJYnam9SzBLbsVYDu2HscLDe+GU1AUJtfcDAc/vA=
github.com/aws/aws-sdk-go-v2/config v1.32.0/go.mod h1:pSRm/+D3TxBixGMXlgtX4+MPO9VNtEEtiFmNpxksoxw=
github.com/aws/aws-sdk-go-v2/credentials v1.19.0 h1:7zm+ez+qEqLaNsCSRaistkvJRJv8sByDOVuCnyHbP7M=
github.com/aws/aws-sdk-go-v2/credentials v1.19.0/go.mod h1:pHKPblrT7hqFGkNLxqoS3FlGoPrQg4hMIa+4asZzBfs=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.14 h1:WZVR5DbDgxzA0BJeudId89Kmgy6DIU4ORpxwsVHz0qA=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.14/go.mod h1:Dadl9QO0kHgbrH1GRqGiZdYtW5w+IXXaBNCHTIaheM4=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.19.9 h1:Z1897HnnfLLgbs3pcUv8xLvtbai9TEfPUZfA0BFw968=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.19.9/go.mod h1:8oVESJIPBYGWdZhaHcIvTm7BnI6hbsR3ggKn0uyRMhk=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.13 h1:a+8/MLcWlIxo1lF9xaGt3J/u3yOZx+CdSveSNwjhD40=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.13/go.mod h1:oGnKwIYZ4XttyU2JWxFrwvhF6YKiK/9/wmE3v3Iu9K8=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.13 h1:HBSI2kDkMdWz4ZM7FjwE7e/pWDEZ+nR95x8Ztet1ooY=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.13/go.mod h1:YE94ZoDArI7awZqJzBAZ3PDD2zSfuP7w6P2knOzIn8M=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14 h1:PZHqQACxYb8mYgms4RZbhZG0a7dPW06xOjmaH0EJC/I=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14/go.mod h1:VymhrMJUWs69D8u0/lZ7jSB6WgaG/NqHi3gX0aYf6U0=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14 h1:bOS19y6zlJwagBfHxs0ESzr1XCOU2KXJCWcq3E2vfjY=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14/go.mod h1:1ipeGBMAxZ0xcTm6y6paC2C/J6f6OO7LBODV9afuAyM=
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/internal/v4a v1.4.13 h1:eg/WYAa12vqTphzIdWMzqYRVKKnCboVPRlvaybNCqPA=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.13/go.mod h1:/FDdxWhz1486obGrKKC1HONd7krpk38LBt+dutLcN9k=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.14 h1:ITi7qiDSv/mSGDSWNpZ4k4Ve0DQR6Ug2SJQ8zEHoDXg=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.14/go.mod h1:k1xtME53H1b6YpZt74YmwlONMWf4ecM+lut1WQLAF/U=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3 h1:x2Ibm/Af8Fi+BH+Hsn9TXGdT+hKbDd5XOTZxTMxDk7o=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3/go.mod h1:IW1jwyrQgMdhisceG8fQLmQIydcT/jWY21rFhzgaKwo=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.4 h1:NvMjwvv8hpGUILarKw7Z4Q0w1H9anXKsesMxtw++MA4=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.4/go.mod h1:455WPHSwaGj2waRSpQp7TsnpOnBfw8iDfPfbwl7KPJE=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13 h1:kDqdFvMY4AtKoACfzIGD8A0+hbT41KTKF//gq7jITfM=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13/go.mod h1:lmKuogqSU3HzQCwZ9ZtcqOc5XGMqtDK7OIc2+DxiUEg=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.13 h1:zhBJXdhWIFZ1acfDYIhu4+LCzdUS2Vbcum7D01dXlHQ=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.13/go.mod h1:JaaOeCE368qn2Hzi3sEzY6FgAZVCIYcC2nwbro2QCh8=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.5 h1:Hjkh7kE6D81PgrHlE/m9gx+4TyyeLHuY8xJs7yXN5C4=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.5/go.mod h1:nPRXgyCfAurhyaTMoBMwRBYBhaHI4lNPAnJmjM0Tslc=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.14 h1:FIouAnCE46kyYqyhs0XEBDFFSREtdnr8HQuLPQPLCrY=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.14/go.mod h1:UTwDc5COa5+guonQU8qBikJo1ZJ4ln2r1MkF7Dqag1E=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.14 h1:FzQE21lNtUor0Fb7QNgnEyiRCBlolLTX/Z1j65S7teM=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.14/go.mod h1:s1ydyWG9pm3ZwmmYN21HKyG9WzAZhYVW85wMHs5FV6w=
github.com/aws/aws-sdk-go-v2/service/kms v1.46.1 h1:zbNE7uLqCc9vLYV6p/wv0h05WmYStXO2uXFE+cFvvYA=
github.com/aws/aws-sdk-go-v2/service/kms v1.46.1/go.mod h1:YXPskkMuiMgp6qUG96NSTl7UpideOQT/Kx0u9Y1MKn0=
github.com/aws/aws-sdk-go-v2/service/s3 v1.90.2 h1:DhdbtDl4FdNlj31+xiRXANxEE+eC7n8JQz+/ilwQ8Uc=
github.com/aws/aws-sdk-go-v2/service/s3 v1.90.2/go.mod h1:+wArOOrcHUevqdto9k1tKOF5++YTe9JEcPSc9Tx2ZSw=
github.com/aws/aws-sdk-go-v2/service/s3 v1.92.0 h1:8FshVvnV2sr9kOSAbOnc/vwVmmAwMjOedKH6JW2ddPM=
github.com/aws/aws-sdk-go-v2/service/s3 v1.92.0/go.mod h1:wYNqY3L02Z3IgRYxOBPH9I1zD9Cjh9hI5QOy/eOjQvw=
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.39.7 h1:ac9qk31MWmUlUci1tthz0iREvkjFktEeGaDF1fAgeCU=
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.39.7/go.mod h1:A3WcpfEY2lhQvpnS6SJbMfljJuskxIKIVDcuYbIbXeE=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.1 h1:BDgIUYGEo5TkayOWv/oBLPphWwNm/A91AebUjAu5L5g=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.1/go.mod h1:iS6EPmNeqCsGo+xQmXv0jIMjyYtQfnwg36zl2FwEouk=
github.com/aws/aws-sdk-go-v2/service/ssm v1.66.2 h1:f1d7XwtcPywunzl/2vFZ9nxumsvhCjKVaFsEy7kHQDE=
github.com/aws/aws-sdk-go-v2/service/ssm v1.66.2/go.mod h1:CpiCR+ZLofnmhb0zRIq2FxVgfKIdevx43rIENOgN1vY=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.3 h1:NjShtS1t8r5LUfFVtFeI8xLAHQNTa7UI0VawXlrBMFQ=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.3/go.mod h1:fKvyjJcz63iL/ftA6RaM8sRCtN4r4zl4tjL3qw5ec7k=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.7 h1:gTsnx0xXNQ6SBbymoDvcoRHL+q4l/dAFsQuKfDWSaGc=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.7/go.mod h1:klO+ejMvYsB4QATfEOIXk8WAEwN4N0aBfJpvC+5SZBo=
github.com/aws/aws-sdk-go-v2/service/sts v1.40.2 h1:HK5ON3KmQV2HcAunnx4sKLB9aPf3gKGwVAf7xnx0QT0=
github.com/aws/aws-sdk-go-v2/service/sts v1.40.2/go.mod h1:E19xDjpzPZC7LS2knI9E6BaRFDK43Eul7vd6rSq2HWk=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.4 h1:U//SlnkE1wOQiIImxzdY5PXat4Wq+8rlfVEw4Y7J8as=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.4/go.mod h1:av+ArJpoYf3pgyrj6tcehSFW+y9/QvAY8kMooR9bZCw=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.8 h1:MvlNs/f+9eM0mOjD9JzBUbf5jghyTk3p+O9yHMXX94Y=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.8/go.mod h1:/j67Z5XBVDx8nZVp9EuFM9/BS5dvBznbqILGuu73hug=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.1 h1:GdGmKtG+/Krag7VfyOXV17xjTCz0i9NT+JnqLTOI5nA=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.1/go.mod h1:6TxbXoDSgBQ225Qd8Q+MbxUxUh6TtNKwbRt/EPS9xso=
github.com/aws/smithy-go v1.23.2 h1:Crv0eatJUQhaManss33hS5r40CG3ZFH+21XSkqMrIUM=
github.com/aws/smithy-go v1.23.2/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
@ -800,8 +802,8 @@ 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.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
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=
@ -942,8 +944,8 @@ helm.sh/helm/v3 v3.19.2 h1:psQjaM8aIWrSVEly6PgYtLu/y6MRSmok4ERiGhZmtUY=
helm.sh/helm/v3 v3.19.2/go.mod h1:gX10tB5ErM+8fr7bglUUS/UfTOO8UUTYWIBH1IYNnpE=
helm.sh/helm/v4 v4.0.0 h1:Ppai7cygdmyxSR+JR9djUoVrRmyMI/yY5P5TBd25oHs=
helm.sh/helm/v4 v4.0.0/go.mod h1:G1Y5AE+lJPQSAjh7nbXnhZrtGtxo+I6POSu9DruYiGI=
k8s.io/api v0.34.1 h1:jC+153630BMdlFukegoEL8E/yT7aLyQkIVuwhmwDgJM=
k8s.io/api v0.34.1/go.mod h1:SB80FxFtXn5/gwzCoN6QCtPD7Vbu5w2n1S0J5gFfTYk=
k8s.io/api v0.34.2 h1:fsSUNZhV+bnL6Aqrp6O7lMTy6o5x2C4XLjnh//8SLYY=
k8s.io/api v0.34.2/go.mod h1:MMBPaWlED2a8w4RSeanD76f7opUoypY8TFYkSM+3XHw=
k8s.io/apiextensions-apiserver v0.34.1 h1:NNPBva8FNAPt1iSVwIE0FsdrVriRXMsaWFMqJbII2CI=
k8s.io/apiextensions-apiserver v0.34.1/go.mod h1:hP9Rld3zF5Ay2Of3BeEpLAToP+l4s5UlxiHfqRaRcMc=
k8s.io/apimachinery v0.34.2 h1:zQ12Uk3eMHPxrsbUJgNF8bTauTVR2WgqJsTmwTE/NW4=
@ -952,8 +954,8 @@ k8s.io/apiserver v0.34.1 h1:U3JBGdgANK3dfFcyknWde1G6X1F4bg7PXuvlqt8lITA=
k8s.io/apiserver v0.34.1/go.mod h1:eOOc9nrVqlBI1AFCvVzsob0OxtPZUCPiUJL45JOTBG0=
k8s.io/cli-runtime v0.34.1 h1:btlgAgTrYd4sk8vJTRG6zVtqBKt9ZMDeQZo2PIzbL7M=
k8s.io/cli-runtime v0.34.1/go.mod h1:aVA65c+f0MZiMUPbseU/M9l1Wo2byeaGwUuQEQVVveE=
k8s.io/client-go v0.34.1 h1:ZUPJKgXsnKwVwmKKdPfw4tB58+7/Ik3CrjOEhsiZ7mY=
k8s.io/client-go v0.34.1/go.mod h1:kA8v0FP+tk6sZA0yKLRG67LWjqufAoSHA2xVGKw9Of8=
k8s.io/client-go v0.34.2 h1:Co6XiknN+uUZqiddlfAjT68184/37PS4QAzYvQvDR8M=
k8s.io/client-go v0.34.2/go.mod h1:2VYDl1XXJsdcAxw7BenFslRQX28Dxz91U9MWKjX97fE=
k8s.io/component-base v0.34.1 h1:v7xFgG+ONhytZNFpIz5/kecwD+sUhVE6HU7qQUiRM4A=
k8s.io/component-base v0.34.1/go.mod h1:mknCpLlTSKHzAQJJnnHVKqjxR7gBeHRv0rPXA7gdtQ0=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=

View File

@ -15,6 +15,7 @@ import (
"go.uber.org/zap"
"github.com/helmfile/helmfile/pkg/argparser"
"github.com/helmfile/helmfile/pkg/cluster"
"github.com/helmfile/helmfile/pkg/envvar"
"github.com/helmfile/helmfile/pkg/filesystem"
"github.com/helmfile/helmfile/pkg/helmexec"
@ -28,14 +29,15 @@ var Cancel goContext.CancelFunc
// App is the main application object.
type App struct {
OverrideKubeContext string
OverrideHelmBinary string
OverrideKustomizeBinary string
EnableLiveOutput bool
StripArgsValuesOnExitError bool
DisableForceUpdate bool
EnforcePluginVerification bool
HelmOCIPlainHTTP bool
OverrideKubeContext string
OverrideHelmBinary string
OverrideKustomizeBinary string
EnableLiveOutput bool
StripArgsValuesOnExitError bool
DisableForceUpdate bool
EnforcePluginVerification bool
HelmOCIPlainHTTP bool
DisableKubeVersionAutoDetection bool
Logger *zap.SugaredLogger
Kubeconfig string
@ -1193,7 +1195,7 @@ func printDAG(batches [][]state.Release) string {
_ = w.Flush()
return buf.String()
return trimTrailingWhitespace(buf.String())
}
// nolint: unparam
@ -1556,6 +1558,8 @@ func (a *App) apply(r *Run, c ApplyConfigProvider) (bool, bool, []error) {
// helm must be 2.11+ and helm-diff should be provided `--detailed-exitcode` in order for `helmfile apply` to work properly
detailedExitCode := true
detectedKubeVersion := a.detectKubeVersion(st)
diffOpts := &state.DiffOpts{
Color: c.Color(),
NoColor: c.NoColor(),
@ -1572,6 +1576,7 @@ func (a *App) apply(r *Run, c ApplyConfigProvider) (bool, bool, []error) {
SkipSchemaValidation: c.SkipSchemaValidation(),
SuppressOutputLineRegex: c.SuppressOutputLineRegex(),
TakeOwnership: c.TakeOwnership(),
DetectedKubeVersion: detectedKubeVersion,
}
infoMsg, releasesToBeUpdated, releasesToBeDeleted, errs := r.diff(false, detailedExitCode, c, diffOpts)
@ -1789,6 +1794,29 @@ Do you really want to delete?
return true, errs
}
// detectKubeVersion auto-detects the Kubernetes cluster version if not specified in helmfile.yaml.
// This prevents helm-diff from falling back to v1.20.0 (issue #2275).
// Returns empty string when kubeVersion is already set in helmfile.yaml (not needed),
// when auto-detection is disabled, or if detection fails.
func (a *App) detectKubeVersion(st *state.HelmState) string {
if st.KubeVersion != "" {
return ""
}
// Allow tests to disable auto-detection to avoid connecting to real clusters
if a.DisableKubeVersionAutoDetection {
return ""
}
version, err := cluster.DetectServerVersion(a.Kubeconfig, a.OverrideKubeContext)
if err != nil {
// If detection fails, we silently continue - helm-diff will handle it
return ""
}
return version
}
func (a *App) diff(r *Run, c DiffConfigProvider) (*string, bool, bool, []error) {
var (
infoMsg *string
@ -1802,6 +1830,8 @@ func (a *App) diff(r *Run, c DiffConfigProvider) (*string, bool, bool, []error)
helm.SetExtraArgs(GetArgs(c.Args(), r.state)...)
detectedKubeVersion := a.detectKubeVersion(st)
opts := &state.DiffOpts{
Context: c.Context(),
Output: c.DiffOutput(),
@ -1817,6 +1847,7 @@ func (a *App) diff(r *Run, c DiffConfigProvider) (*string, bool, bool, []error)
SkipSchemaValidation: c.SkipSchemaValidation(),
SuppressOutputLineRegex: c.SuppressOutputLineRegex(),
TakeOwnership: c.TakeOwnership(),
DetectedKubeVersion: detectedKubeVersion,
}
filtered := &Run{

View File

@ -61,10 +61,11 @@ func TestApply_hooks(t *testing.T) {
}
app := appWithFs(&App{
OverrideHelmBinary: DefaultHelmBinary,
OverrideKubeContext: "default",
Env: "default",
Logger: logger,
OverrideHelmBinary: DefaultHelmBinary,
OverrideKubeContext: "default",
DisableKubeVersionAutoDetection: true,
Env: "default",
Logger: logger,
helms: map[helmKey]helmexec.Interface{
createHelmKey("helm", "default"): helm,
},

View File

@ -61,11 +61,12 @@ func TestApply_3(t *testing.T) {
}
app := appWithFs(&App{
OverrideHelmBinary: DefaultHelmBinary,
fs: filesystem.DefaultFileSystem(),
OverrideKubeContext: "",
Env: "default",
Logger: logger,
OverrideHelmBinary: DefaultHelmBinary,
fs: filesystem.DefaultFileSystem(),
OverrideKubeContext: "",
DisableKubeVersionAutoDetection: true,
Env: "default",
Logger: logger,
helms: map[helmKey]helmexec.Interface{
createHelmKey("helm", ""): helm,
},

View File

@ -61,11 +61,12 @@ func TestApply_2(t *testing.T) {
}
app := appWithFs(&App{
OverrideHelmBinary: DefaultHelmBinary,
fs: filesystem.DefaultFileSystem(),
OverrideKubeContext: "default",
Env: "default",
Logger: logger,
OverrideHelmBinary: DefaultHelmBinary,
fs: filesystem.DefaultFileSystem(),
OverrideKubeContext: "default",
DisableKubeVersionAutoDetection: true,
Env: "default",
Logger: logger,
helms: map[helmKey]helmexec.Interface{
createHelmKey("helm", "default"): helm,
},

View File

@ -101,11 +101,12 @@ releases:
}
app := appWithFs(&App{
OverrideHelmBinary: DefaultHelmBinary,
fs: filesystem.DefaultFileSystem(),
OverrideKubeContext: "default",
Env: "default",
Logger: logger,
OverrideHelmBinary: DefaultHelmBinary,
fs: filesystem.DefaultFileSystem(),
OverrideKubeContext: "default",
DisableKubeVersionAutoDetection: true,
Env: "default",
Logger: logger,
helms: map[helmKey]helmexec.Interface{
createHelmKey("helm", "default"): helm,
},
@ -331,11 +332,12 @@ func TestDiffWithInstalled(t *testing.T) {
}
app := appWithFs(&App{
OverrideHelmBinary: DefaultHelmBinary,
fs: filesystem.DefaultFileSystem(),
OverrideKubeContext: "default",
Env: "default",
Logger: logger,
OverrideHelmBinary: DefaultHelmBinary,
fs: filesystem.DefaultFileSystem(),
OverrideKubeContext: "default",
DisableKubeVersionAutoDetection: true,
Env: "default",
Logger: logger,
helms: map[helmKey]helmexec.Interface{
createHelmKey("helm", "default"): helm,
},

View File

@ -29,11 +29,12 @@ func TestGetHelmWithEmptyDefaultHelmBinary(t *testing.T) {
logger := newAppTestLogger()
app := &App{
OverrideHelmBinary: "",
OverrideKubeContext: "",
Logger: logger,
Env: "default",
ctx: goContext.Background(),
OverrideHelmBinary: "",
OverrideKubeContext: "",
DisableKubeVersionAutoDetection: true,
Logger: logger,
Env: "default",
ctx: goContext.Background(),
}
// This should NOT fail because app.getHelm() defaults empty DefaultHelmBinary to "helm"

View File

@ -103,11 +103,12 @@ releases:
}
app := appWithFs(&App{
OverrideHelmBinary: DefaultHelmBinary,
fs: ffs.DefaultFileSystem(),
OverrideKubeContext: "default",
Env: "default",
Logger: logger,
OverrideHelmBinary: DefaultHelmBinary,
fs: ffs.DefaultFileSystem(),
OverrideKubeContext: "default",
DisableKubeVersionAutoDetection: true,
Env: "default",
Logger: logger,
helms: map[helmKey]helmexec.Interface{
createHelmKey("helm", "default"): helm,
},

View File

@ -44,17 +44,17 @@ environments:
---
releases:
- name: logging
chart: incubator/raw
chart: incubator/raw
namespace: kube-system
- name: kubernetes-external-secrets
chart: incubator/raw
chart: incubator/raw
namespace: kube-system
needs:
- kube-system/logging
- name: external-secrets
chart: incubator/raw
chart: incubator/raw
namespace: default
labels:
app: test
@ -62,7 +62,7 @@ releases:
- kube-system/kubernetes-external-secrets
- name: my-release
chart: incubator/raw
chart: incubator/raw
namespace: default
labels:
app: test
@ -72,17 +72,17 @@ releases:
# Disabled releases are treated as missing
- name: disabled
chart: incubator/raw
chart: incubator/raw
namespace: kube-system
installed: false
- name: test2
chart: incubator/raw
chart: incubator/raw
needs:
- kube-system/disabled
- name: test3
chart: incubator/raw
chart: incubator/raw
needs:
- test2
`,
@ -111,18 +111,19 @@ releases:
"/path/to/helmfile.d/helmfile_3.yaml": `
releases:
- name: global
chart: incubator/raw
chart: incubator/raw
namespace: kube-system
`,
}
app := appWithFs(&App{
OverrideHelmBinary: DefaultHelmBinary,
fs: ffs.DefaultFileSystem(),
OverrideKubeContext: "default",
Env: tc.environment,
Logger: logger,
valsRuntime: valsRuntime,
OverrideHelmBinary: DefaultHelmBinary,
fs: ffs.DefaultFileSystem(),
OverrideKubeContext: "default",
DisableKubeVersionAutoDetection: true,
Env: tc.environment,
Logger: logger,
valsRuntime: valsRuntime,
}, files)
expectNoCallsToHelm(app)
@ -160,15 +161,15 @@ releases:
check(t, testcase{
environment: "default",
expected: `NAME NAMESPACE ENABLED INSTALLED LABELS CHART VERSION
test2 true true chart:raw,name:test2,namespace: incubator/raw
test3 true true chart:raw,name:test3,namespace: incubator/raw
external-secrets default true true app:test,chart:raw,name:external-secrets,namespace:default incubator/raw
my-release default true true app:test,chart:raw,name:my-release,namespace:default incubator/raw
disabled kube-system true false chart:raw,name:disabled,namespace:kube-system incubator/raw
global kube-system true true chart:raw,name:global,namespace:kube-system incubator/raw
kubernetes-external-secrets kube-system true true chart:raw,name:kubernetes-external-secrets,namespace:kube-system incubator/raw
logging kube-system true true chart:raw,name:logging,namespace:kube-system incubator/raw
cache my-app true true app:test,chart:redis,name:cache,namespace:my-app bitnami/redis 17.0.7
test2 true true chart:raw,name:test2,namespace: incubator/raw
test3 true true chart:raw,name:test3,namespace: incubator/raw
external-secrets default true true app:test,chart:raw,name:external-secrets,namespace:default incubator/raw
my-release default true true app:test,chart:raw,name:my-release,namespace:default incubator/raw
disabled kube-system true false chart:raw,name:disabled,namespace:kube-system incubator/raw
global kube-system true true chart:raw,name:global,namespace:kube-system incubator/raw
kubernetes-external-secrets kube-system true true chart:raw,name:kubernetes-external-secrets,namespace:kube-system incubator/raw
logging kube-system true true chart:raw,name:logging,namespace:kube-system incubator/raw
cache my-app true true app:test,chart:redis,name:cache,namespace:my-app bitnami/redis 17.0.7
database my-app true true chart:postgres,name:database,namespace:my-app bitnami/postgres 11.6.22
`,
}, cfg)
@ -186,8 +187,8 @@ database my-app true true chart:postgres,name:da
environment: "development",
selectors: []string{"app=test"},
expected: `NAME NAMESPACE ENABLED INSTALLED LABELS CHART VERSION
external-secrets default true true app:test,chart:raw,name:external-secrets,namespace:default incubator/raw
my-release default true true app:test,chart:raw,name:my-release,namespace:default incubator/raw
external-secrets default true true app:test,chart:raw,name:external-secrets,namespace:default incubator/raw
my-release default true true app:test,chart:raw,name:my-release,namespace:default incubator/raw
`,
}, cfg)
})
@ -196,7 +197,7 @@ my-release default true true app:test,chart:raw,name:my-release,
check(t, testcase{
environment: "test",
expected: `NAME NAMESPACE ENABLED INSTALLED LABELS CHART VERSION
cache my-app true true app:test,chart:redis,name:cache,namespace:my-app bitnami/redis 17.0.7
cache my-app true true app:test,chart:redis,name:cache,namespace:my-app bitnami/redis 17.0.7
database my-app true true chart:postgres,name:database,namespace:my-app bitnami/postgres 11.6.22
`,
}, cfg)
@ -207,14 +208,14 @@ database my-app true true chart:postgres,name:database,namespace:my-a
environment: "shared",
// 'global' release has no environments, so is still excluded
expected: `NAME NAMESPACE ENABLED INSTALLED LABELS CHART VERSION
test2 true true chart:raw,name:test2,namespace: incubator/raw
test3 true true chart:raw,name:test3,namespace: incubator/raw
external-secrets default true true app:test,chart:raw,name:external-secrets,namespace:default incubator/raw
my-release default true true app:test,chart:raw,name:my-release,namespace:default incubator/raw
disabled kube-system true false chart:raw,name:disabled,namespace:kube-system incubator/raw
kubernetes-external-secrets kube-system true true chart:raw,name:kubernetes-external-secrets,namespace:kube-system incubator/raw
logging kube-system true true chart:raw,name:logging,namespace:kube-system incubator/raw
cache my-app true true app:test,chart:redis,name:cache,namespace:my-app bitnami/redis 17.0.7
test2 true true chart:raw,name:test2,namespace: incubator/raw
test3 true true chart:raw,name:test3,namespace: incubator/raw
external-secrets default true true app:test,chart:raw,name:external-secrets,namespace:default incubator/raw
my-release default true true app:test,chart:raw,name:my-release,namespace:default incubator/raw
disabled kube-system true false chart:raw,name:disabled,namespace:kube-system incubator/raw
kubernetes-external-secrets kube-system true true chart:raw,name:kubernetes-external-secrets,namespace:kube-system incubator/raw
logging kube-system true true chart:raw,name:logging,namespace:kube-system incubator/raw
cache my-app true true app:test,chart:redis,name:cache,namespace:my-app bitnami/redis 17.0.7
database my-app true true chart:postgres,name:database,namespace:my-app bitnami/postgres 11.6.22
`,
}, cfg)
@ -270,12 +271,13 @@ releases:
logger := helmexec.NewLogger(syncWriter, "debug")
app := appWithFs(&App{
OverrideHelmBinary: DefaultHelmBinary,
fs: ffs.DefaultFileSystem(),
OverrideKubeContext: "default",
Env: "default",
Logger: logger,
Namespace: "testNamespace",
OverrideHelmBinary: DefaultHelmBinary,
fs: ffs.DefaultFileSystem(),
OverrideKubeContext: "default",
DisableKubeVersionAutoDetection: true,
Env: "default",
Logger: logger,
Namespace: "testNamespace",
}, files)
expectNoCallsToHelm(app)

View File

@ -49,13 +49,14 @@ releases:
}
app := appWithFs(&App{
OverrideHelmBinary: DefaultHelmBinary,
fs: ffs.DefaultFileSystem(),
OverrideKubeContext: "default",
Env: "default",
Logger: logger,
valsRuntime: valsRuntime,
FileOrDir: "/path/to/helmfile.d",
OverrideHelmBinary: DefaultHelmBinary,
fs: ffs.DefaultFileSystem(),
OverrideKubeContext: "default",
DisableKubeVersionAutoDetection: true,
Env: "default",
Logger: logger,
valsRuntime: valsRuntime,
FileOrDir: "/path/to/helmfile.d",
}, files)
expectNoCallsToHelm(app)
@ -116,13 +117,14 @@ releases:
}
app := appWithFs(&App{
OverrideHelmBinary: DefaultHelmBinary,
fs: ffs.DefaultFileSystem(),
OverrideKubeContext: "default",
Env: "default",
Logger: logger,
valsRuntime: valsRuntime,
FileOrDir: "/path/to/helmfile.d",
OverrideHelmBinary: DefaultHelmBinary,
fs: ffs.DefaultFileSystem(),
OverrideKubeContext: "default",
DisableKubeVersionAutoDetection: true,
Env: "default",
Logger: logger,
valsRuntime: valsRuntime,
FileOrDir: "/path/to/helmfile.d",
}, files)
expectNoCallsToHelm(app)

View File

@ -59,11 +59,12 @@ func TestSync(t *testing.T) {
}
app := appWithFs(&App{
OverrideHelmBinary: DefaultHelmBinary,
fs: ffs.DefaultFileSystem(),
OverrideKubeContext: "default",
Env: "default",
Logger: logger,
OverrideHelmBinary: DefaultHelmBinary,
fs: ffs.DefaultFileSystem(),
OverrideKubeContext: "default",
DisableKubeVersionAutoDetection: true,
Env: "default",
Logger: logger,
helms: map[helmKey]helmexec.Interface{
createHelmKey("helm", "default"): helm,
},

View File

@ -106,11 +106,12 @@ releases:
}
app := appWithFs(&App{
OverrideHelmBinary: DefaultHelmBinary,
fs: &ffs.FileSystem{Glob: filepath.Glob},
OverrideKubeContext: "default",
Env: "default",
Logger: logger,
OverrideHelmBinary: DefaultHelmBinary,
fs: &ffs.FileSystem{Glob: filepath.Glob},
OverrideKubeContext: "default",
DisableKubeVersionAutoDetection: true,
Env: "default",
Logger: logger,
helms: map[helmKey]helmexec.Interface{
createHelmKey("helm", "default"): helm,
},
@ -377,11 +378,12 @@ releases:
}
app := appWithFs(&App{
OverrideHelmBinary: DefaultHelmBinary,
fs: &ffs.FileSystem{Glob: filepath.Glob},
OverrideKubeContext: "default",
Env: "default",
Logger: logger,
OverrideHelmBinary: DefaultHelmBinary,
fs: &ffs.FileSystem{Glob: filepath.Glob},
OverrideKubeContext: "default",
DisableKubeVersionAutoDetection: true,
Env: "default",
Logger: logger,
helms: map[helmKey]helmexec.Interface{
createHelmKey("helm", "default"): helm,
},
@ -478,11 +480,12 @@ releases:
}
app := appWithFs(&App{
OverrideHelmBinary: DefaultHelmBinary,
fs: &ffs.FileSystem{Glob: filepath.Glob},
OverrideKubeContext: "default",
Env: "default",
Logger: logger,
OverrideHelmBinary: DefaultHelmBinary,
fs: &ffs.FileSystem{Glob: filepath.Glob},
OverrideKubeContext: "default",
DisableKubeVersionAutoDetection: true,
Env: "default",
Logger: logger,
helms: map[helmKey]helmexec.Interface{
createHelmKey("helm", "default"): helm,
},

View File

@ -92,12 +92,13 @@ releases:
fs := testhelper.NewTestFs(files)
fs.GlobFixtures["/path/to/helmfile.d/a*.yaml"] = []string{"/path/to/helmfile.d/a2.yaml", "/path/to/helmfile.d/a1.yaml"}
app := &App{
OverrideHelmBinary: DefaultHelmBinary,
OverrideKubeContext: "default",
Logger: newAppTestLogger(),
Namespace: "",
Env: "default",
FileOrDir: "helmfile.yaml",
OverrideHelmBinary: DefaultHelmBinary,
OverrideKubeContext: "default",
DisableKubeVersionAutoDetection: true,
Logger: newAppTestLogger(),
Namespace: "",
Env: "default",
FileOrDir: "helmfile.yaml",
}
expectNoCallsToHelm(app)
@ -150,12 +151,13 @@ BAZ: 4
fs := testhelper.NewTestFs(files)
fs.GlobFixtures["/path/to/env.*.yaml"] = []string{"/path/to/env.2.yaml", "/path/to/env.1.yaml"}
app := &App{
OverrideHelmBinary: DefaultHelmBinary,
OverrideKubeContext: "default",
Logger: newAppTestLogger(),
Namespace: "",
Env: "default",
FileOrDir: "helmfile.yaml",
OverrideHelmBinary: DefaultHelmBinary,
OverrideKubeContext: "default",
DisableKubeVersionAutoDetection: true,
Logger: newAppTestLogger(),
Namespace: "",
Env: "default",
FileOrDir: "helmfile.yaml",
}
expectNoCallsToHelm(app)
@ -193,12 +195,13 @@ releases:
}
fs := testhelper.NewTestFs(files)
app := &App{
OverrideHelmBinary: DefaultHelmBinary,
OverrideKubeContext: "default",
Logger: newAppTestLogger(),
Namespace: "",
Env: "default",
FileOrDir: "helmfile.yaml",
OverrideHelmBinary: DefaultHelmBinary,
OverrideKubeContext: "default",
DisableKubeVersionAutoDetection: true,
Logger: newAppTestLogger(),
Namespace: "",
Env: "default",
FileOrDir: "helmfile.yaml",
}
expectNoCallsToHelm(app)
@ -279,12 +282,13 @@ func TestUpdateStrategyParamValidation(t *testing.T) {
for idx, c := range cases {
fs := testhelper.NewTestFs(c.files)
app := &App{
OverrideHelmBinary: DefaultHelmBinary,
OverrideKubeContext: "default",
Logger: newAppTestLogger(),
Namespace: "",
Env: "default",
FileOrDir: "helmfile.yaml",
OverrideHelmBinary: DefaultHelmBinary,
OverrideKubeContext: "default",
DisableKubeVersionAutoDetection: true,
Logger: newAppTestLogger(),
Namespace: "",
Env: "default",
FileOrDir: "helmfile.yaml",
}
expectNoCallsToHelm(app)
@ -331,11 +335,12 @@ releases:
}
fs := testhelper.NewTestFs(files)
app := &App{
OverrideHelmBinary: DefaultHelmBinary,
OverrideKubeContext: "default",
Logger: newAppTestLogger(),
Namespace: "",
Env: "test",
OverrideHelmBinary: DefaultHelmBinary,
OverrideKubeContext: "default",
DisableKubeVersionAutoDetection: true,
Logger: newAppTestLogger(),
Namespace: "",
Env: "test",
}
expectNoCallsToHelm(app)
@ -383,12 +388,13 @@ releases:
}
fs := testhelper.NewTestFs(files)
app := &App{
OverrideHelmBinary: DefaultHelmBinary,
OverrideKubeContext: "default",
Logger: newAppTestLogger(),
Namespace: "",
Env: "default",
FileOrDir: "helmfile.yaml",
OverrideHelmBinary: DefaultHelmBinary,
OverrideKubeContext: "default",
DisableKubeVersionAutoDetection: true,
Logger: newAppTestLogger(),
Namespace: "",
Env: "default",
FileOrDir: "helmfile.yaml",
}
expectNoCallsToHelm(app)
@ -449,13 +455,14 @@ releases:
fs := testhelper.NewTestFs(files)
fs.GlobFixtures["/path/to/helmfile.d/a*.yaml"] = []string{"/path/to/helmfile.d/a2.yaml", "/path/to/helmfile.d/a1.yaml"}
app := &App{
OverrideHelmBinary: DefaultHelmBinary,
OverrideKubeContext: "default",
Logger: newAppTestLogger(),
Selectors: []string{fmt.Sprintf("name=%s", testcase.name)},
Namespace: "",
Env: "default",
FileOrDir: "helmfile.yaml",
OverrideHelmBinary: DefaultHelmBinary,
OverrideKubeContext: "default",
DisableKubeVersionAutoDetection: true,
Logger: newAppTestLogger(),
Selectors: []string{fmt.Sprintf("name=%s", testcase.name)},
Namespace: "",
Env: "default",
FileOrDir: "helmfile.yaml",
}
expectNoCallsToHelm(app)
@ -506,13 +513,14 @@ releases:
for _, testcase := range testcases {
app := appWithFs(&App{
OverrideHelmBinary: DefaultHelmBinary,
OverrideKubeContext: "default",
Logger: newAppTestLogger(),
Namespace: "",
Selectors: []string{},
Env: testcase.name,
FileOrDir: "helmfile.yaml",
OverrideHelmBinary: DefaultHelmBinary,
OverrideKubeContext: "default",
DisableKubeVersionAutoDetection: true,
Logger: newAppTestLogger(),
Namespace: "",
Selectors: []string{},
Env: testcase.name,
FileOrDir: "helmfile.yaml",
}, files)
expectNoCallsToHelm(app)
@ -620,13 +628,14 @@ releases:
}
app := appWithFs(&App{
OverrideHelmBinary: DefaultHelmBinary,
OverrideKubeContext: "default",
Logger: helmexec.NewLogger(&ctxLogger{label: testcase.label}, "debug"),
Namespace: "",
Selectors: []string{testcase.label},
Env: "default",
FileOrDir: "helmfile.yaml",
OverrideHelmBinary: DefaultHelmBinary,
OverrideKubeContext: "default",
DisableKubeVersionAutoDetection: true,
Logger: helmexec.NewLogger(&ctxLogger{label: testcase.label}, "debug"),
Namespace: "",
Selectors: []string{testcase.label},
Env: "default",
FileOrDir: "helmfile.yaml",
}, files)
expectNoCallsToHelm(app)
@ -861,13 +870,14 @@ func runFilterSubHelmFilesTests(testcases []struct {
}
app := appWithFs(&App{
OverrideHelmBinary: DefaultHelmBinary,
OverrideKubeContext: "default",
Logger: newAppTestLogger(),
Namespace: "",
Selectors: []string{testcase.label},
Env: "default",
FileOrDir: "helmfile.yaml",
OverrideHelmBinary: DefaultHelmBinary,
OverrideKubeContext: "default",
DisableKubeVersionAutoDetection: true,
Logger: newAppTestLogger(),
Namespace: "",
Selectors: []string{testcase.label},
Env: "default",
FileOrDir: "helmfile.yaml",
}, files)
expectNoCallsToHelm(app)
@ -943,13 +953,14 @@ ns: INLINE_NS
}
app := appWithFs(&App{
OverrideHelmBinary: DefaultHelmBinary,
OverrideKubeContext: "default",
Logger: newAppTestLogger(),
Namespace: "",
Selectors: []string{},
Env: "default",
FileOrDir: "/path/to/helmfile.yaml.gotmpl",
OverrideHelmBinary: DefaultHelmBinary,
OverrideKubeContext: "default",
DisableKubeVersionAutoDetection: true,
Logger: newAppTestLogger(),
Namespace: "",
Selectors: []string{},
Env: "default",
FileOrDir: "/path/to/helmfile.yaml.gotmpl",
}, files)
expectNoCallsToHelm(app)
@ -1047,13 +1058,14 @@ releases:
return false, []error{}
}
app := appWithFs(&App{
OverrideHelmBinary: DefaultHelmBinary,
OverrideKubeContext: "default",
Logger: newAppTestLogger(),
Namespace: "",
Selectors: []string{},
Env: "default",
FileOrDir: "helmfile.yaml",
OverrideHelmBinary: DefaultHelmBinary,
OverrideKubeContext: "default",
DisableKubeVersionAutoDetection: true,
Logger: newAppTestLogger(),
Namespace: "",
Selectors: []string{},
Env: "default",
FileOrDir: "helmfile.yaml",
}, files)
expectNoCallsToHelm(app)
@ -1111,15 +1123,16 @@ bar: "bar1"
return false, []error{}
}
app := appWithFs(&App{
OverrideHelmBinary: DefaultHelmBinary,
OverrideKubeContext: "default",
Logger: newAppTestLogger(),
Namespace: "",
Selectors: []string{},
Env: "default",
ValuesFiles: []string{"overrides.yaml"},
Set: map[string]any{"bar": "bar2", "baz": "baz1"},
FileOrDir: "helmfile.yaml.gotmpl",
OverrideHelmBinary: DefaultHelmBinary,
OverrideKubeContext: "default",
DisableKubeVersionAutoDetection: true,
Logger: newAppTestLogger(),
Namespace: "",
Selectors: []string{},
Env: "default",
ValuesFiles: []string{"overrides.yaml"},
Set: map[string]any{"bar": "bar2", "baz": "baz1"},
FileOrDir: "helmfile.yaml.gotmpl",
}, files)
expectNoCallsToHelm(app)
@ -1232,15 +1245,16 @@ x:
return false, []error{}
}
app := appWithFs(&App{
OverrideHelmBinary: DefaultHelmBinary,
OverrideKubeContext: "default",
Logger: newAppTestLogger(),
Namespace: "",
Selectors: []string{},
Env: testcase.env,
ValuesFiles: []string{"overrides.yaml"},
Set: map[string]any{"x": map[string]any{"hoge": "hoge_set", "fuga": "fuga_set"}},
FileOrDir: "helmfile.yaml.gotmpl",
OverrideHelmBinary: DefaultHelmBinary,
OverrideKubeContext: "default",
DisableKubeVersionAutoDetection: true,
Logger: newAppTestLogger(),
Namespace: "",
Selectors: []string{},
Env: testcase.env,
ValuesFiles: []string{"overrides.yaml"},
Set: map[string]any{"x": map[string]any{"hoge": "hoge_set", "fuga": "fuga_set"}},
FileOrDir: "helmfile.yaml.gotmpl",
}, files)
expectNoCallsToHelm(app)
@ -1285,13 +1299,14 @@ releases:
return false, []error{}
}
app := appWithFs(&App{
OverrideHelmBinary: DefaultHelmBinary,
OverrideKubeContext: "default",
Logger: newAppTestLogger(),
Namespace: "",
Env: "default",
Selectors: []string{},
FileOrDir: "helmfile.yaml",
OverrideHelmBinary: DefaultHelmBinary,
OverrideKubeContext: "default",
DisableKubeVersionAutoDetection: true,
Logger: newAppTestLogger(),
Namespace: "",
Env: "default",
Selectors: []string{},
FileOrDir: "helmfile.yaml",
}, files)
expectNoCallsToHelm(app)
@ -1341,13 +1356,14 @@ releases:
return false, []error{}
}
app := appWithFs(&App{
OverrideHelmBinary: DefaultHelmBinary,
OverrideKubeContext: "default",
Logger: newAppTestLogger(),
Namespace: "",
Selectors: []string{},
Env: "default",
FileOrDir: "helmfile.yaml",
OverrideHelmBinary: DefaultHelmBinary,
OverrideKubeContext: "default",
DisableKubeVersionAutoDetection: true,
Logger: newAppTestLogger(),
Namespace: "",
Selectors: []string{},
Env: "default",
FileOrDir: "helmfile.yaml",
}, files)
expectNoCallsToHelm(app)
@ -1391,12 +1407,13 @@ releases:
return false, []error{}
}
app := appWithFs(&App{
OverrideHelmBinary: DefaultHelmBinary,
OverrideKubeContext: "default",
Logger: newAppTestLogger(),
Namespace: "",
Env: "default",
FileOrDir: "helmfile.yaml",
OverrideHelmBinary: DefaultHelmBinary,
OverrideKubeContext: "default",
DisableKubeVersionAutoDetection: true,
Logger: newAppTestLogger(),
Namespace: "",
Env: "default",
FileOrDir: "helmfile.yaml",
}, files)
expectNoCallsToHelmVersion(app)
@ -1433,12 +1450,13 @@ releases:
return false, []error{}
}
app := appWithFs(&App{
OverrideHelmBinary: DefaultHelmBinary,
OverrideKubeContext: "default",
Logger: newAppTestLogger(),
Namespace: "",
Env: "default",
FileOrDir: "helmfile.yaml",
OverrideHelmBinary: DefaultHelmBinary,
OverrideKubeContext: "default",
DisableKubeVersionAutoDetection: true,
Logger: newAppTestLogger(),
Namespace: "",
Env: "default",
FileOrDir: "helmfile.yaml",
}, files)
expectNoCallsToHelmVersion(app)
@ -1479,12 +1497,13 @@ releases:
return false, []error{}
}
app := appWithFs(&App{
OverrideHelmBinary: DefaultHelmBinary,
OverrideKubeContext: "default",
Logger: newAppTestLogger(),
Namespace: "",
Env: "default",
FileOrDir: "helmfile.yaml",
OverrideHelmBinary: DefaultHelmBinary,
OverrideKubeContext: "default",
DisableKubeVersionAutoDetection: true,
Logger: newAppTestLogger(),
Namespace: "",
Env: "default",
FileOrDir: "helmfile.yaml",
}, files)
expectNoCallsToHelmVersion(app)
@ -1524,11 +1543,12 @@ func TestLoadDesiredStateFromYaml_DuplicateReleaseName(t *testing.T) {
}
fs := ffs.FromFileSystem(ffs.FileSystem{ReadFile: readFile})
app := &App{
OverrideHelmBinary: DefaultHelmBinary,
OverrideKubeContext: "default",
fs: fs,
Env: "default",
Logger: newAppTestLogger(),
OverrideHelmBinary: DefaultHelmBinary,
OverrideKubeContext: "default",
DisableKubeVersionAutoDetection: true,
fs: fs,
Env: "default",
Logger: newAppTestLogger(),
}
expectNoCallsToHelm(app)
@ -1582,11 +1602,12 @@ helmDefaults:
`,
})
app := &App{
OverrideHelmBinary: DefaultHelmBinary,
OverrideKubeContext: "default",
fs: testFs.ToFileSystem(),
Env: "default",
Logger: newAppTestLogger(),
OverrideHelmBinary: DefaultHelmBinary,
OverrideKubeContext: "default",
DisableKubeVersionAutoDetection: true,
fs: testFs.ToFileSystem(),
Env: "default",
Logger: newAppTestLogger(),
}
app.remote = remote.NewRemote(app.Logger, "", app.fs)
@ -2753,11 +2774,12 @@ releases:
}
app := appWithFs(&App{
OverrideHelmBinary: DefaultHelmBinary,
fs: ffs.DefaultFileSystem(),
OverrideKubeContext: "default",
Env: "default",
Logger: logger,
OverrideHelmBinary: DefaultHelmBinary,
fs: ffs.DefaultFileSystem(),
OverrideKubeContext: "default",
DisableKubeVersionAutoDetection: true,
Env: "default",
Logger: logger,
helms: map[helmKey]helmexec.Interface{
createHelmKey("helm", "default"): helm,
},
@ -2826,10 +2848,11 @@ releases:
}
app := appWithFs(&App{
OverrideHelmBinary: DefaultHelmBinary,
OverrideKubeContext: "default",
Env: "default",
Logger: logger,
OverrideHelmBinary: DefaultHelmBinary,
OverrideKubeContext: "default",
DisableKubeVersionAutoDetection: true,
Env: "default",
Logger: logger,
helms: map[helmKey]helmexec.Interface{
createHelmKey("helm", "default"): helm,
},
@ -3909,11 +3932,12 @@ releases:
}
app := appWithFs(&App{
OverrideHelmBinary: DefaultHelmBinary,
fs: ffs.DefaultFileSystem(),
OverrideKubeContext: "default",
Env: "default",
Logger: logger,
OverrideHelmBinary: DefaultHelmBinary,
fs: ffs.DefaultFileSystem(),
OverrideKubeContext: "default",
DisableKubeVersionAutoDetection: true,
Env: "default",
Logger: logger,
helms: map[helmKey]helmexec.Interface{
createHelmKey("helm", "default"): helm,
},
@ -4028,11 +4052,12 @@ releases:
t.Helper()
app := appWithFs(&App{
OverrideHelmBinary: DefaultHelmBinary,
fs: ffs.DefaultFileSystem(),
OverrideKubeContext: "default",
Env: "default",
Logger: logger,
OverrideHelmBinary: DefaultHelmBinary,
fs: ffs.DefaultFileSystem(),
OverrideKubeContext: "default",
DisableKubeVersionAutoDetection: true,
Env: "default",
Logger: logger,
helms: map[helmKey]helmexec.Interface{
createHelmKey("helm", "default"): helm,
},
@ -4084,12 +4109,13 @@ releases:
logger := helmexec.NewLogger(syncWriter, "debug")
app := appWithFs(&App{
OverrideHelmBinary: DefaultHelmBinary,
fs: ffs.DefaultFileSystem(),
OverrideKubeContext: "default",
Env: "default",
Logger: logger,
Namespace: "testNamespace",
OverrideHelmBinary: DefaultHelmBinary,
fs: ffs.DefaultFileSystem(),
OverrideKubeContext: "default",
DisableKubeVersionAutoDetection: true,
Env: "default",
Logger: logger,
Namespace: "testNamespace",
}, files)
expectNoCallsToHelm(app)
@ -4134,12 +4160,13 @@ releases:
logger := helmexec.NewLogger(syncWriter, "debug")
app := appWithFs(&App{
OverrideHelmBinary: DefaultHelmBinary,
fs: ffs.DefaultFileSystem(),
OverrideKubeContext: "default",
Env: "default",
Logger: logger,
Namespace: "testNamespace",
OverrideHelmBinary: DefaultHelmBinary,
fs: ffs.DefaultFileSystem(),
OverrideKubeContext: "default",
DisableKubeVersionAutoDetection: true,
Env: "default",
Logger: logger,
Namespace: "testNamespace",
}, files)
expectNoCallsToHelm(app)
@ -4197,12 +4224,13 @@ releases:
logger := helmexec.NewLogger(syncWriter, "debug")
app := appWithFs(&App{
OverrideHelmBinary: DefaultHelmBinary,
fs: ffs.DefaultFileSystem(),
OverrideKubeContext: "default",
Env: "default",
Logger: logger,
Namespace: "testNamespace",
OverrideHelmBinary: DefaultHelmBinary,
fs: ffs.DefaultFileSystem(),
OverrideKubeContext: "default",
DisableKubeVersionAutoDetection: true,
Env: "default",
Logger: logger,
Namespace: "testNamespace",
}, files)
expectNoCallsToHelm(app)
@ -4214,10 +4242,10 @@ releases:
assert.NoError(t, err)
expected := `NAME NAMESPACE ENABLED INSTALLED LABELS CHART VERSION
myrelease1 testNamespace true false chart:mychart1,common:label,id:myrelease1,name:myrelease1,namespace:testNamespace mychart1
myrelease2 testNamespace false true chart:mychart1,common:label,name:myrelease2,namespace:testNamespace mychart1
myrelease3 testNamespace true true chart:mychart1,name:myrelease3,namespace:testNamespace mychart1
myrelease4 testNamespace true true chart:mychart1,id:myrelease1,name:myrelease4,namespace:testNamespace mychart1
myrelease1 testNamespace true false chart:mychart1,common:label,id:myrelease1,name:myrelease1,namespace:testNamespace mychart1
myrelease2 testNamespace false true chart:mychart1,common:label,name:myrelease2,namespace:testNamespace mychart1
myrelease3 testNamespace true true chart:mychart1,name:myrelease3,namespace:testNamespace mychart1
myrelease4 testNamespace true true chart:mychart1,id:myrelease1,name:myrelease4,namespace:testNamespace mychart1
`
assert.Equal(t, expected, out)
@ -4252,11 +4280,12 @@ releases:
{Name: "name", Value: "val"}}
app := appWithFs(&App{
OverrideHelmBinary: DefaultHelmBinary,
OverrideKubeContext: "default",
Logger: newAppTestLogger(),
Env: "default",
FileOrDir: "helmfile.yaml.gotmpl",
OverrideHelmBinary: DefaultHelmBinary,
OverrideKubeContext: "default",
DisableKubeVersionAutoDetection: true,
Logger: newAppTestLogger(),
Env: "default",
FileOrDir: "helmfile.yaml.gotmpl",
}, files)
expectNoCallsToHelm(app)
@ -4324,11 +4353,12 @@ releases:
{Name: "name", Value: "val"}}
app := appWithFs(&App{
OverrideHelmBinary: DefaultHelmBinary,
OverrideKubeContext: "default",
Logger: newAppTestLogger(),
Env: "default",
FileOrDir: "helmfile.yaml.gotmpl",
OverrideHelmBinary: DefaultHelmBinary,
OverrideKubeContext: "default",
DisableKubeVersionAutoDetection: true,
Logger: newAppTestLogger(),
Env: "default",
FileOrDir: "helmfile.yaml.gotmpl",
}, files)
expectNoCallsToHelm(app)

View File

@ -84,12 +84,13 @@ releases:
}
app := appWithFs(&App{
OverrideHelmBinary: DefaultHelmBinary,
fs: ffs.DefaultFileSystem(),
OverrideKubeContext: "default",
Env: tc.environment,
Logger: logger,
valsRuntime: valsRuntime,
OverrideHelmBinary: DefaultHelmBinary,
fs: ffs.DefaultFileSystem(),
OverrideKubeContext: "default",
DisableKubeVersionAutoDetection: true,
Env: tc.environment,
Logger: logger,
valsRuntime: valsRuntime,
}, files)
expectNoCallsToHelm(app)
@ -127,8 +128,8 @@ releases:
check(t, testcase{
environment: "default",
expected: `GROUP RELEASE DEPENDENCIES
1 default/kube-system/logging
1 default/kube-system/disabled
1 default/kube-system/logging
1 default/kube-system/disabled
2 default/kube-system/kubernetes-external-secrets default/kube-system/logging
2 default//test2 default/kube-system/disabled
3 default/default/external-secrets default/kube-system/kubernetes-external-secrets

View File

@ -54,11 +54,12 @@ func TestDestroy_2(t *testing.T) {
}
app := appWithFs(&App{
OverrideHelmBinary: DefaultHelmBinary,
fs: ffs.DefaultFileSystem(),
OverrideKubeContext: "",
Env: "default",
Logger: logger,
OverrideHelmBinary: DefaultHelmBinary,
fs: ffs.DefaultFileSystem(),
OverrideKubeContext: "",
DisableKubeVersionAutoDetection: true,
Env: "default",
Logger: logger,
helms: map[helmKey]helmexec.Interface{
createHelmKey("helm", ""): helm,
},

View File

@ -131,11 +131,12 @@ func TestDestroy(t *testing.T) {
}
app := appWithFs(&App{
OverrideHelmBinary: DefaultHelmBinary,
fs: ffs.DefaultFileSystem(),
OverrideKubeContext: "default",
Env: "default",
Logger: logger,
OverrideHelmBinary: DefaultHelmBinary,
fs: ffs.DefaultFileSystem(),
OverrideKubeContext: "default",
DisableKubeVersionAutoDetection: true,
Env: "default",
Logger: logger,
helms: map[helmKey]helmexec.Interface{
createHelmKey("helm", "default"): helm,
},

View File

@ -806,11 +806,12 @@ releases:
}
app := appWithFs(&App{
OverrideHelmBinary: DefaultHelmBinary,
fs: ffs.DefaultFileSystem(),
OverrideKubeContext: "",
Env: "default",
Logger: logger,
OverrideHelmBinary: DefaultHelmBinary,
fs: ffs.DefaultFileSystem(),
OverrideKubeContext: "",
DisableKubeVersionAutoDetection: true,
Env: "default",
Logger: logger,
helms: map[helmKey]helmexec.Interface{
createHelmKey("helm", ""): helm,
},

View File

@ -1170,11 +1170,12 @@ releases:
}
app := appWithFs(&App{
OverrideHelmBinary: DefaultHelmBinary,
fs: ffs.DefaultFileSystem(),
OverrideKubeContext: overrideKubeContext,
Env: "default",
Logger: logger,
OverrideHelmBinary: DefaultHelmBinary,
fs: ffs.DefaultFileSystem(),
OverrideKubeContext: overrideKubeContext,
DisableKubeVersionAutoDetection: true,
Env: "default",
Logger: logger,
helms: map[helmKey]helmexec.Interface{
createHelmKey("helm", overrideKubeContext): helm,
},

View File

@ -3,10 +3,25 @@ package app
import (
"encoding/json"
"fmt"
"strings"
"github.com/gosuri/uitable"
)
// trimTrailingWhitespace removes trailing whitespace from each line in the input string.
// This ensures consistent output formatting by removing spaces and tabs that table
// formatting libraries may add to pad empty columns.
func trimTrailingWhitespace(s string) string {
lines := strings.Split(s, "\n")
for i, line := range lines {
// Only modify lines that actually have trailing whitespace
if trimmed := strings.TrimRight(line, " \t"); trimmed != line {
lines[i] = trimmed
}
}
return strings.Join(lines, "\n")
}
func FormatAsTable(releases []*HelmRelease) error {
table := uitable.New()
table.AddRow("NAME", "NAMESPACE", "ENABLED", "INSTALLED", "LABELS", "CHART", "VERSION")
@ -15,7 +30,8 @@ func FormatAsTable(releases []*HelmRelease) error {
table.AddRow(r.Name, r.Namespace, fmt.Sprintf("%t", r.Enabled), fmt.Sprintf("%t", r.Installed), r.Labels, r.Chart, r.Version)
}
fmt.Println(table.String())
output := trimTrailingWhitespace(table.String())
fmt.Println(output)
return nil
}

View File

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

View File

@ -1,3 +1,3 @@
NAME NAMESPACE ENABLED INSTALLED LABELS CHART VERSION
test test true true test test test
test1 test2 false false test1 test1 test1
test test true true test test test
test1 test2 false false test1 test1 test1

53
pkg/cluster/version.go Normal file
View File

@ -0,0 +1,53 @@
package cluster
import (
"fmt"
"k8s.io/client-go/discovery"
"k8s.io/client-go/tools/clientcmd"
)
// DetectServerVersion detects the Kubernetes server version by connecting to the cluster.
// It returns the version in the format "major.minor.patch" (e.g., "1.34.1").
// Returns an error if detection fails.
func DetectServerVersion(kubeconfig, context string) (string, error) {
// Build the kubeconfig
loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
if kubeconfig != "" {
loadingRules.ExplicitPath = kubeconfig
}
configOverrides := &clientcmd.ConfigOverrides{}
if context != "" {
configOverrides.CurrentContext = context
}
config, err := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(
loadingRules,
configOverrides,
).ClientConfig()
if err != nil {
return "", fmt.Errorf("failed to load kubeconfig: %w", err)
}
// Create discovery client
discoveryClient, err := discovery.NewDiscoveryClientForConfig(config)
if err != nil {
return "", fmt.Errorf("failed to create discovery client: %w", err)
}
// Get server version
serverVersion, err := discoveryClient.ServerVersion()
if err != nil {
return "", fmt.Errorf("failed to get server version: %w", err)
}
// ServerVersion.GitVersion includes "v" prefix (e.g., "v1.34.1")
// Strip the "v" prefix to match Helm's --kube-version format (e.g., "1.34.1")
version := serverVersion.GitVersion
if len(version) > 0 && version[0] == 'v' {
version = version[1:]
}
return version, nil
}

View File

@ -0,0 +1,36 @@
package cluster
import (
"testing"
"github.com/stretchr/testify/require"
)
// TestDetectServerVersion_Integration tests the cluster version detection
// against a real Kubernetes cluster (if available).
// This test will be skipped if no cluster is accessible.
func TestDetectServerVersion_Integration(t *testing.T) {
// Try to detect version with default kubeconfig
version, err := DetectServerVersion("", "")
if err != nil {
t.Skipf("Skipping test - no accessible Kubernetes cluster: %v", err)
return
}
// If we got a version, verify it's in a valid format
require.NotEmpty(t, version, "Version should not be empty")
require.NotContains(t, version, "v", "Version should not have 'v' prefix")
// Version should look like "1.xx.y" format
require.Regexp(t, `^\d+\.\d+\.\d+`, version, "Version should match semver format")
}
// TestDetectServerVersion_InvalidConfig tests error handling
func TestDetectServerVersion_InvalidConfig(t *testing.T) {
// Try with a non-existent kubeconfig file
_, err := DetectServerVersion("/non/existent/path/kubeconfig", "")
require.Error(t, err, "Should return error for invalid kubeconfig")
require.Contains(t, err.Error(), "failed to load kubeconfig", "Error should mention kubeconfig loading")
}

View File

@ -81,13 +81,10 @@ func (e *Environment) Merge(other *Environment) (*Environment, error) {
func (e *Environment) GetMergedValues() (map[string]any, error) {
vals := map[string]any{}
if err := mergo.Merge(&vals, e.Defaults, mergo.WithOverride); err != nil {
return nil, err
}
if err := mergo.Merge(&vals, e.Values, mergo.WithOverride); err != nil {
return nil, err
}
// Use MergeMaps instead of mergo.Merge to properly handle array merging element-by-element
// This fixes issue #2281 where arrays were being replaced entirely instead of merged
vals = maputil.MergeMaps(vals, e.Defaults)
vals = maputil.MergeMaps(vals, e.Values)
vals, err := maputil.CastKeysToStrings(vals)
if err != nil {

View File

@ -669,6 +669,12 @@ func (helm *execer) DiffRelease(context HelmContext, name, chart, namespace stri
overrideEnableLiveOutput = &enableLiveOutput
}
// Issue #2280: In Helm 4, the --color flag is parsed by Helm before reaching the plugin,
// causing it to consume the next argument. Remove color flags and use HELM_DIFF_COLOR env var.
if helm.IsHelm4() {
flags = helm.filterColorFlagsForHelm4(flags, env)
}
out, err := helm.exec(append(append(preArgs, "diff", "upgrade", "--allow-unreleased", name, chart), flags...), env, overrideEnableLiveOutput)
// Do our best to write STDOUT only when diff existed
// Unfortunately, this works only when you run helmfile with `--detailed-exitcode`
@ -693,6 +699,37 @@ func (helm *execer) DiffRelease(context HelmContext, name, chart, namespace stri
return err
}
// filterColorFlagsForHelm4 removes --color and --no-color flags from the flags slice
// and sets the HELM_DIFF_COLOR environment variable instead.
// In Helm 4, the --color flag is parsed by Helm itself before reaching the helm-diff plugin,
// causing Helm to consume the next argument as the color value (issue #2280).
// The helm-diff plugin supports HELM_DIFF_COLOR=[true|false] env var as an alternative.
func (helm *execer) filterColorFlagsForHelm4(flags []string, env map[string]string) []string {
filtered := make([]string, 0, len(flags))
for _, flag := range flags {
switch flag {
case "--color":
// Use environment variable instead of flag for Helm 4
// Only set if not already present (defensive check)
if _, exists := env["HELM_DIFF_COLOR"]; !exists {
env["HELM_DIFF_COLOR"] = "true"
}
case "--no-color":
// Use environment variable instead of flag for Helm 4
// Only set if not already present (defensive check)
if _, exists := env["HELM_DIFF_COLOR"]; !exists {
env["HELM_DIFF_COLOR"] = "false"
}
default:
// Keep all other flags unchanged
filtered = append(filtered, flag)
}
}
return filtered
}
func (helm *execer) Lint(name, chart string, flags ...string) error {
helm.logger.Infof("Linting release=%v, chart=%v", name, chart)
out, err := helm.exec(append([]string{"lint", chart}, flags...), map[string]string{}, nil)

View File

@ -700,6 +700,140 @@ exec: helm --kubeconfig config --kube-context dev diff upgrade --allow-unrelease
}
}
func Test_DiffRelease_ColorFlagHelm4(t *testing.T) {
// Test that --color and --no-color flags are removed and HELM_DIFF_COLOR env var is set for Helm 4
var buffer bytes.Buffer
logger := NewLogger(&buffer, "debug")
// MockExecer creates a Helm 4 execer by default (returns v4.0.0)
helm, err := MockExecer(logger, "config", "dev")
if err != nil {
t.Errorf("unexpected error: %v", err)
}
// Verify it's Helm 4
if !helm.IsHelm4() {
t.Errorf("expected Helm 4, got version: %v", helm.GetVersion())
}
// Test with --color flag
buffer.Reset()
err = helm.DiffRelease(HelmContext{}, "release", "chart", "default", false, "--color", "--context", "3")
if err != nil {
t.Errorf("unexpected error: %v", err)
}
// The --color flag should be removed and --context should remain
expected := `Comparing release=release, chart=chart, namespace=default
exec: helm --kubeconfig config --kube-context dev diff upgrade --allow-unreleased release chart --context 3
`
actual := buffer.String()
if actual != expected {
t.Errorf("helmexec.DiffRelease() with --color\nactual = %v\nexpect = %v", actual, expected)
}
// Verify --color flag was removed
if strings.Contains(actual, "--color") {
t.Errorf("--color flag should have been removed, but got: %v", actual)
}
// Test with --no-color flag
buffer.Reset()
err = helm.DiffRelease(HelmContext{}, "release", "chart", "default", false, "--no-color", "--context", "3")
if err != nil {
t.Errorf("unexpected error: %v", err)
}
// The --no-color flag should be removed and --context should remain
expected = `Comparing release=release, chart=chart, namespace=default
exec: helm --kubeconfig config --kube-context dev diff upgrade --allow-unreleased release chart --context 3
`
actual = buffer.String()
if actual != expected {
t.Errorf("helmexec.DiffRelease() with --no-color\nactual = %v\nexpect = %v", actual, expected)
}
// Verify --no-color flag was removed
if strings.Contains(actual, "--no-color") {
t.Errorf("--no-color flag should have been removed, but got: %v", actual)
}
}
func Test_FilterColorFlagsForHelm4(t *testing.T) {
var buffer bytes.Buffer
logger := NewLogger(&buffer, "debug")
helm, err := MockExecer(logger, "config", "dev")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
tests := []struct {
name string
inputFlags []string
expectedFlags []string
expectedEnvKey string
expectedEnvVal string
}{
{
name: "color flag",
inputFlags: []string{"--color", "--context", "3"},
expectedFlags: []string{"--context", "3"},
expectedEnvKey: "HELM_DIFF_COLOR",
expectedEnvVal: "true",
},
{
name: "no-color flag",
inputFlags: []string{"--no-color", "--context", "3"},
expectedFlags: []string{"--context", "3"},
expectedEnvKey: "HELM_DIFF_COLOR",
expectedEnvVal: "false",
},
{
name: "no color flags",
inputFlags: []string{"--context", "3", "--detailed-exitcode"},
expectedFlags: []string{"--context", "3", "--detailed-exitcode"},
expectedEnvKey: "",
expectedEnvVal: "",
},
{
name: "color flag with other flags",
inputFlags: []string{"--detailed-exitcode", "--color", "--suppress", "secret"},
expectedFlags: []string{"--detailed-exitcode", "--suppress", "secret"},
expectedEnvKey: "HELM_DIFF_COLOR",
expectedEnvVal: "true",
},
{
name: "both color and no-color flags (first wins with defensive check)",
inputFlags: []string{"--color", "--no-color", "--context", "3"},
expectedFlags: []string{"--context", "3"},
expectedEnvKey: "HELM_DIFF_COLOR",
expectedEnvVal: "true", // Changed: first flag wins due to defensive check
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
env := make(map[string]string)
actualFlags := helm.filterColorFlagsForHelm4(tt.inputFlags, env)
if !reflect.DeepEqual(actualFlags, tt.expectedFlags) {
t.Errorf("filterColorFlagsForHelm4() flags\nactual = %v\nexpect = %v", actualFlags, tt.expectedFlags)
}
if tt.expectedEnvKey != "" {
if env[tt.expectedEnvKey] != tt.expectedEnvVal {
t.Errorf("filterColorFlagsForHelm4() env[%v]\nactual = %v\nexpect = %v",
tt.expectedEnvKey, env[tt.expectedEnvKey], tt.expectedEnvVal)
}
} else if len(env) > 0 {
t.Errorf("filterColorFlagsForHelm4() expected no env vars, but got: %v", env)
}
})
}
}
func Test_DeleteRelease(t *testing.T) {
var buffer bytes.Buffer
logger := NewLogger(&buffer, "debug")

View File

@ -243,7 +243,68 @@ func MergeMaps(a, b map[string]interface{}) map[string]interface{} {
}
}
}
// Handle array merging element-by-element
vSlice := toInterfaceSlice(v)
if vSlice != nil {
if outVal, exists := out[k]; exists {
outSlice := toInterfaceSlice(outVal)
if outSlice != nil {
// Both are slices - merge element by element
out[k] = mergeSlices(outSlice, vSlice)
continue
}
}
}
out[k] = v
}
return out
}
// toInterfaceSlice converts various slice types to []interface{}
func toInterfaceSlice(v any) []any {
if slice, ok := v.([]any); ok {
return slice
}
return nil
}
// mergeSlices merges two slices element by element
// Elements from override (b) take precedence, but we preserve elements from base (a)
// that don't exist in override
func mergeSlices(base, override []any) []any {
// Determine the maximum length
maxLen := len(base)
if len(override) > maxLen {
maxLen = len(override)
}
result := make([]interface{}, maxLen)
// First copy all elements from base
copy(result, base)
// Then merge/override with elements from override
for i := 0; i < len(override); i++ {
overrideVal := override[i]
// Skip nil values in override - they represent unset indices
if overrideVal == nil {
continue
}
// If both are maps, merge them recursively
if overrideMap, ok := overrideVal.(map[string]any); ok {
if i < len(base) {
if baseMap, ok := base[i].(map[string]any); ok {
result[i] = MergeMaps(baseMap, overrideMap)
continue
}
}
}
// Otherwise, override completely
result[i] = overrideVal
}
return result
}

View File

@ -265,3 +265,355 @@ func TestMapUtil_MergeMaps(t *testing.T) {
t.Errorf("Expected a map with empty value not to overwrite another map's value. Expected: %v, got %v", expectedMap, testMap)
}
}
// TestMapUtil_Issue2281_ArrayMerging tests the bug reported in issue #2281
// where setting nested values in arrays replaces the entire object
func TestMapUtil_Issue2281_ArrayMerging(t *testing.T) {
tests := []struct {
name string
initialMap map[string]any
operations []struct {
key []string
value string
}
expected map[string]any
}{
{
name: "simple array element replacement should preserve other elements",
initialMap: map[string]any{
"top": map[string]any{
"array": []any{"thing1", "thing2"},
},
},
operations: []struct {
key []string
value string
}{
{key: []string{"top", "array[0]"}, value: "cmdlinething1"},
},
expected: map[string]any{
"top": map[string]any{
"array": []any{"cmdlinething1", "thing2"},
},
},
},
{
name: "nested field in array object should merge not replace",
initialMap: map[string]any{
"top": map[string]any{
"complexArray": []any{
map[string]any{
"thing": "a thing",
"anotherThing": "another thing",
},
map[string]any{
"thing": "second thing",
"anotherThing": "a second other thing",
},
},
},
},
operations: []struct {
key []string
value string
}{
{key: []string{"top", "complexArray[1]", "anotherThing"}, value: "cmdline"},
},
expected: map[string]any{
"top": map[string]any{
"complexArray": []any{
map[string]any{
"thing": "a thing",
"anotherThing": "another thing",
},
map[string]any{
"thing": "second thing",
"anotherThing": "cmdline",
},
},
},
},
},
{
name: "complete issue #2281 scenario",
initialMap: map[string]any{
"top": map[string]any{
"array": []any{"thing1", "thing2"},
"complexArray": []any{
map[string]any{
"thing": "a thing",
"anotherThing": "another thing",
},
map[string]any{
"thing": "second thing",
"anotherThing": "a second other thing",
},
},
},
},
operations: []struct {
key []string
value string
}{
{key: []string{"top", "array[0]"}, value: "cmdlinething1"},
{key: []string{"top", "complexArray[1]", "anotherThing"}, value: "cmdline"},
},
expected: map[string]any{
"top": map[string]any{
"array": []any{"cmdlinething1", "thing2"},
"complexArray": []any{
map[string]any{
"thing": "a thing",
"anotherThing": "another thing",
},
map[string]any{
"thing": "second thing",
"anotherThing": "cmdline",
},
},
},
},
},
{
name: "setting nested value in first array element should preserve fields",
initialMap: map[string]any{
"top": map[string]any{
"complexArray": []any{
map[string]any{
"thing": "a thing",
"anotherThing": "another thing",
},
},
},
},
operations: []struct {
key []string
value string
}{
{key: []string{"top", "complexArray[0]", "anotherThing"}, value: "modified"},
},
expected: map[string]any{
"top": map[string]any{
"complexArray": []any{
map[string]any{
"thing": "a thing",
"anotherThing": "modified",
},
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := tt.initialMap
for _, op := range tt.operations {
Set(result, op.key, op.value, false)
}
if !reflect.DeepEqual(result, tt.expected) {
t.Errorf("Result mismatch:\nExpected: %+v\nGot: %+v", tt.expected, result)
}
})
}
}
// TestMapUtil_Issue2281_EmptyMapScenario demonstrates the actual bug
// when starting from an empty map (like --state-values-set does)
func TestMapUtil_Issue2281_EmptyMapScenario(t *testing.T) {
// This test demonstrates what currently happens vs what should happen
// when using --state-values-set with array indices
// What currently happens: setting multiple values creates sparse arrays with nulls
t.Run("current buggy behavior - demonstrates the issue", func(t *testing.T) {
set := map[string]any{}
// Simulating: --state-values-set top.array[0]=cmdlinething1
Set(set, []string{"top", "array[0]"}, "cmdlinething1", false)
// Check what we got
topArray := set["top"].(map[string]any)["array"].([]any)
// Currently this creates: ["cmdlinething1"]
// which is actually correct for a single set operation
if len(topArray) != 1 {
t.Errorf("Expected array length 1, got %d", len(topArray))
}
if topArray[0] != "cmdlinething1" {
t.Errorf("Expected array[0] to be 'cmdlinething1', got %v", topArray[0])
}
})
t.Run("actual bug - setting array index 1 without index 0 creates null at 0", func(t *testing.T) {
set := map[string]any{}
// Simulating: --state-values-set top.complexArray[1].anotherThing=cmdline
// WITHOUT first defining complexArray[0]
Set(set, []string{"top", "complexArray[1]", "anotherThing"}, "cmdline", false)
// Check what we got
topComplexArray := set["top"].(map[string]any)["complexArray"].([]any)
// BUG: This creates [nil, {anotherThing: "cmdline"}]
// The issue description says array entries not referenced are being deleted or set to null
if len(topComplexArray) != 2 {
t.Errorf("Expected array length 2, got %d", len(topComplexArray))
}
// Index 0 should be nil (this is the bug!)
if topComplexArray[0] != nil {
t.Logf("Note: topComplexArray[0] = %v (expected nil for this test showing the bug)", topComplexArray[0])
}
// Index 1 should have the value
obj1 := topComplexArray[1].(map[string]any)
if obj1["anotherThing"] != "cmdline" {
t.Errorf("Expected complexArray[1].anotherThing to be 'cmdline', got %v", obj1["anotherThing"])
}
})
}
// TestMapUtil_Issue2281_MergeArrays tests that MergeMaps should merge arrays element-by-element
func TestMapUtil_Issue2281_MergeArrays(t *testing.T) {
t.Run("merging arrays should preserve elements from base that aren't in override", func(t *testing.T) {
// Base values from helmfile
base := map[string]interface{}{
"top": map[string]any{
"array": []any{"thing1", "thing2"},
},
}
// Override values from --state-values-set top.array[0]=cmdlinething1
override := map[string]interface{}{
"top": map[string]any{
"array": []any{"cmdlinething1"},
},
}
result := MergeMaps(base, override)
// Expected: array should be ["cmdlinething1", "thing2"]
// array[0] is overridden, array[1] is preserved from base
resultArray := result["top"].(map[string]any)["array"].([]any)
expected := []any{"cmdlinething1", "thing2"}
if !reflect.DeepEqual(resultArray, expected) {
t.Errorf("Array merge failed:\nExpected: %+v\nGot: %+v", expected, resultArray)
}
})
t.Run("merging complex arrays should preserve non-overridden elements and fields", func(t *testing.T) {
// Base values from helmfile
base := map[string]interface{}{
"top": map[string]any{
"complexArray": []any{
map[string]any{
"thing": "a thing",
"anotherThing": "another thing",
},
map[string]any{
"thing": "second thing",
"anotherThing": "a second other thing",
},
},
},
}
// Override values from --state-values-set top.complexArray[1].anotherThing=cmdline
override := map[string]interface{}{
"top": map[string]any{
"complexArray": []any{
nil,
map[string]any{
"anotherThing": "cmdline",
},
},
},
}
result := MergeMaps(base, override)
// Expected: complexArray[0] should be unchanged, complexArray[1] should have merged fields
resultArray := result["top"].(map[string]any)["complexArray"].([]any)
// Check array length
if len(resultArray) != 2 {
t.Fatalf("Expected array length 2, got %d", len(resultArray))
}
// Check complexArray[0] is unchanged
elem0 := resultArray[0].(map[string]any)
if elem0["thing"] != "a thing" || elem0["anotherThing"] != "another thing" {
t.Errorf("complexArray[0] was modified:\nGot: %+v", elem0)
}
// Check complexArray[1] has merged fields
elem1 := resultArray[1].(map[string]any)
if elem1["thing"] != "second thing" {
t.Errorf("complexArray[1].thing should be preserved, got %v", elem1["thing"])
}
if elem1["anotherThing"] != "cmdline" {
t.Errorf("complexArray[1].anotherThing should be 'cmdline', got %v", elem1["anotherThing"])
}
})
t.Run("complete issue #2281 scenario with MergeMaps", func(t *testing.T) {
// Base values from helmfile
base := map[string]interface{}{
"top": map[string]any{
"array": []any{"thing1", "thing2"},
"complexArray": []any{
map[string]any{
"thing": "a thing",
"anotherThing": "another thing",
},
map[string]any{
"thing": "second thing",
"anotherThing": "a second other thing",
},
},
},
}
// Override values from:
// --state-values-set top.array[0]=cmdlinething1
// --state-values-set top.complexArray[1].anotherThing=cmdline
override := map[string]interface{}{
"top": map[string]any{
"array": []any{"cmdlinething1"},
"complexArray": []any{
nil,
map[string]any{
"anotherThing": "cmdline",
},
},
},
}
result := MergeMaps(base, override)
// Check array
resultArray := result["top"].(map[string]any)["array"].([]any)
expectedArray := []any{"cmdlinething1", "thing2"}
if !reflect.DeepEqual(resultArray, expectedArray) {
t.Errorf("Array merge failed:\nExpected: %+v\nGot: %+v", expectedArray, resultArray)
}
// Check complexArray
resultComplexArray := result["top"].(map[string]any)["complexArray"].([]any)
if len(resultComplexArray) != 2 {
t.Fatalf("Expected complexArray length 2, got %d", len(resultComplexArray))
}
elem0 := resultComplexArray[0].(map[string]any)
if elem0["thing"] != "a thing" || elem0["anotherThing"] != "another thing" {
t.Errorf("complexArray[0] was modified:\nGot: %+v", elem0)
}
elem1 := resultComplexArray[1].(map[string]any)
if elem1["thing"] != "second thing" || elem1["anotherThing"] != "cmdline" {
t.Errorf("complexArray[1] merge failed:\nExpected: {thing: second thing, anotherThing: cmdline}\nGot: %+v", elem1)
}
})
}

View File

@ -1,6 +1,7 @@
package plugins
import (
"io"
"sync"
"github.com/helmfile/vals"
@ -17,7 +18,13 @@ var once sync.Once
func ValsInstance() (*vals.Runtime, error) {
var err error
once.Do(func() {
instance, err = vals.New(vals.Options{CacheSize: valsCacheSize})
// Set LogOutput to io.Discard to suppress debug logs from AWS SDK and other providers
// This prevents sensitive information (tokens, auth headers) from being logged to stdout
// See issue #2270
instance, err = vals.New(vals.Options{
CacheSize: valsCacheSize,
LogOutput: io.Discard,
})
})
return instance, err

View File

@ -217,6 +217,60 @@ func (c *StateCreator) ParseAndLoad(content []byte, baseDir, file string, envNam
return state, nil
}
// mergeEnvironments deeply merges environment specifications from src into dst.
// Unlike mergo.WithOverride which replaces entire EnvironmentSpec values, this function
// properly merges the Values slices from both environments.
func mergeEnvironments(dst, src map[string]EnvironmentSpec) {
// If dst is nil, there's nothing to merge into
if dst == nil {
return
}
for envName, srcEnv := range src {
if dstEnv, exists := dst[envName]; exists {
// Environment exists in both - merge the Values slices
mergedValues := append([]any{}, dstEnv.Values...)
mergedValues = append(mergedValues, srcEnv.Values...)
// Merge Secrets slices
mergedSecrets := append([]string{}, dstEnv.Secrets...)
mergedSecrets = append(mergedSecrets, srcEnv.Secrets...)
// Create merged environment
merged := EnvironmentSpec{
Values: mergedValues,
Secrets: mergedSecrets,
}
// Override KubeContext if src has it
if srcEnv.KubeContext != "" {
merged.KubeContext = srcEnv.KubeContext
} else {
merged.KubeContext = dstEnv.KubeContext
}
// Override MissingFileHandler if src has it
if srcEnv.MissingFileHandler != nil {
merged.MissingFileHandler = srcEnv.MissingFileHandler
} else {
merged.MissingFileHandler = dstEnv.MissingFileHandler
}
// Override MissingFileHandlerConfig if src has it
if srcEnv.MissingFileHandlerConfig != nil {
merged.MissingFileHandlerConfig = srcEnv.MissingFileHandlerConfig
} else {
merged.MissingFileHandlerConfig = dstEnv.MissingFileHandlerConfig
}
dst[envName] = merged
} else {
// Environment only exists in src - just copy it
dst[envName] = srcEnv
}
}
}
func (c *StateCreator) loadBases(envValues, overrodeEnv *environment.Environment, st *HelmState, baseDir string) (*HelmState, error) {
var newOverrodeEnv *environment.Environment
if overrodeEnv != nil {
@ -234,9 +288,25 @@ func (c *StateCreator) loadBases(envValues, overrodeEnv *environment.Environment
layers = append(layers, st)
for i := 1; i < len(layers); i++ {
// Initialize Environments map if nil to avoid panic in mergeEnvironments
if layers[0].Environments == nil {
layers[0].Environments = make(map[string]EnvironmentSpec)
}
// Manually merge environments to ensure deep merging of environment values
mergeEnvironments(layers[0].Environments, layers[i].Environments)
// Clear the Environments from the source before mergo to avoid override
tmpEnvs := layers[i].Environments
layers[i].Environments = nil
// Now merge the rest of the fields
if err := mergo.Merge(layers[0], layers[i], mergo.WithAppendSlice, mergo.WithOverride); err != nil {
return nil, err
}
// Restore the Environments back to the source layer (in case it's used later)
layers[i].Environments = tmpEnvs
}
return layers[0], nil
@ -342,9 +412,9 @@ func (c *StateCreator) loadEnvValues(st *HelmState, name string, failOnMissingEn
if overrode != nil {
intOverrodeEnv := *newEnv
if err := mergo.Merge(&intOverrodeEnv, overrode, mergo.WithOverride); err != nil {
return nil, fmt.Errorf("error while merging environment overrode values for \"%s\": %v", name, err)
}
// Use MergeMaps instead of mergo.Merge to properly handle array merging element-by-element
// This fixes issue #2281 where arrays were being replaced entirely instead of merged
intOverrodeEnv.Values = maputil.MergeMaps(intOverrodeEnv.Values, overrode.Values)
newEnv = &intOverrodeEnv
}

View File

@ -728,3 +728,154 @@ bases:
})
}
}
// TestEnvironmentMergingWithBases tests that environment values from multiple bases
// are properly merged rather than replaced. This is a regression test for issue #2273.
func TestEnvironmentMergingWithBases(t *testing.T) {
tests := []struct {
name string
files map[string]string
mainFile string
environment string
expectedError bool
checkValues func(t *testing.T, state *HelmState)
}{
{
name: "environment values should merge from multiple bases",
files: map[string]string{
"/path/one.yaml": `environments:
sandbox:
values:
- example:
enabled: true
`,
"/path/two.yaml": `environments:
sandbox: {}
`,
"/path/helmfile.yaml": `bases:
- one.yaml
- two.yaml
---
repositories:
- name: examples
url: https://helm.github.io/examples
releases:
- name: example
chart: examples/hello-world
`,
},
mainFile: "/path/helmfile.yaml",
environment: "sandbox",
checkValues: func(t *testing.T, state *HelmState) {
// Check that the environment has the values from the first base
envSpec, ok := state.Environments["sandbox"]
require.True(t, ok, "sandbox environment should exist")
require.NotNil(t, envSpec.Values, "environment values should not be nil")
require.Greater(t, len(envSpec.Values), 0, "environment should have values from first base")
// Check that RenderedValues has the example.enabled value
require.NotNil(t, state.RenderedValues, "rendered values should not be nil")
exampleVal, ok := state.RenderedValues["example"]
require.True(t, ok, "example key should exist in rendered values")
exampleMap, ok := exampleVal.(map[string]any)
require.True(t, ok, "example should be a map")
enabled, ok := exampleMap["enabled"]
require.True(t, ok, "enabled key should exist")
require.Equal(t, true, enabled, "enabled should be true")
},
},
{
name: "environment values should merge when second base adds values",
files: map[string]string{
"/path/one.yaml": `environments:
sandbox:
values:
- example:
enabled: true
`,
"/path/two.yaml": `environments:
sandbox:
values:
- another:
setting: value
`,
"/path/helmfile.yaml": `bases:
- one.yaml
- two.yaml
---
repositories:
- name: examples
url: https://helm.github.io/examples
releases:
- name: example
chart: examples/hello-world
`,
},
mainFile: "/path/helmfile.yaml",
environment: "sandbox",
checkValues: func(t *testing.T, state *HelmState) {
// Check that both values from both bases are present
require.NotNil(t, state.RenderedValues, "rendered values should not be nil")
exampleVal, ok := state.RenderedValues["example"]
require.True(t, ok, "example key should exist in rendered values")
exampleMap, ok := exampleVal.(map[string]any)
require.True(t, ok, "example should be a map")
enabled, ok := exampleMap["enabled"]
require.True(t, ok, "enabled key should exist")
require.Equal(t, true, enabled, "enabled should be true")
anotherVal, ok := state.RenderedValues["another"]
require.True(t, ok, "another key should exist in rendered values")
anotherMap, ok := anotherVal.(map[string]any)
require.True(t, ok, "another should be a map")
setting, ok := anotherMap["setting"]
require.True(t, ok, "setting key should exist")
require.Equal(t, "value", setting, "setting should be 'value'")
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
creator := &StateCreator{
logger: logger,
fs: &filesystem.FileSystem{
ReadFile: func(filename string) ([]byte, error) {
content, ok := tt.files[filename]
if !ok {
return nil, fmt.Errorf("file not found: %s", filename)
}
return []byte(content), nil
},
},
valsRuntime: valsRuntime,
Strict: true,
}
creator.LoadFile = func(inheritedEnv, overrodeEnv *environment.Environment, baseDir, file string, evaluateBases bool) (*HelmState, error) {
path := filepath.Join(baseDir, file)
content, ok := tt.files[path]
if !ok {
return nil, fmt.Errorf("file not found: %s", path)
}
return creator.ParseAndLoad([]byte(content), filepath.Dir(path), path, tt.environment, true, evaluateBases, inheritedEnv, overrodeEnv)
}
yamlContent, ok := tt.files[tt.mainFile]
if !ok {
t.Fatalf("no file named %q registered", tt.mainFile)
}
state, err := creator.ParseAndLoad([]byte(yamlContent), filepath.Dir(tt.mainFile), tt.mainFile, tt.environment, true, true, nil, nil)
if tt.expectedError {
require.Error(t, err, "expected an error but got none")
return
}
require.NoError(t, err, "unexpected error: %v", err)
if tt.checkValues != nil {
tt.checkValues(t, state)
}
})
}
}

View File

@ -0,0 +1,135 @@
package state
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TestOCIChartVersionHandling tests the handling of OCI chart versions (issue #2247)
func TestOCIChartVersionHandling(t *testing.T) {
tests := []struct {
name string
chart string
version string
devel bool
helmVersion string
expectedVersion string
expectedError bool
expectedQualifiedChart string
}{
{
name: "OCI chart with explicit version",
chart: "oci://registry.example.com/my-chart",
version: "1.2.3",
helmVersion: "3.18.0",
expectedVersion: "1.2.3",
expectedError: false,
expectedQualifiedChart: "registry.example.com/my-chart:1.2.3",
},
{
name: "OCI chart with semver range version",
chart: "oci://registry.example.com/my-chart",
version: "^1.0.0",
helmVersion: "3.18.0",
expectedVersion: "^1.0.0",
expectedError: false,
expectedQualifiedChart: "registry.example.com/my-chart:^1.0.0",
},
{
name: "OCI chart without version should use empty string",
chart: "oci://registry.example.com/my-chart",
version: "",
helmVersion: "3.18.0",
expectedVersion: "",
expectedError: false,
expectedQualifiedChart: "registry.example.com/my-chart",
},
{
name: "OCI chart with explicit 'latest' should fail (any Helm version)",
chart: "oci://registry.example.com/my-chart",
version: "latest",
helmVersion: "3.18.0",
expectedVersion: "",
expectedError: true,
expectedQualifiedChart: "",
},
{
name: "OCI chart with explicit 'latest' should also fail on older Helm",
chart: "oci://registry.example.com/my-chart",
version: "latest",
helmVersion: "3.7.0",
expectedVersion: "",
expectedError: true,
expectedQualifiedChart: "",
},
{
name: "OCI chart without version in devel mode",
chart: "oci://registry.example.com/my-chart",
version: "",
devel: true,
helmVersion: "3.18.0",
expectedVersion: "",
expectedError: false,
expectedQualifiedChart: "registry.example.com/my-chart",
},
{
name: "non-OCI chart returns empty qualified name",
chart: "stable/nginx",
version: "",
helmVersion: "3.18.0",
expectedVersion: "",
expectedError: false,
expectedQualifiedChart: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Create a minimal HelmState
st := &HelmState{
basePath: "/test",
}
// Create a release
release := &ReleaseSpec{
Name: "test-release",
Chart: tt.chart,
Version: tt.version,
}
if tt.devel {
devel := true
release.Devel = &devel
}
// Call the function
qualifiedChartName, chartName, chartVersion, err := st.getOCIQualifiedChartName(release)
// Check error
if tt.expectedError {
require.Error(t, err)
assert.Contains(t, err.Error(), "semver compliant")
} else {
require.NoError(t, err)
}
// Check version
assert.Equal(t, tt.expectedVersion, chartVersion, "chartVersion mismatch")
// Check qualified chart name
assert.Equal(t, tt.expectedQualifiedChart, qualifiedChartName, "qualifiedChartName mismatch")
// Check chart name extraction for OCI charts
if IsOCIChart(tt.chart) && !tt.expectedError {
assert.Equal(t, "my-chart", chartName, "chartName mismatch")
}
})
}
}
// IsOCIChart is a helper function to check if a chart is OCI-based
func IsOCIChart(chart string) bool {
return len(chart) > 6 && chart[:6] == "oci://"
}

121
pkg/state/skip_test.go Normal file
View File

@ -0,0 +1,121 @@
package state
import (
"testing"
"github.com/stretchr/testify/assert"
)
// TestSkipDepsAndSkipRefresh tests that helmDefaults.skipDeps and helmDefaults.skipRefresh
// are properly applied when preparing charts (issue #2269)
func TestSkipDepsAndSkipRefresh(t *testing.T) {
tests := []struct {
name string
helmDefaultsSkipDeps bool
helmDefaultsSkipRefresh bool
releaseSkipDeps *bool
releaseSkipRefresh *bool
optsSkipDeps bool
optsSkipRefresh bool
isLocal bool
expectedSkipDeps bool
expectedSkipRefresh bool
}{
{
name: "helmDefaults.skipDeps=true should skip deps",
helmDefaultsSkipDeps: true,
helmDefaultsSkipRefresh: false,
releaseSkipDeps: nil,
releaseSkipRefresh: nil,
optsSkipDeps: false,
optsSkipRefresh: false,
isLocal: true,
expectedSkipDeps: true,
expectedSkipRefresh: false,
},
{
name: "helmDefaults.skipRefresh=true should skip refresh",
helmDefaultsSkipDeps: false,
helmDefaultsSkipRefresh: true,
releaseSkipDeps: nil,
releaseSkipRefresh: nil,
optsSkipDeps: false,
optsSkipRefresh: false,
isLocal: true,
expectedSkipDeps: false,
expectedSkipRefresh: true,
},
{
name: "both helmDefaults.skipDeps and skipRefresh=true",
helmDefaultsSkipDeps: true,
helmDefaultsSkipRefresh: true,
releaseSkipDeps: nil,
releaseSkipRefresh: nil,
optsSkipDeps: false,
optsSkipRefresh: false,
isLocal: true,
expectedSkipDeps: true,
expectedSkipRefresh: true,
},
{
name: "release.skipRefresh overrides helmDefaults",
helmDefaultsSkipDeps: false,
helmDefaultsSkipRefresh: false,
releaseSkipDeps: nil,
releaseSkipRefresh: boolPtr(true),
optsSkipDeps: false,
optsSkipRefresh: false,
isLocal: true,
expectedSkipDeps: false,
expectedSkipRefresh: true,
},
{
name: "opts.SkipRefresh (CLI flag) has priority",
helmDefaultsSkipDeps: false,
helmDefaultsSkipRefresh: false,
releaseSkipDeps: nil,
releaseSkipRefresh: nil,
optsSkipDeps: false,
optsSkipRefresh: true,
isLocal: true,
expectedSkipDeps: false,
expectedSkipRefresh: true,
},
{
name: "non-local chart always skips refresh",
helmDefaultsSkipDeps: false,
helmDefaultsSkipRefresh: false,
releaseSkipDeps: nil,
releaseSkipRefresh: nil,
optsSkipDeps: false,
optsSkipRefresh: false,
isLocal: false,
expectedSkipDeps: true, // non-local charts skip deps
expectedSkipRefresh: true, // non-local charts skip refresh
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Calculate skipDeps using the actual logic from state.go
skipDepsGlobal := tt.optsSkipDeps
skipDepsRelease := tt.releaseSkipDeps != nil && *tt.releaseSkipDeps
skipDepsDefault := tt.releaseSkipDeps == nil && tt.helmDefaultsSkipDeps
chartFetchedByGoGetter := false
skipDeps := (!tt.isLocal && !chartFetchedByGoGetter) || skipDepsGlobal || skipDepsRelease || skipDepsDefault
// Calculate skipRefresh using the actual logic from state.go (after fix)
skipRefreshGlobal := tt.optsSkipRefresh
skipRefreshRelease := tt.releaseSkipRefresh != nil && *tt.releaseSkipRefresh
skipRefreshDefault := tt.releaseSkipRefresh == nil && tt.helmDefaultsSkipRefresh
skipRefresh := !tt.isLocal || skipRefreshGlobal || skipRefreshRelease || skipRefreshDefault
assert.Equal(t, tt.expectedSkipDeps, skipDeps, "skipDeps mismatch")
assert.Equal(t, tt.expectedSkipRefresh, skipRefresh, "skipRefresh mismatch")
})
}
}
func boolPtr(b bool) *bool {
return &b
}

View File

@ -201,6 +201,11 @@ type HelmSpec struct {
Cascade *string `yaml:"cascade,omitempty"`
// SuppressOutputLineRegex is a list of regexes to suppress output lines
SuppressOutputLineRegex []string `yaml:"suppressOutputLineRegex,omitempty"`
// DisableAutoDetectedKubeVersionForDiff controls whether auto-detected kubeVersion should be passed
// to helm diff. When false (default), auto-detected kubeVersion is passed to fix issue #2275.
// Set to true to only pass explicit kubeVersion from helmfile.yaml, preventing helm-diff from
// normalizing server-side defaults which could hide real changes (e.g., ipFamilyPolicy, ipFamilies).
DisableAutoDetectedKubeVersionForDiff *bool `yaml:"disableAutoDetectedKubeVersionForDiff,omitempty"`
DisableValidation *bool `yaml:"disableValidation,omitempty"`
DisableOpenAPIValidation *bool `yaml:"disableOpenAPIValidation,omitempty"`
@ -414,6 +419,9 @@ type ReleaseSpec struct {
// SuppressOutputLineRegex is a list of regexes to suppress output lines
SuppressOutputLineRegex []string `yaml:"suppressOutputLineRegex,omitempty"`
// DisableAutoDetectedKubeVersionForDiff controls whether auto-detected kubeVersion should be passed
// to helm diff for this release. See HelmSpec.DisableAutoDetectedKubeVersionForDiff for details.
DisableAutoDetectedKubeVersionForDiff *bool `yaml:"disableAutoDetectedKubeVersionForDiff,omitempty"`
// Inherit is used to inherit a release template from a release or another release template
Inherit Inherits `yaml:"inherit,omitempty"`
@ -1318,7 +1326,7 @@ type PrepareChartKey struct {
//
// If exists, it will also patch resources by json patches, strategic-merge patches, and injectors.
// processChartification handles the chartification process
func (st *HelmState) processChartification(chartification *Chartify, release *ReleaseSpec, chartPath string, opts ChartPrepareOptions, skipDeps bool) (string, bool, error) {
func (st *HelmState) processChartification(chartification *Chartify, release *ReleaseSpec, chartPath string, opts ChartPrepareOptions, skipDeps bool, helmfileCommand string) (string, bool, error) {
c := chartify.New(
chartify.HelmBin(st.DefaultHelmBinary),
chartify.KustomizeBin(st.DefaultKustomizeBinary),
@ -1360,6 +1368,36 @@ func (st *HelmState) processChartification(chartification *Chartify, release *Re
}
chartifyOpts.SetFlags = append(chartifyOpts.SetFlags, flags...)
// Enable cluster connectivity for lookup functions when using kustomize patches
// Issue #2271: helm template runs client-side by default, causing lookup() to return empty values
// Pass --dry-run=server to enable cluster access for lookup while still using client-side rendering
// Only do this for operations that already require cluster access
var requiresCluster bool
switch helmfileCommand {
case "diff", "apply", "sync", "destroy", "delete", "test", "status":
// Commands that interact with the cluster
requiresCluster = true
case "template", "lint", "build", "pull", "fetch", "write-values", "list", "show-dag", "deps", "repos", "cache", "init", "completion", "help", "version":
// Commands that work locally without cluster access
requiresCluster = false
default:
// For unknown commands, assume cluster access (safer default)
requiresCluster = true
}
// Enable --dry-run=server for cluster-requiring commands to support lookup() function
// Issue #2271: helm template runs client-side by default, causing lookup() to return empty values
// The lookup() function can be used with or without patches, so we enable cluster access
// for all cluster-requiring operations (diff, apply, sync, etc.) but not for offline
// commands (template, lint, build, etc.)
if requiresCluster {
if chartifyOpts.TemplateArgs == "" {
chartifyOpts.TemplateArgs = "--dry-run=server"
} else if !strings.Contains(chartifyOpts.TemplateArgs, "--dry-run") {
chartifyOpts.TemplateArgs += " --dry-run=server"
}
}
out, err := c.Chartify(release.Name, chartPath, chartify.WithChartifyOpts(chartifyOpts))
if err != nil {
return "", false, err
@ -1481,8 +1519,13 @@ func (st *HelmState) prepareChartForRelease(release *ReleaseSpec, helm helmexec.
skipDepsDefault := release.SkipDeps == nil && st.HelmDefaults.SkipDeps
skipDeps := (!isLocal && !chartFetchedByGoGetter) || skipDepsGlobal || skipDepsRelease || skipDepsDefault
skipRefreshGlobal := opts.SkipRefresh
skipRefreshRelease := release.SkipRefresh != nil && *release.SkipRefresh
skipRefreshDefault := release.SkipRefresh == nil && st.HelmDefaults.SkipRefresh
skipRefresh := !isLocal || skipRefreshGlobal || skipRefreshRelease || skipRefreshDefault
if chartification != nil && helmfileCommand != "pull" {
chartPath, buildDeps, err = st.processChartification(chartification, release, chartPath, opts, skipDeps)
chartPath, buildDeps, err = st.processChartification(chartification, release, chartPath, opts, skipDeps, helmfileCommand)
if err != nil {
return &chartPrepareResult{err: err}
}
@ -1518,7 +1561,7 @@ func (st *HelmState) prepareChartForRelease(release *ReleaseSpec, helm helmexec.
releaseContext: release.KubeContext,
chartPath: chartPath,
buildDeps: buildDeps,
skipRefresh: !isLocal || opts.SkipRefresh,
skipRefresh: skipRefresh,
chartFetchedByGoGetter: chartFetchedByGoGetter,
}
}
@ -2207,6 +2250,9 @@ type DiffOpts struct {
SuppressOutputLineRegex []string
SkipSchemaValidation bool
TakeOwnership bool
// DetectedKubeVersion is the Kubernetes version detected from the cluster.
// This is used when kubeVersion is not specified in helmfile.yaml
DetectedKubeVersion string
}
func (o *DiffOpts) Apply(opts *DiffOpts) {
@ -3109,11 +3155,38 @@ func (st *HelmState) flagsForDiff(helm helmexec.Interface, release *ReleaseSpec,
flags = append(flags, "--disable-validation")
}
// TODO:
// `helm diff` has `--kube-version` flag from v3.5.0, but only respected when `helm diff upgrade --disable-validation`.
// `helm template --validate` and `helm upgrade --dry-run` ignore `--kube-version` flag.
// For the moment, not specifying kubeVersion.
flags = st.appendApiVersionsFlags(flags, release, "")
// Determine which kubeVersion to pass to helm diff based on configuration.
// By default (disableAutoDetectedKubeVersionForDiff=false), pass auto-detected kubeVersion
// to helm-diff. This fixes issue #2275 where charts requiring newer Kubernetes versions
// would fail because helm-diff defaults to v1.20.0.
//
// If disableAutoDetectedKubeVersionForDiff=true, only pass explicit kubeVersion from
// helmfile.yaml. This prevents helm-diff from normalizing server-side defaults which
// could hide real changes (e.g., ipFamilyPolicy, ipFamilies). Use this when server-side
// normalization causes issues with diff output.
disableAutoDetected := false
if release.DisableAutoDetectedKubeVersionForDiff != nil {
disableAutoDetected = *release.DisableAutoDetectedKubeVersionForDiff
} else if st.HelmDefaults.DisableAutoDetectedKubeVersionForDiff != nil {
disableAutoDetected = *st.HelmDefaults.DisableAutoDetectedKubeVersionForDiff
}
kubeVersionForDiff := ""
if disableAutoDetected {
// Only pass explicit kubeVersion from helmfile.yaml
if release.KubeVersion != "" {
kubeVersionForDiff = release.KubeVersion
} else if st.KubeVersion != "" {
kubeVersionForDiff = st.KubeVersion
}
} else {
// Pass auto-detected version (default behavior)
kubeVersionForDiff = ""
if opt != nil && opt.DetectedKubeVersion != "" {
kubeVersionForDiff = opt.DetectedKubeVersion
}
}
flags = st.appendApiVersionsFlags(flags, release, kubeVersionForDiff)
flags = st.appendConnectionFlags(flags, release)
flags = st.appendChartDownloadFlags(flags, release)
@ -4254,7 +4327,7 @@ func (st *HelmState) addToChartCache(key ChartCacheKey, path string) {
}
func (st *HelmState) getOCIChart(release *ReleaseSpec, tempDir string, helm helmexec.Interface, opts ChartPrepareOptions) (*string, error) {
qualifiedChartName, chartName, chartVersion, err := st.getOCIQualifiedChartName(release, helm)
qualifiedChartName, chartName, chartVersion, err := st.getOCIQualifiedChartName(release)
if err != nil {
return nil, err
}
@ -4349,19 +4422,27 @@ func (st *HelmState) IsOCIChart(chart string) bool {
return repo.OCI
}
func (st *HelmState) getOCIQualifiedChartName(release *ReleaseSpec, helm helmexec.Interface) (string, string, string, error) {
chartVersion := "latest"
func (st *HelmState) getOCIQualifiedChartName(release *ReleaseSpec) (string, string, string, error) {
// For issue #2247: Don't default to "latest" - use empty string to let Helm pull the latest version
// Only use the version explicitly provided by the user
chartVersion := release.Version
// In development mode with no version, omit version flag so --devel works correctly
if st.isDevelopment(release) && release.Version == "" {
// omit version, otherwise --devel flag is ignored by helm and helm-diff
chartVersion = ""
} else if release.Version != "" {
chartVersion = release.Version
}
if !st.IsOCIChart(release.Chart) {
return "", "", chartVersion, nil
}
// Reject explicit "latest" for OCI charts (issue #1047, #2247)
// This only applies if user explicitly specified "latest", not when version is omitted
// We reject for all Helm versions to ensure consistent behavior
if release.Version == "latest" {
return "", "", "", fmt.Errorf("the version for OCI charts should be semver compliant, the latest tag is not supported")
}
var qualifiedChartName, chartName string
if strings.HasPrefix(release.Chart, "oci://") {
parts := strings.Split(release.Chart, "/")
@ -4374,10 +4455,6 @@ func (st *HelmState) getOCIQualifiedChartName(release *ReleaseSpec, helm helmexe
}
qualifiedChartName = strings.TrimSuffix(qualifiedChartName, ":")
if chartVersion == "latest" && helm.IsVersionAtLeast("3.8.0") {
return "", "", "", fmt.Errorf("the version for OCI charts should be semver compliant, the latest tag is not supported anymore for helm >= 3.8.0")
}
return qualifiedChartName, chartName, chartVersion, nil
}

View File

@ -0,0 +1,75 @@
package state
import (
"testing"
"github.com/stretchr/testify/require"
)
// TestAppendApiVersionsFlags_KubeVersion tests that kubeVersion is properly
// passed to helm diff. This is a regression test for issue #2275.
// Priority: 1) paramKubeVersion (auto-detected), 2) release.KubeVersion, 3) state.KubeVersion (helmfile.yaml)
func TestAppendApiVersionsFlags_KubeVersion(t *testing.T) {
tests := []struct {
name string
stateKubeVersion string // kubeVersion from HelmState (helmfile.yaml)
paramKubeVersion string // kubeVersion parameter passed to appendApiVersionsFlags
expectedVersion string // which version should be in the flags
}{
{
name: "state kubeVersion should be used when param is empty",
stateKubeVersion: "1.34.0",
paramKubeVersion: "",
expectedVersion: "1.34.0",
},
{
name: "param kubeVersion takes precedence over state",
stateKubeVersion: "1.34.0",
paramKubeVersion: "1.30.0",
expectedVersion: "1.30.0",
},
{
name: "no version when both are empty",
stateKubeVersion: "",
paramKubeVersion: "",
expectedVersion: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
state := &HelmState{
ReleaseSetSpec: ReleaseSetSpec{
KubeVersion: tt.stateKubeVersion,
},
}
release := &ReleaseSpec{
Name: "test-release",
Chart: "test/chart",
}
result := state.appendApiVersionsFlags([]string{}, release, tt.paramKubeVersion)
if tt.expectedVersion != "" {
// Should have --kube-version flag
foundKubeVersion := false
for i := 0; i < len(result)-1; i++ {
if result[i] == "--kube-version" {
require.Equal(t, tt.expectedVersion, result[i+1],
"kube-version value should match expected")
foundKubeVersion = true
break
}
}
require.True(t, foundKubeVersion, "Should have --kube-version flag in result")
} else {
// Should NOT have --kube-version flag
for i := 0; i < len(result); i++ {
require.NotEqual(t, "--kube-version", result[i],
"Should not have --kube-version flag when nothing is set")
}
}
})
}
}

View File

@ -18,7 +18,6 @@ import (
"github.com/helmfile/helmfile/pkg/filesystem"
"github.com/helmfile/helmfile/pkg/helmexec"
"github.com/helmfile/helmfile/pkg/testhelper"
"github.com/helmfile/helmfile/pkg/testutil"
)
var logger = helmexec.NewLogger(io.Discard, "warn")
@ -3439,6 +3438,7 @@ func TestGetOCIQualifiedChartName(t *testing.T) {
},
},
helmVersion: "3.7.0",
wantErr: true, // Now rejects "latest" for all Helm versions
},
{
state: HelmState{
@ -3492,9 +3492,8 @@ func TestGetOCIQualifiedChartName(t *testing.T) {
for _, tt := range tests {
t.Run(fmt.Sprintf("%+v", tt.expected), func(t *testing.T) {
helm := testutil.NewVersionHelmExec(tt.helmVersion)
for i, r := range tt.state.Releases {
qualifiedChartName, chartName, chartVersion, err := tt.state.getOCIQualifiedChartName(&r, helm)
qualifiedChartName, chartName, chartVersion, err := tt.state.getOCIQualifiedChartName(&r)
if tt.wantErr {
require.Error(t, err, "getOCIQualifiedChartName() error = nil, want error")
return

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-67dc97cbcb",
want: "foo-values-66f7fd6f7b",
})
run(testcase{
subject: "different bytes content",
release: ReleaseSpec{Name: "foo", Chart: "incubator/raw"},
data: []byte(`{"k":"v"}`),
want: "foo-values-75d7c4758c",
want: "foo-values-6664979cd7",
})
run(testcase{
subject: "different map content",
release: ReleaseSpec{Name: "foo", Chart: "incubator/raw"},
data: map[string]any{"k": "v"},
want: "foo-values-685f8cf685",
want: "foo-values-78897dfd49",
})
run(testcase{
subject: "different chart",
release: ReleaseSpec{Name: "foo", Chart: "stable/envoy"},
want: "foo-values-75597d9c57",
want: "foo-values-64b7846cb7",
})
run(testcase{
subject: "different name",
release: ReleaseSpec{Name: "bar", Chart: "incubator/raw"},
want: "bar-values-7b77df65ff",
want: "bar-values-576cb7ddc7",
})
run(testcase{
subject: "specific ns",
release: ReleaseSpec{Name: "foo", Chart: "incubator/raw", Namespace: "myns"},
want: "myns-foo-values-85f979545c",
want: "myns-foo-values-6c567f54c",
})
for id, n := range ids {

View File

@ -5,6 +5,8 @@ import (
"context"
"fmt"
"io"
"net"
"net/http"
"os"
"os/exec"
"path/filepath"
@ -18,9 +20,7 @@ import (
"github.com/helmfile/chartify/helmtesting"
"github.com/stretchr/testify/require"
"github.com/helmfile/helmfile/pkg/app"
"github.com/helmfile/helmfile/pkg/envvar"
"github.com/helmfile/helmfile/pkg/helmexec"
"github.com/helmfile/helmfile/pkg/yaml"
)
@ -46,10 +46,160 @@ type Config struct {
HelmfileArgs []string `yaml:"helmfileArgs"`
}
type fakeInit struct{}
// getFreePort asks the kernel for a free open port that is ready to use.
// This has a small race condition between the time we get the port and when we use it,
// but it's the standard approach for dynamic port allocation in tests.
// Callers should implement retry logic to handle this race condition - see setupLocalDockerRegistry().
func getFreePort() (int, error) {
addr, err := net.ResolveTCPAddr("tcp", "localhost:0")
if err != nil {
return 0, fmt.Errorf("failed to resolve TCP address: %w", err)
}
func (f fakeInit) Force() bool {
return true
l, err := net.ListenTCP("tcp", addr)
if err != nil {
return 0, fmt.Errorf("failed to listen on TCP port: %w", err)
}
defer l.Close()
return l.Addr().(*net.TCPAddr).Port, nil
}
// waitForRegistry polls the Docker registry health endpoint until it's ready
// or the timeout is reached. Docker Registry v2 exposes /v2/ which returns
// 200 OK when the registry is healthy and ready to accept requests.
func waitForRegistry(t *testing.T, port int, timeout time.Duration) error {
t.Helper()
endpoint := fmt.Sprintf("http://localhost:%d/v2/", port)
client := &http.Client{Timeout: 2 * time.Second}
deadline := time.Now().Add(timeout)
for time.Now().Before(deadline) {
resp, err := client.Get(endpoint)
if err == nil {
resp.Body.Close()
if resp.StatusCode == http.StatusOK {
t.Logf("Registry at port %d is ready", port)
return nil
}
}
time.Sleep(500 * time.Millisecond)
}
return fmt.Errorf("registry at port %d did not become ready within %v", port, timeout)
}
// prepareInputFile substitutes $REGISTRY_PORT placeholder in the input file
// with the actual allocated port for Docker registry tests. It also converts
// relative chart paths to absolute paths since the input file is copied to a
// temp directory.
func prepareInputFile(t *testing.T, originalFile, tmpDir string, hostPort int, chartsDir, postrenderersDir string) string {
t.Helper()
inputContent, err := os.ReadFile(originalFile)
require.NoError(t, err, "Failed to read input file")
// Replace $REGISTRY_PORT placeholder with actual port
inputStr := string(inputContent)
inputStr = strings.ReplaceAll(inputStr, "$REGISTRY_PORT", fmt.Sprintf("%d", hostPort))
// Convert relative chart paths to absolute paths
// This is necessary because the input file is copied to a temp directory,
// breaking relative paths like ../../charts/raw-0.1.0
inputStr = strings.ReplaceAll(inputStr, "../../charts/", chartsDir+"/")
// Convert relative postrenderer paths to absolute paths for Helm 3 only
// Helm 3 resolves postrenderer paths relative to the helmfile location.
// When the input file is copied to a temp directory, relative paths break.
// Helm 4 extracts the plugin name from the path, so it works with relative paths.
if !isHelm4(t) && postrenderersDir != "" {
inputStr = strings.ReplaceAll(inputStr, "../../postrenderers/", postrenderersDir+"/")
}
// Write to temporary file with restrictive permissions (owner read/write only)
tmpInputFile := filepath.Join(tmpDir, "input.yaml.gotmpl")
err = os.WriteFile(tmpInputFile, []byte(inputStr), 0600)
require.NoError(t, err, "Failed to write temporary input file")
return tmpInputFile
}
// setupLocalDockerRegistry sets up a local Docker registry for OCI chart testing.
// It dynamically allocates a port if not configured, starts the registry container,
// and pushes test charts to it. Returns the allocated port.
func setupLocalDockerRegistry(t *testing.T, config Config, name, defaultChartsDir string) int {
t.Helper()
containerName := strings.Join([]string{"helmfile_docker_registry", name}, "_")
hostPort := config.LocalDockerRegistry.Port
if hostPort <= 0 {
// Dynamically allocate an unused port to avoid conflicts
// Retry up to 3 times in case of race condition where port gets taken
// between getFreePort() and docker run
const maxRetries = 3
var err error
for attempt := 1; attempt <= maxRetries; attempt++ {
hostPort, err = getFreePort()
require.NoError(t, err, "Failed to get free port for Docker registry")
t.Logf("Attempt %d: Allocated dynamic port %d for Docker registry in test %s", attempt, hostPort, name)
// Try to start Docker with this port
cmd := exec.Command("docker", "run", "--rm", "-d", "-p", fmt.Sprintf("%d:5000", hostPort), "--name", containerName, "registry:2")
output, err := cmd.CombinedOutput()
if err == nil {
// Success! Docker started successfully
t.Cleanup(func() {
execDocker(t, "stop", containerName)
})
break
}
// Check if error is due to port conflict
if strings.Contains(string(output), "address already in use") {
if attempt < maxRetries {
t.Logf("Port %d was taken (race condition), retrying with new port...", hostPort)
continue
}
t.Fatalf("Failed to start Docker registry after %d attempts due to port conflicts", maxRetries)
}
// Other error - fail immediately
t.Fatalf("Failed to start Docker registry: %s\nOutput: %s", err, string(output))
}
} else {
// Use configured port
execDocker(t, "run", "--rm", "-d", "-p", fmt.Sprintf("%d:5000", hostPort), "--name", containerName, "registry:2")
t.Cleanup(func() {
execDocker(t, "stop", containerName)
})
}
// Wait for registry to be ready by polling its health endpoint
err := waitForRegistry(t, hostPort, 30*time.Second)
require.NoError(t, err, "Registry failed to become ready")
// We helm-package and helm-push every test chart saved in the ./testdata/charts directory
// to the local registry, so that they can be accessed by helmfile and helm invoked while testing.
chartDir := config.LocalDockerRegistry.ChartDir
if chartDir == "" {
chartDir = defaultChartsDir
}
charts, err := os.ReadDir(chartDir)
require.NoError(t, err)
for _, c := range charts {
chartPath := filepath.Join(chartDir, c.Name())
if !c.IsDir() {
t.Fatalf("%s is not a directory", c)
}
tgzFile := execHelmPackage(t, chartPath)
_, err := execHelmPush(t, tgzFile, fmt.Sprintf("oci://localhost:%d/myrepo", hostPort))
require.NoError(t, err, "Unable to run helm push to local registry: %v", err)
}
return hostPort
}
func TestHelmfileTemplateWithBuildCommand(t *testing.T) {
@ -67,17 +217,6 @@ func testHelmfileTemplateWithBuildCommand(t *testing.T, GoYamlV3 bool) {
localChartPortSets := make(map[int]struct{})
logger := helmexec.NewLogger(os.Stderr, "info")
runner := &helmexec.ShellRunner{
Logger: logger,
Ctx: context.TODO(),
}
c := fakeInit{}
helmfileInit := app.NewHelmfileInit("helm", c, logger, runner)
err := helmfileInit.CheckHelmPlugins()
require.NoError(t, err)
_, filename, _, _ := goruntime.Caller(0)
projectRoot := filepath.Join(filepath.Dir(filename), "..", "..", "..", "..")
helmfileBin := filepath.Join(projectRoot, "helmfile")
@ -164,40 +303,9 @@ func testHelmfileTemplateWithBuildCommand(t *testing.T, GoYamlV3 bool) {
// If localDockerRegistry.enabled is set to `true`,
// run the docker registry v2 and push the test charts to the registry
// so that it can be accessed by helm and helmfile as a oci registry based chart repository.
var hostPort int
if config.LocalDockerRegistry.Enabled {
containerName := strings.Join([]string{"helmfile_docker_registry", name}, "_")
hostPort := config.LocalDockerRegistry.Port
if hostPort <= 0 {
hostPort = 5000
}
execDocker(t, "run", "--rm", "-d", "-p", fmt.Sprintf("%d:5000", hostPort), "--name", containerName, "registry:2")
t.Cleanup(func() {
execDocker(t, "stop", containerName)
})
// FIXME: this is a hack to wait for registry to be up and running
// please replace with proper wait for registry
time.Sleep(5 * time.Second)
// We helm-package and helm-push every test chart saved in the ./testdata/charts directory
// to the local registry, so that they can be accessed by helmfile and helm invoked while testing.
if config.LocalDockerRegistry.ChartDir == "" {
config.LocalDockerRegistry.ChartDir = defaultChartsDir
}
charts, err := os.ReadDir(config.LocalDockerRegistry.ChartDir)
require.NoError(t, err)
for _, c := range charts {
chartPath := filepath.Join(config.LocalDockerRegistry.ChartDir, c.Name())
if !c.IsDir() {
t.Fatalf("%s is not a directory", c)
}
tgzFile := execHelmPackage(t, chartPath)
_, err := execHelmPush(t, tgzFile, fmt.Sprintf("oci://localhost:%d/myrepo", hostPort))
require.NoError(t, err, "Unable to run helm push to local registry: %v", err)
}
hostPort = setupLocalDockerRegistry(t, config, name, defaultChartsDir)
}
tmpDir := t.TempDir()
@ -230,6 +338,14 @@ func testHelmfileTemplateWithBuildCommand(t *testing.T, GoYamlV3 bool) {
}
inputFile := filepath.Join(testdataDir, name, "input.yaml.gotmpl")
// If using dynamic Docker registry port, substitute $REGISTRY_PORT in input file
if config.LocalDockerRegistry.Enabled {
chartsDir := filepath.Join(wd, defaultChartsDir)
postrenderersDir := filepath.Join(wd, "testdata/postrenderers")
inputFile = prepareInputFile(t, inputFile, tmpDir, hostPort, chartsDir, postrenderersDir)
}
outputFile := ""
if GoYamlV3 {
outputFile = filepath.Join(testdataDir, name, "gopkg.in-yaml.v3-output.yaml")
@ -280,6 +396,10 @@ func testHelmfileTemplateWithBuildCommand(t *testing.T, GoYamlV3 bool) {
gotStr = helmShortVersionRegex.ReplaceAllString(gotStr, `$$HelmVersion`)
if config.LocalDockerRegistry.Enabled {
// Normalize the dynamic port to $REGISTRY_PORT placeholder for test comparison
gotStr = strings.ReplaceAll(gotStr, fmt.Sprintf("localhost:%d", hostPort), "localhost:$REGISTRY_PORT")
gotStr = strings.ReplaceAll(gotStr, fmt.Sprintf("oci__localhost_%d", hostPort), "oci__localhost_$REGISTRY_PORT")
sc := bufio.NewScanner(strings.NewReader(gotStr))
for sc.Scan() {
if !strings.HasPrefix(sc.Text(), "Templating ") {

View File

@ -1,6 +1,6 @@
localDockerRegistry:
enabled: true
port: 5000
# Port is not specified, will be dynamically allocated to avoid conflicts
chartifyTempDir: temp2
helmfileArgs:
- fetch

View File

@ -1,6 +1,6 @@
releases:
- name: foo
chart: oci://localhost:5000/myrepo/raw
chart: oci://localhost:$REGISTRY_PORT/myrepo/raw
version: 0.1.0
values:
- templates:

View File

@ -1 +1 @@
Pulling localhost:5000/myrepo/raw:0.1.0
Pulling localhost:$REGISTRY_PORT/myrepo/raw:0.1.0

View File

@ -1,3 +1,3 @@
NAME NAMESPACE ENABLED INSTALLED LABELS CHART VERSION
release1 myNamespace true true app:myapp,chart:test,group:myGroup,name:release1,namespace:myNamespace,project:myProject test
release2 myNamespace true true app:myapp,chart:test,group:myGroup,name:release2,namespace:myNamespace,project:myProject test
release1 myNamespace true true app:myapp,chart:test,group:myGroup,name:release1,namespace:myNamespace,project:myProject test
release2 myNamespace true true app:myapp,chart:test,group:myGroup,name:release2,namespace:myNamespace,project:myProject test

View File

@ -1,6 +1,6 @@
localDockerRegistry:
enabled: true
port: 5001
# Port is not specified, will be dynamically allocated to avoid conflicts
chartifyTempDir: temp2
helmfileArgs:
- template

View File

@ -1,6 +1,6 @@
repositories:
- name: myrepo
url: localhost:5001/myrepo
url: localhost:$REGISTRY_PORT/myrepo
oci: true
releases:

View File

@ -1,4 +1,4 @@
Pulling localhost:5001/myrepo/raw:0.1.0
Pulling localhost:$REGISTRY_PORT/myrepo/raw:0.1.0
Templating release=foo, chart=$HELMFILE_CACHE_HOME/myrepo/raw/0.1.0/raw
---
# Source: raw/templates/resources.yaml

View File

@ -1,6 +1,6 @@
localDockerRegistry:
enabled: true
port: 5001
# Port is not specified, will be dynamically allocated to avoid conflicts
chartifyTempDir: temp2
helmfileArgs:
- template

View File

@ -1,6 +1,6 @@
releases:
- name: foo
chart: oci://localhost:5001/myrepo/raw
chart: oci://localhost:$REGISTRY_PORT/myrepo/raw
version: 0.1.0
values: &oci_chart_pull_direct
- templates:
@ -16,7 +16,7 @@ releases:
values: {{`{{ .Release.Name }}`}}
- name: bar
chart: oci://localhost:5001/myrepo/raw
chart: oci://localhost:$REGISTRY_PORT/myrepo/raw
version: 0.1.0
namespace: ns2
values: *oci_chart_pull_direct

View File

@ -1,5 +1,5 @@
Pulling localhost:5001/myrepo/raw:0.1.0
Templating release=foo, chart=$HELMFILE_CACHE_HOME/oci__localhost_5001/myrepo/raw/0.1.0/raw
Pulling localhost:$REGISTRY_PORT/myrepo/raw:0.1.0
Templating release=foo, chart=$HELMFILE_CACHE_HOME/oci__localhost_$REGISTRY_PORT/myrepo/raw/0.1.0/raw
---
# Source: raw/templates/resources.yaml
apiVersion: v1
@ -12,7 +12,7 @@ metadata:
data:
values: foo
Templating release=bar, chart=$HELMFILE_CACHE_HOME/oci__localhost_5001/myrepo/raw/0.1.0/raw
Templating release=bar, chart=$HELMFILE_CACHE_HOME/oci__localhost_$REGISTRY_PORT/myrepo/raw/0.1.0/raw
---
# Source: raw/templates/resources.yaml
apiVersion: v1

View File

@ -1,7 +1,7 @@
# Templating two versions of the same chart with only one pulling of each version
localDockerRegistry:
enabled: true
port: 5001
# Port is not specified, will be dynamically allocated to avoid conflicts
chartifyTempDir: temp3
helmfileArgs:
# Prevent releases from racing and randomizing the log

View File

@ -1,6 +1,6 @@
repositories:
- name: myrepo
url: localhost:5001/myrepo
url: localhost:$REGISTRY_PORT/myrepo
oci: true
releases:

View File

@ -1,5 +1,5 @@
Pulling localhost:5001/myrepo/raw:0.1.0
Pulling localhost:5001/myrepo/raw:0.0.1
Pulling localhost:$REGISTRY_PORT/myrepo/raw:0.1.0
Pulling localhost:$REGISTRY_PORT/myrepo/raw:0.0.1
Templating release=foo, chart=$HELMFILE_CACHE_HOME/myrepo/raw/0.1.0/raw
---
# Source: raw/templates/resources.yaml

View File

@ -1,7 +1,7 @@
# Templating few releases with the same chart\version and only one pulling
localDockerRegistry:
enabled: true
port: 5001
# Port is not specified, will be dynamically allocated to avoid conflicts
chartifyTempDir: temp3
helmfileArgs:
- template

View File

@ -1,6 +1,6 @@
repositories:
- name: myrepo
url: localhost:5001/myrepo
url: localhost:$REGISTRY_PORT/myrepo
oci: true
releases:

View File

@ -1,4 +1,4 @@
Pulling localhost:5001/myrepo/raw:0.1.0
Pulling localhost:$REGISTRY_PORT/myrepo/raw:0.1.0
Templating release=release-0, chart=$HELMFILE_CACHE_HOME/myrepo/raw/0.1.0/raw
---
# Source: raw/templates/resources.yaml

View File

@ -1,6 +1,6 @@
localDockerRegistry:
enabled: true
port: 5002
# Port is not specified, will be dynamically allocated to avoid conflicts
chartifyTempDir: temp1
helmfileArgs:
- template

View File

@ -23,5 +23,5 @@ releases:
bar: BAR
dependencies:
- alias: dep
chart: oci://localhost:5002/myrepo/raw
chart: oci://localhost:$REGISTRY_PORT/myrepo/raw
version: 0.1.0

View File

@ -1,6 +1,6 @@
Building dependency release=foo, chart=$WD/temp1/foo
Saving 1 charts
Downloading raw from repo oci://localhost:5002/myrepo
Downloading raw from repo oci://localhost:$REGISTRY_PORT/myrepo
Deleting outdated charts
Templating release=foo, chart=$WD/temp1/foo

View File

@ -1,6 +1,6 @@
localDockerRegistry:
enabled: true
port: 5001
# Port is not specified, will be dynamically allocated to avoid conflicts
chartifyTempDir: temp1
helmfileArgs:
- template

View File

@ -39,5 +39,5 @@ releases:
baz: BAZ
dependencies:
- alias: dep
chart: oci://localhost:5001/myrepo/raw
chart: oci://localhost:$REGISTRY_PORT/myrepo/raw
version: 0.1.0

View File

@ -1,10 +1,10 @@
Building dependency release=foo, chart=../../charts/raw-0.1.0
Building dependency release=foo, chart=$WD/testdata/charts/raw-0.1.0
Building dependency release=baz, chart=$WD/temp1/baz
Saving 1 charts
Downloading raw from repo oci://localhost:5001/myrepo
Downloading raw from repo oci://localhost:$REGISTRY_PORT/myrepo
Deleting outdated charts
Templating release=foo, chart=../../charts/raw-0.1.0
Templating release=foo, chart=$WD/testdata/charts/raw-0.1.0
---
# Source: generated-by-postrender-1.yaml
apiVersion: v1

View File

@ -1,10 +1,10 @@
Building dependency release=foo, chart=../../charts/raw-0.1.0
Building dependency release=foo, chart=$WD/testdata/charts/raw-0.1.0
Building dependency release=baz, chart=$WD/temp1/baz
Saving 1 charts
Downloading raw from repo oci://localhost:5001/myrepo
Downloading raw from repo oci://localhost:$REGISTRY_PORT/myrepo
Deleting outdated charts
Templating release=foo, chart=../../charts/raw-0.1.0
Templating release=foo, chart=$WD/testdata/charts/raw-0.1.0
---
# Source: raw/templates/resources.yaml
apiVersion: v1

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.14.0}"
HELM_DIFF_VERSION="${HELM_DIFF_VERSION:-3.14.1}"
HELM_GIT_VERSION="${HELM_GIT_VERSION:-1.4.1}"
HELM_SECRETS_VERSION="${HELM_SECRETS_VERSION:-4.7.0}"
export GNUPGHOME="${PWD}/${dir}/.gnupg"
@ -114,6 +114,8 @@ ${kubectl} create namespace ${test_ns} || fail "Could not create namespace ${tes
. ${dir}/test-cases/issue-1749.sh
. ${dir}/test-cases/issue-1893.sh
. ${dir}/test-cases/state-values-set-cli-args-in-environments.sh
. ${dir}/test-cases/issue-2281-array-merge.sh
. ${dir}/test-cases/issue-2247.sh
# ALL DONE -----------------------------------------------------------------------------------------------------------

View File

@ -0,0 +1,273 @@
#!/usr/bin/env bash
# Test for issue #2247: Allow OCI charts without version
# This test combines validation tests (fast) with registry tests (comprehensive)
issue_2247_input_dir="${cases_dir}/issue-2247/input"
issue_2247_chart_dir="${cases_dir}/issue-2247/chart"
issue_2247_tmp_dir=$(mktemp -d)
test_start "issue-2247: OCI charts without version"
# ==============================================================================================================
# PART 1: Fast Validation Tests (no registry required)
# ==============================================================================================================
info "Part 1: Validation tests (no registry required)"
# Test 1.1: Explicit "latest" should error (issue #1047 behavior)
info "Test 1.1: Verifying explicit 'latest' version triggers validation error"
set +e # Disable exit on error since we expect this command to fail
${helmfile} -f "${issue_2247_input_dir}/helmfile-with-latest.yaml" template > "${issue_2247_tmp_dir}/latest.txt" 2>&1
code=$?
set -e # Re-enable exit on error
# Debug: show output if command succeeded
if [ $code -eq 0 ]; then
info "helmfile command succeeded when it should have failed. Output:"
cat "${issue_2247_tmp_dir}/latest.txt"
info "Helm version:"
${helm} version --short 2>&1 || echo "helm version command failed"
rm -rf "${issue_2247_tmp_dir}"
fail "Expected error for explicit 'latest' version but command succeeded"
fi
if ! grep -q "semver compliant" "${issue_2247_tmp_dir}/latest.txt"; then
cat "${issue_2247_tmp_dir}/latest.txt"
rm -rf "${issue_2247_tmp_dir}"
fail "Expected 'semver compliant' error message for explicit 'latest' version"
fi
info "SUCCESS: Explicit 'latest' version correctly triggers validation error"
# Test 1.2: No version should NOT error (issue #2247 fix)
info "Test 1.2: Verifying OCI charts without version do NOT trigger validation error"
set +e # Disable exit on error since this command may fail (registry doesn't exist)
${helmfile} -f "${issue_2247_input_dir}/helmfile-no-version.yaml" template > "${issue_2247_tmp_dir}/no-version.txt" 2>&1
code=$?
set -e # Re-enable exit on error
# Note: The command will fail because the OCI registry doesn't exist,
# but it should NOT fail with the "semver compliant" validation error
if grep -q "semver compliant" "${issue_2247_tmp_dir}/no-version.txt"; then
cat "${issue_2247_tmp_dir}/no-version.txt"
rm -rf "${issue_2247_tmp_dir}"
fail "Issue #2247 regression: OCI charts without version trigger validation error"
fi
info "SUCCESS: OCI charts without version do not trigger validation error"
# ==============================================================================================================
# PART 2: Comprehensive Registry Tests (requires Docker)
# ==============================================================================================================
# Check if Docker is available
if ! command -v docker &> /dev/null; then
info "Skipping registry tests (Docker not available)"
rm -rf "${issue_2247_tmp_dir}"
test_pass "issue-2247: OCI charts without version (validation tests only)"
return 0
fi
# Check if Docker daemon is running
if ! docker info &> /dev/null; then
info "Skipping registry tests (Docker daemon not running)"
rm -rf "${issue_2247_tmp_dir}"
test_pass "issue-2247: OCI charts without version (validation tests only)"
return 0
fi
info "Part 2: Comprehensive tests with real OCI registry"
registry_container_name="helmfile-test-registry-2247"
registry_port=5000
# Cleanup function
cleanup_registry() {
info "Cleaning up test registry"
docker stop ${registry_container_name} &>/dev/null || true
docker rm ${registry_container_name} &>/dev/null || true
rm -rf "${issue_2247_tmp_dir}"
}
# Ensure cleanup on exit
trap cleanup_registry EXIT
# Test 2.1: Start local OCI registry
info "Test 2.1: Starting local OCI registry on port ${registry_port}"
docker run -d \
--name ${registry_container_name} \
-p ${registry_port}:5000 \
--rm \
registry:2 &> "${issue_2247_tmp_dir}/registry-start.log"
if [ $? -ne 0 ]; then
cat "${issue_2247_tmp_dir}/registry-start.log"
warn "Failed to start Docker registry - skipping registry tests"
rm -rf "${issue_2247_tmp_dir}"
test_pass "issue-2247: OCI charts without version (validation tests only)"
return 0
fi
# Wait for registry to be ready
info "Waiting for registry to be ready..."
max_attempts=30
attempt=0
while [ $attempt -lt $max_attempts ]; do
if curl -s http://localhost:${registry_port}/v2/ > /dev/null 2>&1; then
info "Registry is ready"
break
fi
attempt=$((attempt + 1))
sleep 1
done
if [ $attempt -eq $max_attempts ]; then
warn "Registry did not become ready in time - skipping registry tests"
cleanup_registry
test_pass "issue-2247: OCI charts without version (validation tests only)"
return 0
fi
# Test 2.2: Package and push the test chart
info "Test 2.2: Packaging and pushing test charts"
set +e # Disable exit on error to handle failures gracefully
${helm} package "${issue_2247_chart_dir}" -d "${issue_2247_tmp_dir}" > "${issue_2247_tmp_dir}/package.log" 2>&1
if [ $? -ne 0 ]; then
set -e # Re-enable before cleanup
cat "${issue_2247_tmp_dir}/package.log"
warn "Failed to package chart - skipping registry tests"
cleanup_registry
test_pass "issue-2247: OCI charts without version (validation tests only)"
return 0
fi
set -e # Re-enable exit on error after successful package
info "Pushing chart version 1.0.0 to local registry"
set +e # Disable exit on error to handle failures gracefully
${helm} push "${issue_2247_tmp_dir}/test-chart-2247-1.0.0.tgz" oci://localhost:${registry_port} > "${issue_2247_tmp_dir}/push.log" 2>&1
if [ $? -ne 0 ]; then
set -e # Re-enable before cleanup
cat "${issue_2247_tmp_dir}/push.log"
warn "Failed to push chart to registry - skipping registry tests"
cleanup_registry
test_pass "issue-2247: OCI charts without version (validation tests only)"
return 0
fi
set -e # Re-enable exit on error after successful push
# Create version 2.0.0 as well to test "latest" behavior
info "Creating and pushing version 2.0.0"
cp -r "${issue_2247_chart_dir}" "${issue_2247_tmp_dir}/chart-v2"
sed -i.bak 's/version: 1.0.0/version: 2.0.0/' "${issue_2247_tmp_dir}/chart-v2/Chart.yaml"
set +e # Disable exit on error for package/push operations
${helm} package "${issue_2247_tmp_dir}/chart-v2" -d "${issue_2247_tmp_dir}" > "${issue_2247_tmp_dir}/package-v2.log" 2>&1
${helm} push "${issue_2247_tmp_dir}/test-chart-2247-2.0.0.tgz" oci://localhost:${registry_port} > "${issue_2247_tmp_dir}/push-v2.log" 2>&1
set -e # Re-enable exit on error
info "Successfully pushed chart versions 1.0.0 and 2.0.0"
# Test 2.3: Test helmfile with OCI chart WITHOUT version
info "Test 2.3: helmfile template with OCI chart without version (should pull latest = 2.0.0)"
cat > "${issue_2247_tmp_dir}/helmfile-oci-registry.yaml" <<EOF
releases:
- name: test-oci-no-version
namespace: default
chart: oci://localhost:${registry_port}/test-chart-2247
# No version specified - should pull latest (issue #2247 fix)
EOF
set +e # Disable exit on error to check result
${helmfile} -f "${issue_2247_tmp_dir}/helmfile-oci-registry.yaml" template --skip-deps > "${issue_2247_tmp_dir}/template-no-version.yaml" 2>&1
code=$?
set -e # Re-enable exit on error
# Should NOT have the semver validation error
if grep -q "semver compliant" "${issue_2247_tmp_dir}/template-no-version.yaml"; then
cat "${issue_2247_tmp_dir}/template-no-version.yaml"
cleanup_registry
fail "Issue #2247 regression: OCI chart without version triggered validation error"
fi
# Should succeed
if [ $code -eq 0 ]; then
info "SUCCESS: helmfile template succeeded with OCI chart without version"
# Verify it pulled version 2.0.0 (the latest)
if grep -q "Hello from test chart 2.0.0" "${issue_2247_tmp_dir}/template-no-version.yaml"; then
info "SUCCESS: Correctly pulled latest version (2.0.0)"
else
info "Note: Could not verify exact version pulled (non-critical)"
fi
else
# Check if it failed for a reason other than our validation
if ! grep -q "semver compliant" "${issue_2247_tmp_dir}/template-no-version.yaml"; then
info "helmfile failed but not due to version validation (acceptable)"
else
cat "${issue_2247_tmp_dir}/template-no-version.yaml"
cleanup_registry
fail "Unexpected validation error"
fi
fi
# Test 2.4: Test helmfile with explicit "latest" version
info "Test 2.4: helmfile template with explicit 'latest' version (should error)"
cat > "${issue_2247_tmp_dir}/helmfile-explicit-latest.yaml" <<EOF
releases:
- name: test-oci-explicit-latest
namespace: default
chart: oci://localhost:${registry_port}/test-chart-2247
version: "latest" # Should trigger validation error
EOF
set +e # Disable exit on error since we expect this command to fail
${helmfile} -f "${issue_2247_tmp_dir}/helmfile-explicit-latest.yaml" template --skip-deps > "${issue_2247_tmp_dir}/template-latest.yaml" 2>&1
code=$?
set -e # Re-enable exit on error
# Should have the validation error
if ! grep -q "semver compliant" "${issue_2247_tmp_dir}/template-latest.yaml"; then
cat "${issue_2247_tmp_dir}/template-latest.yaml"
cleanup_registry
fail "Expected validation error for explicit 'latest' version"
fi
if [ $code -eq 0 ]; then
cat "${issue_2247_tmp_dir}/template-latest.yaml"
cleanup_registry
fail "helmfile should have failed with validation error for explicit 'latest'"
fi
info "SUCCESS: Explicit 'latest' version correctly triggered validation error"
# Test 2.5: Test helmfile with specific version
info "Test 2.5: helmfile template with specific version 1.0.0"
cat > "${issue_2247_tmp_dir}/helmfile-specific-version.yaml" <<EOF
releases:
- name: test-oci-specific
namespace: default
chart: oci://localhost:${registry_port}/test-chart-2247
version: "1.0.0"
EOF
set +e # Disable exit on error to check result
${helmfile} -f "${issue_2247_tmp_dir}/helmfile-specific-version.yaml" template --skip-deps > "${issue_2247_tmp_dir}/template-specific.yaml" 2>&1
code=$?
set -e # Re-enable exit on error
if grep -q "semver compliant" "${issue_2247_tmp_dir}/template-specific.yaml"; then
cat "${issue_2247_tmp_dir}/template-specific.yaml"
cleanup_registry
fail "Unexpected validation error for specific version"
fi
if [ $code -eq 0 ]; then
info "SUCCESS: helmfile template succeeded with specific version"
if grep -q "Hello from test chart 1.0.0" "${issue_2247_tmp_dir}/template-specific.yaml"; then
info "SUCCESS: Correctly used version 1.0.0"
fi
else
info "helmfile failed but not due to version validation (acceptable)"
fi
# All tests passed!
test_pass "issue-2247: OCI charts without version (all tests including registry)"

View File

@ -0,0 +1,6 @@
apiVersion: v2
name: test-chart-2247
description: Test chart for issue #2247
type: application
version: 1.0.0
appVersion: "1.0"

View File

@ -0,0 +1,8 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Chart.Name }}-config
labels:
app: {{ .Chart.Name }}
data:
message: "Hello from test chart {{ .Chart.Version }}"

View File

@ -0,0 +1,3 @@
# Default values for test-chart-2247
# This is a minimal chart for testing OCI version handling
replicaCount: 1

View File

@ -0,0 +1,5 @@
releases:
- name: test-oci-no-version
namespace: default
chart: oci://registry.example.com/my-chart
# No version specified - this should NOT error (issue #2247 fix)

View File

@ -0,0 +1,5 @@
releases:
- name: test-oci-no-version
namespace: default
chart: oci://localhost:5000/test-chart-2247
# No version specified - should pull latest (issue #2247 fix)

View File

@ -0,0 +1,5 @@
releases:
- name: test-oci-latest
namespace: default
chart: oci://registry.example.com/my-chart
version: "latest" # This should trigger the validation error

View File

@ -0,0 +1,61 @@
#!/usr/bin/env bash
# Test for issue #2271: lookup function should work with strategicMergePatches
# Without this fix, helm template runs client-side and lookup() returns empty values
issue_2271_input_dir="${cases_dir}/issue-2271/input"
issue_2271_tmp_dir=$(mktemp -d)
cd "${issue_2271_input_dir}"
test_start "issue-2271: lookup function with strategicMergePatches"
# Test 1: Install chart without kustomize patches
info "Installing chart without kustomize patches"
${helmfile} -f helmfile-no-kustomize.yaml apply --suppress-diff > "${issue_2271_tmp_dir}/test-2271-install.txt" 2>&1
code=$?
if [ $code -ne 0 ]; then
cat "${issue_2271_tmp_dir}/test-2271-install.txt"
rm -rf "${issue_2271_tmp_dir}"
fail "Failed to install chart"
fi
info "Chart installed successfully"
# Test 2: Modify ConfigMap value manually to simulate an upgrade scenario
info "Modifying ConfigMap value to test lookup preservation"
${kubectl} patch configmap test-release-2271-config --type merge -p '{"data":{"preserved-value":"test-preserved-value"}}' > /dev/null 2>&1
# Verify the value was changed
current_value=$(${kubectl} get configmap test-release-2271-config -o jsonpath='{.data.preserved-value}')
if [ "$current_value" != "test-preserved-value" ]; then
rm -rf "${issue_2271_tmp_dir}"
fail "Failed to update ConfigMap value. Got: $current_value"
fi
info "ConfigMap value updated to: $current_value"
# Test 3: Diff with strategicMergePatches should preserve the lookup value
info "Testing diff with strategicMergePatches - lookup should preserve value"
${helmfile} -f helmfile.yaml diff > "${issue_2271_tmp_dir}/test-2271-diff.txt" 2>&1
code=$?
# Check if the diff contains the preserved value (not "initial-value")
if grep -q "preserved-value.*test-preserved-value" "${issue_2271_tmp_dir}/test-2271-diff.txt"; then
info "SUCCESS: lookup function preserved the value with kustomize patches"
elif grep -q "preserved-value.*initial-value" "${issue_2271_tmp_dir}/test-2271-diff.txt"; then
cat "${issue_2271_tmp_dir}/test-2271-diff.txt"
rm -rf "${issue_2271_tmp_dir}"
fail "Issue #2271 regression: lookup function returned empty value with kustomize"
else
# No diff for ConfigMap means value is perfectly preserved
info "SUCCESS: No ConfigMap changes detected (value perfectly preserved)"
fi
# Cleanup
${helm} uninstall test-release-2271 --namespace default 2>/dev/null || true
rm -rf "${issue_2271_tmp_dir}"
test_pass "issue-2271: lookup function with strategicMergePatches"

View File

@ -0,0 +1,14 @@
# Kustomize strategic merge patch to set DNS ndots
# This simulates the grafana-dns-ndots.yaml.gotmpl from the issue
apiVersion: apps/v1
kind: Deployment
metadata:
name: test-release-2271-app
namespace: default
spec:
template:
spec:
dnsConfig:
options:
- name: ndots
value: "1"

View File

@ -0,0 +1,6 @@
releases:
- name: test-release-2271
namespace: default
chart: ./test-chart
installed: true
# No strategicMergePatches - lookup function should work

View File

@ -0,0 +1,8 @@
releases:
- name: test-release-2271
namespace: default
chart: ./test-chart
installed: true
# Using strategicMergePatches causes lookup function to not execute
strategicMergePatches:
- dns-patch.yaml

View File

@ -0,0 +1,6 @@
apiVersion: v2
name: test-chart-issue-2271
description: Test chart for issue #2271 - lookup function with kustomize
type: application
version: 1.0.0
appVersion: "1.0"

View File

@ -0,0 +1,16 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-config
namespace: {{ .Release.Namespace }}
data:
# Use lookup to preserve existing value if ConfigMap already exists
# This simulates the Grafana PVC volumeName preservation use case
{{- $existing := lookup "v1" "ConfigMap" .Release.Namespace (printf "%s-config" .Release.Name) }}
{{- if $existing }}
preserved-value: {{ index $existing.data "preserved-value" | default "initial-value" | quote }}
{{- else }}
preserved-value: "initial-value"
{{- end }}
# This value can change on upgrades
current-version: {{ .Chart.Version | quote }}

View File

@ -0,0 +1,20 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .Release.Name }}-app
namespace: {{ .Release.Namespace }}
spec:
replicas: 1
selector:
matchLabels:
app: {{ .Release.Name }}
template:
metadata:
labels:
app: {{ .Release.Name }}
spec:
containers:
- name: nginx
image: nginx:latest
ports:
- containerPort: 80

View File

@ -0,0 +1,89 @@
#!/usr/bin/env bash
# Test for issue #2275: helmfile should detect cluster version and pass to helm-diff
# Without this fix, helm-diff falls back to v1.20.0 and fails for charts requiring newer versions
issue_2275_input_dir="${cases_dir}/issue-2275/input"
issue_2275_tmp_dir=$(mktemp -d)
cd "${issue_2275_input_dir}"
test_start "issue-2275: auto-detect kubernetes version for helm-diff"
info "Testing helmfile apply with chart requiring Kubernetes >=1.25.0"
info "Expected: Success with auto-detected cluster version"
# Test 1: Apply should succeed with auto-detected cluster version
${helmfile} apply --skip-diff-on-install --suppress-diff > "${issue_2275_tmp_dir}/test-2275-output.txt" 2>&1
code=$?
if [ $code -ne 0 ]; then
if grep -q "incompatible with Kubernetes v1.20.0" "${issue_2275_tmp_dir}/test-2275-output.txt"; then
cat "${issue_2275_tmp_dir}/test-2275-output.txt"
rm -rf "${issue_2275_tmp_dir}"
fail "Issue #2275 regression: helm-diff fell back to v1.20.0"
else
cat "${issue_2275_tmp_dir}/test-2275-output.txt"
rm -rf "${issue_2275_tmp_dir}"
fail "Unexpected error during apply"
fi
fi
info "Chart installed successfully with auto-detected version"
# Test 2: Diff should work with auto-detected version
info "Testing helmfile diff with auto-detected cluster version"
${helmfile} diff > "${issue_2275_tmp_dir}/test-2275-diff-output.txt" 2>&1
code=$?
if [ $code -ne 0 ] && [ $code -ne 2 ]; then
if grep -q "incompatible with Kubernetes v1.20.0" "${issue_2275_tmp_dir}/test-2275-diff-output.txt"; then
cat "${issue_2275_tmp_dir}/test-2275-diff-output.txt"
rm -rf "${issue_2275_tmp_dir}"
fail "Issue #2275 regression in diff: helm-diff fell back to v1.20.0"
else
cat "${issue_2275_tmp_dir}/test-2275-diff-output.txt"
rm -rf "${issue_2275_tmp_dir}"
fail "Unexpected error during diff"
fi
fi
info "Diff succeeded with auto-detected version"
# Test 3: Second apply (upgrade scenario) - this is the critical test case from issue #2275
# The first apply worked with --skip-diff-on-install, but second apply would fail without the fix
info "Testing second helmfile apply (upgrade scenario) - critical test for issue #2275"
info "Modifying chart to trigger an actual upgrade..."
# Update chart version to trigger an upgrade
sed -i.bak 's/version: 1.0.0/version: 1.0.1/' test-chart/Chart.yaml
info "Running helmfile apply to upgrade chart (this will run diff)"
info "This would fail with 'incompatible with Kubernetes v1.20.0' before the fix"
${helmfile} apply --suppress-diff > "${issue_2275_tmp_dir}/test-2275-apply2-output.txt" 2>&1
code=$?
# Restore original chart version
mv test-chart/Chart.yaml.bak test-chart/Chart.yaml
if [ $code -ne 0 ]; then
if grep -q "incompatible with Kubernetes v1.20.0" "${issue_2275_tmp_dir}/test-2275-apply2-output.txt"; then
cat "${issue_2275_tmp_dir}/test-2275-apply2-output.txt"
rm -rf "${issue_2275_tmp_dir}"
fail "Issue #2275 regression: upgrade failed - helm-diff fell back to v1.20.0"
else
cat "${issue_2275_tmp_dir}/test-2275-apply2-output.txt"
rm -rf "${issue_2275_tmp_dir}"
fail "Unexpected error during upgrade"
fi
fi
info "Upgrade succeeded with auto-detected version"
# Cleanup
${helm} uninstall test-release-2275 --namespace default 2>/dev/null || true
rm -rf "${issue_2275_tmp_dir}"
test_pass "issue-2275: auto-detect kubernetes version for helm-diff"

View File

@ -0,0 +1,5 @@
releases:
- name: test-release-2275
namespace: default
chart: ./test-chart
installed: true

View File

@ -0,0 +1,9 @@
apiVersion: v2
name: test-chart-issue-2275
description: Test chart for issue #2275
type: application
version: 1.0.0
appVersion: "1.0"
# This chart requires Kubernetes 1.25 or higher
# Without the fix, helm-diff uses v1.20.0 and fails
kubeVersion: ">=1.25.0"

View File

@ -0,0 +1,19 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: test-app
spec:
replicas: 1
selector:
matchLabels:
app: test-app
template:
metadata:
labels:
app: test-app
spec:
containers:
- name: nginx
image: nginx:latest
ports:
- containerPort: 80

View File

@ -0,0 +1,80 @@
#!/usr/bin/env bash
# Test for issue #2280: --color flag conflict with Helm 4
# In Helm 4, the --color flag is parsed by Helm before reaching the helm-diff plugin
# The fix removes --color/--no-color flags and uses HELM_DIFF_COLOR env var instead
# Only run this test on Helm 4
if [ "${HELMFILE_HELM4}" != "1" ]; then
info "Skipping issue-2280 test (Helm 4 only)"
return 0
fi
issue_2280_input_dir="${cases_dir}/issue-2280/input"
issue_2280_tmp_dir=$(mktemp -d)
cd "${issue_2280_input_dir}"
test_start "issue-2280: --color flag with Helm 4"
# Test 1: Install the chart first
info "Installing chart for issue #2280 test"
${helmfile} -f helmfile.yaml apply --suppress-diff > "${issue_2280_tmp_dir}/install.txt" 2>&1
code=$?
if [ $code -ne 0 ]; then
cat "${issue_2280_tmp_dir}/install.txt"
rm -rf "${issue_2280_tmp_dir}"
fail "Failed to install chart"
fi
info "Chart installed successfully"
# Test 2: Run diff with --color and --context flags
# This is the exact scenario from issue #2280
# Before the fix, --color flag would be parsed by Helm 4 before reaching helm-diff plugin,
# consuming --context as its value, resulting in error: invalid color mode "--context"
# After the fix, --color is removed and HELM_DIFF_COLOR env var is set instead
info "Running diff with --color and --context flags"
${helmfile} -f helmfile.yaml diff --color --context 3 > "${issue_2280_tmp_dir}/diff-color.txt" 2>&1
code=$?
# Check for the error from issue #2280
if grep -q "invalid color mode" "${issue_2280_tmp_dir}/diff-color.txt"; then
cat "${issue_2280_tmp_dir}/diff-color.txt"
rm -rf "${issue_2280_tmp_dir}"
fail "Issue #2280 regression: --color flag consumed --context argument"
fi
# diff command should succeed (exit code 0 or 2 with --detailed-exitcode)
if [ $code -ne 0 ]; then
# Check if it's a diff-related error (not the color mode error)
if ! grep -q "Comparing release" "${issue_2280_tmp_dir}/diff-color.txt"; then
cat "${issue_2280_tmp_dir}/diff-color.txt"
rm -rf "${issue_2280_tmp_dir}"
fail "Diff command failed unexpectedly"
fi
fi
info "SUCCESS: --color flag did not interfere with --context flag"
# Test 3: Also test with --no-color
info "Running diff with --no-color and --context flags"
${helmfile} -f helmfile.yaml diff --no-color --context 3 > "${issue_2280_tmp_dir}/diff-no-color.txt" 2>&1
code=$?
if grep -q "invalid color mode" "${issue_2280_tmp_dir}/diff-no-color.txt"; then
cat "${issue_2280_tmp_dir}/diff-no-color.txt"
rm -rf "${issue_2280_tmp_dir}"
fail "Issue #2280 regression: --no-color flag consumed --context argument"
fi
info "SUCCESS: --no-color flag did not interfere with --context flag"
# Cleanup
${helm} uninstall test-release-2280 --namespace default 2>/dev/null || true
rm -rf "${issue_2280_tmp_dir}"
test_pass "issue-2280: --color flag with Helm 4"

View File

@ -0,0 +1,5 @@
apiVersion: v2
name: test-chart-2280
description: Test chart for issue #2280
version: 0.1.0
appVersion: "1.0"

View File

@ -0,0 +1,6 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-config
data:
message: {{ .Values.message | quote }}

View File

@ -0,0 +1 @@
message: "default message"

View File

@ -0,0 +1,6 @@
releases:
- name: test-release-2280
namespace: default
chart: ./chart
values:
- message: "test message"

View File

@ -0,0 +1,13 @@
issue_2281_array_merge_input_dir="${cases_dir}/issue-2281-array-merge/input"
issue_2281_array_merge_output_dir="${cases_dir}/issue-2281-array-merge/output"
issue_2281_array_merge_tmp=$(mktemp -d)
issue_2281_array_merge_reverse=${issue_2281_array_merge_tmp}/issue.2281.array.merge.yaml
test_start "issue 2281 - array merge with state-values-set"
info "Comparing issue 2281 array merge output ${issue_2281_array_merge_reverse} with ${issue_2281_array_merge_output_dir}/output.yaml"
${helmfile} -f ${issue_2281_array_merge_input_dir}/helmfile.yaml.gotmpl template $(cat "$issue_2281_array_merge_input_dir/helmfile-extra-args") --skip-deps > "${issue_2281_array_merge_reverse}" || fail "\"helmfile template\" shouldn't fail"
./dyff between -bs "${issue_2281_array_merge_output_dir}/output.yaml" "${issue_2281_array_merge_reverse}" || fail "\"helmfile template\" output should match expected output - arrays should be merged element-by-element"
test_pass "issue 2281 - array merge with state-values-set"

View File

@ -0,0 +1 @@
--state-values-set top.array[0]=cmdlinething1 --state-values-set top.complexArray[1].anotherThing=cmdline

View File

@ -0,0 +1,25 @@
values:
- top:
array:
- thing1
- thing2
complexArray:
- thing: a thing
anotherThing: another thing
- thing: second thing
anotherThing: a second other thing
---
releases:
- name: test
chart: ../../../charts/raw
values:
- top:
{{ toYaml .Values.top | indent 10 }}
templates:
- |
apiVersion: v1
kind: ConfigMap
metadata:
name: TestConfig
data:
{{ toYaml .Values.top | indent 14 }}

View File

@ -0,0 +1,15 @@
---
# Source: raw/templates/resources.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: TestConfig
data:
array:
- cmdlinething1
- thing2
complexArray:
- anotherThing: another thing
thing: a thing
- anotherThing: cmdline
thing: second thing

View File

@ -2,6 +2,9 @@ helmDefaults:
suppressOutputLineRegex:
- "helm.sh/chart"
- "app.kubernetes.io/version"
# Disable auto-detected kubeVersion to prevent helm-diff from normalizing server-side defaults
# which would hide changes like ipFamilyPolicy and ipFamilies being removed
disableAutoDetectedKubeVersionForDiff: true
repositories:
- name: ingress-nginx