Merge branch 'fix/to-pretty-json-html-escaping' of https://github.com/piotrlaczykowski/helm; branch 'main' of https://github.com/helm/helm into fix/to-pretty-json-html-escaping

pull/31964/head
piotr.laczykowski 4 days ago
commit 5203982eea

2
.github/env vendored

@ -1,2 +1,2 @@
GOLANG_VERSION=1.25
GOLANG_VERSION=1.26
GOLANGCI_LINT_VERSION=v2.11.3

@ -22,7 +22,7 @@ jobs:
- name: Add variables to environment file
run: cat ".github/env" >> "$GITHUB_ENV"
- name: Setup Go
uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # pin@6.2.0
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # pin@6.4.0
with:
go-version: '${{ env.GOLANG_VERSION }}'
check-latest: true

@ -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@e296a935590eb16afc0c0108289f68c87e2a89a5 # pinv4.30.7
uses: github/codeql-action/init@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # pinv4.36.0
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@e296a935590eb16afc0c0108289f68c87e2a89a5 # pinv4.30.7
uses: github/codeql-action/autobuild@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # pinv4.36.0
# 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@e296a935590eb16afc0c0108289f68c87e2a89a5 # pinv4.30.7
uses: github/codeql-action/analyze@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # pinv4.36.0

@ -17,11 +17,11 @@ jobs:
- name: Add variables to environment file
run: cat ".github/env" >> "$GITHUB_ENV"
- name: Setup Go
uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # pin@6.2.0
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # pin@6.4.0
with:
go-version: '${{ env.GOLANG_VERSION }}'
check-latest: true
- name: golangci-lint
uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20 #pin@9.2.0
uses: golangci/golangci-lint-action@82606bf257cbaff209d206a39f5134f0cfbfd2ee #pin@9.2.1
with:
version: ${{ env.GOLANGCI_LINT_VERSION }}

@ -25,7 +25,7 @@ jobs:
- name: Add variables to environment file
run: cat ".github/env" >> "$GITHUB_ENV"
- name: Setup Go
uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # pin@6.2.0
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # pin@6.4.0
with:
go-version: '${{ env.GOLANG_VERSION }}'
check-latest: true

@ -28,7 +28,7 @@ jobs:
run: cat ".github/env" >> "$GITHUB_ENV"
- name: Setup Go
uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # pin@6.2.0
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # pin@6.4.0
with:
go-version: '${{ env.GOLANG_VERSION }}'
check-latest: true
@ -86,12 +86,14 @@ 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"
- name: Setup Go
uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # pin@6.2.0
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # pin@6.4.0
with:
go-version: '${{ env.GOLANG_VERSION }}'
check-latest: true

@ -55,7 +55,7 @@ jobs:
# Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
# format to the repository Actions tab.
- name: "Upload artifact"
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: SARIF file
path: results.sarif
@ -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@v4
uses: github/codeql-action/upload-sarif@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0
with:
sarif_file: results.sarif

@ -3,11 +3,16 @@ 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
- uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # v10.3.0
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
stale-issue-message: 'This issue has been marked as stale because it has been open for 90 days with no activity. This thread will be automatically closed in 30 days if no further activity occurs.'

1
.gitignore vendored

@ -3,6 +3,7 @@
.DS_Store
.coverage/
.idea
.claude
.vimrc
.vscode/
.devcontainer/

@ -19,6 +19,7 @@ linters:
# Keep sorted alphabetically
enable:
- bidichk
- depguard
- dupl
- exhaustive
@ -33,10 +34,12 @@ linters:
- revive
- sloglint
- staticcheck
- testifylint
- thelper
- unused
- usestdlibvars
- usetesting
- whitespace
exclusions:
@ -95,6 +98,22 @@ linters:
- helpers
- models
testifylint:
disable:
- empty
- encoded-compare
- equal-values
- 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

@ -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:'

