diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index b796eefb..22a4f14c 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -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 diff --git a/.github/workflows/images.yaml b/.github/workflows/images.yaml index a9e1c1bc..0c3d2174 100644 --- a/.github/workflows/images.yaml +++ b/.github/workflows/images.yaml @@ -39,7 +39,7 @@ jobs: suffix: "-ubuntu" steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: fetch-depth: 0 diff --git a/.github/workflows/releaser.yaml b/.github/workflows/releaser.yaml index c9816375..81703ac0 100644 --- a/.github/workflows/releaser.yaml +++ b/.github/workflows/releaser.yaml @@ -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 diff --git a/Dockerfile b/Dockerfile index c6ea7654..c6f80a99 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 && \ diff --git a/Dockerfile.debian-stable-slim b/Dockerfile.debian-stable-slim index b07b4a28..f5a9d1b5 100644 --- a/Dockerfile.debian-stable-slim +++ b/Dockerfile.debian-stable-slim @@ -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 && \ diff --git a/Dockerfile.ubuntu b/Dockerfile.ubuntu index e4bde36c..fbb5a9b2 100644 --- a/Dockerfile.ubuntu +++ b/Dockerfile.ubuntu @@ -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 && \ diff --git a/go.mod b/go.mod index 99c80f39..bbf29ca9 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 2232f078..0695ba2e 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/pkg/app/app.go b/pkg/app/app.go index f6ee7cc8..6dffe8ed 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -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{ diff --git a/pkg/app/app_apply_hooks_test.go b/pkg/app/app_apply_hooks_test.go index 0b8e5fc2..0bce491e 100644 --- a/pkg/app/app_apply_hooks_test.go +++ b/pkg/app/app_apply_hooks_test.go @@ -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, }, diff --git a/pkg/app/app_apply_nokubectx_test.go b/pkg/app/app_apply_nokubectx_test.go index e3246e4e..d4377be6 100644 --- a/pkg/app/app_apply_nokubectx_test.go +++ b/pkg/app/app_apply_nokubectx_test.go @@ -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, }, diff --git a/pkg/app/app_apply_test.go b/pkg/app/app_apply_test.go index 8529954c..2f235f1a 100644 --- a/pkg/app/app_apply_test.go +++ b/pkg/app/app_apply_test.go @@ -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, }, diff --git a/pkg/app/app_diff_test.go b/pkg/app/app_diff_test.go index 9729af28..5b9e8d0b 100644 --- a/pkg/app/app_diff_test.go +++ b/pkg/app/app_diff_test.go @@ -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, }, diff --git a/pkg/app/app_gethelm_test.go b/pkg/app/app_gethelm_test.go index 3703e0ad..2a6ef4fb 100644 --- a/pkg/app/app_gethelm_test.go +++ b/pkg/app/app_gethelm_test.go @@ -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" diff --git a/pkg/app/app_lint_test.go b/pkg/app/app_lint_test.go index c8dca9dd..f9b10836 100644 --- a/pkg/app/app_lint_test.go +++ b/pkg/app/app_lint_test.go @@ -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, }, diff --git a/pkg/app/app_list_test.go b/pkg/app/app_list_test.go index edda7d33..f8084682 100644 --- a/pkg/app/app_list_test.go +++ b/pkg/app/app_list_test.go @@ -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) diff --git a/pkg/app/app_parallel_test.go b/pkg/app/app_parallel_test.go index 7b59ad2b..8cc6b9dc 100644 --- a/pkg/app/app_parallel_test.go +++ b/pkg/app/app_parallel_test.go @@ -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) diff --git a/pkg/app/app_sync_test.go b/pkg/app/app_sync_test.go index 0836cd1d..948e8c06 100644 --- a/pkg/app/app_sync_test.go +++ b/pkg/app/app_sync_test.go @@ -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, }, diff --git a/pkg/app/app_template_test.go b/pkg/app/app_template_test.go index d13f643c..780249cc 100644 --- a/pkg/app/app_template_test.go +++ b/pkg/app/app_template_test.go @@ -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, }, diff --git a/pkg/app/app_test.go b/pkg/app/app_test.go index 4f3a6f30..1987c64d 100644 --- a/pkg/app/app_test.go +++ b/pkg/app/app_test.go @@ -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) diff --git a/pkg/app/dag_test.go b/pkg/app/dag_test.go index 599e9ad0..29a057ea 100644 --- a/pkg/app/dag_test.go +++ b/pkg/app/dag_test.go @@ -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 diff --git a/pkg/app/destroy_nokubectx_test.go b/pkg/app/destroy_nokubectx_test.go index 828ec56c..279621c4 100644 --- a/pkg/app/destroy_nokubectx_test.go +++ b/pkg/app/destroy_nokubectx_test.go @@ -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, }, diff --git a/pkg/app/destroy_test.go b/pkg/app/destroy_test.go index 39349ff5..45908f02 100644 --- a/pkg/app/destroy_test.go +++ b/pkg/app/destroy_test.go @@ -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, }, diff --git a/pkg/app/diff_nokubectx_test.go b/pkg/app/diff_nokubectx_test.go index b6624542..64aaf81e 100644 --- a/pkg/app/diff_nokubectx_test.go +++ b/pkg/app/diff_nokubectx_test.go @@ -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, }, diff --git a/pkg/app/diff_test.go b/pkg/app/diff_test.go index b813dbdd..3693bb3c 100644 --- a/pkg/app/diff_test.go +++ b/pkg/app/diff_test.go @@ -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, }, diff --git a/pkg/app/formatters.go b/pkg/app/formatters.go index 7ec9d886..9716e495 100644 --- a/pkg/app/formatters.go +++ b/pkg/app/formatters.go @@ -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 } diff --git a/pkg/app/init.go b/pkg/app/init.go index c0e7a015..8f31a24c 100644 --- a/pkg/app/init.go +++ b/pkg/app/init.go @@ -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" diff --git a/pkg/app/testdata/formatters/tableoutput b/pkg/app/testdata/formatters/tableoutput index 4630d29a..76da0860 100644 --- a/pkg/app/testdata/formatters/tableoutput +++ b/pkg/app/testdata/formatters/tableoutput @@ -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 diff --git a/pkg/cluster/version.go b/pkg/cluster/version.go new file mode 100644 index 00000000..23d74c55 --- /dev/null +++ b/pkg/cluster/version.go @@ -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 +} diff --git a/pkg/cluster/version_test.go b/pkg/cluster/version_test.go new file mode 100644 index 00000000..cd6ba644 --- /dev/null +++ b/pkg/cluster/version_test.go @@ -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") +} diff --git a/pkg/environment/environment.go b/pkg/environment/environment.go index 325d73c9..7cddf102 100644 --- a/pkg/environment/environment.go +++ b/pkg/environment/environment.go @@ -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 { diff --git a/pkg/helmexec/exec.go b/pkg/helmexec/exec.go index 90c7b98a..b2644541 100644 --- a/pkg/helmexec/exec.go +++ b/pkg/helmexec/exec.go @@ -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) diff --git a/pkg/helmexec/exec_test.go b/pkg/helmexec/exec_test.go index 0400265a..154f810f 100644 --- a/pkg/helmexec/exec_test.go +++ b/pkg/helmexec/exec_test.go @@ -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") diff --git a/pkg/maputil/maputil.go b/pkg/maputil/maputil.go index e5cf925b..ff2e9d05 100644 --- a/pkg/maputil/maputil.go +++ b/pkg/maputil/maputil.go @@ -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 +} diff --git a/pkg/maputil/maputil_test.go b/pkg/maputil/maputil_test.go index 74bd9b0a..24b2f663 100644 --- a/pkg/maputil/maputil_test.go +++ b/pkg/maputil/maputil_test.go @@ -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) + } + }) +} diff --git a/pkg/plugins/vals.go b/pkg/plugins/vals.go index da6756a6..c8264ca4 100644 --- a/pkg/plugins/vals.go +++ b/pkg/plugins/vals.go @@ -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 diff --git a/pkg/state/create.go b/pkg/state/create.go index cd30d731..3e68b1eb 100644 --- a/pkg/state/create.go +++ b/pkg/state/create.go @@ -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 } diff --git a/pkg/state/create_test.go b/pkg/state/create_test.go index e8394458..0fff0173 100644 --- a/pkg/state/create_test.go +++ b/pkg/state/create_test.go @@ -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) + } + }) + } +} diff --git a/pkg/state/oci_chart_version_test.go b/pkg/state/oci_chart_version_test.go new file mode 100644 index 00000000..dc931a94 --- /dev/null +++ b/pkg/state/oci_chart_version_test.go @@ -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://" +} diff --git a/pkg/state/skip_test.go b/pkg/state/skip_test.go new file mode 100644 index 00000000..cfcd3f2d --- /dev/null +++ b/pkg/state/skip_test.go @@ -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 +} diff --git a/pkg/state/state.go b/pkg/state/state.go index 89d627f3..69b7d79c 100644 --- a/pkg/state/state.go +++ b/pkg/state/state.go @@ -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 } diff --git a/pkg/state/state_kubeversion_test.go b/pkg/state/state_kubeversion_test.go new file mode 100644 index 00000000..f338ee20 --- /dev/null +++ b/pkg/state/state_kubeversion_test.go @@ -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") + } + } + }) + } +} diff --git a/pkg/state/state_test.go b/pkg/state/state_test.go index 7dd5b3a6..3206cf26 100644 --- a/pkg/state/state_test.go +++ b/pkg/state/state_test.go @@ -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 diff --git a/pkg/state/temp_test.go b/pkg/state/temp_test.go index 7e082d51..811fd084 100644 --- a/pkg/state/temp_test.go +++ b/pkg/state/temp_test.go @@ -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 { diff --git a/test/e2e/template/helmfile/snapshot_test.go b/test/e2e/template/helmfile/snapshot_test.go index 3bbdec26..eeb58341 100644 --- a/test/e2e/template/helmfile/snapshot_test.go +++ b/test/e2e/template/helmfile/snapshot_test.go @@ -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 ") { diff --git a/test/e2e/template/helmfile/testdata/snapshot/issue_473_oci_chart_url_fetch/config.yaml b/test/e2e/template/helmfile/testdata/snapshot/issue_473_oci_chart_url_fetch/config.yaml index 20aefc41..470200fe 100644 --- a/test/e2e/template/helmfile/testdata/snapshot/issue_473_oci_chart_url_fetch/config.yaml +++ b/test/e2e/template/helmfile/testdata/snapshot/issue_473_oci_chart_url_fetch/config.yaml @@ -1,6 +1,6 @@ localDockerRegistry: enabled: true - port: 5000 + # Port is not specified, will be dynamically allocated to avoid conflicts chartifyTempDir: temp2 helmfileArgs: - fetch diff --git a/test/e2e/template/helmfile/testdata/snapshot/issue_473_oci_chart_url_fetch/input.yaml.gotmpl b/test/e2e/template/helmfile/testdata/snapshot/issue_473_oci_chart_url_fetch/input.yaml.gotmpl index c534fb4f..d87ee3cb 100644 --- a/test/e2e/template/helmfile/testdata/snapshot/issue_473_oci_chart_url_fetch/input.yaml.gotmpl +++ b/test/e2e/template/helmfile/testdata/snapshot/issue_473_oci_chart_url_fetch/input.yaml.gotmpl @@ -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: diff --git a/test/e2e/template/helmfile/testdata/snapshot/issue_473_oci_chart_url_fetch/output.yaml b/test/e2e/template/helmfile/testdata/snapshot/issue_473_oci_chart_url_fetch/output.yaml index d1d56924..3cf0c358 100644 --- a/test/e2e/template/helmfile/testdata/snapshot/issue_473_oci_chart_url_fetch/output.yaml +++ b/test/e2e/template/helmfile/testdata/snapshot/issue_473_oci_chart_url_fetch/output.yaml @@ -1 +1 @@ -Pulling localhost:5000/myrepo/raw:0.1.0 +Pulling localhost:$REGISTRY_PORT/myrepo/raw:0.1.0 diff --git a/test/e2e/template/helmfile/testdata/snapshot/issue_493_template_yaml_anchors_merge/output.yaml b/test/e2e/template/helmfile/testdata/snapshot/issue_493_template_yaml_anchors_merge/output.yaml index 9c7b08f7..7bf5bb6e 100644 --- a/test/e2e/template/helmfile/testdata/snapshot/issue_493_template_yaml_anchors_merge/output.yaml +++ b/test/e2e/template/helmfile/testdata/snapshot/issue_493_template_yaml_anchors_merge/output.yaml @@ -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 diff --git a/test/e2e/template/helmfile/testdata/snapshot/oci_chart_pull/config.yaml b/test/e2e/template/helmfile/testdata/snapshot/oci_chart_pull/config.yaml index c9145492..49e44448 100644 --- a/test/e2e/template/helmfile/testdata/snapshot/oci_chart_pull/config.yaml +++ b/test/e2e/template/helmfile/testdata/snapshot/oci_chart_pull/config.yaml @@ -1,6 +1,6 @@ localDockerRegistry: enabled: true - port: 5001 + # Port is not specified, will be dynamically allocated to avoid conflicts chartifyTempDir: temp2 helmfileArgs: - template diff --git a/test/e2e/template/helmfile/testdata/snapshot/oci_chart_pull/input.yaml.gotmpl b/test/e2e/template/helmfile/testdata/snapshot/oci_chart_pull/input.yaml.gotmpl index 194400a6..0536275b 100644 --- a/test/e2e/template/helmfile/testdata/snapshot/oci_chart_pull/input.yaml.gotmpl +++ b/test/e2e/template/helmfile/testdata/snapshot/oci_chart_pull/input.yaml.gotmpl @@ -1,6 +1,6 @@ repositories: - name: myrepo - url: localhost:5001/myrepo + url: localhost:$REGISTRY_PORT/myrepo oci: true releases: diff --git a/test/e2e/template/helmfile/testdata/snapshot/oci_chart_pull/output.yaml b/test/e2e/template/helmfile/testdata/snapshot/oci_chart_pull/output.yaml index 197bd4c4..90d91214 100644 --- a/test/e2e/template/helmfile/testdata/snapshot/oci_chart_pull/output.yaml +++ b/test/e2e/template/helmfile/testdata/snapshot/oci_chart_pull/output.yaml @@ -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 diff --git a/test/e2e/template/helmfile/testdata/snapshot/oci_chart_pull_direct/config.yaml b/test/e2e/template/helmfile/testdata/snapshot/oci_chart_pull_direct/config.yaml index c9145492..49e44448 100644 --- a/test/e2e/template/helmfile/testdata/snapshot/oci_chart_pull_direct/config.yaml +++ b/test/e2e/template/helmfile/testdata/snapshot/oci_chart_pull_direct/config.yaml @@ -1,6 +1,6 @@ localDockerRegistry: enabled: true - port: 5001 + # Port is not specified, will be dynamically allocated to avoid conflicts chartifyTempDir: temp2 helmfileArgs: - template diff --git a/test/e2e/template/helmfile/testdata/snapshot/oci_chart_pull_direct/input.yaml.gotmpl b/test/e2e/template/helmfile/testdata/snapshot/oci_chart_pull_direct/input.yaml.gotmpl index f3e1f31f..8a919b53 100644 --- a/test/e2e/template/helmfile/testdata/snapshot/oci_chart_pull_direct/input.yaml.gotmpl +++ b/test/e2e/template/helmfile/testdata/snapshot/oci_chart_pull_direct/input.yaml.gotmpl @@ -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 diff --git a/test/e2e/template/helmfile/testdata/snapshot/oci_chart_pull_direct/output.yaml b/test/e2e/template/helmfile/testdata/snapshot/oci_chart_pull_direct/output.yaml index 313a9209..213d8876 100644 --- a/test/e2e/template/helmfile/testdata/snapshot/oci_chart_pull_direct/output.yaml +++ b/test/e2e/template/helmfile/testdata/snapshot/oci_chart_pull_direct/output.yaml @@ -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 diff --git a/test/e2e/template/helmfile/testdata/snapshot/oci_chart_pull_once/config.yaml b/test/e2e/template/helmfile/testdata/snapshot/oci_chart_pull_once/config.yaml index f0bb6728..0c7b7780 100644 --- a/test/e2e/template/helmfile/testdata/snapshot/oci_chart_pull_once/config.yaml +++ b/test/e2e/template/helmfile/testdata/snapshot/oci_chart_pull_once/config.yaml @@ -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 diff --git a/test/e2e/template/helmfile/testdata/snapshot/oci_chart_pull_once/input.yaml.gotmpl b/test/e2e/template/helmfile/testdata/snapshot/oci_chart_pull_once/input.yaml.gotmpl index 394cc82d..f88336df 100644 --- a/test/e2e/template/helmfile/testdata/snapshot/oci_chart_pull_once/input.yaml.gotmpl +++ b/test/e2e/template/helmfile/testdata/snapshot/oci_chart_pull_once/input.yaml.gotmpl @@ -1,6 +1,6 @@ repositories: - name: myrepo - url: localhost:5001/myrepo + url: localhost:$REGISTRY_PORT/myrepo oci: true releases: diff --git a/test/e2e/template/helmfile/testdata/snapshot/oci_chart_pull_once/output.yaml b/test/e2e/template/helmfile/testdata/snapshot/oci_chart_pull_once/output.yaml index b865783e..3a41ca8e 100644 --- a/test/e2e/template/helmfile/testdata/snapshot/oci_chart_pull_once/output.yaml +++ b/test/e2e/template/helmfile/testdata/snapshot/oci_chart_pull_once/output.yaml @@ -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 diff --git a/test/e2e/template/helmfile/testdata/snapshot/oci_chart_pull_once2/config.yaml b/test/e2e/template/helmfile/testdata/snapshot/oci_chart_pull_once2/config.yaml index bd0820e0..9c0ad782 100644 --- a/test/e2e/template/helmfile/testdata/snapshot/oci_chart_pull_once2/config.yaml +++ b/test/e2e/template/helmfile/testdata/snapshot/oci_chart_pull_once2/config.yaml @@ -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 diff --git a/test/e2e/template/helmfile/testdata/snapshot/oci_chart_pull_once2/input.yaml.gotmpl b/test/e2e/template/helmfile/testdata/snapshot/oci_chart_pull_once2/input.yaml.gotmpl index 9aea4b81..84965bd1 100644 --- a/test/e2e/template/helmfile/testdata/snapshot/oci_chart_pull_once2/input.yaml.gotmpl +++ b/test/e2e/template/helmfile/testdata/snapshot/oci_chart_pull_once2/input.yaml.gotmpl @@ -1,6 +1,6 @@ repositories: - name: myrepo - url: localhost:5001/myrepo + url: localhost:$REGISTRY_PORT/myrepo oci: true releases: diff --git a/test/e2e/template/helmfile/testdata/snapshot/oci_chart_pull_once2/output.yaml b/test/e2e/template/helmfile/testdata/snapshot/oci_chart_pull_once2/output.yaml index b94a5cef..6d277d59 100644 --- a/test/e2e/template/helmfile/testdata/snapshot/oci_chart_pull_once2/output.yaml +++ b/test/e2e/template/helmfile/testdata/snapshot/oci_chart_pull_once2/output.yaml @@ -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 diff --git a/test/e2e/template/helmfile/testdata/snapshot/oci_need/config.yaml b/test/e2e/template/helmfile/testdata/snapshot/oci_need/config.yaml index c3a26f4a..33f724b3 100644 --- a/test/e2e/template/helmfile/testdata/snapshot/oci_need/config.yaml +++ b/test/e2e/template/helmfile/testdata/snapshot/oci_need/config.yaml @@ -1,6 +1,6 @@ localDockerRegistry: enabled: true - port: 5002 + # Port is not specified, will be dynamically allocated to avoid conflicts chartifyTempDir: temp1 helmfileArgs: - template diff --git a/test/e2e/template/helmfile/testdata/snapshot/oci_need/input.yaml.gotmpl b/test/e2e/template/helmfile/testdata/snapshot/oci_need/input.yaml.gotmpl index 1e63d06b..b13cdd41 100644 --- a/test/e2e/template/helmfile/testdata/snapshot/oci_need/input.yaml.gotmpl +++ b/test/e2e/template/helmfile/testdata/snapshot/oci_need/input.yaml.gotmpl @@ -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 diff --git a/test/e2e/template/helmfile/testdata/snapshot/oci_need/output.yaml b/test/e2e/template/helmfile/testdata/snapshot/oci_need/output.yaml index baf64c9d..3c5796df 100644 --- a/test/e2e/template/helmfile/testdata/snapshot/oci_need/output.yaml +++ b/test/e2e/template/helmfile/testdata/snapshot/oci_need/output.yaml @@ -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 diff --git a/test/e2e/template/helmfile/testdata/snapshot/postrenderer/config.yaml b/test/e2e/template/helmfile/testdata/snapshot/postrenderer/config.yaml index deb6629a..33f724b3 100644 --- a/test/e2e/template/helmfile/testdata/snapshot/postrenderer/config.yaml +++ b/test/e2e/template/helmfile/testdata/snapshot/postrenderer/config.yaml @@ -1,6 +1,6 @@ localDockerRegistry: enabled: true - port: 5001 + # Port is not specified, will be dynamically allocated to avoid conflicts chartifyTempDir: temp1 helmfileArgs: - template diff --git a/test/e2e/template/helmfile/testdata/snapshot/postrenderer/input.yaml.gotmpl b/test/e2e/template/helmfile/testdata/snapshot/postrenderer/input.yaml.gotmpl index a317ee0e..4aea39d7 100644 --- a/test/e2e/template/helmfile/testdata/snapshot/postrenderer/input.yaml.gotmpl +++ b/test/e2e/template/helmfile/testdata/snapshot/postrenderer/input.yaml.gotmpl @@ -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 diff --git a/test/e2e/template/helmfile/testdata/snapshot/postrenderer/output-helm4.yaml b/test/e2e/template/helmfile/testdata/snapshot/postrenderer/output-helm4.yaml index 92c27143..1c75e976 100644 --- a/test/e2e/template/helmfile/testdata/snapshot/postrenderer/output-helm4.yaml +++ b/test/e2e/template/helmfile/testdata/snapshot/postrenderer/output-helm4.yaml @@ -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 diff --git a/test/e2e/template/helmfile/testdata/snapshot/postrenderer/output.yaml b/test/e2e/template/helmfile/testdata/snapshot/postrenderer/output.yaml index f71b03aa..d661ef51 100644 --- a/test/e2e/template/helmfile/testdata/snapshot/postrenderer/output.yaml +++ b/test/e2e/template/helmfile/testdata/snapshot/postrenderer/output.yaml @@ -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 diff --git a/test/integration/run.sh b/test/integration/run.sh index e93cd120..da635768 100755 --- a/test/integration/run.sh +++ b/test/integration/run.sh @@ -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 ----------------------------------------------------------------------------------------------------------- diff --git a/test/integration/test-cases/issue-2247.sh b/test/integration/test-cases/issue-2247.sh new file mode 100755 index 00000000..bbc8d439 --- /dev/null +++ b/test/integration/test-cases/issue-2247.sh @@ -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" < "${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" < "${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" < "${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)" diff --git a/test/integration/test-cases/issue-2247/chart/Chart.yaml b/test/integration/test-cases/issue-2247/chart/Chart.yaml new file mode 100644 index 00000000..58a74d38 --- /dev/null +++ b/test/integration/test-cases/issue-2247/chart/Chart.yaml @@ -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" diff --git a/test/integration/test-cases/issue-2247/chart/templates/configmap.yaml b/test/integration/test-cases/issue-2247/chart/templates/configmap.yaml new file mode 100644 index 00000000..67b956ed --- /dev/null +++ b/test/integration/test-cases/issue-2247/chart/templates/configmap.yaml @@ -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 }}" diff --git a/test/integration/test-cases/issue-2247/chart/values.yaml b/test/integration/test-cases/issue-2247/chart/values.yaml new file mode 100644 index 00000000..022cafaf --- /dev/null +++ b/test/integration/test-cases/issue-2247/chart/values.yaml @@ -0,0 +1,3 @@ +# Default values for test-chart-2247 +# This is a minimal chart for testing OCI version handling +replicaCount: 1 diff --git a/test/integration/test-cases/issue-2247/input/helmfile-no-version.yaml b/test/integration/test-cases/issue-2247/input/helmfile-no-version.yaml new file mode 100644 index 00000000..5114c57e --- /dev/null +++ b/test/integration/test-cases/issue-2247/input/helmfile-no-version.yaml @@ -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) diff --git a/test/integration/test-cases/issue-2247/input/helmfile-oci-registry.yaml b/test/integration/test-cases/issue-2247/input/helmfile-oci-registry.yaml new file mode 100644 index 00000000..ee794fd3 --- /dev/null +++ b/test/integration/test-cases/issue-2247/input/helmfile-oci-registry.yaml @@ -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) diff --git a/test/integration/test-cases/issue-2247/input/helmfile-with-latest.yaml b/test/integration/test-cases/issue-2247/input/helmfile-with-latest.yaml new file mode 100644 index 00000000..6577497f --- /dev/null +++ b/test/integration/test-cases/issue-2247/input/helmfile-with-latest.yaml @@ -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 diff --git a/test/integration/test-cases/issue-2271.sh b/test/integration/test-cases/issue-2271.sh new file mode 100755 index 00000000..660af0c1 --- /dev/null +++ b/test/integration/test-cases/issue-2271.sh @@ -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" diff --git a/test/integration/test-cases/issue-2271/input/dns-patch.yaml b/test/integration/test-cases/issue-2271/input/dns-patch.yaml new file mode 100644 index 00000000..2f51c26e --- /dev/null +++ b/test/integration/test-cases/issue-2271/input/dns-patch.yaml @@ -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" diff --git a/test/integration/test-cases/issue-2271/input/helmfile-no-kustomize.yaml b/test/integration/test-cases/issue-2271/input/helmfile-no-kustomize.yaml new file mode 100644 index 00000000..04c1881e --- /dev/null +++ b/test/integration/test-cases/issue-2271/input/helmfile-no-kustomize.yaml @@ -0,0 +1,6 @@ +releases: + - name: test-release-2271 + namespace: default + chart: ./test-chart + installed: true + # No strategicMergePatches - lookup function should work diff --git a/test/integration/test-cases/issue-2271/input/helmfile.yaml b/test/integration/test-cases/issue-2271/input/helmfile.yaml new file mode 100644 index 00000000..c6e40009 --- /dev/null +++ b/test/integration/test-cases/issue-2271/input/helmfile.yaml @@ -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 diff --git a/test/integration/test-cases/issue-2271/input/test-chart/Chart.yaml b/test/integration/test-cases/issue-2271/input/test-chart/Chart.yaml new file mode 100644 index 00000000..10a401d2 --- /dev/null +++ b/test/integration/test-cases/issue-2271/input/test-chart/Chart.yaml @@ -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" diff --git a/test/integration/test-cases/issue-2271/input/test-chart/templates/configmap.yaml b/test/integration/test-cases/issue-2271/input/test-chart/templates/configmap.yaml new file mode 100644 index 00000000..42f47b57 --- /dev/null +++ b/test/integration/test-cases/issue-2271/input/test-chart/templates/configmap.yaml @@ -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 }} diff --git a/test/integration/test-cases/issue-2271/input/test-chart/templates/deployment.yaml b/test/integration/test-cases/issue-2271/input/test-chart/templates/deployment.yaml new file mode 100644 index 00000000..6699d56a --- /dev/null +++ b/test/integration/test-cases/issue-2271/input/test-chart/templates/deployment.yaml @@ -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 diff --git a/test/integration/test-cases/issue-2275.sh b/test/integration/test-cases/issue-2275.sh new file mode 100755 index 00000000..230552b1 --- /dev/null +++ b/test/integration/test-cases/issue-2275.sh @@ -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" diff --git a/test/integration/test-cases/issue-2275/input/helmfile.yaml b/test/integration/test-cases/issue-2275/input/helmfile.yaml new file mode 100644 index 00000000..5262b1e9 --- /dev/null +++ b/test/integration/test-cases/issue-2275/input/helmfile.yaml @@ -0,0 +1,5 @@ +releases: + - name: test-release-2275 + namespace: default + chart: ./test-chart + installed: true diff --git a/test/integration/test-cases/issue-2275/input/test-chart/Chart.yaml b/test/integration/test-cases/issue-2275/input/test-chart/Chart.yaml new file mode 100644 index 00000000..b9092b26 --- /dev/null +++ b/test/integration/test-cases/issue-2275/input/test-chart/Chart.yaml @@ -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" diff --git a/test/integration/test-cases/issue-2275/input/test-chart/templates/deployment.yaml b/test/integration/test-cases/issue-2275/input/test-chart/templates/deployment.yaml new file mode 100644 index 00000000..463d14e5 --- /dev/null +++ b/test/integration/test-cases/issue-2275/input/test-chart/templates/deployment.yaml @@ -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 diff --git a/test/integration/test-cases/issue-2280.sh b/test/integration/test-cases/issue-2280.sh new file mode 100644 index 00000000..8af819ce --- /dev/null +++ b/test/integration/test-cases/issue-2280.sh @@ -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" diff --git a/test/integration/test-cases/issue-2280/input/chart/Chart.yaml b/test/integration/test-cases/issue-2280/input/chart/Chart.yaml new file mode 100644 index 00000000..c30671cf --- /dev/null +++ b/test/integration/test-cases/issue-2280/input/chart/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v2 +name: test-chart-2280 +description: Test chart for issue #2280 +version: 0.1.0 +appVersion: "1.0" diff --git a/test/integration/test-cases/issue-2280/input/chart/templates/configmap.yaml b/test/integration/test-cases/issue-2280/input/chart/templates/configmap.yaml new file mode 100644 index 00000000..3344b726 --- /dev/null +++ b/test/integration/test-cases/issue-2280/input/chart/templates/configmap.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ .Release.Name }}-config +data: + message: {{ .Values.message | quote }} diff --git a/test/integration/test-cases/issue-2280/input/chart/values.yaml b/test/integration/test-cases/issue-2280/input/chart/values.yaml new file mode 100644 index 00000000..21277d71 --- /dev/null +++ b/test/integration/test-cases/issue-2280/input/chart/values.yaml @@ -0,0 +1 @@ +message: "default message" diff --git a/test/integration/test-cases/issue-2280/input/helmfile.yaml b/test/integration/test-cases/issue-2280/input/helmfile.yaml new file mode 100644 index 00000000..385f934b --- /dev/null +++ b/test/integration/test-cases/issue-2280/input/helmfile.yaml @@ -0,0 +1,6 @@ +releases: + - name: test-release-2280 + namespace: default + chart: ./chart + values: + - message: "test message" diff --git a/test/integration/test-cases/issue-2281-array-merge.sh b/test/integration/test-cases/issue-2281-array-merge.sh new file mode 100644 index 00000000..72ad6c5e --- /dev/null +++ b/test/integration/test-cases/issue-2281-array-merge.sh @@ -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" diff --git a/test/integration/test-cases/issue-2281-array-merge/input/helmfile-extra-args b/test/integration/test-cases/issue-2281-array-merge/input/helmfile-extra-args new file mode 100644 index 00000000..3399b57c --- /dev/null +++ b/test/integration/test-cases/issue-2281-array-merge/input/helmfile-extra-args @@ -0,0 +1 @@ +--state-values-set top.array[0]=cmdlinething1 --state-values-set top.complexArray[1].anotherThing=cmdline diff --git a/test/integration/test-cases/issue-2281-array-merge/input/helmfile.yaml.gotmpl b/test/integration/test-cases/issue-2281-array-merge/input/helmfile.yaml.gotmpl new file mode 100644 index 00000000..d23f9433 --- /dev/null +++ b/test/integration/test-cases/issue-2281-array-merge/input/helmfile.yaml.gotmpl @@ -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 }} diff --git a/test/integration/test-cases/issue-2281-array-merge/output/output.yaml b/test/integration/test-cases/issue-2281-array-merge/output/output.yaml new file mode 100644 index 00000000..fc0360eb --- /dev/null +++ b/test/integration/test-cases/issue-2281-array-merge/output/output.yaml @@ -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 diff --git a/test/integration/test-cases/suppress-output-line-regex/input/helmfile.yaml.gotmpl b/test/integration/test-cases/suppress-output-line-regex/input/helmfile.yaml.gotmpl index 634a5f76..d08fee3c 100644 --- a/test/integration/test-cases/suppress-output-line-regex/input/helmfile.yaml.gotmpl +++ b/test/integration/test-cases/suppress-output-line-regex/input/helmfile.yaml.gotmpl @@ -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