diff --git a/.github/env b/.github/env index fc6f21880..43ebdd36e 100644 --- a/.github/env +++ b/.github/env @@ -1,2 +1,2 @@ -GOLANG_VERSION=1.25 +GOLANG_VERSION=1.26 GOLANGCI_LINT_VERSION=v2.11.3 diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 972602fea..256d26cc3 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -24,14 +24,15 @@ on: schedule: - cron: '29 6 * * 6' -permissions: - contents: read - security-events: write +permissions: {} jobs: analyze: name: Analyze runs-on: ubuntu-latest + permissions: + contents: read + security-events: write strategy: fail-fast: false @@ -47,7 +48,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # pinv4.35.2 + uses: github/codeql-action/init@68bde559dea0fdcac2102bfdf6230c5f70eb485e # pinv4.35.4 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -58,7 +59,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # pinv4.35.2 + uses: github/codeql-action/autobuild@68bde559dea0fdcac2102bfdf6230c5f70eb485e # pinv4.35.4 # â„šī¸ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -72,4 +73,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # pinv4.35.2 + uses: github/codeql-action/analyze@68bde559dea0fdcac2102bfdf6230c5f70eb485e # pinv4.35.4 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 593003192..ab8a4a509 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -86,6 +86,8 @@ jobs: steps: - name: Checkout source code uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # pin@v6.0.2 + with: + fetch-depth: 0 - name: Add variables to environment file run: cat ".github/env" >> "$GITHUB_ENV" diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index 41e2f1254..16a9a8c13 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -64,6 +64,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard (optional). # Commenting out will disable upload of results to your repo's Code Scanning dashboard - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2 + uses: github/codeql-action/upload-sarif@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4 with: sarif_file: results.sarif diff --git a/.github/workflows/stale.yaml b/.github/workflows/stale.yaml index 7d41280ad..9b0c29952 100644 --- a/.github/workflows/stale.yaml +++ b/.github/workflows/stale.yaml @@ -3,9 +3,14 @@ on: schedule: - cron: "0 0 * * *" +permissions: {} + jobs: stale: runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write steps: - uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0 with: diff --git a/.golangci.yml b/.golangci.yml index 856d0fce9..1ed3353b4 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -33,6 +33,7 @@ linters: - revive - sloglint - staticcheck + - testifylint - thelper - unused - usestdlibvars @@ -95,6 +96,24 @@ linters: - helpers - models + testifylint: + disable: + - empty + - encoded-compare + - equal-values + - error-is-as + - error-nil + - expected-actual + - float-compare + - go-require + - len + - nil-compare + - require-error + - suite-dont-use-pkg + - suite-extra-assert-call + # Intentionally enable all testifylint rules so new checks are adopted automatically. + enable-all: true + run: timeout: 10m diff --git a/.goreleaser.yaml b/.goreleaser.yaml new file mode 100644 index 000000000..ab2089882 --- /dev/null +++ b/.goreleaser.yaml @@ -0,0 +1,74 @@ +version: 2 + +project_name: helm + +dist: _dist +builds: + - env: + - CGO_ENABLED=0 + goos: + - linux + - windows + - darwin + goarch: + - amd64 + - arm64 + - arm + - "386" + - ppc64le + - s390x + - riscv64 + - loong64 + goamd64: + - v1 + goarm: + - "7" + goarm64: + - v8.0 + go386: + - sse2 + goriscv64: + - rva20u64 + ignore: + - goos: darwin + goarch: "386" + - goos: darwin + goarch: arm + - goos: darwin + goarch: ppc64le + - goos: darwin + goarch: s390x + - goos: darwin + goarch: riscv64 + - goos: darwin + goarch: loong64 + - goos: windows + goarch: "386" + - goos: windows + goarch: arm + - goos: windows + goarch: ppc64le + - goos: windows + goarch: s390x + - goos: windows + goarch: riscv64 + - goos: windows + goarch: loong64 + main: ./cmd/helm + no_unique_dist_dir: true + binary: "{{ .Os }}-{{ .Arch }}/helm" + ldflags: + - "{{ .Env.LDFLAGS }}" + flags: + - -trimpath + dir: . + +snapshot: + version_template: "{{ if index .Env \"GORELEASER_CURRENT_TAG\" }}{{ .Env.GORELEASER_CURRENT_TAG }}{{ else }}{{ incpatch .Version }}-next{{ end }}" + +changelog: + sort: asc + filters: + exclude: + - '^docs:' + - '^test:' diff --git a/Makefile b/Makefile index a18b83f0d..81b149a68 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,6 @@ BINDIR := $(CURDIR)/bin INSTALL_PATH ?= /usr/local/bin DIST_DIRS := find * -type d -exec -TARGETS := darwin/amd64 darwin/arm64 linux/amd64 linux/386 linux/arm linux/arm64 linux/loong64 linux/ppc64le linux/s390x linux/riscv64 windows/amd64 windows/arm64 TARGET_OBJS ?= darwin-amd64.tar.gz darwin-amd64.tar.gz.sha256 darwin-amd64.tar.gz.sha256sum darwin-arm64.tar.gz darwin-arm64.tar.gz.sha256 darwin-arm64.tar.gz.sha256sum linux-amd64.tar.gz linux-amd64.tar.gz.sha256 linux-amd64.tar.gz.sha256sum linux-386.tar.gz linux-386.tar.gz.sha256 linux-386.tar.gz.sha256sum linux-arm.tar.gz linux-arm.tar.gz.sha256 linux-arm.tar.gz.sha256sum linux-arm64.tar.gz linux-arm64.tar.gz.sha256 linux-arm64.tar.gz.sha256sum linux-loong64.tar.gz linux-loong64.tar.gz.sha256 linux-loong64.tar.gz.sha256sum linux-ppc64le.tar.gz linux-ppc64le.tar.gz.sha256 linux-ppc64le.tar.gz.sha256sum linux-s390x.tar.gz linux-s390x.tar.gz.sha256 linux-s390x.tar.gz.sha256sum linux-riscv64.tar.gz linux-riscv64.tar.gz.sha256 linux-riscv64.tar.gz.sha256sum windows-amd64.zip windows-amd64.zip.sha256 windows-amd64.zip.sha256sum windows-arm64.zip windows-arm64.zip.sha256 windows-arm64.zip.sha256sum BINNAME ?= helm @@ -9,7 +8,7 @@ GOBIN = $(shell go env GOBIN) ifeq ($(GOBIN),) GOBIN = $(shell go env GOPATH)/bin endif -GOX = $(GOBIN)/gox +GORELEASER = $(GOBIN)/goreleaser GOIMPORTS = $(GOBIN)/goimports ARCH = $(shell go env GOARCH) @@ -130,8 +129,7 @@ test-source-headers: @scripts/validate-license.sh .PHONY: test-acceptance -test-acceptance: TARGETS = linux/amd64 -test-acceptance: build build-cross +test-acceptance: build @if [ -d "${ACCEPTANCE_DIR}" ]; then \ cd ${ACCEPTANCE_DIR} && \ ROBOT_RUN_TESTS=$(ACCEPTANCE_RUN_TESTS) ROBOT_HELM_PATH='$(BINDIR)' make acceptance; \ @@ -162,8 +160,8 @@ gen-test-golden: test-unit # dependencies to the go.mod file. To avoid that we change to a directory # without a go.mod file when downloading the following dependencies -$(GOX): - (cd /; go install github.com/mitchellh/gox@v1.0.2-0.20220701044238-9f712387e2d2) +$(GORELEASER): + (cd /; go install github.com/goreleaser/goreleaser/v2@latest) $(GOIMPORTS): (cd /; go install golang.org/x/tools/cmd/goimports@latest) @@ -173,8 +171,8 @@ $(GOIMPORTS): .PHONY: build-cross build-cross: LDFLAGS += -extldflags "-static" -build-cross: $(GOX) - GOFLAGS="-trimpath" CGO_ENABLED=0 $(GOX) -parallel=3 -output="_dist/{{.OS}}-{{.Arch}}/$(BINNAME)" -osarch='$(TARGETS)' $(GOFLAGS) -tags '$(TAGS)' -ldflags '$(LDFLAGS)' ./cmd/helm +build-cross: $(GORELEASER) + LDFLAGS='$(LDFLAGS)' $(GORELEASER) build --snapshot --clean .PHONY: dist dist: diff --git a/go.mod b/go.mod index 7e734a01e..40c02c7e1 100644 --- a/go.mod +++ b/go.mod @@ -1,23 +1,23 @@ module helm.sh/helm/v4 -go 1.25.0 +go 1.26.0 require ( github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 github.com/BurntSushi/toml v1.6.0 github.com/DATA-DOG/go-sqlmock v1.5.2 - github.com/Masterminds/semver/v3 v3.4.0 + github.com/Masterminds/semver/v3 v3.5.0 github.com/Masterminds/sprig/v3 v3.3.0 github.com/Masterminds/squirrel v1.5.4 github.com/Masterminds/vcs v1.13.3 github.com/ProtonMail/go-crypto v1.4.1 github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 github.com/cyphar/filepath-securejoin v0.6.1 - github.com/distribution/distribution/v3 v3.1.0 + github.com/distribution/distribution/v3 v3.1.1 github.com/evanphx/json-patch/v5 v5.9.11 github.com/extism/go-sdk v1.7.1 github.com/fatih/color v1.19.0 - github.com/fluxcd/cli-utils v1.0.0 + github.com/fluxcd/cli-utils v1.2.0 github.com/foxcpp/go-mockdns v1.2.0 github.com/gobwas/glob v0.2.3 github.com/gofrs/flock v0.13.0 @@ -39,16 +39,16 @@ require ( golang.org/x/term v0.42.0 golang.org/x/text v0.36.0 gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/api v0.35.4 - k8s.io/apiextensions-apiserver v0.35.4 - k8s.io/apimachinery v0.35.4 - k8s.io/apiserver v0.35.4 - k8s.io/cli-runtime v0.35.4 - k8s.io/client-go v0.35.4 - k8s.io/klog/v2 v2.130.1 - k8s.io/kubectl v0.35.4 + k8s.io/api v0.36.0 + k8s.io/apiextensions-apiserver v0.36.0 + k8s.io/apimachinery v0.36.0 + k8s.io/apiserver v0.36.0 + k8s.io/cli-runtime v0.36.0 + k8s.io/client-go v0.36.0 + k8s.io/klog/v2 v2.140.0 + k8s.io/kubectl v0.36.0 oras.land/oras-go/v2 v2.6.0 - sigs.k8s.io/controller-runtime v0.23.3 + sigs.k8s.io/controller-runtime v0.24.0 sigs.k8s.io/kustomize/kyaml v0.21.1 sigs.k8s.io/yaml v1.6.0 ) @@ -65,7 +65,7 @@ require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/chai2010/gettext-go v1.0.2 // indirect github.com/cloudflare/circl v1.6.3 // indirect - github.com/coreos/go-systemd/v22 v22.5.0 // indirect + github.com/coreos/go-systemd/v22 v22.7.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect @@ -74,7 +74,7 @@ require ( github.com/docker/go-events v0.0.0-20250808211157-605354379745 // indirect github.com/docker/go-metrics v0.0.1 // indirect github.com/dylibso/observe-sdk/go v0.0.0-20240819160327-2d926c5d788a // indirect - github.com/emicklei/go-restful/v3 v3.12.2 // indirect + github.com/emicklei/go-restful/v3 v3.13.0 // indirect github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect @@ -91,7 +91,6 @@ require ( github.com/google/uuid v1.6.0 // indirect github.com/gorilla/handlers v1.5.2 // indirect github.com/gorilla/mux v1.8.1 // indirect - github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect github.com/hashicorp/golang-lru/arc/v2 v2.0.5 // indirect github.com/hashicorp/golang-lru/v2 v2.0.5 // indirect @@ -168,15 +167,15 @@ require ( google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect google.golang.org/grpc v1.80.0 // indirect - google.golang.org/protobuf v1.36.11 // indirect + google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect - k8s.io/component-base v0.35.4 // indirect - k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect - k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect + k8s.io/component-base v0.36.0 // indirect + k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a // indirect + k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 // indirect sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/kustomize/api v0.21.1 // indirect sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.2 // indirect ) diff --git a/go.sum b/go.sum index 9614bfb90..f1a2ca9f8 100644 --- a/go.sum +++ b/go.sum @@ -14,8 +14,8 @@ github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= -github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= -github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE= +github.com/Masterminds/semver/v3 v3.5.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM= @@ -53,8 +53,8 @@ github.com/chai2010/gettext-go v1.0.2 h1:1Lwwip6Q2QGsAdl/ZKPCwTe9fe0CjlUbqj5bFNS github.com/chai2010/gettext-go v1.0.2/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA= github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= -github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= -github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/coreos/go-systemd/v22 v22.7.0 h1:LAEzFkke61DFROc7zNLX/WA2i5J8gYqe0rSj9KI28KA= +github.com/coreos/go-systemd/v22 v22.7.0/go.mod h1:xNUYtjHu2EDXbsxz1i41wouACIwT7Ybq9o0BQhMwD0w= github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= @@ -67,8 +67,8 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= -github.com/distribution/distribution/v3 v3.1.0 h1:u1v788HreKTLGdNY6s7px8Exgrs9mZ9UrCDjSrpCM8g= -github.com/distribution/distribution/v3 v3.1.0/go.mod h1:73BuF5/ziMHNVt7nnL1roYpH4Eg/FgUlKZm3WryIx/o= +github.com/distribution/distribution/v3 v3.1.1 h1:KUbk7C8CfaLXy8kbf/hGq9cad/wCoLB6dbWH6DMbmX0= +github.com/distribution/distribution/v3 v3.1.1/go.mod h1:d7lXwZpph0bVcOj4Aqn0nMrWHIwRQGdiV5TLeI+/w6Y= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= @@ -81,8 +81,8 @@ github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQ github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= github.com/dylibso/observe-sdk/go v0.0.0-20240819160327-2d926c5d788a h1:UwSIFv5g5lIvbGgtf3tVwC7Ky9rmMFBp0RMs+6f6YqE= github.com/dylibso/observe-sdk/go v0.0.0-20240819160327-2d926c5d788a/go.mod h1:C8DzXehI4zAbrdlbtOByKX6pfivJTBiV9Jjqv56Yd9Q= -github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= -github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= +github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f h1:Wl78ApPPB2Wvf/TIe2xdyJxTlb6obmF18d8QdkxNDu4= @@ -93,8 +93,8 @@ github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/fluxcd/cli-utils v1.0.0 h1:+luz8igR6dM5f7uHwkkMTECsl+jp0kR69POuV5aOoDs= -github.com/fluxcd/cli-utils v1.0.0/go.mod h1:ANTIXWLLsNmn5bMNxbyoY22rtwRSR/fbu+IFy756fs0= +github.com/fluxcd/cli-utils v1.2.0 h1:1o07pXTMxJ/XJ1GpAbLtjdXwfCUMq4Ku1OcnvJHLohI= +github.com/fluxcd/cli-utils v1.2.0/go.mod h1:d5HdTDdR5sCbsIbgtOQ7x7srKYwYeZORU6CD2yn4j/M= github.com/foxcpp/go-mockdns v1.2.0 h1:omK3OrHRD1IWJz1FuFBCFquhXslXoF17OvBS6JPzZF0= github.com/foxcpp/go-mockdns v1.2.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= @@ -128,7 +128,6 @@ github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1v github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= -github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw= github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= @@ -155,8 +154,6 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gosuri/uitable v0.0.4 h1:IG2xLKRvErL3uhY6e1BylFzG+aJiwQviDDTfOKeKTpY= github.com/gosuri/uitable v0.0.4/go.mod h1:tKR86bXuXPZazfOTG1FIzvjIdXzd0mo4Vtn16vt0PJo= -github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA= -github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= github.com/hashicorp/golang-lru/arc/v2 v2.0.5 h1:l2zaLDubNhW4XO3LnliVj0GXO3+/CGNJAg1dcN2Fpfw= @@ -373,8 +370,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= @@ -472,8 +469,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 h1: google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM= google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4= -google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= -google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af h1:+5/Sw3GsDNlEmu7TfklWKPdQ0Ykja5VEmq2i817+jbI= +google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= @@ -488,32 +485,32 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -k8s.io/api v0.35.4 h1:P7nFYKl5vo9AGUp1Z+Pmd3p2tA7bX2wbFWCvDeRv988= -k8s.io/api v0.35.4/go.mod h1:yl4lqySWOgYJJf9RERXKUwE9g2y+CkuwG+xmcOK8wXU= -k8s.io/apiextensions-apiserver v0.35.4 h1:HeP+Upp7ItdvnyGmub0yoix+2z5+ev4M5cE5TCgtOUU= -k8s.io/apiextensions-apiserver v0.35.4/go.mod h1:ogQlk+stIE8mnoRthSYCwlOS12fVqgWFiErMwPaXA7c= -k8s.io/apimachinery v0.35.4 h1:xtdom9RG7e+yDp71uoXoJDWEE2eOiHgeO4GdBzwWpds= -k8s.io/apimachinery v0.35.4/go.mod h1:NNi1taPOpep0jOj+oRha3mBJPqvi0hGdaV8TCqGQ+cc= -k8s.io/apiserver v0.35.4 h1:vtuFqNFmF9bPRdHDL2lpK6qCTPWDreZJL4LRPwVM6ho= -k8s.io/apiserver v0.35.4/go.mod h1:JnBcb+J8kFXKpZkgcbcUnPBBHi4qgBii1I7dLxFY/oo= -k8s.io/cli-runtime v0.35.4 h1:8QRCXSDvopflFNM65Vkkdv42BljPdRSiqf6HFyI1iik= -k8s.io/cli-runtime v0.35.4/go.mod h1:MKLFuZxiJpm87UxjVeQRNy3sCaczHrSOPKN9pinlrM0= -k8s.io/client-go v0.35.4 h1:DN6fyaGuzK64UvnKO5fOA6ymSjvfGAnCAHAR0C66kD8= -k8s.io/client-go v0.35.4/go.mod h1:2Pg9WpsS4NeOpoYTfHHfMxBG8zFMSAUi4O/qoiJC3nY= -k8s.io/component-base v0.35.4 h1:6n1tNJ87johN0Hif0Fs8K2GMthsaUwMqCebUDLYyv7U= -k8s.io/component-base v0.35.4/go.mod h1:qaDJgz5c1KYKla9occFmlJEfPpkuA55s90G509R+PeY= -k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= -k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= -k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE= -k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= -k8s.io/kubectl v0.35.4 h1:IHitney6OUeH29rBQnt6Cas6az8HpFeSAohormITNMc= -k8s.io/kubectl v0.35.4/go.mod h1:CGWAaof9ae4vGDAyhnSf1bSQN/U7jiWQHLVbMbLMjRI= -k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= -k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +k8s.io/api v0.36.0 h1:SgqDhZzHdOtMk40xVSvCXkP9ME0H05hPM3p9AB1kL80= +k8s.io/api v0.36.0/go.mod h1:m1LVrGPNYax5NBHdO+QuAedXyuzTt4RryI/qnmNvs34= +k8s.io/apiextensions-apiserver v0.36.0 h1:Wt7E8J+VBCbj4FjiBfDTK/neXDDjyJVJc7xfuOHImZ0= +k8s.io/apiextensions-apiserver v0.36.0/go.mod h1:kGDjH0msuiIB3tgsYRV0kS9GqpMYMUsQ3GHv7TApyug= +k8s.io/apimachinery v0.36.0 h1:jZyPzhd5Z+3h9vJLt0z9XdzW9VzNzWAUw+P1xZ9PXtQ= +k8s.io/apimachinery v0.36.0/go.mod h1:FklypaRJt6n5wUIwWXIP6GJlIpUizTgfo1T/As+Tyxc= +k8s.io/apiserver v0.36.0 h1:Jg5OFAENUACByUCg15CmhZAYrr5ZyJ+jodyA1mHl3YE= +k8s.io/apiserver v0.36.0/go.mod h1:mHvwdHf+qKEm+1/hYm756SV+oREOKSPnsjagOpx6Vho= +k8s.io/cli-runtime v0.36.0 h1:HNxciQpQMMOKS0/GiUXcKDyA6J2FDILJj9NmP2BZrTg= +k8s.io/cli-runtime v0.36.0/go.mod h1:KObkknK9Ro5LYX+1RdiKc7C8CvGg4aX+V/Zv+E8WPHA= +k8s.io/client-go v0.36.0 h1:pOYi7C4RHChYjMiHpZSpSbIM6ZxVbRXBy7CuiIwqA3c= +k8s.io/client-go v0.36.0/go.mod h1:ZKKcpwF0aLYfkHFCjillCKaTK/yBkEDHTDXCFY6AS9Y= +k8s.io/component-base v0.36.0 h1:hFjEktssxiJhrK1zfybkH4kJOi8iZuF+mIDCqS5+jRo= +k8s.io/component-base v0.36.0/go.mod h1:JZvIfcNHk+uck+8LhJzhSBtydWXaZNQwX2OdL+Mnwsk= +k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc= +k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0= +k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a h1:xCeOEAOoGYl2jnJoHkC3hkbPJgdATINPMAxaynU2Ovg= +k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a/go.mod h1:uGBT7iTA6c6MvqUvSXIaYZo9ukscABYi2btjhvgKGZ0= +k8s.io/kubectl v0.36.0 h1:hEGr8NvIm2Wjqs2Xy48Uzmvo6lpHdGKlLyMvau2gTms= +k8s.io/kubectl v0.36.0/go.mod h1:iDe8aV5BEi45W8k+5n71I2pJ/nwE0PHDu+/2cejzYoo= +k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 h1:AZYQSJemyQB5eRxqcPky+/7EdBj0xi3g0ZcxxJ7vbWU= +k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk= oras.land/oras-go/v2 v2.6.0 h1:X4ELRsiGkrbeox69+9tzTu492FMUu7zJQW6eJU+I2oc= oras.land/oras-go/v2 v2.6.0/go.mod h1:magiQDfG6H1O9APp+rOsvCPcW1GD2MM7vgnKY0Y+u1o= -sigs.k8s.io/controller-runtime v0.23.3 h1:VjB/vhoPoA9l1kEKZHBMnQF33tdCLQKJtydy4iqwZ80= -sigs.k8s.io/controller-runtime v0.23.3/go.mod h1:B6COOxKptp+YaUT5q4l6LqUJTRpizbgf9KSRNdQGns0= +sigs.k8s.io/controller-runtime v0.24.0 h1:Ck6N2LdS8Lovy1o25BB4r1xjvLEKUl1s2o9kU+KWDE4= +sigs.k8s.io/controller-runtime v0.24.0/go.mod h1:vFkfY5fGt5xAC/sKb8IBFKgWPNKG9OUG29dR8Y2wImw= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/kustomize/api v0.21.1 h1:lzqbzvz2CSvsjIUZUBNFKtIMsEw7hVLJp0JeSIVmuJs= @@ -522,7 +519,7 @@ sigs.k8s.io/kustomize/kyaml v0.21.1 h1:IVlbmhC076nf6foyL6Taw4BkrLuEsXUXNpsE+ScX7 sigs.k8s.io/kustomize/kyaml v0.21.1/go.mod h1:hmxADesM3yUN2vbA5z1/YTBnzLJ1dajdqpQonwBL1FQ= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 h1:2WOzJpHUBVrrkDjU4KBT8n5LDcj824eX0I5UKcgeRUs= -sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/structured-merge-diff/v6 v6.3.2 h1:kwVWMx5yS1CrnFWA/2QHyRVJ8jM6dBA80uLmm0wJkk8= +sigs.k8s.io/structured-merge-diff/v6 v6.3.2/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/internal/chart/v3/chart_test.go b/internal/chart/v3/chart_test.go index 07cbf4b39..5f6ca548d 100644 --- a/internal/chart/v3/chart_test.go +++ b/internal/chart/v3/chart_test.go @@ -124,8 +124,8 @@ func TestIsRoot(t *testing.T) { is := assert.New(t) - is.Equal(false, chrt1.IsRoot()) - is.Equal(true, chrt2.IsRoot()) + is.False(chrt1.IsRoot()) + is.True(chrt2.IsRoot()) } func TestChartPath(t *testing.T) { diff --git a/internal/chart/v3/lint/rules/template.go b/internal/chart/v3/lint/rules/template.go index 35e4940ab..a8ae910eb 100644 --- a/internal/chart/v3/lint/rules/template.go +++ b/internal/chart/v3/lint/rules/template.go @@ -28,8 +28,8 @@ import ( "slices" "strings" + "k8s.io/apimachinery/pkg/api/validate/content" "k8s.io/apimachinery/pkg/api/validation" - apipath "k8s.io/apimachinery/pkg/api/validation/path" "k8s.io/apimachinery/pkg/util/validation/field" "k8s.io/apimachinery/pkg/util/yaml" @@ -292,7 +292,7 @@ func validateMetadataNameFunc(obj *k8sYamlStruct) validation.ValidateNameFunc { case "role", "clusterrole", "rolebinding", "clusterrolebinding": // https://github.com/kubernetes/kubernetes/blob/v1.20.0/pkg/apis/rbac/validation/validation.go#L32-L34 return func(name string, _ bool) []string { - return apipath.IsValidPathSegmentName(name) + return content.IsPathSegmentName(name) } default: return validation.NameIsDNSSubdomain diff --git a/internal/version/version.go b/internal/version/version.go index 3daf80893..007f79f16 100644 --- a/internal/version/version.go +++ b/internal/version/version.go @@ -34,7 +34,7 @@ var ( // // Increment major number for new feature additions and behavioral changes. // Increment minor number for bug fixes and performance enhancements. - version = "v4.1" + version = "v4.2" // metadata is extra build time data metadata = "" diff --git a/pkg/action/action.go b/pkg/action/action.go index c93950103..8c1888144 100644 --- a/pkg/action/action.go +++ b/pkg/action/action.go @@ -88,6 +88,33 @@ const ( DryRunServer DryRunStrategy = "server" ) +// PostRenderStrategy determines how hooks and regular templates are passed +// to the configured post-renderer. +type PostRenderStrategy string + +const ( + // PostRenderStrategyCombined sends hooks and regular templates together + // as a single stream to the post-renderer. This is the default in Helm 4. + PostRenderStrategyCombined PostRenderStrategy = "combined" + + // PostRenderStrategySeparate sends hooks and regular templates to the + // post-renderer in independent invocations. This avoids duplicate-resource + // errors from post-renderers that de-duplicate by resource identity + // (for example Kustomize) when the same resource appears in both a hook + // and a regular template. Passing hooks to post-renderers was introduced + // in Helm 4; Helm 3 never did so, which is why the issue only surfaces + // with the Helm 4 combined default. + PostRenderStrategySeparate PostRenderStrategy = "separate" + + // PostRenderStrategyNoHooks sends only regular templates to the + // post-renderer and leaves hooks untouched. This matches the Helm 3 + // behavior and is useful for post-renderers that declare transforms + // targeting template-only resources (for example Kustomize patches + // against a Deployment that exists in templates but not in hooks), + // which would otherwise fail against the hook stream. + PostRenderStrategyNoHooks PostRenderStrategy = "nohooks" +) + // Configuration injects the dependencies that all actions share. type Configuration struct { // RESTClientGetter is an interface that loads Kubernetes clients. @@ -198,7 +225,14 @@ func annotateAndMerge(files map[string]string) (string, error) { // splitAndDeannotate reconstructs individual files from a merged YAML stream, // removing filename annotations and grouping documents by their original filenames. -func splitAndDeannotate(postrendered string) (map[string]string, error) { +// Documents without a filename annotation are assigned a synthesized name of the +// form "generated-by-postrender--.yaml" (or +// "generated-by-postrender-.yaml" when fallbackPrefix is empty). The prefix +// disambiguates fallback filenames across multiple post-render invocations (for +// example when PostRenderStrategySeparate runs the post-renderer once per +// group), so that merging results from different invocations does not collide +// on the same synthetic key. +func splitAndDeannotate(postrendered, fallbackPrefix string) (map[string]string, error) { manifests, err := kio.ParseAll(postrendered) if err != nil { return nil, fmt.Errorf("error parsing YAML: %w", err) @@ -212,7 +246,11 @@ func splitAndDeannotate(postrendered string) (map[string]string, error) { } fname := meta.Annotations[filenameAnnotation] if fname == "" { - fname = fmt.Sprintf("generated-by-postrender-%d.yaml", i) + if fallbackPrefix == "" { + fname = fmt.Sprintf("generated-by-postrender-%d.yaml", i) + } else { + fname = fmt.Sprintf("generated-by-postrender-%s-%d.yaml", fallbackPrefix, i) + } } if err := manifest.PipeE(kyaml.ClearAnnotation(filenameAnnotation)); err != nil { return nil, fmt.Errorf("clearing filename annotation: %w", err) @@ -237,7 +275,7 @@ func splitAndDeannotate(postrendered string) (map[string]string, error) { // TODO: As part of the refactor the duplicate code in cmd/helm/template.go should be removed // // This code has to do with writing files to disk. -func (cfg *Configuration) renderResources(ch *chart.Chart, values common.Values, releaseName, outputDir string, subNotes, useReleaseName, includeCrds bool, pr postrenderer.PostRenderer, interactWithRemote, enableDNS, hideSecret bool) ([]*release.Hook, *bytes.Buffer, string, error) { +func (cfg *Configuration) renderResources(ch *chart.Chart, values common.Values, releaseName, outputDir string, subNotes, useReleaseName, includeCrds bool, pr postrenderer.PostRenderer, interactWithRemote, enableDNS, hideSecret bool, postRenderStrategy PostRenderStrategy) ([]*release.Hook, *bytes.Buffer, string, error) { var hs []*release.Hook b := bytes.NewBuffer(nil) @@ -301,29 +339,122 @@ func (cfg *Configuration) renderResources(ch *chart.Chart, values common.Values, notes := notesBuffer.String() if pr != nil { - // We need to send files to the post-renderer before sorting and splitting - // hooks from manifests. The post-renderer interface expects a stream of - // manifests (similar to what tools like Kustomize and kubectl expect), whereas - // the sorter uses filenames. - // Here, we merge the documents into a stream, post-render them, and then split - // them back into a map of filename -> content. - - // Merge files as stream of documents for sending to post renderer - merged, err := annotateAndMerge(files) - if err != nil { - return hs, b, notes, fmt.Errorf("error merging manifests: %w", err) - } + switch postRenderStrategy { + case PostRenderStrategySeparate, PostRenderStrategyNoHooks: + // Split hooks from manifests before post-rendering. For "separate", + // hooks and templates are sent to the post-renderer as independent + // streams to avoid duplicate-resource errors when the same resource + // appears in both (e.g. a ServiceAccount used by a pre-install hook + // that is also declared in the chart's regular templates). For + // "nohooks", hooks skip the post-renderer entirely, matching the + // Helm 3 behavior. + sortedHooks, sortedManifests, err := releaseutil.SortManifests(files, nil, releaseutil.InstallOrder) + if err != nil { + for name, content := range files { + if strings.TrimSpace(content) == "" { + continue + } + fmt.Fprintf(b, "---\n# Source: %s\n%s\n", name, content) + } + return hs, b, "", err + } - // Run the post renderer - postRendered, err := pr.Run(bytes.NewBufferString(merged)) - if err != nil { - return hs, b, notes, fmt.Errorf("error while running post render on files: %w", err) - } + // Build separate files maps for hooks and manifests. + hookFiles := make(map[string]string) + for _, h := range sortedHooks { + if existing, ok := hookFiles[h.Path]; ok { + hookFiles[h.Path] = existing + "\n---\n" + h.Manifest + } else { + hookFiles[h.Path] = h.Manifest + } + } + manifestFiles := make(map[string]string) + for _, m := range sortedManifests { + if existing, ok := manifestFiles[m.Name]; ok { + manifestFiles[m.Name] = existing + "\n---\n" + m.Content + } else { + manifestFiles[m.Name] = m.Content + } + } - // Use the file list and contents received from the post renderer - files, err = splitAndDeannotate(postRendered.String()) - if err != nil { - return hs, b, notes, fmt.Errorf("error while parsing post rendered output: %w", err) + // Decide which groups to post-render. "nohooks" passes hooks + // through untouched and only post-renders manifests. + groups := []struct { + name string + files map[string]string + postRender bool + }{ + {"hooks", hookFiles, postRenderStrategy == PostRenderStrategySeparate}, + {"manifests", manifestFiles, true}, + } + + files = make(map[string]string) + for _, group := range groups { + if len(group.files) == 0 { + continue + } + + if !group.postRender { + for k, v := range group.files { + if existing, ok := files[k]; ok { + files[k] = existing + "\n---\n" + v + } else { + files[k] = v + } + } + continue + } + + merged, err := annotateAndMerge(group.files) + if err != nil { + return hs, b, notes, fmt.Errorf("error merging %s: %w", group.name, err) + } + + postRendered, err := pr.Run(bytes.NewBufferString(merged)) + if err != nil { + return hs, b, notes, fmt.Errorf("error while running post render on %s: %w", group.name, err) + } + + rendered, err := splitAndDeannotate(postRendered.String(), group.name) + if err != nil { + return hs, b, notes, fmt.Errorf("error while parsing post rendered output for %s: %w", group.name, err) + } + + for k, v := range rendered { + if existing, ok := files[k]; ok { + files[k] = existing + "\n---\n" + v + } else { + files[k] = v + } + } + } + case PostRenderStrategyCombined, "": + // We need to send files to the post-renderer before sorting and splitting + // hooks from manifests. The post-renderer interface expects a stream of + // manifests (similar to what tools like Kustomize and kubectl expect), whereas + // the sorter uses filenames. + // Here, we merge the documents into a stream, post-render them, and then split + // them back into a map of filename -> content. + + // Merge files as stream of documents for sending to post renderer + merged, err := annotateAndMerge(files) + if err != nil { + return hs, b, notes, fmt.Errorf("error merging manifests: %w", err) + } + + // Run the post renderer + postRendered, err := pr.Run(bytes.NewBufferString(merged)) + if err != nil { + return hs, b, notes, fmt.Errorf("error while running post render on files: %w", err) + } + + // Use the file list and contents received from the post renderer + files, err = splitAndDeannotate(postRendered.String(), "") + if err != nil { + return hs, b, notes, fmt.Errorf("error while parsing post rendered output: %w", err) + } + default: + return hs, b, notes, fmt.Errorf("unknown post-render strategy: '%s'", postRenderStrategy) } } diff --git a/pkg/action/action_test.go b/pkg/action/action_test.go index d6575a791..54b07273b 100644 --- a/pkg/action/action_test.go +++ b/pkg/action/action_test.go @@ -1722,7 +1722,7 @@ metadata: data: key: value`, expectedFiles: map[string]string{ - "generated-by-postrender-0.yaml": `apiVersion: v1 + "generated-by-postrender-test-0.yaml": `apiVersion: v1 kind: ConfigMap metadata: name: test-cm @@ -1735,7 +1735,7 @@ data: for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - files, err := splitAndDeannotate(tt.input) + files, err := splitAndDeannotate(tt.input, "test") if tt.expectedError != "" { assert.Error(t, err) @@ -1789,7 +1789,7 @@ data: require.NoError(t, err) // Split and deannotate - reconstructed, err := splitAndDeannotate(merged) + reconstructed, err := splitAndDeannotate(merged, "test") require.NoError(t, err) // Compare the results @@ -1824,7 +1824,7 @@ func TestRenderResources_PostRenderer_Success(t *testing.T) { hooks, buf, notes, err := cfg.renderResources( ch, values, "test-release", "", false, false, false, - mockPR, false, false, false, + mockPR, false, false, false, PostRenderStrategyCombined, ) assert.NoError(t, err) @@ -1871,7 +1871,7 @@ func TestRenderResources_PostRenderer_Error(t *testing.T) { _, _, _, err := cfg.renderResources( ch, values, "test-release", "", false, false, false, - mockPR, false, false, false, + mockPR, false, false, false, PostRenderStrategyCombined, ) assert.Error(t, err) @@ -1899,7 +1899,7 @@ func TestRenderResources_PostRenderer_MergeError(t *testing.T) { _, _, _, err := cfg.renderResources( ch, values, "test-release", "", false, false, false, - mockPR, false, false, false, + mockPR, false, false, false, PostRenderStrategyCombined, ) assert.Error(t, err) @@ -1921,7 +1921,7 @@ func TestRenderResources_PostRenderer_SplitError(t *testing.T) { _, _, _, err := cfg.renderResources( ch, values, "test-release", "", false, false, false, - mockPR, false, false, false, + mockPR, false, false, false, PostRenderStrategyCombined, ) assert.Error(t, err) @@ -1942,7 +1942,7 @@ func TestRenderResources_PostRenderer_Integration(t *testing.T) { hooks, buf, notes, err := cfg.renderResources( ch, values, "test-release", "", false, false, false, - mockPR, false, false, false, + mockPR, false, false, false, PostRenderStrategyCombined, ) assert.NoError(t, err) @@ -1981,7 +1981,7 @@ func TestRenderResources_NoPostRenderer(t *testing.T) { hooks, buf, notes, err := cfg.renderResources( ch, values, "test-release", "", false, false, false, - nil, false, false, false, + nil, false, false, false, PostRenderStrategyCombined, ) assert.NoError(t, err) @@ -1990,6 +1990,305 @@ func TestRenderResources_NoPostRenderer(t *testing.T) { assert.Equal(t, "", notes) } +func TestRenderResources_PostRenderer_DuplicateResourceInHookAndTemplate(t *testing.T) { + cfg := actionConfigFixture(t) + + // Simulate a chart where the same ServiceAccount appears both as a + // pre-install hook and as a regular template. This is a valid Helm pattern + // but previously caused post-renderers like Kustomize to fail with + // "may not add resource with an already registered id" because hooks and + // templates were merged into a single stream before post-rendering. + saHook := `apiVersion: v1 +kind: ServiceAccount +metadata: + name: my-app + annotations: + "helm.sh/hook": pre-install + "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded` + + saTemplate := `apiVersion: v1 +kind: ServiceAccount +metadata: + name: my-app` + + deployment := `apiVersion: apps/v1 +kind: Deployment +metadata: + name: my-app +spec: + template: + spec: + serviceAccountName: my-app` + + modTime := time.Now() + ch := buildChartWithTemplates([]*common.File{ + {Name: "templates/sa-hook.yaml", ModTime: modTime, Data: []byte(saHook)}, + {Name: "templates/sa.yaml", ModTime: modTime, Data: []byte(saTemplate)}, + {Name: "templates/deployment.yaml", ModTime: modTime, Data: []byte(deployment)}, + }) + + // Use a post-renderer that rejects duplicate resource IDs, similar to + // how Kustomize behaves. We verify that no single post-render call + // receives the ServiceAccount twice. + mockPR := &mockPostRenderer{ + transform: func(content string) string { + count := strings.Count(content, "kind: ServiceAccount") + if count > 1 { + t.Errorf("post-renderer received %d ServiceAccount resources in a single stream, expected at most 1", count) + } + return content + }, + } + + hooks, buf, _, err := cfg.renderResources( + ch, nil, "test-release", "", false, false, false, + mockPR, false, false, false, PostRenderStrategySeparate, + ) + + assert.NoError(t, err) + assert.Len(t, hooks, 1) + assert.Equal(t, "my-app", hooks[0].Name) + assert.Contains(t, buf.String(), "kind: Deployment") + assert.Contains(t, buf.String(), "kind: ServiceAccount") +} + +func TestRenderResources_PostRenderer_CombinedInvokesOnceWithEverything(t *testing.T) { + cfg := actionConfigFixture(t) + + hookManifest := `apiVersion: v1 +kind: ConfigMap +metadata: + name: hook-cm + annotations: + "helm.sh/hook": pre-install` + templateManifest := `apiVersion: v1 +kind: ConfigMap +metadata: + name: template-cm` + + modTime := time.Now() + ch := buildChartWithTemplates([]*common.File{ + {Name: "templates/hook.yaml", ModTime: modTime, Data: []byte(hookManifest)}, + {Name: "templates/cm.yaml", ModTime: modTime, Data: []byte(templateManifest)}, + }) + + var calls int + var lastInput string + mockPR := &mockPostRenderer{ + transform: func(content string) string { + calls++ + lastInput = content + return content + }, + } + + _, _, _, err := cfg.renderResources( + ch, nil, "test-release", "", false, false, false, + mockPR, false, false, false, PostRenderStrategyCombined, + ) + + assert.NoError(t, err) + assert.Equal(t, 1, calls, "combined strategy should invoke the post-renderer exactly once") + assert.Contains(t, lastInput, "hook-cm") + assert.Contains(t, lastInput, "template-cm") +} + +func TestRenderResources_PostRenderer_ZeroValueStrategyActsAsCombined(t *testing.T) { + cfg := actionConfigFixture(t) + + modTime := time.Now() + ch := buildChartWithTemplates([]*common.File{ + {Name: "templates/cm.yaml", ModTime: modTime, Data: []byte(`apiVersion: v1 +kind: ConfigMap +metadata: + name: template-cm`)}, + {Name: "templates/hook.yaml", ModTime: modTime, Data: []byte(`apiVersion: v1 +kind: ConfigMap +metadata: + name: hook-cm + annotations: + "helm.sh/hook": pre-install`)}, + }) + + var calls int + mockPR := &mockPostRenderer{ + transform: func(content string) string { + calls++ + return content + }, + } + + _, _, _, err := cfg.renderResources( + ch, nil, "test-release", "", false, false, false, + mockPR, false, false, false, PostRenderStrategy(""), + ) + + assert.NoError(t, err) + assert.Equal(t, 1, calls, "unset strategy must preserve backwards-compatible combined behavior") +} + +func TestRenderResources_PostRenderer_SeparateSplitsHooksAndTemplates(t *testing.T) { + cfg := actionConfigFixture(t) + + modTime := time.Now() + ch := buildChartWithTemplates([]*common.File{ + {Name: "templates/hook.yaml", ModTime: modTime, Data: []byte(`apiVersion: v1 +kind: ConfigMap +metadata: + name: hook-cm + annotations: + "helm.sh/hook": pre-install`)}, + {Name: "templates/cm.yaml", ModTime: modTime, Data: []byte(`apiVersion: v1 +kind: ConfigMap +metadata: + name: template-cm`)}, + }) + + var inputs []string + mockPR := &mockPostRenderer{ + transform: func(content string) string { + inputs = append(inputs, content) + return content + }, + } + + _, _, _, err := cfg.renderResources( + ch, nil, "test-release", "", false, false, false, + mockPR, false, false, false, PostRenderStrategySeparate, + ) + + assert.NoError(t, err) + assert.Len(t, inputs, 2, "separate strategy should invoke the post-renderer twice when both hooks and templates exist") + for _, in := range inputs { + hasHook := strings.Contains(in, "hook-cm") + hasTemplate := strings.Contains(in, "template-cm") + assert.False(t, hasHook && hasTemplate, "a single post-render invocation must not contain both hook and template resources") + assert.True(t, hasHook || hasTemplate, "each post-render invocation must contain either a hook or a template") + } +} + +func TestRenderResources_PostRenderer_SeparateWithOnlyTemplates(t *testing.T) { + cfg := actionConfigFixture(t) + + modTime := time.Now() + ch := buildChartWithTemplates([]*common.File{ + {Name: "templates/cm.yaml", ModTime: modTime, Data: []byte(`apiVersion: v1 +kind: ConfigMap +metadata: + name: template-cm`)}, + }) + + var calls int + mockPR := &mockPostRenderer{ + transform: func(content string) string { + calls++ + return content + }, + } + + _, _, _, err := cfg.renderResources( + ch, nil, "test-release", "", false, false, false, + mockPR, false, false, false, PostRenderStrategySeparate, + ) + + assert.NoError(t, err) + assert.Equal(t, 1, calls, "separate strategy should skip the empty hook group and invoke the post-renderer only once") +} + +func TestRenderResources_PostRenderer_NoHooksSkipsHooks(t *testing.T) { + cfg := actionConfigFixture(t) + + modTime := time.Now() + ch := buildChartWithTemplates([]*common.File{ + {Name: "templates/hook.yaml", ModTime: modTime, Data: []byte(`apiVersion: v1 +kind: ConfigMap +metadata: + name: hook-cm + annotations: + "helm.sh/hook": pre-install`)}, + {Name: "templates/cm.yaml", ModTime: modTime, Data: []byte(`apiVersion: v1 +kind: ConfigMap +metadata: + name: template-cm`)}, + }) + + var inputs []string + mockPR := &mockPostRenderer{ + transform: func(content string) string { + inputs = append(inputs, content) + return content + }, + } + + hooks, manifestDoc, _, err := cfg.renderResources( + ch, nil, "test-release", "", false, false, false, + mockPR, false, false, false, PostRenderStrategyNoHooks, + ) + + assert.NoError(t, err) + assert.Len(t, inputs, 1, "nohooks strategy should invoke the post-renderer exactly once (for templates only)") + assert.NotContains(t, inputs[0], "hook-cm", "hooks must not be sent to the post-renderer") + assert.Contains(t, inputs[0], "template-cm", "templates must be sent to the post-renderer") + + // Hooks still round-trip through the release so they can execute. + require.Len(t, hooks, 1) + assert.Contains(t, hooks[0].Manifest, "hook-cm") + assert.Contains(t, manifestDoc.String(), "template-cm") +} + +func TestRenderResources_PostRenderer_NoHooksWithOnlyHooks(t *testing.T) { + cfg := actionConfigFixture(t) + + modTime := time.Now() + ch := buildChartWithTemplates([]*common.File{ + {Name: "templates/hook.yaml", ModTime: modTime, Data: []byte(`apiVersion: v1 +kind: ConfigMap +metadata: + name: hook-cm + annotations: + "helm.sh/hook": pre-install`)}, + }) + + var calls int + mockPR := &mockPostRenderer{ + transform: func(content string) string { + calls++ + return content + }, + } + + _, _, _, err := cfg.renderResources( + ch, nil, "test-release", "", false, false, false, + mockPR, false, false, false, PostRenderStrategyNoHooks, + ) + + assert.NoError(t, err) + assert.Equal(t, 0, calls, "nohooks strategy should not invoke the post-renderer when the chart only has hooks") +} + +func TestRenderResources_PostRenderer_UnknownStrategyErrors(t *testing.T) { + cfg := actionConfigFixture(t) + + modTime := time.Now() + ch := buildChartWithTemplates([]*common.File{ + {Name: "templates/cm.yaml", ModTime: modTime, Data: []byte(`apiVersion: v1 +kind: ConfigMap +metadata: + name: template-cm`)}, + }) + + mockPR := &mockPostRenderer{} + + _, _, _, err := cfg.renderResources( + ch, nil, "test-release", "", false, false, false, + mockPR, false, false, false, PostRenderStrategy("bogus"), + ) + + assert.Error(t, err) + assert.Contains(t, err.Error(), "unknown post-render strategy") + assert.Contains(t, err.Error(), "bogus") +} + func TestDetermineReleaseSSAApplyMethod(t *testing.T) { assert.Equal(t, release.ApplyMethodClientSideApply, determineReleaseSSApplyMethod(false)) assert.Equal(t, release.ApplyMethodServerSideApply, determineReleaseSSApplyMethod(true)) diff --git a/pkg/action/get_values_test.go b/pkg/action/get_values_test.go index 01ee4c3f1..c4cbdf2d5 100644 --- a/pkg/action/get_values_test.go +++ b/pkg/action/get_values_test.go @@ -37,7 +37,7 @@ func TestNewGetValues(t *testing.T) { assert.NotNil(t, client) assert.Equal(t, cfg, client.cfg) assert.Equal(t, 0, client.Version) - assert.Equal(t, false, client.AllValues) + assert.False(t, client.AllValues) } func TestGetValues_Run_UserConfigOnly(t *testing.T) { diff --git a/pkg/action/install.go b/pkg/action/install.go index 50df13c05..580b8a0cb 100644 --- a/pkg/action/install.go +++ b/pkg/action/install.go @@ -130,6 +130,10 @@ type Install struct { // TakeOwnership will ignore the check for helm annotations and take ownership of the resources. TakeOwnership bool PostRenderer postrenderer.PostRenderer + // PostRenderStrategy controls how hooks and regular templates are passed + // to the configured post-renderer. See PostRenderStrategy for the + // available modes. Defaults to PostRenderStrategyCombined. + PostRenderStrategy PostRenderStrategy // Lock to control raceconditions when the process receives a SIGTERM Lock sync.Mutex goroutineCount atomic.Int32 @@ -158,9 +162,10 @@ type ChartPathOptions struct { // NewInstall creates a new Install object with the given configuration. func NewInstall(cfg *Configuration) *Install { in := &Install{ - cfg: cfg, - ServerSideApply: true, // Must always match the CLI default. - DryRunStrategy: DryRunNone, + cfg: cfg, + ServerSideApply: true, // Must always match the CLI default. + DryRunStrategy: DryRunNone, + PostRenderStrategy: PostRenderStrategyCombined, } in.registryClient = cfg.RegistryClient @@ -370,7 +375,7 @@ func (i *Install) RunWithContext(ctx context.Context, ch ci.Charter, vals map[st rel := i.createRelease(chrt, vals, i.Labels) var manifestDoc *bytes.Buffer - rel.Hooks, manifestDoc, rel.Info.Notes, err = i.cfg.renderResources(chrt, valuesToRender, i.ReleaseName, i.OutputDir, i.SubNotes, i.UseReleaseName, i.IncludeCRDs, i.PostRenderer, interactWithServer(i.DryRunStrategy), i.EnableDNS, i.HideSecret) + rel.Hooks, manifestDoc, rel.Info.Notes, err = i.cfg.renderResources(chrt, valuesToRender, i.ReleaseName, i.OutputDir, i.SubNotes, i.UseReleaseName, i.IncludeCRDs, i.PostRenderer, interactWithServer(i.DryRunStrategy), i.EnableDNS, i.HideSecret, i.PostRenderStrategy) // Even for errors, attach this if available if manifestDoc != nil { rel.Manifest = manifestDoc.String() diff --git a/pkg/action/push_test.go b/pkg/action/push_test.go index 35c6f3efc..125799252 100644 --- a/pkg/action/push_test.go +++ b/pkg/action/push_test.go @@ -47,14 +47,14 @@ func TestNewPushWithInsecureSkipTLSVerify(t *testing.T) { client := NewPushWithOpts(WithInsecureSkipTLSVerify(true)) assert.NotNil(t, client) - assert.Equal(t, true, client.insecureSkipTLSVerify) + assert.True(t, client.insecureSkipTLSVerify) } func TestNewPushWithPlainHTTP(t *testing.T) { client := NewPushWithOpts(WithPlainHTTP(true)) assert.NotNil(t, client) - assert.Equal(t, true, client.plainHTTP) + assert.True(t, client.plainHTTP) } func TestNewPushWithPushOptWriter(t *testing.T) { diff --git a/pkg/action/registry_login_test.go b/pkg/action/registry_login_test.go index de2450d9d..590673b3a 100644 --- a/pkg/action/registry_login_test.go +++ b/pkg/action/registry_login_test.go @@ -48,7 +48,7 @@ func TestWithInsecure(t *testing.T) { opt := WithInsecure(true) assert.Nil(t, opt(client)) - assert.Equal(t, true, client.insecure) + assert.True(t, client.insecure) } func TestWithKeyFile(t *testing.T) { @@ -80,5 +80,5 @@ func TestWithPlainHTTPLogin(t *testing.T) { opt := WithPlainHTTPLogin(true) assert.Nil(t, opt(client)) - assert.Equal(t, true, client.plainHTTP) + assert.True(t, client.plainHTTP) } diff --git a/pkg/action/upgrade.go b/pkg/action/upgrade.go index 103ab4fdb..00939ffa6 100644 --- a/pkg/action/upgrade.go +++ b/pkg/action/upgrade.go @@ -121,6 +121,10 @@ type Upgrade struct { // If this is non-nil, then after templates are rendered, they will be sent to the // post renderer before sending to the Kubernetes API server. PostRenderer postrenderer.PostRenderer + // PostRenderStrategy controls how hooks and regular templates are passed + // to the configured post-renderer. See PostRenderStrategy for the + // available modes. Defaults to PostRenderStrategyCombined. + PostRenderStrategy PostRenderStrategy // DisableOpenAPIValidation controls whether OpenAPI validation is enforced. DisableOpenAPIValidation bool // Get missing dependencies @@ -141,9 +145,10 @@ type resultMessage struct { // NewUpgrade creates a new Upgrade object with the given configuration. func NewUpgrade(cfg *Configuration) *Upgrade { up := &Upgrade{ - cfg: cfg, - ServerSideApply: "auto", // Must always match the CLI default. - DryRunStrategy: DryRunNone, + cfg: cfg, + ServerSideApply: "auto", // Must always match the CLI default. + DryRunStrategy: DryRunNone, + PostRenderStrategy: PostRenderStrategyCombined, } up.registryClient = cfg.RegistryClient @@ -296,7 +301,7 @@ func (u *Upgrade) prepareUpgrade(name string, chart *chartv2.Chart, vals map[str return nil, nil, false, err } - hooks, manifestDoc, notesTxt, err := u.cfg.renderResources(chart, valuesToRender, "", "", u.SubNotes, false, false, u.PostRenderer, interactWithServer(u.DryRunStrategy), u.EnableDNS, u.HideSecret) + hooks, manifestDoc, notesTxt, err := u.cfg.renderResources(chart, valuesToRender, "", "", u.SubNotes, false, false, u.PostRenderer, interactWithServer(u.DryRunStrategy), u.EnableDNS, u.HideSecret, u.PostRenderStrategy) if err != nil { return nil, nil, false, err } diff --git a/pkg/chart/common/capabilities_test.go b/pkg/chart/common/capabilities_test.go index c9f026cbb..c8d47581c 100644 --- a/pkg/chart/common/capabilities_test.go +++ b/pkg/chart/common/capabilities_test.go @@ -60,8 +60,8 @@ func TestDefaultCapabilities(t *testing.T) { } hv := caps.HelmVersion - if hv.Version != "v4.1" { - t.Errorf("Expected default HelmVersion to be v4.1, got %q", hv.Version) + if hv.Version != "v4.2" { + t.Errorf("Expected default HelmVersion to be v4.2, got %q", hv.Version) } } diff --git a/pkg/chart/v2/chart_test.go b/pkg/chart/v2/chart_test.go index d0837eb16..d44e7251b 100644 --- a/pkg/chart/v2/chart_test.go +++ b/pkg/chart/v2/chart_test.go @@ -124,8 +124,8 @@ func TestIsRoot(t *testing.T) { is := assert.New(t) - is.Equal(false, chrt1.IsRoot()) - is.Equal(true, chrt2.IsRoot()) + is.False(chrt1.IsRoot()) + is.True(chrt2.IsRoot()) } func TestChartPath(t *testing.T) { diff --git a/pkg/chart/v2/lint/rules/template.go b/pkg/chart/v2/lint/rules/template.go index 43665aa3a..94210dec8 100644 --- a/pkg/chart/v2/lint/rules/template.go +++ b/pkg/chart/v2/lint/rules/template.go @@ -28,8 +28,8 @@ import ( "slices" "strings" + "k8s.io/apimachinery/pkg/api/validate/content" "k8s.io/apimachinery/pkg/api/validation" - apipath "k8s.io/apimachinery/pkg/api/validation/path" "k8s.io/apimachinery/pkg/util/validation/field" "k8s.io/apimachinery/pkg/util/yaml" @@ -323,7 +323,7 @@ func validateMetadataNameFunc(obj *k8sYamlStruct) validation.ValidateNameFunc { case "role", "clusterrole", "rolebinding", "clusterrolebinding": // https://github.com/kubernetes/kubernetes/blob/v1.20.0/pkg/apis/rbac/validation/validation.go#L32-L34 return func(name string, _ bool) []string { - return apipath.IsValidPathSegmentName(name) + return content.IsPathSegmentName(name) } default: return validation.NameIsDNSSubdomain diff --git a/pkg/cli/environment.go b/pkg/cli/environment.go index 83b6bdbba..45d773eb4 100644 --- a/pkg/cli/environment.go +++ b/pkg/cli/environment.go @@ -35,7 +35,7 @@ import ( "helm.sh/helm/v4/internal/version" "helm.sh/helm/v4/pkg/helmpath" - "helm.sh/helm/v4/pkg/kube" + "helm.sh/helm/v4/pkg/kubeenv" ) // defaultMaxHistory sets the maximum number of releases to 0: unlimited @@ -134,7 +134,7 @@ func New() *EnvSettings { config.Burst = env.BurstLimit config.QPS = env.QPS config.Wrap(func(rt http.RoundTripper) http.RoundTripper { - return &kube.RetryingRoundTripper{Wrapped: rt} + return &kubeenv.RetryingRoundTripper{Wrapped: rt} }) config.UserAgent = version.GetUserAgent() return config diff --git a/pkg/cmd/install.go b/pkg/cmd/install.go index 3030bc6f9..67e2a9fab 100644 --- a/pkg/cmd/install.go +++ b/pkg/cmd/install.go @@ -210,6 +210,26 @@ func addInstallFlags(cmd *cobra.Command, f *pflag.FlagSet, client *action.Instal f.BoolVar(&client.EnableDNS, "enable-dns", false, "enable DNS lookups when rendering templates") f.BoolVar(&client.HideNotes, "hide-notes", false, "if set, do not show notes in install output. Does not affect presence in chart metadata") f.BoolVar(&client.TakeOwnership, "take-ownership", false, "if set, install will ignore the check for helm annotations and take ownership of the existing resources") + + // For `helm template`, these notes flags are legacy, unused, and should not show in help, but + // must remain accepted for backwards compatibility in Helm 4. Deprecate and hide them for now + // TODO remove these from template command in Helm 5 + if cmd.Name() == "template" { + if err := cmd.Flags().MarkDeprecated("hide-notes", "this flag has no effect for 'helm template' and will be removed in Helm 5"); err != nil { + log.Fatal(err) + } + if err := cmd.Flags().MarkHidden("hide-notes"); err != nil { + log.Fatal(err) + } + + if err := cmd.Flags().MarkDeprecated("render-subchart-notes", "this flag has no effect for 'helm template' and will be removed in Helm 5"); err != nil { + log.Fatal(err) + } + if err := cmd.Flags().MarkHidden("render-subchart-notes"); err != nil { + log.Fatal(err) + } + } + addValueOptionsFlags(f, valueOpts) addChartPathOptionsFlags(f, &client.ChartPathOptions) AddWaitFlag(cmd, &client.WaitStrategy) diff --git a/pkg/cmd/lint_test.go b/pkg/cmd/lint_test.go index a13ec423b..82fe249f6 100644 --- a/pkg/cmd/lint_test.go +++ b/pkg/cmd/lint_test.go @@ -84,7 +84,7 @@ func TestLintCmdWithKubeVersionFlag(t *testing.T) { wantError: false, }, { name: "lint chart with deprecated api version with older kube version", - cmd: "lint --kube-version 1.21.0 --strict " + testChart, + cmd: "lint --kube-version 1.20.0 --strict " + testChart, golden: "output/lint-chart-with-deprecated-api-old-k8s.txt", wantError: false, }} diff --git a/pkg/cmd/testdata/output/lint-chart-with-deprecated-api-strict.txt b/pkg/cmd/testdata/output/lint-chart-with-deprecated-api-strict.txt index a1ec4394e..c25efc1df 100644 --- a/pkg/cmd/testdata/output/lint-chart-with-deprecated-api-strict.txt +++ b/pkg/cmd/testdata/output/lint-chart-with-deprecated-api-strict.txt @@ -1,5 +1,5 @@ ==> Linting testdata/testcharts/chart-with-deprecated-api [INFO] Chart.yaml: icon is recommended -[WARNING] templates/horizontalpodautoscaler.yaml: autoscaling/v2beta1 HorizontalPodAutoscaler is deprecated in v1.22+, unavailable in v1.25+; use autoscaling/v2 HorizontalPodAutoscaler +[WARNING] templates/poddisruptionbudget.yaml: policy/v1beta1 PodDisruptionBudget is deprecated in v1.21+, unavailable in v1.25+; use policy/v1 PodDisruptionBudget Error: 1 chart(s) linted, 1 chart(s) failed diff --git a/pkg/cmd/testdata/output/lint-chart-with-deprecated-api.txt b/pkg/cmd/testdata/output/lint-chart-with-deprecated-api.txt index dac54620c..08dbde95d 100644 --- a/pkg/cmd/testdata/output/lint-chart-with-deprecated-api.txt +++ b/pkg/cmd/testdata/output/lint-chart-with-deprecated-api.txt @@ -1,5 +1,5 @@ ==> Linting testdata/testcharts/chart-with-deprecated-api [INFO] Chart.yaml: icon is recommended -[WARNING] templates/horizontalpodautoscaler.yaml: autoscaling/v2beta1 HorizontalPodAutoscaler is deprecated in v1.22+, unavailable in v1.25+; use autoscaling/v2 HorizontalPodAutoscaler +[WARNING] templates/poddisruptionbudget.yaml: policy/v1beta1 PodDisruptionBudget is deprecated in v1.21+, unavailable in v1.25+; use policy/v1 PodDisruptionBudget 1 chart(s) linted, 0 chart(s) failed diff --git a/pkg/cmd/testdata/output/version-short.txt b/pkg/cmd/testdata/output/version-short.txt index 8cf4318fb..2fa2c5705 100644 --- a/pkg/cmd/testdata/output/version-short.txt +++ b/pkg/cmd/testdata/output/version-short.txt @@ -1 +1 @@ -v4.1 +v4.2 diff --git a/pkg/cmd/testdata/output/version-template.txt b/pkg/cmd/testdata/output/version-template.txt index 8fd8b4962..8f2491e54 100644 --- a/pkg/cmd/testdata/output/version-template.txt +++ b/pkg/cmd/testdata/output/version-template.txt @@ -1 +1 @@ -Version: v4.1 \ No newline at end of file +Version: v4.2 \ No newline at end of file diff --git a/pkg/cmd/testdata/output/version.txt b/pkg/cmd/testdata/output/version.txt index 1f4cf4d4a..331c6d5e9 100644 --- a/pkg/cmd/testdata/output/version.txt +++ b/pkg/cmd/testdata/output/version.txt @@ -1 +1 @@ -version.BuildInfo{Version:"v4.1", GitCommit:"", GitTreeState:"", GoVersion:"", KubeClientVersion:"v1.20"} +version.BuildInfo{Version:"v4.2", GitCommit:"", GitTreeState:"", GoVersion:"", KubeClientVersion:"v1.20"} diff --git a/pkg/cmd/testdata/testcharts/chart-with-deprecated-api/templates/horizontalpodautoscaler.yaml b/pkg/cmd/testdata/testcharts/chart-with-deprecated-api/templates/horizontalpodautoscaler.yaml deleted file mode 100644 index b77a4beeb..000000000 --- a/pkg/cmd/testdata/testcharts/chart-with-deprecated-api/templates/horizontalpodautoscaler.yaml +++ /dev/null @@ -1,9 +0,0 @@ -apiVersion: autoscaling/v2beta1 -kind: HorizontalPodAutoscaler -metadata: - name: deprecated -spec: - scaleTargetRef: - kind: Pod - name: pod - maxReplicas: 3 \ No newline at end of file diff --git a/pkg/cmd/testdata/testcharts/chart-with-deprecated-api/templates/poddisruptionbudget.yaml b/pkg/cmd/testdata/testcharts/chart-with-deprecated-api/templates/poddisruptionbudget.yaml new file mode 100644 index 000000000..214d3cb68 --- /dev/null +++ b/pkg/cmd/testdata/testcharts/chart-with-deprecated-api/templates/poddisruptionbudget.yaml @@ -0,0 +1,9 @@ +apiVersion: policy/v1beta1 +kind: PodDisruptionBudget +metadata: + name: deprecated +spec: + maxUnavailable: 1 + selector: + matchLabels: + app: deprecated diff --git a/pkg/getter/httpgetter.go b/pkg/getter/httpgetter.go index 2bc12bdbf..2eb2d5d8c 100644 --- a/pkg/getter/httpgetter.go +++ b/pkg/getter/httpgetter.go @@ -20,6 +20,7 @@ import ( "crypto/tls" "fmt" "io" + "log/slog" "net/http" "net/url" "sync" @@ -87,11 +88,13 @@ func (g *HTTPGetter) get(href string, opts getterOptions) (*bytes.Buffer, error) return nil, err } + slog.Debug("fetching", "url", href) resp, err := client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() + slog.Debug("fetch complete", "url", href, "status", resp.Status, "content-length", resp.ContentLength) if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("failed to fetch %s : %s", href, resp.Status) } diff --git a/pkg/kube/client_test.go b/pkg/kube/client_test.go index e98d87520..ed871c05a 100644 --- a/pkg/kube/client_test.go +++ b/pkg/kube/client_test.go @@ -972,6 +972,7 @@ func TestGetPodList(t *testing.T) { podList, err := c.GetPodList(namespace, metav1.ListOptions{}) clientAssertions := assert.New(t) clientAssertions.NoError(err) + podList.ResourceVersion = "" clientAssertions.Equal(&responsePodList, podList) } diff --git a/pkg/kube/roundtripper.go b/pkg/kube/roundtripper.go index 52cb5bad2..e13f2103a 100644 --- a/pkg/kube/roundtripper.go +++ b/pkg/kube/roundtripper.go @@ -16,65 +16,9 @@ limitations under the License. package kube -import ( - "bytes" - "encoding/json" - "io" - "net/http" - "strings" -) +import "helm.sh/helm/v4/pkg/kubeenv" -type RetryingRoundTripper struct { - Wrapped http.RoundTripper -} - -func (rt *RetryingRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { - return rt.roundTrip(req, 1, nil) -} - -func (rt *RetryingRoundTripper) roundTrip(req *http.Request, retry int, prevResp *http.Response) (*http.Response, error) { - if retry < 0 { - return prevResp, nil - } - resp, rtErr := rt.Wrapped.RoundTrip(req) - if rtErr != nil { - return resp, rtErr - } - if resp.StatusCode < 500 { - return resp, rtErr - } - if resp.Header.Get("content-type") != "application/json" { - return resp, rtErr - } - b, err := io.ReadAll(resp.Body) - resp.Body.Close() - if err != nil { - return resp, err - } - - var ke kubernetesError - r := bytes.NewReader(b) - err = json.NewDecoder(r).Decode(&ke) - r.Seek(0, io.SeekStart) - resp.Body = io.NopCloser(r) - if err != nil { - return resp, err - } - if ke.Code < 500 { - return resp, nil - } - // Matches messages like "etcdserver: leader changed" - if strings.HasSuffix(ke.Message, "etcdserver: leader changed") { - return rt.roundTrip(req, retry-1, resp) - } - // Matches messages like "rpc error: code = Unknown desc = raft proposal dropped" - if strings.HasSuffix(ke.Message, "raft proposal dropped") { - return rt.roundTrip(req, retry-1, resp) - } - return resp, nil -} - -type kubernetesError struct { - Message string `json:"message"` - Code int `json:"code"` -} +// RetryingRoundTripper retries transient Kubernetes API server errors on a +// wrapped [http.RoundTripper]. The implementation lives in [kubeenv] so +// consumers can depend on that package without importing all of kube. +type RetryingRoundTripper = kubeenv.RetryingRoundTripper diff --git a/pkg/kubeenv/roundtripper.go b/pkg/kubeenv/roundtripper.go new file mode 100644 index 000000000..e00f93984 --- /dev/null +++ b/pkg/kubeenv/roundtripper.go @@ -0,0 +1,84 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package kubeenv holds small, cycle-free Kubernetes client helpers shared by +// higher-level packages (for example pkg/cli and pkg/kube). +package kubeenv + +import ( + "bytes" + "encoding/json" + "io" + "net/http" + "strings" +) + +// RetryingRoundTripper retries transient Kubernetes API server errors on a +// wrapped [http.RoundTripper]. +type RetryingRoundTripper struct { + Wrapped http.RoundTripper +} + +func (rt *RetryingRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + return rt.roundTrip(req, 1, nil) +} + +func (rt *RetryingRoundTripper) roundTrip(req *http.Request, retry int, prevResp *http.Response) (*http.Response, error) { + if retry < 0 { + return prevResp, nil + } + resp, rtErr := rt.Wrapped.RoundTrip(req) + if rtErr != nil { + return resp, rtErr + } + if resp.StatusCode < 500 { + return resp, rtErr + } + if resp.Header.Get("content-type") != "application/json" { + return resp, rtErr + } + b, err := io.ReadAll(resp.Body) + resp.Body.Close() + if err != nil { + return resp, err + } + + var ke kubernetesError + r := bytes.NewReader(b) + err = json.NewDecoder(r).Decode(&ke) + r.Seek(0, io.SeekStart) + resp.Body = io.NopCloser(r) + if err != nil { + return resp, err + } + if ke.Code < 500 { + return resp, nil + } + // Matches messages like "etcdserver: leader changed" + if strings.HasSuffix(ke.Message, "etcdserver: leader changed") { + return rt.roundTrip(req, retry-1, resp) + } + // Matches messages like "rpc error: code = Unknown desc = raft proposal dropped" + if strings.HasSuffix(ke.Message, "raft proposal dropped") { + return rt.roundTrip(req, retry-1, resp) + } + return resp, nil +} + +type kubernetesError struct { + Message string `json:"message"` + Code int `json:"code"` +} diff --git a/pkg/kube/roundtripper_test.go b/pkg/kubeenv/roundtripper_test.go similarity index 99% rename from pkg/kube/roundtripper_test.go rename to pkg/kubeenv/roundtripper_test.go index 96602c1f4..b921eac82 100644 --- a/pkg/kube/roundtripper_test.go +++ b/pkg/kubeenv/roundtripper_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package kube +package kubeenv import ( "encoding/json"