@ -15,7 +15,7 @@ chance to try to fix the issue before it is exploited in the wild.
Helm v4 development takes place on the `main` branch while Helm v3 is on the `dev-v3` branch.
Helm v3 will continue to receive bug fixes and updates for new Kubernetes releases until July 8th 2026. Security enhancement will still be applied until November 11th 2026. See the blog <https://helm.sh/blog/helm-4-released#helm-v3-support> for more details.
Helm v3 will continue to receive bug fixes and updates for new Kubernetes releases until July 8th 2026. Security enhancements will still be applied until November 11th 2026. See the blog <https://helm.sh/blog/helm-4-released#helm-v3-support> for more details.
Bugs should first be fixed on Helm v4 and then backported to Helm v3. Helm v3 (and the `dev-v3` branch) is no longer accepting new features.
@ -162,9 +162,9 @@ There are 5 types of issues (each with their own corresponding [label](#labels))
for future reference. Generally these are questions that are too complex or large to store in the
Slack channel or have particular interest to the community as a whole. Depending on the
discussion, these can turn into `feature` or `bug` issues.
- `proposal`: Used for items (like this one) that propose a new ideas or functionality that require
- `proposal`: Used for items (like this one) that propose new ideas or functionality that require
a larger community discussion. This allows for feedback from others in the community before a
feature is actually developed. This is not needed for small additions. Final word on whether
feature is actually developed. This is not needed for small additions. Final word on whether
a feature needs a proposal is up to the core maintainers. All issues that are proposals should
both have a label and an issue title of "Proposal: [the rest of the title]." A proposal can become
a `feature` and does not require a milestone.

@ -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:

@ -32,8 +32,8 @@ Think of it like apt/yum/homebrew for Kubernetes.
## Helm Development and Stable Versions
Helm v4 is currently under development on the `main` branch. This is unstable and the APIs within the Go SDK and at the command line are changing.
Helm v3 (current stable) is maintained on the `dev-v3` branch. APIs there follow semantic versioning.
Helm v4 is the current stable release, developed on the `main` branch.
Helm v3 is in support mode on the `dev-v3` branch: bug fixes until July 8th 2026, security fixes until November 11th 2026.
## Install
@ -64,7 +64,7 @@ Get started with the [Quick Start guide](https://helm.sh/docs/intro/quickstart/)
The [Helm roadmap uses GitHub milestones](https://github.com/helm/helm/milestones) to track the progress of the project.
The development of Helm v4 is currently happening on the `main` branch while the development of Helm v3, the stable branch, is happening on the `dev-v3` branch. Changes should be made to the `main` branch prior to being added to the `dev-v3` branch so that all changes are carried along to Helm v4.
Helm v4 development happens on the `main` branch. Helm v3 is in support mode on the `dev-v3` branch and receives only bug and security fixes.
## Community, discussion, contribution, and support

@ -69,7 +69,7 @@ func TestCliPluginExitCode(t *testing.T) {
assert.Empty(t, stdout.String())
expectedStderr := "Error: plugin \"exitwith\" exited with error\n"
expectedStderr := "level=WARN msg=\"failed to load plugin (ignoring)\" plugin_yaml=../../pkg/cmd/testdata/helmhome/helm/plugins/noversion/plugin.yaml error=\"failed to load plugin \\\"../../pkg/cmd/testdata/helmhome/helm/plugins/noversion\\\": plugin `version` is required\"\nError: plugin \"exitwith\" exited with error\n"
if stderr.String() != expectedStderr {
t.Errorf("Expected %q written to stderr: Got %q", expectedStderr, stderr.String())
}

131
go.mod

@ -1,30 +1,30 @@
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.0.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 v0.37.2-flux.1
github.com/fluxcd/cli-utils v1.2.1
github.com/foxcpp/go-mockdns v1.2.0
github.com/gobwas/glob v0.2.3
github.com/gofrs/flock v0.13.0
github.com/gosuri/uitable v0.0.4
github.com/jmoiron/sqlx v1.4.0
github.com/lib/pq v1.12.0
github.com/mattn/go-shellwords v1.0.12
github.com/lib/pq v1.12.3
github.com/mattn/go-shellwords v1.0.13
github.com/moby/term v0.5.2
github.com/opencontainers/go-digest v1.0.0
github.com/opencontainers/image-spec v1.1.1
@ -33,22 +33,22 @@ require (
github.com/spf13/cobra v1.10.2
github.com/spf13/pflag v1.0.10
github.com/stretchr/testify v1.11.1
github.com/tetratelabs/wazero v1.11.0
github.com/tetratelabs/wazero v1.12.0
go.yaml.in/yaml/v3 v3.0.4
golang.org/x/crypto v0.49.0
golang.org/x/term v0.41.0
golang.org/x/text v0.35.0
golang.org/x/crypto v0.52.0
golang.org/x/term v0.43.0
golang.org/x/text v0.37.0
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/api v0.35.1
k8s.io/apiextensions-apiserver v0.35.1
k8s.io/apimachinery v0.35.1
k8s.io/apiserver v0.35.1
k8s.io/cli-runtime v0.35.1
k8s.io/client-go v0.35.1
k8s.io/klog/v2 v2.130.1
k8s.io/kubectl v0.35.1
k8s.io/api v0.36.1
k8s.io/apiextensions-apiserver v0.36.1
k8s.io/apimachinery v0.36.1
k8s.io/apiserver v0.36.1
k8s.io/cli-runtime v0.36.1
k8s.io/client-go v0.36.1
k8s.io/klog/v2 v2.140.0
k8s.io/kubectl v0.36.1
oras.land/oras-go/v2 v2.6.0
sigs.k8s.io/controller-runtime v0.23.3
sigs.k8s.io/controller-runtime v0.24.1
sigs.k8s.io/kustomize/kyaml v0.21.1
sigs.k8s.io/yaml v1.6.0
)
@ -60,21 +60,21 @@ require (
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/blang/semver/v4 v4.0.0 // indirect
github.com/bshuster-repo/logrus-logstash-hook v1.0.0 // indirect
github.com/bshuster-repo/logrus-logstash-hook v1.1.0 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
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
github.com/distribution/reference v0.6.0 // indirect
github.com/docker/docker-credential-helpers v0.8.2 // indirect
github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c // indirect
github.com/docker/docker-credential-helpers v0.9.5 // indirect
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,8 +91,7 @@ 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.27.7 // 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
github.com/huandu/xstrings v1.5.0 // indirect
@ -100,7 +99,7 @@ require (
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/klauspost/compress v1.18.4 // indirect
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect
@ -116,67 +115,67 @@ require (
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/onsi/gomega v1.39.0 // indirect
github.com/onsi/gomega v1.39.1 // indirect
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_golang v1.23.2 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.67.5 // indirect
github.com/prometheus/otlptranslator v1.0.0 // indirect
github.com/prometheus/procfs v0.19.2 // indirect
github.com/prometheus/procfs v0.20.1 // indirect
github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5 // indirect
github.com/redis/go-redis/extra/redisotel/v9 v9.0.5 // indirect
github.com/redis/go-redis/v9 v9.7.3 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/sirupsen/logrus v1.9.4 // indirect
github.com/spf13/cast v1.7.0 // indirect
github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/xlab/treeprint v1.2.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/bridges/prometheus v0.65.0 // indirect
go.opentelemetry.io/contrib/exporters/autoexport v0.65.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 // indirect
go.opentelemetry.io/otel v1.40.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.16.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.16.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.40.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.40.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0 // indirect
go.opentelemetry.io/otel/exporters/prometheus v0.62.0 // indirect
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.16.0 // indirect
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.40.0 // indirect
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.40.0 // indirect
go.opentelemetry.io/otel/log v0.16.0 // indirect
go.opentelemetry.io/otel/metric v1.40.0 // indirect
go.opentelemetry.io/otel/sdk v1.40.0 // indirect
go.opentelemetry.io/otel/sdk/log v0.16.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.40.0 // indirect
go.opentelemetry.io/otel/trace v1.40.0 // indirect
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
go.opentelemetry.io/contrib/bridges/prometheus v0.67.0 // indirect
go.opentelemetry.io/contrib/exporters/autoexport v0.67.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 // indirect
go.opentelemetry.io/otel v1.43.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.18.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.42.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 // indirect
go.opentelemetry.io/otel/exporters/prometheus v0.64.0 // indirect
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.18.0 // indirect
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.42.0 // indirect
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.42.0 // indirect
go.opentelemetry.io/otel/log v0.19.0 // indirect
go.opentelemetry.io/otel/metric v1.43.0 // indirect
go.opentelemetry.io/otel/sdk v1.43.0 // indirect
go.opentelemetry.io/otel/sdk/log v0.19.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect
go.opentelemetry.io/otel/trace v1.43.0 // indirect
go.opentelemetry.io/proto/otlp v1.10.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
golang.org/x/mod v0.33.0 // indirect
golang.org/x/net v0.51.0 // indirect
golang.org/x/oauth2 v0.34.0 // indirect
golang.org/x/mod v0.35.0 // indirect
golang.org/x/net v0.55.0 // indirect
golang.org/x/oauth2 v0.36.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/time v0.12.0 // indirect
golang.org/x/tools v0.42.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect
google.golang.org/grpc v1.79.3 // indirect
google.golang.org/protobuf v1.36.11 // indirect
golang.org/x/sys v0.45.0 // indirect
golang.org/x/time v0.15.0 // indirect
golang.org/x/tools v0.44.0 // indirect
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.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.1 // 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.1 // 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
)

278
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=
@ -26,6 +26,8 @@ github.com/ProtonMail/go-crypto v1.4.1 h1:9RfcZHqEQUvP8RzecWEUafnZVtEvrBVL9BiF67
github.com/ProtonMail/go-crypto v1.4.1/go.mod h1:e1OaTyu5SYVrO9gKOEhTc+5UcXtTUa+P3uLudwcgPqo=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alicebob/miniredis/v2 v2.35.0 h1:QwLphYqCEAo1eu1TqPRN2jgVMPBweeQcR21jeqDCONI=
github.com/alicebob/miniredis/v2 v2.35.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
@ -34,8 +36,8 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
github.com/bshuster-repo/logrus-logstash-hook v1.0.0 h1:e+C0SB5R1pu//O4MQ3f9cFuPGoOVeF2fE4Og9otCc70=
github.com/bshuster-repo/logrus-logstash-hook v1.0.0/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk=
github.com/bshuster-repo/logrus-logstash-hook v1.1.0 h1:o2FzZifLg+z/DN1OFmzTWzZZx/roaqt8IPZCIVco8r4=
github.com/bshuster-repo/logrus-logstash-hook v1.1.0/go.mod h1:Q2aXOe7rNuPgbBtPCOzYyWDvKX7+FpxE5sRdvcPoui0=
github.com/bsm/ginkgo/v2 v2.7.0/go.mod h1:AiKlXPm7ItEHNc/2+OkrNG4E0ITzojb9/xWzvQ9XZ9w=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
@ -51,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=
@ -65,22 +67,22 @@ 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.0.0 h1:q4R8wemdRQDClzoNNStftB2ZAfqOiN6UX90KJc4HjyM=
github.com/distribution/distribution/v3 v3.0.0/go.mod h1:tRNuFoZsUdyRVegq8xGNeds4KLjwLCRin/tTo6i1DhU=
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=
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo=
github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M=
github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c h1:+pKlWGMw7gf6bQ+oDZB4KHQFypsfjYlq/C4rfL7D3g8=
github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA=
github.com/docker/docker-credential-helpers v0.9.5 h1:EFNN8DHvaiK8zVqFA2DT6BjXE0GzfLOZ38ggPTKePkY=
github.com/docker/docker-credential-helpers v0.9.5/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c=
github.com/docker/go-events v0.0.0-20250808211157-605354379745 h1:yOn6Ze6IbYI/KAw2lw/83ELYvZh6hvsygTVkD0dzMC4=
github.com/docker/go-events v0.0.0-20250808211157-605354379745/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA=
github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8=
github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw=
github.com/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=
@ -91,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 v0.37.2-flux.1 h1:tQ588ghtRN+E+kHq415FddfqA9v4brn/1WWgrP6rQR0=
github.com/fluxcd/cli-utils v0.37.2-flux.1/go.mod h1:LcWSu1NYET8d8U7O326RhEm5JkQXCMK6ITu4G1CT02c=
github.com/fluxcd/cli-utils v1.2.1 h1:ug9CicKW7H9QXnvNDapTSKuryZvWcu4Nw7pRvQa6jDY=
github.com/fluxcd/cli-utils v1.2.1/go.mod h1:cky6M6eHvTQkoPtsuFYLIgAMYdpTCSLoor4IA6vueSw=
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=
@ -126,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=
@ -153,10 +154,8 @@ 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.27.7 h1:X+2YciYSxvMQK0UZ7sg45ZVabVZBeBuvMkmuI2V3Fak=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7/go.mod h1:lW34nIZuQ8UDPdkon5fmfp2l3+ZkQ2me/+oecHYLOII=
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=
github.com/hashicorp/golang-lru/arc/v2 v2.0.5/go.mod h1:ny6zBSQZi2JxIeYcv7kt2sH2PXJtirBN7RDhRpxPkxU=
github.com/hashicorp/golang-lru/v2 v2.0.5 h1:wW7h1TG88eUIJ2i69gaE3uNVtEPIagzhGvHgwfx2Vm4=
@ -177,8 +176,8 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
@ -192,8 +191,8 @@ github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtB
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk=
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lib/pq v1.12.0 h1:mC1zeiNamwKBecjHarAr26c/+d8V5w/u4J0I/yASbJo=
github.com/lib/pq v1.12.0/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
github.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ=
github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0=
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE=
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
@ -204,8 +203,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk=
github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
github.com/mattn/go-shellwords v1.0.13 h1:DC0OMEpGjm6LfNFU4ckYcvbQKyp2vE8atyFGXNtDcf4=
github.com/mattn/go-shellwords v1.0.13/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
@ -234,8 +233,8 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8m
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/onsi/ginkgo/v2 v2.28.1 h1:S4hj+HbZp40fNKuLUQOYLDgZLwNUVn19N3Atb98NCyI=
github.com/onsi/ginkgo/v2 v2.28.1/go.mod h1:CLtbVInNckU3/+gC8LzkGUb9oF+e8W8TdUsxPwvdOgE=
github.com/onsi/gomega v1.39.0 h1:y2ROC3hKFmQZJNFeGAMeHZKkjBL65mIZcvrLQBF9k6Q=
github.com/onsi/gomega v1.39.0/go.mod h1:ZCU1pkQcXDO5Sl9/VVEGlDyp+zm0m1cmeG5TOzLgdh4=
github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28=
github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
@ -266,8 +265,8 @@ github.com/prometheus/otlptranslator v1.0.0/go.mod h1:vRYWnXvI6aWGpsdY/mOT/cbeVR
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc=
github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5 h1:EaDatTxkdHG+U3Bk4EUr+DZ7fOGwTfezUiUJMaIcaho=
github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5/go.mod h1:fyalQWdtzDBECAQFBJuQe5bzQ02jGd5Qcbgb97Flm7U=
github.com/redis/go-redis/extra/redisotel/v9 v9.0.5 h1:EfpWLLCyXw8PSM2/XNJLjI3Pb27yVE+gIAfeqp8LUCc=
@ -288,8 +287,9 @@ github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepq
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w=
github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
@ -309,67 +309,69 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834 h1:ZF+QBjOI+tILZjBaFj3HgFonKXUcwgJ4djLb6i42S3Q=
github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834/go.mod h1:m9ymHTgNSEjuxvw8E7WWe4Pl4hZQHXONY8wE6dMLaRk=
github.com/tetratelabs/wazero v1.11.0 h1:+gKemEuKCTevU4d7ZTzlsvgd1uaToIDtlQlmNbwqYhA=
github.com/tetratelabs/wazero v1.11.0/go.mod h1:eV28rsN8Q+xwjogd7f4/Pp4xFxO7uOGbLcD/LzB1wiU=
github.com/tetratelabs/wazero v1.12.0 h1:DuWcpNu/FzgEXgGBDp8J1Spc+CWOvvtvVyjKlaZopYU=
github.com/tetratelabs/wazero v1.12.0/go.mod h1:LvKtzl2RqO4gyF27BiXU+nKAjcV8f38U+kP/q2vgxh0=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ=
github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/bridges/prometheus v0.65.0 h1:I/7S/yWobR3QHFLqHsJ8QOndoiFsj1VgHpQiq43KlUI=
go.opentelemetry.io/contrib/bridges/prometheus v0.65.0/go.mod h1:jPF6gn3y1E+nozCAEQj3c6NZ8KY+tvAgSVfvoOJUFac=
go.opentelemetry.io/contrib/exporters/autoexport v0.65.0 h1:2gApdml7SznX9szEKFjKjM4qGcGSvAybYLBY319XG3g=
go.opentelemetry.io/contrib/exporters/autoexport v0.65.0/go.mod h1:0QqAGlbHXhmPYACG3n5hNzO5DnEqqtg4VcK5pr22RI0=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0=
go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=
go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.16.0 h1:ZVg+kCXxd9LtAaQNKBxAvJ5NpMf7LpvEr4MIZqb0TMQ=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.16.0/go.mod h1:hh0tMeZ75CCXrHd9OXRYxTlCAdxcXioWHFIpYw2rZu8=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.16.0 h1:djrxvDxAe44mJUrKataUbOhCKhR3F8QCyWucO16hTQs=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.16.0/go.mod h1:dt3nxpQEiSoKvfTVxp3TUg5fHPLhKtbcnN3Z1I1ePD0=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.40.0 h1:NOyNnS19BF2SUDApbOKbDtWZ0IK7b8FJ2uAGdIWOGb0=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.40.0/go.mod h1:VL6EgVikRLcJa9ftukrHu/ZkkhFBSo1lzvdBC9CF1ss=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.40.0 h1:9y5sHvAxWzft1WQ4BwqcvA+IFVUJ1Ya75mSAUnFEVwE=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.40.0/go.mod h1:eQqT90eR3X5Dbs1g9YSM30RavwLF725Ris5/XSXWvqE=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 h1:QKdN8ly8zEMrByybbQgv8cWBcdAarwmIPZ6FThrWXJs=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0/go.mod h1:bTdK1nhqF76qiPoCCdyFIV+N/sRHYXYCTQc+3VCi3MI=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 h1:DvJDOPmSWQHWywQS6lKL+pb8s3gBLOZUtw4N+mavW1I=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0/go.mod h1:EtekO9DEJb4/jRyN4v4Qjc2yA7AtfCBuz2FynRUWTXs=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0 h1:wVZXIWjQSeSmMoxF74LzAnpVQOAFDo3pPji9Y4SOFKc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0/go.mod h1:khvBS2IggMFNwZK/6lEeHg/W57h/IX6J4URh57fuI40=
go.opentelemetry.io/otel/exporters/prometheus v0.62.0 h1:krvC4JMfIOVdEuNPTtQ0ZjCiXrybhv+uOHMfHRmnvVo=
go.opentelemetry.io/otel/exporters/prometheus v0.62.0/go.mod h1:fgOE6FM/swEnsVQCqCnbOfRV4tOnWPg7bVeo4izBuhQ=
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.16.0 h1:ivlbaajBWJqhcCPniDqDJmRwj4lc6sRT+dCAVKNmxlQ=
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.16.0/go.mod h1:u/G56dEKDDwXNCVLsbSrllB2o8pbtFLUC4HpR66r2dc=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.40.0 h1:ZrPRak/kS4xI3AVXy8F7pipuDXmDsrO8Lg+yQjBLjw0=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.40.0/go.mod h1:3y6kQCWztq6hyW8Z9YxQDDm0Je9AJoFar2G0yDcmhRk=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.40.0 h1:MzfofMZN8ulNqobCmCAVbqVL5syHw+eB2qPRkCMA/fQ=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.40.0/go.mod h1:E73G9UFtKRXrxhBsHtG00TB5WxX57lpsQzogDkqBTz8=
go.opentelemetry.io/otel/log v0.16.0 h1:DeuBPqCi6pQwtCK0pO4fvMB5eBq6sNxEnuTs88pjsN4=
go.opentelemetry.io/otel/log v0.16.0/go.mod h1:rWsmqNVTLIA8UnwYVOItjyEZDbKIkMxdQunsIhpUMes=
go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=
go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc=
go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8=
go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE=
go.opentelemetry.io/otel/sdk/log v0.16.0 h1:e/b4bdlQwC5fnGtG3dlXUrNOnP7c8YLVSpSfEBIkTnI=
go.opentelemetry.io/otel/sdk/log v0.16.0/go.mod h1:JKfP3T6ycy7QEuv3Hj8oKDy7KItrEkus8XJE6EoSzw4=
go.opentelemetry.io/otel/sdk/log/logtest v0.16.0 h1:/XVkpZ41rVRTP4DfMgYv1nEtNmf65XPPyAdqV90TMy4=
go.opentelemetry.io/otel/sdk/log/logtest v0.16.0/go.mod h1:iOOPgQr5MY9oac/F5W86mXdeyWZGleIx3uXO98X2R6Y=
go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw=
go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg=
go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw=
go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=
go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
go.opentelemetry.io/contrib/bridges/prometheus v0.67.0 h1:dkBzNEAIKADEaFnuESzcXvpd09vxvDZsOjx11gjUqLk=
go.opentelemetry.io/contrib/bridges/prometheus v0.67.0/go.mod h1:Z5RIwRkZgauOIfnG5IpidvLpERjhTninpP1dTG2jTl4=
go.opentelemetry.io/contrib/exporters/autoexport v0.67.0 h1:4fnRcNpc6YFtG3zsFw9achKn3XgmxPxuMuqIL5rE8e8=
go.opentelemetry.io/contrib/exporters/autoexport v0.67.0/go.mod h1:qTvIHMFKoxW7HXg02gm6/Wofhq5p3Ib/A/NNt1EoBSQ=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg=
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.18.0 h1:deI9UQMoGFgrg5iLPgzueqFPHevDl+28YKfSpPTI6rY=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.18.0/go.mod h1:PFx9NgpNUKXdf7J4Q3agRxMs3Y07QhTCVipKmLsMKnU=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0 h1:HIBTQ3VO5aupLKjC90JgMqpezVXwFuq6Ryjn0/izoag=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0/go.mod h1:ji9vId85hMxqfvICA0Jt8JqEdrXaAkcpkI9HPXya0ro=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.42.0 h1:MdKucPl/HbzckWWEisiNqMPhRrAOQX8r4jTuGr636gk=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.42.0/go.mod h1:RolT8tWtfHcjajEH5wFIZ4Dgh5jpPdFXYV9pTAk/qjc=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0 h1:w1K+pCJoPpQifuVpsKamUdn9U0zM3xUziVOqsGksUrY=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0/go.mod h1:HBy4BjzgVE8139ieRI75oXm3EcDN+6GhD88JT1Kjvxg=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0 h1:zWWrB1U6nqhS/k6zYB74CjRpuiitRtLLi68VcgmOEto=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0/go.mod h1:2qXPNBX1OVRC0IwOnfo1ljoid+RD0QK3443EaqVlsOU=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 h1:3iZJKlCZufyRzPzlQhUIWVmfltrXuGyfjREgGP3UUjc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0/go.mod h1:/G+nUPfhq2e+qiXMGxMwumDrP5jtzU+mWN7/sjT2rak=
go.opentelemetry.io/otel/exporters/prometheus v0.64.0 h1:g0LRDXMX/G1SEZtK8zl8Chm4K6GBwRkjPKE36LxiTYs=
go.opentelemetry.io/otel/exporters/prometheus v0.64.0/go.mod h1:UrgcjnarfdlBDP3GjDIJWe6HTprwSazNjwsI+Ru6hro=
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.18.0 h1:KJVjPD3rcPb98rIs3HznyJlrfx9ge5oJvxxlGR+P/7s=
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.18.0/go.mod h1:K3kRa2ckmHWQaTWQdPRHc7qGXASuVuoEQXzrvlA98Ws=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.42.0 h1:lSZHgNHfbmQTPfuTmWVkEu8J8qXaQwuV30pjCcAUvP8=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.42.0/go.mod h1:so9ounLcuoRDu033MW/E0AD4hhUjVqswrMF5FoZlBcw=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.42.0 h1:s/1iRkCKDfhlh1JF26knRneorus8aOwVIDhvYx9WoDw=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.42.0/go.mod h1:UI3wi0FXg1Pofb8ZBiBLhtMzgoTm1TYkMvn71fAqDzs=
go.opentelemetry.io/otel/log v0.19.0 h1:KUZs/GOsw79TBBMfDWsXS+KZ4g2Ckzksd1ymzsIEbo4=
go.opentelemetry.io/otel/log v0.19.0/go.mod h1:5DQYeGmxVIr4n0/BcJvF4upsraHjg6vudJJpnkL6Ipk=
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
go.opentelemetry.io/otel/sdk/log v0.19.0 h1:scYVLqT22D2gqXItnWiocLUKGH9yvkkeql5dBDiXyko=
go.opentelemetry.io/otel/sdk/log v0.19.0/go.mod h1:vFBowwXGLlW9AvpuF7bMgnNI95LiW10szrOdvzBHlAg=
go.opentelemetry.io/otel/sdk/log/logtest v0.19.0 h1:BEbF7ZBB6qQloV/Ub1+3NQoOUnVtcGkU3XX4Ws3GQfk=
go.opentelemetry.io/otel/sdk/log/logtest v0.19.0/go.mod h1:Lua81/3yM0wOmoHTokLj9y9ADeA02v1naRrVrkAZuKk=
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
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=
@ -380,14 +382,14 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g=
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@ -398,10 +400,10 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ=
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -416,11 +418,11 @@ golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@ -428,8 +430,8 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@ -437,8 +439,8 @@ golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
golang.org/x/term v0.14.0/go.mod h1:TySc+nGkYR6qt8km8wUhuFRTVSMIX3XPR58y2lC8vww=
golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=
golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=
golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4=
golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
@ -446,29 +448,29 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.15.0/go.mod h1:hpksKq4dtpQWS1uQ61JkdqWM3LscIS6Slf+VVkm+wQk=
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 h1:merA0rdPeUV3YIIfHHcH4qBkiQAc1nfCKSI7lB4cV2M=
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409/go.mod h1:fl8J1IvUjCilwZzQowmw2b7HQB2eAuYBabMXzWurF+I=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=
google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA=
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 h1:m8qni9SQFH0tJc1X0vmnpw/0t+AImlSvp30sEupozUg=
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.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=
@ -483,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.1 h1:0PO/1FhlK/EQNVK5+txc4FuhQibV25VLSdLMmGpDE/Q=
k8s.io/api v0.35.1/go.mod h1:28uR9xlXWml9eT0uaGo6y71xK86JBELShLy4wR1XtxM=
k8s.io/apiextensions-apiserver v0.35.1 h1:p5vvALkknlOcAqARwjS20kJffgzHqwyQRM8vHLwgU7w=
k8s.io/apiextensions-apiserver v0.35.1/go.mod h1:2CN4fe1GZ3HMe4wBr25qXyJnJyZaquy4nNlNmb3R7AQ=
k8s.io/apimachinery v0.35.1 h1:yxO6gV555P1YV0SANtnTjXYfiivaTPvCTKX6w6qdDsU=
k8s.io/apimachinery v0.35.1/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns=
k8s.io/apiserver v0.35.1 h1:potxdhhTL4i6AYAa2QCwtlhtB1eCdWQFvJV6fXgJzxs=
k8s.io/apiserver v0.35.1/go.mod h1:BiL6Dd3A2I/0lBnteXfWmCFobHM39vt5+hJQd7Lbpi4=
k8s.io/cli-runtime v0.35.1 h1:uKcXFe8J7AMAM4Gm2JDK4mp198dBEq2nyeYtO+JfGJE=
k8s.io/cli-runtime v0.35.1/go.mod h1:55/hiXIq1C8qIJ3WBrWxEwDLdHQYhBNRdZOz9f7yvTw=
k8s.io/client-go v0.35.1 h1:+eSfZHwuo/I19PaSxqumjqZ9l5XiTEKbIaJ+j1wLcLM=
k8s.io/client-go v0.35.1/go.mod h1:1p1KxDt3a0ruRfc/pG4qT/3oHmUj1AhSHEcxNSGg+OA=
k8s.io/component-base v0.35.1 h1:XgvpRf4srp037QWfGBLFsYMUQJkE5yMa94UsJU7pmcE=
k8s.io/component-base v0.35.1/go.mod h1:HI/6jXlwkiOL5zL9bqA3en1Ygv60F03oEpnuU1G56Bs=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
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.1 h1:zP3Er8C5i1dcAFUMh9Eva0kVvZHptXIn/+8NtRWMxwg=
k8s.io/kubectl v0.35.1/go.mod h1:cQ2uAPs5IO/kx8R5s5J3Ihv3VCYwrx0obCXum0CvnXo=
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.1 h1:XbL/EMj8K2aJpJtePmqUyQMsM0D4QI2pvl7YKJ20FTY=
k8s.io/api v0.36.1/go.mod h1:KOWo4ey3TINlXjeHVuwB3i+tXXnu+UcwFBHlI/9dvEo=
k8s.io/apiextensions-apiserver v0.36.1 h1:6JfYmPUsuUIHuN+3QxutXYWj492RqF5fBSx67GYK5Ks=
k8s.io/apiextensions-apiserver v0.36.1/go.mod h1:pLzZin90riwisdzKwv/GoTwENooytoIx5zWJb4Hkby8=
k8s.io/apimachinery v0.36.1 h1:G63Gjx2W+q0YD+72Vo8oY0nDnePVwnuzTmmy5ENrVSA=
k8s.io/apimachinery v0.36.1/go.mod h1:ibYOR00vW/I1kzvi5SF0dRuJ52BvKtfvRdOn35GPQ+8=
k8s.io/apiserver v0.36.1 h1:iMS5V+rPUertv5P9RaqJgmHHTuh4quWpoxchvMUY+JY=
k8s.io/apiserver v0.36.1/go.mod h1:Cby1PbLWztu0GDOxoO6iFOyyqIsziHNEW+w9zVQ22Kw=
k8s.io/cli-runtime v0.36.1 h1:yuC/BGnnj1YYPh6D1P+pZnzinCs6DvMq86yAeNqoqzM=
k8s.io/cli-runtime v0.36.1/go.mod h1:ZQWHGt8xAF7KnviB79vX0lYNyUUqKIpU+LQg7exuFAw=
k8s.io/client-go v0.36.1 h1:FN/K8QIT2CEDt+2WB2HnWrUANZ50AP5GII43/SP2JR0=
k8s.io/client-go v0.36.1/go.mod h1:s6rAnCtTGYDQnpNjEhSaISV+2O8jwruZ6m3QOYBFbtU=
k8s.io/component-base v0.36.1 h1:iG6GsELftXqTNG9HG6kiVjatSgAw1sf5pJ6R5a6N0kA=
k8s.io/component-base v0.36.1/go.mod h1:nf9XPlntRdqO6WMeEWAA5F93Y4ICZQdeT9GeqLDB3JI=
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.1 h1:96HqS9twIdHM0MlJLTwbo14b9kUKPkOzZ4tlRDLv4qI=
k8s.io/kubectl v0.36.1/go.mod h1:/DGPAIewKsFWF9VFgGvkPhao2Ev4SNuE3BioZo8yPbk=
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.1 h1:miPEwrmirImAvgME1L9qebGHrOnGJoVmVdtOU9fRfo4=
sigs.k8s.io/controller-runtime v0.24.1/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=
@ -517,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=

@ -104,7 +104,7 @@ func TestMetadata(t *testing.T) {
is.Equal("foo.yaml", chrt.Name())
is.Equal("1.0.0", chrt.AppVersion())
is.Equal(nil, chrt.Validate())
is.NoError(chrt.Validate())
}
func TestIsRoot(t *testing.T) {
@ -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) {

@ -44,7 +44,6 @@ func WithSkipSchemaValidation(skipSchemaValidation bool) LinterOption {
}
func RunAll(baseDir string, values map[string]any, namespace string, options ...LinterOption) support.Linter {
chartDir, _ := filepath.Abs(baseDir)
lo := linterOptions{}

@ -28,7 +28,7 @@ import (
kscheme "k8s.io/client-go/kubernetes/scheme"
)
// deprecatedAPIError indicates than an API is deprecated in Kubernetes
// deprecatedAPIError indicates that an API is deprecated in Kubernetes
type deprecatedAPIError struct {
Deprecated string
Message string

@ -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

@ -242,7 +242,6 @@ data:
//
// See https://github.com/helm/helm/issues/7483
func TestStrictTemplateParsingMapError(t *testing.T) {
ch := chart.Chart{
Metadata: &chart.Metadata{
Name: "regression7483",
@ -371,7 +370,6 @@ func TestValidateTopIndentLevel(t *testing.T) {
t.Errorf("Expected %t for %q", shouldFail, doc)
}
}
}
// TestEmptyWithCommentsManifests checks the lint is not failing against empty manifests that contains only comments
@ -463,5 +461,4 @@ func TestIsYamlFileExtension(t *testing.T) {
t.Errorf("isYamlFileExtension(%s) = %v; want %v", test.filename, result, test.expected)
}
}
}

@ -337,7 +337,6 @@ icon: https://example.com/64x64.png
if text.String() != "" {
t.Errorf("Expected no message to Stderr, got %s", text.String())
}
}
// Packaging the chart on a Windows machine will produce an
@ -607,7 +606,6 @@ func verifyChart(t *testing.T, c *chart.Chart) {
t.Errorf("Expected %s version %s, got %s", dep.Name(), exp["version"], dep.Metadata.Version)
}
}
}
func verifyDependencies(t *testing.T, c *chart.Chart) {

@ -112,6 +112,9 @@ func (md *Metadata) Validate() error {
return ValidationError("chart.metadata.name is required")
}
if md.Name == "." || md.Name == ".." {
return ValidationErrorf("chart.metadata.name %q is not allowed", md.Name)
}
if md.Name != filepath.Base(md.Name) {
return ValidationErrorf("chart.metadata.name %q is invalid", md.Name)
}

@ -41,6 +41,16 @@ func TestValidate(t *testing.T) {
&Metadata{APIVersion: "v3", Version: "1.0"},
ValidationError("chart.metadata.name is required"),
},
{
"chart with dot name",
&Metadata{Name: ".", APIVersion: "v3", Version: "1.0"},
ValidationError("chart.metadata.name \".\" is not allowed"),
},
{
"chart with dotdot name",
&Metadata{Name: "..", APIVersion: "v3", Version: "1.0"},
ValidationError("chart.metadata.name \"..\" is not allowed"),
},
{
"chart without name",
&Metadata{Name: "../../test", APIVersion: "v3", Version: "1.0"},

@ -702,7 +702,6 @@ func CreateFrom(chartfile *chart.Metadata, dest, src string) error {
// error. In such a case, this will attempt to clean up by removing the
// new chart directory.
func Create(name, dir string) (string, error) {
// Sanity-check the name of a chart so user doesn't create one that causes problems.
if err := validateChartName(name); err != nil {
return "", err

@ -19,6 +19,7 @@ import (
"errors"
"fmt"
"log/slog"
"slices"
"strings"
chart "helm.sh/helm/v4/internal/chart/v3"
@ -242,8 +243,8 @@ func set(path []string, data map[string]any) map[string]any {
return nil
}
cur := data
for i := len(path) - 1; i >= 0; i-- {
cur = map[string]any{path[i]: cur}
for _, v := range slices.Backward(path) {
cur = map[string]any{v: cur}
}
return cur
}

@ -459,7 +459,6 @@ func TestDependentChartAliases(t *testing.T) {
if aliasChart := getAliasDependency(c.Dependencies(), req[2]); aliasChart != nil {
t.Fatalf("expected no chart but got %s", aliasChart.Name())
}
}
func TestDependentChartWithSubChartsAbsentInDependency(t *testing.T) {

@ -42,4 +42,4 @@ into a Chart.
When creating charts in memory, use the 'helm.sh/helm/pkg/chart'
package directly.
*/
package util // import chartutil "helm.sh/helm/v4/internal/chart/v3/util"
package util // import "helm.sh/helm/v4/internal/chart/v3/util"

@ -52,6 +52,17 @@ func Expand(dir string, r io.Reader) error {
return errors.New("chart name not specified")
}
// Reject chart names that are POSIX path dot-segments or dot-dot segments or contain path separators.
// A dot-segment name (e.g. ".") causes SecureJoin to resolve to the root
// directory and extraction then to write files directly into that extraction root
// instead of a per-chart subdirectory.
if chartName == "." || chartName == ".." {
return fmt.Errorf("chart name %q is not allowed", chartName)
}
if chartName != filepath.Base(chartName) {
return fmt.Errorf("chart name %q must not contain path separators", chartName)
}
// Find the base directory
// The directory needs to be cleaned prior to passing to SecureJoin or the location may end up
// being wrong or returning an error. This was introduced in v0.4.0.
@ -61,6 +72,12 @@ func Expand(dir string, r io.Reader) error {
return err
}
// Defense-in-depth: the chart directory must be a subdirectory of dir,
// never dir itself.
if chartdir == dir {
return fmt.Errorf("chart name %q resolves to the extraction root", chartName)
}
// Copy all files verbatim. We don't parse these files because parsing can remove
// comments.
for _, file := range files {

@ -17,11 +17,73 @@ limitations under the License.
package util
import (
"archive/tar"
"bytes"
"compress/gzip"
"io/fs"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// makeTestChartArchive builds a gzipped tar archive from the given sourceDir directory, file entries are prefixed with the given chartName
func makeTestChartArchive(t *testing.T, chartName, sourceDir string) *bytes.Buffer {
t.Helper()
var result bytes.Buffer
gw := gzip.NewWriter(&result)
tw := tar.NewWriter(gw)
dir := os.DirFS(sourceDir)
writeFile := func(relPath string) {
t.Helper()
f, err := dir.Open(relPath)
require.NoError(t, err)
fStat, err := f.Stat()
require.NoError(t, err)
err = tw.WriteHeader(&tar.Header{
Name: filepath.Join(chartName, relPath),
Mode: int64(fStat.Mode()),
Size: fStat.Size(),
})
require.NoError(t, err)
data, err := fs.ReadFile(dir, relPath)
require.NoError(t, err)
tw.Write(data)
}
err := fs.WalkDir(dir, ".", func(path string, d os.DirEntry, walkErr error) error {
if walkErr != nil {
return walkErr
}
if d.IsDir() {
return nil
}
writeFile(path)
return nil
})
if err != nil {
t.Fatal(err)
}
err = tw.Close()
require.NoError(t, err)
err = gw.Close()
require.NoError(t, err)
return &result
}
func TestExpand(t *testing.T) {
dest := t.TempDir()
@ -75,6 +137,28 @@ func TestExpand(t *testing.T) {
}
}
func TestExpandError(t *testing.T) {
tests := map[string]struct {
chartName string
chartDir string
wantErr string
}{
"dot name": {"dotname", "testdata/dotname", "not allowed"},
"dotdot name": {"dotdotname", "testdata/dotdotname", "not allowed"},
"slash in name": {"slashinname", "testdata/slashinname", "must not contain path separators"},
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
archive := makeTestChartArchive(t, tt.chartName, tt.chartDir)
dest := t.TempDir()
err := Expand(dest, archive)
assert.ErrorContains(t, err, tt.wantErr)
})
}
}
func TestExpandFile(t *testing.T) {
dest := t.TempDir()

@ -0,0 +1,4 @@
apiVersion: v3
name: ..
description: A Helm chart for Kubernetes
version: 0.1.0

@ -0,0 +1,4 @@
apiVersion: v3
name: .
description: A Helm chart for Kubernetes
version: 0.1.0

@ -0,0 +1,4 @@
apiVersion: v3
name: a/../b
description: A Helm chart for Kubernetes
version: 0.1.0

@ -79,7 +79,6 @@ func ValidateReleaseName(name string) error {
// This case is preserved for backwards compatibility
if name == "" {
return errMissingName
}
if len(name) > maxReleaseNameLen || !validName.MatchString(name) {
return errInvalidName

@ -24,7 +24,6 @@ import (
)
func TestColorizeStatus(t *testing.T) {
tests := []struct {
name string
status common.Status
@ -107,7 +106,6 @@ func TestColorizeStatus(t *testing.T) {
}
func TestColorizeHeader(t *testing.T) {
tests := []struct {
name string
header string
@ -149,7 +147,6 @@ func TestColorizeHeader(t *testing.T) {
}
func TestColorizeNamespace(t *testing.T) {
tests := []struct {
name string
namespace string

@ -33,7 +33,6 @@ type Client struct {
// New creates a new client
func New(u string) (*Client, error) {
// Validate we have a URL
if err := validate(u); err != nil {
return nil, err
@ -46,7 +45,6 @@ func New(u string) (*Client, error) {
// Validate if the base URL for monocular is valid.
func validate(u string) error {
// Check if it is parsable
p, err := url.Parse(u)
if err != nil {

@ -99,7 +99,6 @@ type ChartVersion struct {
// Search performs a search against the monocular search API
func (c *Client) Search(term string) ([]SearchResult, error) {
// Create the URL to the search endpoint
// Note, this is currently an internal API for the Hub. This should be
// formatted without showing how monocular operates.

@ -27,7 +27,6 @@ import (
var searchResult = `{"data":[{"id":"stable/phpmyadmin","type":"chart","attributes":{"name":"phpmyadmin","repo":{"name":"stable","url":"https://charts.helm.sh/stable"},"description":"phpMyAdmin is an mysql administration frontend","home":"https://www.phpmyadmin.net/","keywords":["mariadb","mysql","phpmyadmin"],"maintainers":[{"name":"Bitnami","email":"containers@bitnami.com"}],"sources":["https://github.com/bitnami/bitnami-docker-phpmyadmin"],"icon":""},"links":{"self":"/v1/charts/stable/phpmyadmin"},"relationships":{"latestChartVersion":{"data":{"version":"3.0.0","app_version":"4.9.0-1","created":"2019-08-08T17:57:31.38Z","digest":"119c499251bffd4b06ff0cd5ac98c2ce32231f84899fb4825be6c2d90971c742","urls":["https://charts.helm.sh/stable/phpmyadmin-3.0.0.tgz"],"readme":"/v1/assets/stable/phpmyadmin/versions/3.0.0/README.md","values":"/v1/assets/stable/phpmyadmin/versions/3.0.0/values.yaml"},"links":{"self":"/v1/charts/stable/phpmyadmin/versions/3.0.0"}}}},{"id":"bitnami/phpmyadmin","type":"chart","attributes":{"name":"phpmyadmin","repo":{"name":"bitnami","url":"https://charts.bitnami.com"},"description":"phpMyAdmin is an mysql administration frontend","home":"https://www.phpmyadmin.net/","keywords":["mariadb","mysql","phpmyadmin"],"maintainers":[{"name":"Bitnami","email":"containers@bitnami.com"}],"sources":["https://github.com/bitnami/bitnami-docker-phpmyadmin"],"icon":""},"links":{"self":"/v1/charts/bitnami/phpmyadmin"},"relationships":{"latestChartVersion":{"data":{"version":"3.0.0","app_version":"4.9.0-1","created":"2019-08-08T18:34:13.341Z","digest":"66d77cf6d8c2b52c488d0a294cd4996bd5bad8dc41d3829c394498fb401c008a","urls":["https://charts.bitnami.com/bitnami/phpmyadmin-3.0.0.tgz"],"readme":"/v1/assets/bitnami/phpmyadmin/versions/3.0.0/README.md","values":"/v1/assets/bitnami/phpmyadmin/versions/3.0.0/values.yaml"},"links":{"self":"/v1/charts/bitnami/phpmyadmin/versions/3.0.0"}}}}]}`
func TestSearch(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
fmt.Fprintln(w, searchResult)
}))

@ -35,7 +35,6 @@ func TestPath(t *testing.T) {
}
for _, tt := range tests {
t.Setenv("HELM_PLUGINS", tt.helmPluginsDir)
baseIns := newBase(tt.source)
baseInsPath := baseIns.Path()

@ -90,7 +90,6 @@ func NewExtractor(source string) (Extractor, error) {
// - Beginning a path with a path separator is illegal
// - Rudimentary symlink protections are offered by SecureJoin.
func cleanJoin(root, dest string) (string, error) {
// On Windows, this is a drive separator. On UNIX-like, this is the path list separator.
// In neither case do we want to trust a TAR that contains these.
if strings.Contains(dest, ":") {

@ -124,7 +124,6 @@ func TestHTTPInstaller(t *testing.T) {
} else if err.Error() != "plugin already exists" {
t.Fatalf("expected error for plugin exists, got (%v)", err)
}
}
func TestHTTPInstallerNonExistentVersion(t *testing.T) {
@ -157,7 +156,6 @@ func TestHTTPInstallerNonExistentVersion(t *testing.T) {
if err := Install(i); err == nil {
t.Fatal("expected error from http client")
}
}
func TestHTTPInstallerUpdate(t *testing.T) {
@ -297,7 +295,6 @@ func TestExtract(t *testing.T) {
t.Fatalf("Expected %s to have %o mode but has %o (umask: %o)",
readmeFullPath, expectedReadmePerm, info.Mode().Perm(), currentUmask)
}
}
func TestCleanJoin(t *testing.T) {
@ -327,11 +324,9 @@ func TestCleanJoin(t *testing.T) {
t.Errorf("Test %d: Expected %q but got %q", i, fixture.expect, out)
}
}
}
func TestMediaTypeToExtension(t *testing.T) {
for mt, shouldPass := range map[string]bool{
"": false,
"application/gzip": true,

@ -72,7 +72,6 @@ type VerificationResult struct {
// InstallWithOptions installs a plugin with options.
func InstallWithOptions(i Installer, opts Options) (*VerificationResult, error) {
if err := os.MkdirAll(filepath.Dir(i.Path()), 0755); err != nil {
return nil, err
}
@ -98,24 +97,23 @@ func InstallWithOptions(i Installer, opts Options) (*VerificationResult, error)
// Check if provenance data exists
if len(provData) == 0 {
// No .prov file found - emit warning but continue installation
fmt.Fprint(os.Stderr, "WARNING: No provenance file found for plugin. Plugin is not signed and cannot be verified.\n")
} else {
// Provenance data exists - verify the plugin
verification, err := plugin.VerifyPlugin(archiveData, provData, filename, opts.Keyring)
if err != nil {
return nil, fmt.Errorf("plugin verification failed: %w", err)
}
return nil, errors.New("plugin verification failed: no provenance file (.prov) found")
}
// Collect verification info
result = &VerificationResult{
SignedBy: make([]string, 0),
Fingerprint: fmt.Sprintf("%X", verification.SignedBy.PrimaryKey.Fingerprint),
FileHash: verification.FileHash,
}
for name := range verification.SignedBy.Identities {
result.SignedBy = append(result.SignedBy, name)
}
// Provenance data exists - verify the plugin
verification, err := plugin.VerifyPlugin(archiveData, provData, filename, opts.Keyring)
if err != nil {
return nil, fmt.Errorf("plugin verification failed: %w", err)
}
// Collect verification info
result = &VerificationResult{
SignedBy: make([]string, 0),
Fingerprint: fmt.Sprintf("%X", verification.SignedBy.PrimaryKey.Fingerprint),
FileHash: verification.FileHash,
}
for name := range verification.SignedBy.Identities {
result.SignedBy = append(result.SignedBy, name)
}
}

@ -185,5 +185,4 @@ func TestVCSInstallerUpdate(t *testing.T) {
} else if err.Error() != "plugin repo was modified" {
t.Fatalf("expected error for plugin modified, got (%v)", err)
}
}

@ -16,10 +16,8 @@ limitations under the License.
package installer
import (
"bytes"
"crypto/sha256"
"fmt"
"io"
"os"
"path/filepath"
"strings"
@ -44,33 +42,49 @@ func TestInstallWithOptions_VerifyMissingProvenance(t *testing.T) {
}
defer os.RemoveAll(installer.Path())
// Capture stderr to check warning message
oldStderr := os.Stderr
r, w, _ := os.Pipe()
os.Stderr = w
// Install with verification enabled (should warn but succeed)
// Install with verification enabled should fail when .prov is missing
result, err := InstallWithOptions(installer, Options{Verify: true, Keyring: "dummy"})
// Restore stderr and read captured output
w.Close()
os.Stderr = oldStderr
var buf bytes.Buffer
io.Copy(&buf, r)
output := buf.String()
// Should succeed with nil result (no verification performed)
if err != nil {
t.Fatalf("Expected installation to succeed despite missing .prov file, got error: %v", err)
// Should fail with a missing provenance error
if err == nil {
t.Fatal("Expected installation to fail when .prov file is missing and verification is enabled")
}
if !strings.Contains(err.Error(), "no provenance file") {
t.Errorf("Expected 'no provenance file' in error message, got: %v", err)
}
if result != nil {
t.Errorf("Expected nil verification result when .prov file is missing, got: %+v", result)
}
// Should contain warning message
expectedWarning := "WARNING: No provenance file found for plugin"
if !strings.Contains(output, expectedWarning) {
t.Errorf("Expected warning message '%s' in output, got: %s", expectedWarning, output)
// Plugin should NOT be installed
if _, err := os.Stat(installer.Path()); !os.IsNotExist(err) {
t.Error("Plugin should not be installed when verification fails due to missing .prov")
}
}
func TestInstallWithOptions_NoVerifyMissingProvenance(t *testing.T) {
ensure.HelmHome(t)
// Create a temporary plugin tarball without .prov file
pluginDir := createTestPluginDir(t)
pluginTgz := createTarballFromPluginDir(t, pluginDir)
defer os.Remove(pluginTgz)
// Create local installer
installer, err := NewLocalInstaller(pluginTgz)
if err != nil {
t.Fatalf("Failed to create installer: %v", err)
}
defer os.RemoveAll(installer.Path())
// Install with verification explicitly disabled should succeed without .prov
result, err := InstallWithOptions(installer, Options{Verify: false})
if err != nil {
t.Fatalf("Expected installation to succeed with --verify=false, got error: %v", err)
}
if result != nil {
t.Errorf("Expected nil verification result when verification is disabled, got: %+v", result)
}
// Plugin should be installed

@ -19,6 +19,7 @@ import (
"bytes"
"fmt"
"io"
"log/slog"
"os"
"path/filepath"
@ -44,7 +45,6 @@ func peekAPIVersion(r io.Reader) (string, error) {
}
func loadMetadataLegacy(metadataData []byte) (*Metadata, error) {
var ml MetadataLegacy
d := yaml.NewDecoder(bytes.NewReader(metadataData))
// NOTE: No strict unmarshalling for legacy plugins - maintain backwards compatibility
@ -64,7 +64,6 @@ func loadMetadataLegacy(metadataData []byte) (*Metadata, error) {
}
func loadMetadataV1(metadataData []byte) (*Metadata, error) {
var mv1 MetadataV1
d := yaml.NewDecoder(bytes.NewReader(metadataData))
d.KnownFields(true)
@ -108,7 +107,6 @@ type prototypePluginManager struct {
}
func newPrototypePluginManager() (*prototypePluginManager, error) {
cc, err := wazero.NewCompilationCacheWithDir(helmpath.CachePath("wazero-build"))
if err != nil {
return nil, fmt.Errorf("failed to create wazero compilation cache: %w", err)
@ -158,18 +156,27 @@ func LoadDir(dirname string) (Plugin, error) {
return pm.CreatePlugin(dirname, m)
}
// LoadAll loads all plugins found beneath the base directory.
func LogIgnorePluginLoadErrorFilterFunc(pluginYAML string, err error) error {
slog.Warn("failed to load plugin (ignoring)", slog.String("plugin_yaml", pluginYAML), slog.Any("error", err))
return nil
}
// errorFilterFunc is a function that can filter errors during plugin loading
type ErrorFilterFunc func(string, error) error
// LoadAllDir load all plugins found beneath the base directory, using the provided error filter to determine whether to fail on individual plugin load errors.
//
// This scans only one directory level.
func LoadAll(basedir string) ([]Plugin, error) {
var plugins []Plugin
// We want basedir/*/plugin.yaml
func LoadAllDir(basedir string, errorFilter ErrorFilterFunc) ([]Plugin, error) {
// We want <basedir>/*/plugin.yaml
scanpath := filepath.Join(basedir, "*", PluginFileName)
matches, err := filepath.Glob(scanpath)
if err != nil {
return nil, fmt.Errorf("failed to search for plugins in %q: %w", scanpath, err)
}
plugins := make([]Plugin, 0, len(matches))
// empty dir should load
if len(matches) == 0 {
return plugins, nil
@ -179,9 +186,12 @@ func LoadAll(basedir string) ([]Plugin, error) {
dir := filepath.Dir(yamlFile)
p, err := LoadDir(dir)
if err != nil {
return plugins, err
if errNew := errorFilter(yamlFile, err); errNew != nil {
return plugins, errNew
}
} else {
plugins = append(plugins, p)
}
plugins = append(plugins, p)
}
return plugins, detectDuplicates(plugins)
}
@ -193,8 +203,12 @@ type findFunc func(pluginsDir string) ([]Plugin, error)
type filterFunc func(Plugin) bool
// FindPlugins returns a list of plugins that match the descriptor
// Errors loading a plugin are ignored with a warning
func FindPlugins(pluginsDirs []string, descriptor Descriptor) ([]Plugin, error) {
return findPlugins(pluginsDirs, LoadAll, makeDescriptorFilter(descriptor))
loadAllIgnoreErrors := func(pluginsDir string) ([]Plugin, error) {
return LoadAllDir(pluginsDir, LogIgnorePluginLoadErrorFilterFunc)
}
return findPlugins(pluginsDirs, loadAllIgnoreErrors, makeDescriptorFilter(descriptor))
}
// findPlugins is the internal implementation that uses the find and filter functions
@ -212,7 +226,6 @@ func findPlugins(pluginsDirs []string, findFn findFunc, filterFn filterFunc) ([]
found = append(found, p)
}
}
}
return found, nil
@ -225,7 +238,6 @@ func makeDescriptorFilter(descriptor Descriptor) filterFunc {
// If name is specified, it must match
if descriptor.Name != "" && p.Metadata().Name != descriptor.Name {
return false
}
// If type is specified, it must match
if descriptor.Type != "" && p.Metadata().Type != descriptor.Type {
@ -237,7 +249,11 @@ func makeDescriptorFilter(descriptor Descriptor) filterFunc {
// FindPlugin returns a single plugin that matches the descriptor
func FindPlugin(dirs []string, descriptor Descriptor) (Plugin, error) {
plugins, err := FindPlugins(dirs, descriptor)
loadAllIgnoreErrors := func(pluginsDir string) ([]Plugin, error) {
return LoadAllDir(pluginsDir, LogIgnorePluginLoadErrorFilterFunc)
}
plugins, err := findPlugins(dirs, loadAllIgnoreErrors, makeDescriptorFilter(descriptor))
if err != nil {
return nil, err
}

@ -62,7 +62,6 @@ name: "test-plugin"
}
func TestLoadDir(t *testing.T) {
makeMetadata := func(apiVersion string) Metadata {
usage := "hello [params]..."
if apiVersion == "legacy" {
@ -204,16 +203,16 @@ func TestDetectDuplicates(t *testing.T) {
}
}
func TestLoadAll(t *testing.T) {
// Verify that empty dir loads:
{
plugs, err := LoadAll("testdata")
require.NoError(t, err)
assert.Len(t, plugs, 0)
}
func TestLoadAllDir_Empty(t *testing.T) {
emptyDir := t.TempDir()
plugs, err := LoadAllDir(emptyDir, func(_ string, err error) error { return err })
require.NoError(t, err)
assert.Len(t, plugs, 0)
}
func TestLoadAllPluginsDir(t *testing.T) {
basedir := "testdata/plugdir/good"
plugs, err := LoadAll(basedir)
plugs, err := LoadAllDir(basedir, func(_ string, err error) error { return err })
require.NoError(t, err)
require.NotEmpty(t, plugs, "expected plugins to be loaded from %s", basedir)
@ -232,7 +231,7 @@ func TestLoadAll(t *testing.T) {
assert.Contains(t, plugsMap, "postrenderer-v1")
}
func TestFindPlugins(t *testing.T) {
func TestLoadAllPluginsDir_Zero(t *testing.T) {
cases := []struct {
name string
plugdirs string
@ -240,28 +239,20 @@ func TestFindPlugins(t *testing.T) {
}{
{
name: "plugdirs is empty",
plugdirs: "",
expected: 0,
plugdirs: t.TempDir(),
},
{
name: "plugdirs isn't dir",
plugdirs: "./plugin_test.go",
expected: 0,
},
{
name: "plugdirs doesn't have plugin",
plugdirs: ".",
expected: 0,
},
{
name: "normal",
plugdirs: "./testdata/plugdir/good",
expected: 7,
},
}
for _, c := range cases {
t.Run(t.Name(), func(t *testing.T) {
plugin, err := LoadAll(c.plugdirs)
plugin, err := LoadAllDir(c.plugdirs, func(_ string, err error) error { return err })
require.NoError(t, err)
assert.Len(t, plugin, c.expected, "expected %d plugins, got %d", c.expected, len(plugin))
})
@ -337,6 +328,7 @@ runtime: subprocess
"correct name field": {
yaml: `apiVersion: v1
name: my-plugin
version: 1.0.0
type: cli/v1
runtime: subprocess
`,

@ -19,9 +19,17 @@ import (
"errors"
"fmt"
"github.com/Masterminds/semver/v3"
"helm.sh/helm/v4/internal/plugin/schema"
)
// isValidSemver checks if the given string is a valid semantic version
func isValidSemver(v string) bool {
_, err := semver.StrictNewVersion(v)
return err == nil
}
// Metadata of a plugin, converted from the "on-disk" legacy or v1 plugin.yaml
// Specifically, Config and RuntimeConfig are converted to their respective types based on the plugin type and runtime
type Metadata struct {
@ -57,6 +65,11 @@ func (m Metadata) Validate() error {
errs = append(errs, fmt.Errorf("invalid plugin name %q: must contain only a-z, A-Z, 0-9, _ and -", m.Name))
}
// Require version to be valid semver if specified
if m.Version != "" && !isValidSemver(m.Version) {
errs = append(errs, fmt.Errorf("invalid plugin version %q: must be valid semver", m.Version))
}
if m.APIVersion == "" {
errs = append(errs, errors.New("empty APIVersion"))
}

@ -72,6 +72,11 @@ func (m *MetadataLegacy) Validate() error {
if !validPluginName.MatchString(m.Name) {
return fmt.Errorf("invalid plugin name %q: must contain only a-z, A-Z, 0-9, _ and -", m.Name)
}
if m.Version != "" && !isValidSemver(m.Version) {
return fmt.Errorf("invalid plugin version %q: must be valid semver", m.Version)
}
m.Usage = sanitizeString(m.Usage)
if len(m.PlatformCommand) > 0 && len(m.Command) > 0 {

@ -26,6 +26,10 @@ func TestMetadataLegacyValidate(t *testing.T) {
"valid metadata": {
Name: "myplugin",
},
"valid metadata (empty version)": {
Name: "myplugin",
Version: "",
},
"valid with command": {
Name: "myplugin",
Command: "echo hello",
@ -59,6 +63,13 @@ func TestMetadataLegacyValidate(t *testing.T) {
},
},
},
"valid with version": {
Name: "myplugin",
Version: "1.0.0",
},
"valid with empty version": {
Name: "myplugin",
},
}
for testName, metadata := range testsValid {
@ -116,6 +127,14 @@ func TestMetadataLegacyValidate(t *testing.T) {
},
},
},
"path traversal version": {
Name: "myplugin",
Version: "../../../../tmp/evil",
},
"invalid version": {
Name: "myplugin",
Version: "not-a-version",
},
}
for testName, metadata := range testsInvalid {

@ -18,10 +18,11 @@ package plugin
import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
func TestValidatePluginData(t *testing.T) {
// A mock plugin with no commands
mockNoCommand := mockSubprocessCLIPlugin(t, "foo")
mockNoCommand.metadata.RuntimeConfig = &RuntimeConfigSubprocess{
@ -72,6 +73,43 @@ func TestValidatePluginData(t *testing.T) {
}
}
func TestMetadataValidateVersion(t *testing.T) {
testValid := map[string]struct {
version string
}{
"valid semver": {version: "1.0.0"},
"valid semver with prerelease": {version: "1.2.3-alpha.1+build.123"},
"empty version": {version: ""},
}
testInvalid := map[string]struct {
version string
}{
"valid semver with v prefix": {version: "v1.0.0"},
"path traversal": {version: "../../../../tmp/evil"},
"path traversal in version": {version: "1.0.0/../../etc"},
"not a version": {version: "not-a-version"},
}
for name, tc := range testValid {
t.Run(name, func(t *testing.T) {
m := mockSubprocessCLIPlugin(t, "testplugin")
m.metadata.Version = tc.version
err := m.Metadata().Validate()
assert.NoError(t, err)
})
}
for name, tc := range testInvalid {
t.Run(name, func(t *testing.T) {
m := mockSubprocessCLIPlugin(t, "testplugin")
m.metadata.Version = tc.version
err := m.Metadata().Validate()
assert.ErrorContains(t, err, "invalid plugin version")
})
}
}
func TestMetadataValidateMultipleErrors(t *testing.T) {
// Create metadata with multiple validation issues
metadata := Metadata{

@ -52,6 +52,13 @@ func (m *MetadataV1) Validate() error {
return errors.New("invalid plugin `name`")
}
if m.Version == "" {
return errors.New("plugin `version` is required")
}
if !isValidSemver(m.Version) {
return fmt.Errorf("invalid plugin `version` %q: must be valid semver", m.Version)
}
if m.APIVersion != "v1" {
return fmt.Errorf("invalid `apiVersion`: %q", m.APIVersion)
}

@ -0,0 +1,85 @@
/*
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 plugin
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestMetadataV1ValidateVersion(t *testing.T) {
base := func() MetadataV1 {
return MetadataV1{
APIVersion: "v1",
Name: "myplugin",
Type: "cli/v1",
Runtime: "subprocess",
Version: "1.0.0",
}
}
testsValid := map[string]string{
"simple version": "1.0.0",
"with prerelease": "1.2.3-alpha.1",
"with build meta": "1.2.3+build.123",
"full prerelease": "1.2.3-alpha.1+build.123",
}
for name, version := range testsValid {
t.Run("valid/"+name, func(t *testing.T) {
m := base()
m.Version = version
assert.NoError(t, m.Validate())
})
}
testsInvalid := map[string]struct {
version string
errMsg string
}{
"empty version": {
version: "",
errMsg: "plugin `version` is required",
},
"v prefix": {
version: "v1.0.0",
errMsg: "invalid plugin `version` \"v1.0.0\": must be valid semver",
},
"path traversal": {
version: "../../../../tmp/evil",
errMsg: "invalid plugin `version`",
},
"path traversal etc": {
version: "../../../etc/passwd",
errMsg: "invalid plugin `version`",
},
"not a version": {
version: "not-a-version",
errMsg: "invalid plugin `version`",
},
}
for name, tc := range testsInvalid {
t.Run("invalid/"+name, func(t *testing.T) {
m := base()
m.Version = tc.version
err := m.Validate()
assert.Error(t, err)
assert.Contains(t, err.Error(), tc.errMsg)
})
}
}

@ -77,5 +77,5 @@ type Output struct {
// validPluginName is a regular expression that validates plugin names.
//
// Plugin names can only contain the ASCII characters a-z, A-Z, 0-9, _ and -.
// Plugin names can only contain the ASCII characters a-z, A-Z, 0-9, _ and -.
var validPluginName = regexp.MustCompile("^[A-Za-z0-9_-]+$")

@ -82,7 +82,7 @@ func mockSubprocessCLIPlugin(t *testing.T, pluginName string) *SubprocessPluginR
return &SubprocessPluginRuntime{
metadata: Metadata{
Name: pluginName,
Version: "v0.1.2",
Version: "0.1.2",
Type: "cli/v1",
APIVersion: "v1",
Runtime: "subprocess",

@ -28,7 +28,6 @@ func TestMakeOutputMessage(t *testing.T) {
ptm := pluginTypesIndex["getter/v1"]
outputType := reflect.Zero(ptm.outputType).Interface()
assert.IsType(t, schema.OutputMessageGetterV1{}, outputType)
}
func TestMakeConfig(t *testing.T) {

@ -99,7 +99,6 @@ type RuntimeExtismV1 struct {
var _ Runtime = (*RuntimeExtismV1)(nil)
func (r *RuntimeExtismV1) CreatePlugin(pluginDir string, metadata *Metadata) (Plugin, error) {
rc, ok := metadata.RuntimeConfig.(*RuntimeConfigExtismV1)
if !ok {
return nil, fmt.Errorf("invalid extism/v1 plugin runtime config type: %T", metadata.RuntimeConfig)
@ -139,7 +138,6 @@ func (p *ExtismV1PluginRuntime) Dir() string {
}
func (p *ExtismV1PluginRuntime) Invoke(ctx context.Context, input *Input) (*Output, error) {
var tmpDir string
if p.rc.FileSystem.CreateTempDir {
tmpDirInner, err := os.MkdirTemp(os.TempDir(), "helm-plugin-*")

@ -79,7 +79,7 @@ func TestRuntimeExtismV1InvokePlugin(t *testing.T) {
Name: "Phippy",
},
})
require.Nil(t, err)
require.NoError(t, err)
msg := output.Message.(schema.OutputMessageTestV1)
assert.Equal(t, "Hello, Phippy! (6)", msg.Greeting)

@ -42,7 +42,7 @@ func mockSubprocessCLIPluginErrorExit(t *testing.T, pluginName string, exitCode
md := Metadata{
Name: pluginName,
Version: "v0.1.2",
Version: "0.1.2",
Type: "cli/v1",
APIVersion: "v1",
Runtime: "subprocess",

@ -48,7 +48,6 @@ func TestPrepareCommand(t *testing.T) {
}
func TestPrepareCommandExtraArgs(t *testing.T) {
cmdMain := "sh"
cmdArgs := []string{"-c", "echo \"test\""}
platformCommand := []PlatformCommand{

@ -21,6 +21,7 @@ import (
"regexp"
"strconv"
"strings"
"unicode"
)
// SimpleHead defines what the structure of the head of a manifest file
@ -35,7 +36,16 @@ type SimpleHead struct {
var sep = regexp.MustCompile("(?:^|\\s*\n)---\\s*")
// SplitManifests takes a string of manifest and returns a map contains individual manifests
// SplitManifests takes a manifest string and returns a map containing individual manifests.
//
// **Note for Chart API v3**: This function (due to the regex above) has allowed _WRONG_
// Go templates to be defined inside charts across the years. The generated text from Go
// templates may contain `---apiVersion: v1`, and this function magically splits this back
// to `---\napiVersion: v1`. This has caused issues recently after Helm 4 introduced
// kio.ParseAll to inject annotations when post-renderers are used. In Chart API v3,
// we should kill this regex with fire (or change it) and expose charts doing the wrong
// thing Go template-wise. Helm should say a big _NO_ to charts doing the wrong thing,
// with or without post-renderers.
func SplitManifests(bigFile string) map[string]string {
// Basically, we're quickly splitting a stream of YAML documents into an
// array of YAML docs. The file name is just a place holder, but should be
@ -44,15 +54,15 @@ func SplitManifests(bigFile string) map[string]string {
tpl := "manifest-%d"
res := map[string]string{}
// Making sure that any extra whitespace in YAML stream doesn't interfere in splitting documents correctly.
bigFileTmp := strings.TrimSpace(bigFile)
bigFileTmp := strings.TrimLeftFunc(bigFile, unicode.IsSpace)
docs := sep.Split(bigFileTmp, -1)
var count int
for _, d := range docs {
if d == "" {
if strings.TrimSpace(d) == "" {
continue
}
d = strings.TrimSpace(d)
d = strings.TrimLeftFunc(d, unicode.IsSpace)
res[fmt.Sprintf(tpl, count)] = d
count = count + 1
}

@ -26,7 +26,6 @@ import (
)
func TestSortManifests(t *testing.T) {
data := []struct {
name []string
path string
@ -183,7 +182,6 @@ metadata:
if !reflect.DeepEqual(expectedHooks, out.Events) {
t.Errorf("expected events: %v but got: %v", expectedHooks, out.Events)
}
}
}
if !found {

@ -21,7 +21,15 @@ import (
"testing"
)
const mockManifestFile = `
func TestSplitManifests(t *testing.T) {
tests := []struct {
name string
input string
expected map[string]string
}{
{
name: "single doc with leading separator and whitespace",
input: `
---
apiVersion: v1
@ -35,9 +43,9 @@ spec:
- name: nemo-test
image: fake-image
cmd: fake-command
`
const expectedManifest = `apiVersion: v1
`,
expected: map[string]string{
"manifest-0": `apiVersion: v1
kind: Pod
metadata:
name: finding-nemo,
@ -47,15 +55,463 @@ spec:
containers:
- name: nemo-test
image: fake-image
cmd: fake-command`
cmd: fake-command
`,
},
},
{
name: "empty input",
input: "",
expected: map[string]string{},
},
{
name: "whitespace only",
input: " \n\n \n",
expected: map[string]string{},
},
{
name: "whitespace-only doc after separator is skipped",
input: "---\napiVersion: v1\nkind: ConfigMap\nmetadata:\n name: cm1\n---\n \n",
expected: map[string]string{
"manifest-0": "apiVersion: v1\nkind: ConfigMap\nmetadata:\n name: cm1",
},
},
{
name: "single doc no separator",
input: `
apiVersion: v1
kind: ConfigMap
metadata:
name: test
`,
expected: map[string]string{
"manifest-0": `apiVersion: v1
kind: ConfigMap
metadata:
name: test
`,
},
},
{
name: "two docs with proper separator",
input: `
apiVersion: v1
kind: ConfigMap
metadata:
name: cm1
---
apiVersion: v1
kind: ConfigMap
metadata:
name: cm2
`,
expected: map[string]string{
"manifest-0": `apiVersion: v1
kind: ConfigMap
metadata:
name: cm1`,
"manifest-1": `apiVersion: v1
kind: ConfigMap
metadata:
name: cm2
`,
},
},
func TestSplitManifest(t *testing.T) {
manifests := SplitManifests(mockManifestFile)
if len(manifests) != 1 {
t.Errorf("Expected 1 manifest, got %v", len(manifests))
// Block scalar chomping indicator tests using | (clip), |- (strip), and |+ (keep)
// inputs with 0, 1, and 2 trailing newlines after the block content.
// Note: the emitter may normalize the output chomping indicator when the
// trailing newline count makes another indicator equivalent for the result.
// | (clip) input — clips trailing newlines to exactly one, though with
// 0 trailing newlines the emitted output may normalize to |-.
{
name: "block scalar clip (|) with 0 trailing newlines",
input: `
apiVersion: v1
kind: ConfigMap
metadata:
name: test
data:
key: |
hello`,
expected: map[string]string{
"manifest-0": `apiVersion: v1
kind: ConfigMap
metadata:
name: test
data:
key: |
hello`,
},
},
{
name: "block scalar clip (|) with 1 trailing newline",
input: `
apiVersion: v1
kind: ConfigMap
metadata:
name: test
data:
key: |
hello
`,
expected: map[string]string{
"manifest-0": `apiVersion: v1
kind: ConfigMap
metadata:
name: test
data:
key: |
hello
`,
},
},
{
name: "block scalar clip (|) with 2 trailing newlines",
input: `
apiVersion: v1
kind: ConfigMap
metadata:
name: test
data:
key: |
hello
`,
expected: map[string]string{
"manifest-0": `apiVersion: v1
kind: ConfigMap
metadata:
name: test
data:
key: |
hello
`,
},
},
// |- (strip)
{
name: "block scalar strip (|-) with 0 trailing newlines",
input: `
apiVersion: v1
kind: ConfigMap
metadata:
name: test
data:
key: |-
hello`,
expected: map[string]string{
"manifest-0": `apiVersion: v1
kind: ConfigMap
metadata:
name: test
data:
key: |-
hello`,
},
},
{
name: "block scalar strip (|-) with 1 trailing newline",
input: `
apiVersion: v1
kind: ConfigMap
metadata:
name: test
data:
key: |-
hello
`,
expected: map[string]string{
"manifest-0": `apiVersion: v1
kind: ConfigMap
metadata:
name: test
data:
key: |-
hello
`,
},
},
{
name: "block scalar strip (|-) with 2 trailing newlines",
input: `
apiVersion: v1
kind: ConfigMap
metadata:
name: test
data:
key: |-
hello
`,
expected: map[string]string{
"manifest-0": `apiVersion: v1
kind: ConfigMap
metadata:
name: test
data:
key: |-
hello
`,
},
},
// |+ (keep)
{
name: "block scalar keep (|+) with 0 trailing newlines",
input: `
apiVersion: v1
kind: ConfigMap
metadata:
name: test
data:
key: |+
hello`,
expected: map[string]string{
"manifest-0": `apiVersion: v1
kind: ConfigMap
metadata:
name: test
data:
key: |+
hello`,
},
},
{
name: "block scalar keep (|+) with 1 trailing newline",
input: `
apiVersion: v1
kind: ConfigMap
metadata:
name: test
data:
key: |+
hello
`,
expected: map[string]string{
"manifest-0": `apiVersion: v1
kind: ConfigMap
metadata:
name: test
data:
key: |+
hello
`,
},
},
{
name: "block scalar keep (|+) with 2 trailing newlines",
input: `
apiVersion: v1
kind: ConfigMap
metadata:
name: test
data:
key: |+
hello
`,
expected: map[string]string{
"manifest-0": `apiVersion: v1
kind: ConfigMap
metadata:
name: test
data:
key: |+
hello
`,
},
},
// Multi-doc with block scalars: the regex consumes \s*\n before ---,
// so trailing newlines from non-last docs are stripped.
{
name: "multi-doc block scalar clip (|) before separator",
input: `
apiVersion: v1
kind: ConfigMap
metadata:
name: test
data:
key: |
hello
---
apiVersion: v1
kind: ConfigMap
metadata:
name: test2
`,
expected: map[string]string{
"manifest-0": `apiVersion: v1
kind: ConfigMap
metadata:
name: test
data:
key: |
hello`,
"manifest-1": `apiVersion: v1
kind: ConfigMap
metadata:
name: test2
`,
},
},
{
name: "multi-doc block scalar keep (|+) with 2 trailing newlines before separator",
input: `
apiVersion: v1
kind: ConfigMap
metadata:
name: test
data:
key: |+
hello
---
apiVersion: v1
kind: ConfigMap
metadata:
name: test2
`,
expected: map[string]string{
"manifest-0": `apiVersion: v1
kind: ConfigMap
metadata:
name: test
data:
key: |+
hello`,
"manifest-1": `apiVersion: v1
kind: ConfigMap
metadata:
name: test2
`,
},
},
// **Note for Chart API v3**: The following tests exercise the lenient
// regex that splits `---apiVersion` back into separate documents.
// In Chart API v3, these inputs should return an _ERROR_ instead.
// See the comment on the SplitManifests function for more details.
{
name: "leading glued separator (---apiVersion)",
input: `
---apiVersion: v1
kind: ConfigMap
metadata:
name: cm1
`,
expected: map[string]string{
"manifest-0": `apiVersion: v1
kind: ConfigMap
metadata:
name: cm1
`,
},
},
{
name: "mid-content glued separator (---apiVersion)",
input: `
apiVersion: v1
kind: ConfigMap
metadata:
name: cm1
---apiVersion: v1
kind: ConfigMap
metadata:
name: cm2
`,
expected: map[string]string{
"manifest-0": `apiVersion: v1
kind: ConfigMap
metadata:
name: cm1`,
"manifest-1": `apiVersion: v1
kind: ConfigMap
metadata:
name: cm2
`,
},
},
{
name: "multiple glued separators",
input: `
---apiVersion: v1
kind: ConfigMap
metadata:
name: cm1
---apiVersion: v1
kind: ConfigMap
metadata:
name: cm2
---apiVersion: v1
kind: ConfigMap
metadata:
name: cm3
`,
expected: map[string]string{
"manifest-0": `apiVersion: v1
kind: ConfigMap
metadata:
name: cm1`,
"manifest-1": `apiVersion: v1
kind: ConfigMap
metadata:
name: cm2`,
"manifest-2": `apiVersion: v1
kind: ConfigMap
metadata:
name: cm3
`,
},
},
{
name: "mixed glued and proper separators",
input: `
apiVersion: v1
kind: ConfigMap
metadata:
name: cm1
---
apiVersion: v1
kind: ConfigMap
metadata:
name: cm2
---apiVersion: v1
kind: ConfigMap
metadata:
name: cm3
`,
expected: map[string]string{
"manifest-0": `apiVersion: v1
kind: ConfigMap
metadata:
name: cm1`,
"manifest-1": `apiVersion: v1
kind: ConfigMap
metadata:
name: cm2`,
"manifest-2": `apiVersion: v1
kind: ConfigMap
metadata:
name: cm3
`,
},
},
}
expected := map[string]string{"manifest-0": expectedManifest}
if !reflect.DeepEqual(manifests, expected) {
t.Errorf("Expected %v, got %v", expected, manifests)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := SplitManifests(tt.input)
if !reflect.DeepEqual(result, tt.expected) {
t.Errorf("SplitManifests() =\n%v\nwant:\n%v", result, tt.expected)
}
})
}
}

@ -54,7 +54,6 @@ func New(chartpath, cachepath string, registryClient *registry.Client) *Resolver
// Resolve resolves dependencies and returns a lock file with the resolution.
func (r *Resolver) Resolve(reqs []*chart.Dependency, repoNames map[string]string) (*chart.Lock, error) {
// Now we clone the dependencies, locking as we go.
locked := make([]*chart.Dependency, len(reqs))
missing := []string{}
@ -146,7 +145,6 @@ func (r *Resolver) Resolve(reqs []*chart.Dependency, repoNames map[string]string
Version: version,
},
}}
} else {
// Retrieve list of tags for repository
ref := fmt.Sprintf("%s/%s", strings.TrimPrefix(d.Repository, registry.OCIScheme+"://"), d.Name)

@ -238,7 +238,6 @@ func TestCopyDirFail_SrcIsNotDir(t *testing.T) {
if !errors.Is(err, errSrcNotDir) {
t.Fatalf("expected %v error for CopyDir(%s, %s), got %s", errSrcNotDir, srcdir, dstdir, err)
}
}
func TestCopyDirFail_DstExists(t *testing.T) {
@ -491,7 +490,6 @@ func setupInaccessibleDir(t *testing.T, op func(dir string) error) func() {
}
func TestIsDir(t *testing.T) {
var currentUID = os.Getuid()
if currentUID == 0 {
@ -544,7 +542,6 @@ func TestIsDir(t *testing.T) {
}
func TestIsSymlink(t *testing.T) {
var currentUID = os.Getuid()
if currentUID == 0 {

@ -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 = ""
@ -77,7 +77,6 @@ func GetUserAgent() string {
// Get returns build info
func Get() BuildInfo {
makeKubeClientVersionString := func() string {
// Test builds don't include debug info / module info
// (And even if they did, we probably want a stable version during tests anyway)

@ -27,6 +27,7 @@ import (
"path"
"path/filepath"
"slices"
"sort"
"strings"
"sync"
"text/template"
@ -87,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.
@ -144,39 +172,6 @@ const (
filenameAnnotation = "postrenderer.helm.sh/postrender-filename"
)
// fixDocSeparators ensures YAML document separators ("---") are always
// followed by a newline in rendered template content. Go template whitespace
// trimming ({{-) can remove the newline after "---", producing e.g.
// "---apiVersion: v1" which is not a valid YAML document separator.
// This function inserts a newline after any "---" at the start of a line
// that is immediately followed by non-whitespace content.
func fixDocSeparators(content string) string {
var b strings.Builder
remaining := content
for {
// Find "---" at the start of a line (or start of content).
idx := strings.Index(remaining, "---")
if idx == -1 {
b.WriteString(remaining)
break
}
// "---" must be at the start of a line: either idx==0 or preceded by '\n'.
if idx > 0 && remaining[idx-1] != '\n' {
b.WriteString(remaining[:idx+3])
remaining = remaining[idx+3:]
continue
}
b.WriteString(remaining[:idx+3])
remaining = remaining[idx+3:]
// If "---" is followed by non-whitespace (e.g. "---apiVersion"),
// insert a newline to make it a proper document separator.
if len(remaining) > 0 && remaining[0] != '\n' && remaining[0] != '\r' && remaining[0] != ' ' && remaining[0] != '\t' {
b.WriteByte('\n')
}
}
return b.String()
}
// annotateAndMerge combines multiple YAML files into a single stream of documents,
// adding filename annotations to each document for later reconstruction.
func annotateAndMerge(files map[string]string) (string, error) {
@ -192,22 +187,32 @@ func annotateAndMerge(files map[string]string) (string, error) {
continue
}
// Fix document separators where Go template whitespace trimming
// ({{-) has removed the newline after "---", producing e.g.
// "---apiVersion: v1" which is not a valid YAML document
// separator. Insert the missing newline so kio.ParseAll can
// parse the content correctly.
content = fixDocSeparators(content)
manifests, err := kio.ParseAll(content)
if err != nil {
return "", fmt.Errorf("parsing %s: %w", fname, err)
// For consistency with the non-post-renderers code path, we need
// to use releaseutil.SplitManifests here to split the file into
// individual documents before feeding them to kio.ParseAll. In
// Chart API before v3 this function had leniency for badly-written
// Go templates, so this must be preserved for older charts.
splitDocs := releaseutil.SplitManifests(content)
keys := make([]string, 0, len(splitDocs))
for k := range splitDocs {
keys = append(keys, k)
}
for _, manifest := range manifests {
if err := manifest.PipeE(kyaml.SetAnnotation(filenameAnnotation, fname)); err != nil {
return "", fmt.Errorf("annotating %s: %w", fname, err)
sort.Sort(releaseutil.BySplitManifestsOrder(keys))
for _, key := range keys {
doc := splitDocs[key]
if strings.TrimSpace(doc) == "" {
continue
}
manifests, err := kio.ParseAll(doc)
if err != nil {
return "", fmt.Errorf("parsing %s: %w", fname, err)
}
for _, manifest := range manifests {
if err := manifest.PipeE(kyaml.SetAnnotation(filenameAnnotation, fname)); err != nil {
return "", fmt.Errorf("annotating %s: %w", fname, err)
}
combinedManifests = append(combinedManifests, manifest)
}
combinedManifests = append(combinedManifests, manifest)
}
}
@ -220,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-<fallbackPrefix>-<i>.yaml" (or
// "generated-by-postrender-<i>.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)
@ -234,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)
@ -259,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)
@ -323,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)
}
}
@ -521,7 +630,6 @@ func GetVersionSet(client discovery.ServerResourcesInterface) (common.VersionSet
var ok bool
for _, r := range resources {
for _, rl := range r.APIResources {
// A Kind at a GroupVersion can show up more than once. We only want
// it displayed once in the final output.
id = path.Join(r.GroupVersion, rl.Kind)

File diff suppressed because it is too large Load Diff

@ -120,7 +120,6 @@ func (d *Dependency) dependencyStatus(chartpath string, dep *chart.Dependency, p
if r := statArchiveForStatus(archive, dep); r != "" {
return r
}
}
// End unnecessary code.

@ -119,7 +119,6 @@ func (m *Metadata) FormattedDepNames() string {
continue
}
depsNames = append(depsNames, ac.Name())
}
sort.StringSlice(depsNames).Sort()

@ -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) {

@ -35,7 +35,6 @@ import (
func (cfg *Configuration) execHook(rl *release.Release, hook release.HookEvent,
waitStrategy kube.WaitStrategy, waitOptions []kube.WaitOption,
timeout time.Duration, serverSideApply bool) error {
shutdown, err := cfg.execHookWithDelayedShutdown(rl, hook, waitStrategy, waitOptions, timeout, serverSideApply)
if shutdown == nil {
return err
@ -59,7 +58,6 @@ func shutdownNoOp() error {
func (cfg *Configuration) execHookWithDelayedShutdown(rl *release.Release, hook release.HookEvent,
waitStrategy kube.WaitStrategy, waitOptions []kube.WaitOption, timeout time.Duration,
serverSideApply bool) (ExecuteShutdownFunc, error) {
executingHooks := []*release.Hook{}
for _, h := range rl.Hooks {
@ -150,8 +148,8 @@ func (cfg *Configuration) execHookWithDelayedShutdown(rl *release.Release, hook
return func() error {
// If all hooks are successful, check the annotation of each hook to determine whether the hook should be deleted
// or output should be logged under succeeded condition. If so, then clear the corresponding resource object in each hook
for i := len(executingHooks) - 1; i >= 0; i-- {
h := executingHooks[i]
for _, v := range slices.Backward(executingHooks) {
h := v
if err := cfg.outputLogsByPolicy(h, rl.Namespace, release.HookOutputOnSucceeded); err != nil {
// We log here as we still want to attempt hook resource deletion even if output logging fails.
log.Printf("error outputting logs for hook failure: %v", err)
@ -179,7 +177,6 @@ func (x hookByWeight) Less(i, j int) bool {
// deleteHookByPolicy deletes a hook if the hook policy instructs it to
func (cfg *Configuration) deleteHookByPolicy(h *release.Hook, policy release.HookDeletePolicy,
waitStrategy kube.WaitStrategy, waitOptions []kube.WaitOption, timeout time.Duration) error {
// Never delete CustomResourceDefinitions; this could cause lots of
// cascading garbage collection.
if h.Kind == "CustomResourceDefinition" {
@ -214,7 +211,6 @@ func (cfg *Configuration) deleteHookByPolicy(h *release.Hook, policy release.Hoo
// deleteHooksByPolicy deletes all hooks if the hook policy instructs it to
func (cfg *Configuration) deleteHooksByPolicy(hooks []*release.Hook, policy release.HookDeletePolicy,
waitStrategy kube.WaitStrategy, waitOptions []kube.WaitOption, timeout time.Duration) error {
for _, h := range hooks {
if err := cfg.deleteHookByPolicy(h, policy, waitStrategy, waitOptions, timeout); err != nil {
return err

@ -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()

@ -737,7 +737,6 @@ func TestInstallRelease_RollbackOnFailure(t *testing.T) {
})
}
func TestInstallRelease_RollbackOnFailure_Interrupted(t *testing.T) {
is := assert.New(t)
instAction := installAction(t)
instAction.ReleaseName = "interrupted-release"
@ -767,7 +766,6 @@ func TestInstallRelease_RollbackOnFailure_Interrupted(t *testing.T) {
is.Equal(goroutines+1, instAction.getGoroutineCount()) // installation goroutine still is in background
time.Sleep(10 * time.Second) // wait for goroutine to finish
is.Equal(goroutines, instAction.getGoroutineCount())
}
func TestNameTemplate(t *testing.T) {
testCases := []nameTemplateTestCase{
@ -804,7 +802,6 @@ func TestNameTemplate(t *testing.T) {
}
for _, tc := range testCases {
n, err := TemplateName(tc.tpl)
if err != nil {
if tc.expectedErrorStr == "" {
@ -867,7 +864,7 @@ func TestInstallReleaseOutputDir(t *testing.T) {
test.AssertGoldenFile(t, filepath.Join(dir, "hello/templates/rbac"), "rbac.txt")
_, err = os.Stat(filepath.Join(dir, "hello/templates/empty"))
is.True(errors.Is(err, fs.ErrNotExist))
is.ErrorIs(err, fs.ErrNotExist)
}
func TestInstallOutputDirWithReleaseName(t *testing.T) {
@ -903,7 +900,7 @@ func TestInstallOutputDirWithReleaseName(t *testing.T) {
test.AssertGoldenFile(t, filepath.Join(newDir, "hello/templates/rbac"), "rbac.txt")
_, err = os.Stat(filepath.Join(newDir, "hello/templates/empty"))
is.True(errors.Is(err, fs.ErrNotExist))
is.ErrorIs(err, fs.ErrNotExist)
}
func TestNameAndChart(t *testing.T) {
@ -1168,7 +1165,7 @@ func TestInstallCRDs_AlreadyExist(t *testing.T) {
mockChart := buildChart(withFile(mockFile))
crdsToInstall := mockChart.CRDObjects()
assert.Nil(t, instAction.installCRDs(crdsToInstall))
assert.NoError(t, instAction.installCRDs(crdsToInstall))
}
func TestInstallCRDs_KubeClient_BuildError(t *testing.T) {
@ -1227,7 +1224,7 @@ func TestCheckDependencies(t *testing.T) {
dependency := chart.Dependency{Name: "hello"}
mockChart := buildChart(withDependency())
assert.Nil(t, CheckDependencies(mockChart, []ci.Dependency{&dependency}))
assert.NoError(t, CheckDependencies(mockChart, []ci.Dependency{&dependency}))
}
func TestCheckDependencies_MissingDependency(t *testing.T) {

@ -150,7 +150,6 @@ func TestValidateVersion(t *testing.T) {
if !errors.Is(err, tt.wantErr) {
t.Errorf("Expected {%v}, got {%v}", tt.wantErr, err)
}
}
})
}

@ -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) {

@ -37,7 +37,7 @@ func TestWithCertFile(t *testing.T) {
certFile := "testdata/cert.pem"
opt := WithCertFile(certFile)
assert.Nil(t, opt(client))
assert.NoError(t, opt(client))
assert.Equal(t, certFile, client.certFile)
}
@ -47,8 +47,8 @@ func TestWithInsecure(t *testing.T) {
opt := WithInsecure(true)
assert.Nil(t, opt(client))
assert.Equal(t, true, client.insecure)
assert.NoError(t, opt(client))
assert.True(t, client.insecure)
}
func TestWithKeyFile(t *testing.T) {
@ -58,7 +58,7 @@ func TestWithKeyFile(t *testing.T) {
keyFile := "testdata/key.pem"
opt := WithKeyFile(keyFile)
assert.Nil(t, opt(client))
assert.NoError(t, opt(client))
assert.Equal(t, keyFile, client.keyFile)
}
@ -69,7 +69,7 @@ func TestWithCAFile(t *testing.T) {
caFile := "testdata/ca.pem"
opt := WithCAFile(caFile)
assert.Nil(t, opt(client))
assert.NoError(t, opt(client))
assert.Equal(t, caFile, client.caFile)
}
@ -79,6 +79,6 @@ func TestWithPlainHTTPLogin(t *testing.T) {
opt := WithPlainHTTPLogin(true)
assert.Nil(t, opt(client))
assert.Equal(t, true, client.plainHTTP)
assert.NoError(t, opt(client))
assert.True(t, client.plainHTTP)
}

@ -18,6 +18,7 @@ package action
import (
"context"
"errors"
"fmt"
"io"
"slices"
@ -25,6 +26,8 @@ import (
"time"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
chartutil "helm.sh/helm/v4/pkg/chart/v2/util"
"helm.sh/helm/v4/pkg/kube"
@ -124,9 +127,9 @@ func (r *ReleaseTesting) GetPodLogs(out io.Writer, rel *release.Release) error {
return fmt.Errorf("unable to get kubernetes client to fetch pod logs: %w", err)
}
hooksByWight := append([]*release.Hook{}, rel.Hooks...)
sort.Stable(hookByWeight(hooksByWight))
for _, h := range hooksByWight {
hooksByWeight := append([]*release.Hook{}, rel.Hooks...)
sort.Stable(hookByWeight(hooksByWeight))
for _, h := range hooksByWeight {
for _, e := range h.Events {
if e == release.HookTest {
if slices.Contains(r.Filters[ExcludeNameFilter], h.Name) {
@ -135,20 +138,43 @@ func (r *ReleaseTesting) GetPodLogs(out io.Writer, rel *release.Release) error {
if len(r.Filters[IncludeNameFilter]) > 0 && !slices.Contains(r.Filters[IncludeNameFilter], h.Name) {
continue
}
req := client.CoreV1().Pods(r.Namespace).GetLogs(h.Name, &v1.PodLogOptions{})
logReader, err := req.Stream(context.Background())
if err != nil {
return fmt.Errorf("unable to get pod logs for %s: %w", h.Name, err)
}
fmt.Fprintf(out, "POD LOGS: %s\n", h.Name)
_, err = io.Copy(out, logReader)
fmt.Fprintln(out)
if err != nil {
return fmt.Errorf("unable to write pod logs for %s: %w", h.Name, err)
if err := r.getContainerLogs(out, client, h.Name); err != nil {
return err
}
}
}
}
return nil
}
// getContainerLogs fetches logs from all containers (init and regular) in the
// named pod and writes them to out. It continues on per-container errors and
// returns all of them joined at the end.
func (r *ReleaseTesting) getContainerLogs(out io.Writer, client kubernetes.Interface, podName string) error {
pod, err := client.CoreV1().Pods(r.Namespace).Get(context.Background(), podName, metav1.GetOptions{})
if err != nil {
return fmt.Errorf("unable to get pod %s: %w", podName, err)
}
allContainers := append(pod.Spec.InitContainers, pod.Spec.Containers...)
var errs []error
for _, c := range allContainers {
opts := &v1.PodLogOptions{Container: c.Name}
req := client.CoreV1().Pods(r.Namespace).GetLogs(podName, opts)
logReader, err := req.Stream(context.Background())
if err != nil {
errs = append(errs, fmt.Errorf("unable to get logs for pod %s, container %s: %w", podName, c.Name, err))
continue
}
fmt.Fprintf(out, "POD LOGS: %s (%s)\n", podName, c.Name)
_, err = io.Copy(out, logReader)
logReader.Close()
fmt.Fprintln(out)
if err != nil {
errs = append(errs, fmt.Errorf("unable to write logs for pod %s, container %s: %w", podName, c.Name, err))
}
}
return errors.Join(errs...)
}

@ -26,6 +26,9 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
fakeclientset "k8s.io/client-go/kubernetes/fake"
"helm.sh/helm/v4/pkg/cli"
"helm.sh/helm/v4/pkg/kube"
@ -89,7 +92,7 @@ func TestReleaseTestingGetPodLogs_PodRetrievalError(t *testing.T) {
},
}
require.ErrorContains(t, client.GetPodLogs(&bytes.Buffer{}, &release.Release{Hooks: hooks}), "unable to get pod logs")
require.ErrorContains(t, client.GetPodLogs(&bytes.Buffer{}, &release.Release{Hooks: hooks}), "unable to get pod")
}
func TestReleaseTesting_WaitOptionsPassedDownstream(t *testing.T) {
@ -117,3 +120,91 @@ func TestReleaseTesting_WaitOptionsPassedDownstream(t *testing.T) {
// Verify that WaitOptions were passed to GetWaiter
is.NotEmpty(failer.RecordedWaitOptions, "WaitOptions should be passed to GetWaiter")
}
func TestGetContainerLogs_MultipleContainers(t *testing.T) {
pod := &v1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "test-pod",
Namespace: "default",
},
Spec: v1.PodSpec{
Containers: []v1.Container{
{Name: "main"},
{Name: "sidecar"},
},
},
}
client := fakeclientset.NewClientset(pod)
rt := &ReleaseTesting{Namespace: "default"}
var buf bytes.Buffer
err := rt.getContainerLogs(&buf, client, "test-pod")
require.NoError(t, err)
output := buf.String()
assert.Contains(t, output, "POD LOGS: test-pod (main)")
assert.Contains(t, output, "POD LOGS: test-pod (sidecar)")
}
func TestGetContainerLogs_WithInitContainers(t *testing.T) {
pod := &v1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "test-pod",
Namespace: "default",
},
Spec: v1.PodSpec{
InitContainers: []v1.Container{
{Name: "init-setup"},
},
Containers: []v1.Container{
{Name: "main"},
},
},
}
client := fakeclientset.NewClientset(pod)
rt := &ReleaseTesting{Namespace: "default"}
var buf bytes.Buffer
err := rt.getContainerLogs(&buf, client, "test-pod")
require.NoError(t, err)
output := buf.String()
// Init containers should appear before regular containers
assert.Contains(t, output, "POD LOGS: test-pod (init-setup)")
assert.Contains(t, output, "POD LOGS: test-pod (main)")
}
func TestGetContainerLogs_PodNotFound(t *testing.T) {
client := fakeclientset.NewClientset()
rt := &ReleaseTesting{Namespace: "default"}
var buf bytes.Buffer
err := rt.getContainerLogs(&buf, client, "nonexistent-pod")
require.Error(t, err)
assert.Contains(t, err.Error(), "unable to get pod nonexistent-pod")
}
func TestGetContainerLogs_OutputHeaderFormat(t *testing.T) {
pod := &v1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "multi-test",
Namespace: "default",
},
Spec: v1.PodSpec{
Containers: []v1.Container{
{Name: "container-a"},
{Name: "container-b"},
},
},
}
client := fakeclientset.NewClientset(pod)
rt := &ReleaseTesting{Namespace: "default"}
var buf bytes.Buffer
err := rt.getContainerLogs(&buf, client, "multi-test")
require.NoError(t, err)
output := buf.String()
assert.Contains(t, output, "POD LOGS: multi-test (container-a)")
assert.Contains(t, output, "POD LOGS: multi-test (container-b)")
}

@ -40,7 +40,6 @@ func filterManifestsToKeep(manifests []releaseutil.Manifest) (keep, remaining []
if resourcePolicyType == kube.KeepPolicy {
keep = append(keep, m)
}
}
return keep, remaining
}

@ -23,3 +23,4 @@ subjects:
- kind: ServiceAccount
name: schedule-agents
namespace: spaced

@ -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
@ -253,7 +258,7 @@ func (u *Upgrade) prepareUpgrade(name string, chart *chartv2.Chart, vals map[str
var cerr error
currentRelease, cerr = releaserToV1Release(currentReleasei)
if cerr != nil {
return nil, nil, false, err
return nil, nil, false, cerr
}
if err != nil {
if errors.Is(err, driver.ErrNoDeployedReleases) &&
@ -263,7 +268,6 @@ func (u *Upgrade) prepareUpgrade(name string, chart *chartv2.Chart, vals map[str
return nil, nil, false, err
}
}
}
// determine if values will be reused
@ -296,7 +300,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
}

@ -446,7 +446,6 @@ func TestUpgradeRelease_Interrupted_Wait(t *testing.T) {
}
func TestUpgradeRelease_Interrupted_RollbackOnFailure(t *testing.T) {
is := assert.New(t)
req := require.New(t)
@ -716,7 +715,7 @@ func TestGetUpgradeServerSideValue(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
serverSideApply, err := getUpgradeServerSideValue(tt.actionServerSideOption, tt.releaseApplyMethod)
assert.Nil(t, err)
assert.NoError(t, err)
assert.Equal(t, tt.expectedServerSideApply, serverSideApply)
})
}
@ -741,7 +740,6 @@ func TestGetUpgradeServerSideValue(t *testing.T) {
assert.ErrorContains(t, err, tt.expectedErrorMsg)
})
}
}
func TestUpgradeRun_UnreachableKubeClient(t *testing.T) {

@ -273,7 +273,6 @@ func TestValidateNameAndGenerateName(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
skip, err := validateNameAndGenerateName(tc.info)
if tc.wantErr {

@ -167,7 +167,6 @@ func makeDefaultCapabilities() (*Capabilities, error) {
}
func newCapabilities(kubeVersionMajor, kubeVersionMinor uint64) (*Capabilities, error) {
version := fmt.Sprintf("v%d.%d.0", kubeVersionMajor, kubeVersionMinor)
return &Capabilities{
KubeVersion: KubeVersion{

@ -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)
}
}

@ -251,6 +251,12 @@ func coalesceValues(printf printFn, c chart.Charter, v map[string]any, prefix st
// If the key is a child chart, coalesce tables with Merge set to true
merge := childChartMergeTrue(c, key, merge)
// When coalescing, clean nils from chart defaults before merging
// so they don't leak into the result.
if !merge {
cleanNilValues(src)
}
// Because v has higher precedence than nv, dest values override src
// values.
coalesceTablesFullKey(printf, dest, src, concatPrefix(subPrefix, key), merge)
@ -258,6 +264,16 @@ func coalesceValues(printf printFn, c chart.Charter, v map[string]any, prefix st
}
} else {
// If the key is not in v, copy it from nv.
// When coalescing, skip chart default nils and clean nils from
// nested maps so they don't shadow globals or produce %!s(<nil>).
if !merge {
if val == nil {
continue
}
if sub, ok := val.(map[string]any); ok {
cleanNilValues(sub)
}
}
v[key] = val
}
}
@ -326,7 +342,6 @@ func coalesceTablesFullKey(printf printFn, dst, src map[string]any, prefix strin
// But if src also has nil (or key not in src), preserve the nil
delete(dst, key)
} else if !ok {
// key not in user values, preserve src value (including nil)
dst[key] = val
} else if istable(val) {
if istable(dv) {
@ -341,6 +356,18 @@ func coalesceTablesFullKey(printf printFn, dst, src map[string]any, prefix strin
return dst
}
// cleanNilValues recursively removes nil entries in-place from a map so that chart
// default nils don't leak into the coalesced result.
func cleanNilValues(m map[string]any) {
for key, val := range m {
if val == nil {
delete(m, key)
} else if sub, ok := val.(map[string]any); ok {
cleanNilValues(sub)
}
}
}
// istable is a special-purpose function to see if the present thing matches the definition of a YAML table.
func istable(v any) bool {
_, ok := v.(map[string]any)

@ -666,7 +666,6 @@ func TestMergeTables(t *testing.T) {
}
func TestCoalesceValuesWarnings(t *testing.T) {
c := withDeps(&chart.Chart{
Metadata: &chart.Metadata{Name: "level1"},
Values: map[string]any{
@ -724,7 +723,6 @@ func TestCoalesceValuesWarnings(t *testing.T) {
assert.Contains(t, warnings, "warning: skipped value for level1.level2.level3.boat: Not a table.")
assert.Contains(t, warnings, "warning: destination for level1.level2.level3.spear.tip is a table. Ignoring non-table value (true)")
assert.Contains(t, warnings, "warning: cannot overwrite table with non table for level1.level2.level3.spear.sail (map[cotton:true])")
}
func TestConcatPrefix(t *testing.T) {
@ -765,3 +763,166 @@ func TestCoalesceValuesEmptyMapWithNils(t *testing.T) {
is.True(ok, "Expected data.baz key to be present but it was removed")
is.Nil(data["baz"], "Expected data.baz key to be nil but it is not")
}
// TestCoalesceValuesSubchartDefaultNilsCleaned tests that nil values in subchart defaults
// are cleaned up during coalescing when the parent doesn't set those keys.
// Regression test for issue #31919.
func TestCoalesceValuesSubchartDefaultNilsCleaned(t *testing.T) {
is := assert.New(t)
// Subchart has a default with nil values (e.g. keyMapping: {password: null})
subchart := &chart.Chart{
Metadata: &chart.Metadata{Name: "child"},
Values: map[string]any{
"keyMapping": map[string]any{
"password": nil,
},
},
}
parent := withDeps(&chart.Chart{
Metadata: &chart.Metadata{Name: "parent"},
Values: map[string]any{},
}, subchart)
// Parent user values don't mention keyMapping at all
vals := map[string]any{}
v, err := CoalesceValues(parent, vals)
is.NoError(err)
childVals, ok := v["child"].(map[string]any)
is.True(ok, "child values should be a map")
keyMapping, ok := childVals["keyMapping"].(map[string]any)
is.True(ok, "keyMapping should be a map")
// The nil "password" key from chart defaults should be cleaned up
_, ok = keyMapping["password"]
is.False(ok, "Expected keyMapping.password (nil from chart defaults) to be removed, but it is still present")
}
// TestCoalesceValuesUserNullErasesSubchartDefault tests that a user-supplied null
// value erases a subchart's default value during coalescing.
// Regression test for issue #31919.
func TestCoalesceValuesUserNullErasesSubchartDefault(t *testing.T) {
is := assert.New(t)
subchart := &chart.Chart{
Metadata: &chart.Metadata{Name: "child"},
Values: map[string]any{
"someKey": "default",
},
}
parent := withDeps(&chart.Chart{
Metadata: &chart.Metadata{Name: "parent"},
Values: map[string]any{},
}, subchart)
// User explicitly nullifies the subchart key via parent values
vals := map[string]any{
"child": map[string]any{
"someKey": nil,
},
}
v, err := CoalesceValues(parent, vals)
is.NoError(err)
childVals, ok := v["child"].(map[string]any)
is.True(ok, "child values should be a map")
// someKey should be erased — user null overrides subchart default
_, ok = childVals["someKey"]
is.False(ok, "Expected someKey to be removed by user null override, but it is still present")
}
// TestCoalesceValuesSubchartNilDoesNotShadowGlobal tests that a nil value in
// subchart defaults doesn't shadow a global value accessible via pluck-like access.
// Regression test for issue #31971.
func TestCoalesceValuesSubchartNilDoesNotShadowGlobal(t *testing.T) {
is := assert.New(t)
subchart := &chart.Chart{
Metadata: &chart.Metadata{Name: "child"},
Values: map[string]any{
"ingress": map[string]any{
"feature": nil, // nil in subchart defaults
},
},
}
parent := withDeps(&chart.Chart{
Metadata: &chart.Metadata{Name: "parent"},
Values: map[string]any{},
}, subchart)
// Parent sets the global value
vals := map[string]any{
"global": map[string]any{
"ingress": map[string]any{
"feature": true,
},
},
}
v, err := CoalesceValues(parent, vals)
is.NoError(err)
childVals, ok := v["child"].(map[string]any)
is.True(ok, "child values should be a map")
ingress, ok := childVals["ingress"].(map[string]any)
is.True(ok, "ingress should be a map")
// The nil "feature" from subchart defaults should be cleaned up,
// so that pluck can fall through to the global value
_, ok = ingress["feature"]
is.False(ok, "Expected ingress.feature (nil from chart defaults) to be removed so global can be used via pluck, but it is still present")
}
// TestCoalesceValuesSubchartNilCleanedWhenUserPartiallyOverrides tests that nil
// values in subchart defaults are cleaned even when the user partially overrides
// the same map. Regression test for the coalesceTablesFullKey merge path.
func TestCoalesceValuesSubchartNilCleanedWhenUserPartiallyOverrides(t *testing.T) {
is := assert.New(t)
subchart := &chart.Chart{
Metadata: &chart.Metadata{Name: "child"},
Values: map[string]any{
"keyMapping": map[string]any{
"password": nil,
"format": "bcrypt",
},
},
}
parent := withDeps(&chart.Chart{
Metadata: &chart.Metadata{Name: "parent"},
Values: map[string]any{},
}, subchart)
// User overrides format but doesn't mention password
vals := map[string]any{
"child": map[string]any{
"keyMapping": map[string]any{
"format": "sha256",
},
},
}
v, err := CoalesceValues(parent, vals)
is.NoError(err)
childVals, ok := v["child"].(map[string]any)
is.True(ok, "child values should be a map")
keyMapping, ok := childVals["keyMapping"].(map[string]any)
is.True(ok, "keyMapping should be a map")
is.Equal("sha256", keyMapping["format"], "User override should be preserved")
_, ok = keyMapping["password"]
is.False(ok, "Expected keyMapping.password (nil from chart defaults) to be removed even when user partially overrides the map")
}

@ -25,7 +25,6 @@ import (
)
func TestToRenderValues(t *testing.T) {
chartValues := map[string]any{
"name": "al Rashid",
"where": map[string]any{

@ -100,7 +100,6 @@ func LoadDir(dir string) (chart.Charter, error) {
default:
return nil, errors.New("unsupported chart version")
}
}
// FileLoader loads a chart from a file

@ -104,7 +104,7 @@ func TestMetadata(t *testing.T) {
is.Equal("foo.yaml", chrt.Name())
is.Equal("1.0.0", chrt.AppVersion())
is.Equal(nil, chrt.Validate())
is.NoError(chrt.Validate())
}
func TestIsRoot(t *testing.T) {
@ -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) {

@ -44,7 +44,6 @@ func WithSkipSchemaValidation(skipSchemaValidation bool) LinterOption {
}
func RunAll(baseDir string, values map[string]any, namespace string, options ...LinterOption) support.Linter {
chartDir, _ := filepath.Abs(baseDir)
lo := linterOptions{}

@ -28,7 +28,7 @@ import (
kscheme "k8s.io/client-go/kubernetes/scheme"
)
// deprecatedAPIError indicates than an API is deprecated in Kubernetes
// deprecatedAPIError indicates that an API is deprecated in Kubernetes
type deprecatedAPIError struct {
Deprecated string
Message string

@ -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"
@ -62,7 +62,6 @@ func TemplateLinterSkipSchemaValidation(skipSchemaValidation bool) TemplateLinte
}
func newTemplateLinter(linter *support.Linter, namespace string, values map[string]any, options ...TemplateLinterOption) templateLinter {
result := templateLinter{
linter: linter,
values: values,
@ -323,7 +322,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

@ -257,7 +257,6 @@ data:
//
// See https://github.com/helm/helm/issues/7483
func TestStrictTemplateParsingMapError(t *testing.T) {
ch := chart.Chart{
Metadata: &chart.Metadata{
Name: "regression7483",
@ -390,7 +389,6 @@ func TestValidateTopIndentLevel(t *testing.T) {
t.Errorf("Expected %t for %q", shouldFail, doc)
}
}
}
// TestEmptyWithCommentsManifests checks the lint is not failing against empty manifests that contains only comments
@ -486,5 +484,4 @@ func TestIsYamlFileExtension(t *testing.T) {
t.Errorf("isYamlFileExtension(%s) = %v; want %v", test.filename, result, test.expected)
}
}
}

@ -390,7 +390,6 @@ icon: https://example.com/64x64.png
if text.String() != "" {
t.Errorf("Expected no message to Stderr, got %s", text.String())
}
}
// Packaging the chart on a Windows machine will produce an
@ -660,7 +659,6 @@ func verifyChart(t *testing.T, c *chart.Chart) {
t.Errorf("Expected %s version %s, got %s", dep.Name(), exp["version"], dep.Metadata.Version)
}
}
}
func verifyDependencies(t *testing.T, c *chart.Chart) {

@ -112,6 +112,9 @@ func (md *Metadata) Validate() error {
return ValidationError("chart.metadata.name is required")
}
if md.Name == "." || md.Name == ".." {
return ValidationErrorf("chart.metadata.name %q is not allowed", md.Name)
}
if md.Name != filepath.Base(md.Name) {
return ValidationErrorf("chart.metadata.name %q is invalid", md.Name)
}

@ -41,6 +41,16 @@ func TestValidate(t *testing.T) {
&Metadata{APIVersion: "v2", Version: "1.0"},
ValidationError("chart.metadata.name is required"),
},
{
"chart with dot name",
&Metadata{Name: ".", APIVersion: "v2", Version: "1.0"},
ValidationError("chart.metadata.name \".\" is not allowed"),
},
{
"chart with dotdot name",
&Metadata{Name: "..", APIVersion: "v2", Version: "1.0"},
ValidationError("chart.metadata.name \"..\" is not allowed"),
},
{
"chart without name",
&Metadata{Name: "../../test", APIVersion: "v2", Version: "1.0"},

@ -701,7 +701,6 @@ func CreateFrom(chartfile *chart.Metadata, dest, src string) error {
// error. In such a case, this will attempt to clean up by removing the
// new chart directory.
func Create(name, dir string) (string, error) {
// Sanity-check the name of a chart so user doesn't create one that causes problems.
if err := validateChartName(name); err != nil {
return "", err

@ -19,6 +19,7 @@ import (
"errors"
"fmt"
"log/slog"
"slices"
"strings"
"helm.sh/helm/v4/internal/copystructure"
@ -242,8 +243,8 @@ func set(path []string, data map[string]any) map[string]any {
return nil
}
cur := data
for i := len(path) - 1; i >= 0; i-- {
cur = map[string]any{path[i]: cur}
for _, v := range slices.Backward(path) {
cur = map[string]any{v: cur}
}
return cur
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save