Merge upstream/main into fix-issue-4256

Resolves a conflict in pkg/cmd/list.go: upstream grouped the trailing
bool parameters of newReleaseListWriter (noHeaders, noColor bool) while
this branch added showSource. Kept both by folding showSource into the
grouped form: (noHeaders, noColor, showSource bool).

Signed-off-by: Shaan Satsangi <shaansatsangi@gmail.com>
pull/32250/head
Shaan Satsangi 4 days ago
commit b56312ca1d

4
.github/env vendored

@ -1,2 +1,2 @@
GOLANG_VERSION=1.26
GOLANGCI_LINT_VERSION=v2.12.2
GOLANG_VERSION=1.27
GOLANGCI_LINT_VERSION=v2.13.1

@ -48,7 +48,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@5595ccaf912efad79be6eef63a5619ff05969be3 # pinv4.37.6
uses: github/codeql-action/init@cdf488f595d80d6e07e03d4674febd5ab45fa938 # pinv4.37.9
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@ -59,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@5595ccaf912efad79be6eef63a5619ff05969be3 # pinv4.37.6
uses: github/codeql-action/autobuild@cdf488f595d80d6e07e03d4674febd5ab45fa938 # pinv4.37.9
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
@ -73,4 +73,4 @@ jobs:
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@5595ccaf912efad79be6eef63a5619ff05969be3 # pinv4.37.6
uses: github/codeql-action/analyze@cdf488f595d80d6e07e03d4674febd5ab45fa938 # pinv4.37.9

@ -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@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6
uses: github/codeql-action/upload-sarif@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9
with:
sarif_file: results.sarif

@ -13,7 +13,8 @@ formatters:
gofumpt:
module-path: helm.sh/helm/v4
extra-rules: false
extra:
group-params: true
goimports:
local-prefixes:
@ -95,7 +96,6 @@ linters:
- exposedSyncMutex
- hugeParam
- importShadow
- paramTypeCombine
- ptrToRefParam
- rangeValCopy
- todoCommentWithoutDetail
@ -119,6 +119,7 @@ linters:
enable-default-rules: true
max-open-files: 2048
rules:
- name: bare-return
- name: early-return
arguments:
- "preserve-scope"

@ -42,8 +42,7 @@ func main() {
}
if err := cmd.Execute(); err != nil {
var cerr helmcmd.CommandError
if errors.As(err, &cerr) {
if cerr, ok := errors.AsType[helmcmd.CommandError](err); ok {
os.Exit(cerr.ExitCode)
}
os.Exit(1)

@ -17,10 +17,10 @@ require (
github.com/evanphx/json-patch/v5 v5.9.11
github.com/extism/go-sdk v1.7.1
github.com/fatih/color v1.19.0
github.com/fluxcd/cli-utils v1.2.2
github.com/fluxcd/cli-utils v1.2.3
github.com/foxcpp/go-mockdns v1.2.0
github.com/gobwas/glob v0.2.3
github.com/gofrs/flock v0.13.0
github.com/gobwas/glob v1.0.0
github.com/gofrs/flock v0.13.1
github.com/gosuri/uitable v0.0.4
github.com/jmoiron/sqlx v1.4.0
github.com/lib/pq v1.12.3
@ -32,21 +32,20 @@ require (
github.com/santhosh-tekuri/jsonschema/v6 v6.0.3
github.com/spf13/cobra v1.10.2
github.com/spf13/pflag v1.0.10
github.com/stretchr/testify v1.11.1
github.com/stretchr/testify v1.12.1
github.com/tetratelabs/wazero v1.12.0
go.yaml.in/yaml/v3 v3.0.5
golang.org/x/crypto v0.55.0
golang.org/x/term v0.45.0
golang.org/x/text v0.41.0
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/api v0.36.3
k8s.io/apiextensions-apiserver v0.36.3
k8s.io/apimachinery v0.36.3
k8s.io/apiserver v0.36.3
k8s.io/cli-runtime v0.36.3
k8s.io/client-go v0.36.3
k8s.io/api v0.37.0
k8s.io/apiextensions-apiserver v0.37.0
k8s.io/apimachinery v0.37.0
k8s.io/apiserver v0.37.0
k8s.io/cli-runtime v0.37.0
k8s.io/client-go v0.37.0
k8s.io/klog/v2 v2.140.0
k8s.io/kubectl v0.36.3
k8s.io/kubectl v0.37.0
oras.land/oras-go/v2 v2.6.2
sigs.k8s.io/controller-runtime v0.24.1
sigs.k8s.io/kustomize/kyaml v0.21.1
@ -77,34 +76,43 @@ require (
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
github.com/fxamacker/cbor/v2 v2.9.1 // indirect
github.com/go-errors/errors v1.5.1 // indirect
github.com/go-gorp/gorp/v3 v3.1.0 // indirect
github.com/go-jose/go-jose/v4 v4.1.4 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v0.21.1 // indirect
github.com/go-openapi/jsonreference v0.21.0 // indirect
github.com/go-openapi/swag v0.23.1 // indirect
github.com/go-openapi/jsonpointer v1.0.0 // indirect
github.com/go-openapi/jsonreference v1.0.0 // indirect
github.com/go-openapi/swag v0.27.1 // indirect
github.com/go-openapi/swag/cmdutils v0.27.1 // indirect
github.com/go-openapi/swag/conv v0.27.1 // indirect
github.com/go-openapi/swag/fileutils v0.27.1 // indirect
github.com/go-openapi/swag/jsonutils v0.27.1 // indirect
github.com/go-openapi/swag/loading v0.27.1 // indirect
github.com/go-openapi/swag/mangling v0.27.1 // indirect
github.com/go-openapi/swag/netutils v0.27.1 // indirect
github.com/go-openapi/swag/pools v0.27.1 // indirect
github.com/go-openapi/swag/stringutils v0.27.1 // indirect
github.com/go-openapi/swag/typeutils v0.27.1 // indirect
github.com/go-openapi/swag/yamlutils v0.27.1 // indirect
github.com/google/btree v1.1.3 // indirect
github.com/google/gnostic-models v0.7.0 // indirect
github.com/google/go-cmp v0.7.0 // indirect
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/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.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
github.com/ianlancetaylor/demangle v0.0.0-20240805132620-81f5be970eca // indirect
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.4 // indirect
github.com/klauspost/compress v1.19.0 // 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
github.com/mailru/easyjson v0.9.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.9 // indirect
@ -119,11 +127,11 @@ require (
github.com/onsi/gomega v1.42.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_golang v1.24.0 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.67.5 // indirect
github.com/prometheus/common v0.70.0 // indirect
github.com/prometheus/otlptranslator v1.0.0 // indirect
github.com/prometheus/procfs v0.20.1 // indirect
github.com/prometheus/procfs v0.21.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
@ -137,14 +145,14 @@ require (
go.opentelemetry.io/auto/sdk v1.2.1 // 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/contrib/instrumentation/net/http/otelhttp v0.69.0 // indirect
go.opentelemetry.io/otel v1.44.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 v1.44.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.44.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
@ -157,7 +165,7 @@ require (
go.opentelemetry.io/otel/sdk/metric v1.44.0 // indirect
go.opentelemetry.io/otel/trace v1.44.0 // indirect
go.opentelemetry.io/proto/otlp v1.10.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
go.yaml.in/yaml/v2 v2.4.4 // indirect
golang.org/x/mod v0.38.0 // indirect
golang.org/x/net v0.57.0 // indirect
golang.org/x/oauth2 v0.36.0 // indirect
@ -165,18 +173,18 @@ require (
golang.org/x/sys v0.47.0 // indirect
golang.org/x/time v0.15.0 // indirect
golang.org/x/tools v0.48.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 // indirect
google.golang.org/grpc v1.82.1 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect
google.golang.org/grpc v1.83.1 // 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.36.3 // indirect
k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a // indirect
k8s.io/utils v0.0.0-20260507154919-ff6756f316d2 // indirect
k8s.io/component-base v0.37.0 // indirect
k8s.io/kube-openapi v0.0.0-20260721132016-d427ff9ee9ad // indirect
k8s.io/utils v0.0.0-20260626114624-be93311217bd // 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.3 // indirect
sigs.k8s.io/structured-merge-diff/v6 v6.4.2 // indirect
)

168
go.sum

@ -93,14 +93,14 @@ github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fluxcd/cli-utils v1.2.2 h1:adDOmwE+LSwTzmYUaoEFPblruOuaQEKAg1ZNTmPJObE=
github.com/fluxcd/cli-utils v1.2.2/go.mod h1:FsghNGY+3Sr70c0FOB7I5So0kzoYVdvQ8GTid3XXVWM=
github.com/fluxcd/cli-utils v1.2.3 h1:RcBLij09aROVOtFWzQzg2My6S68R/sIm4OfQLTXAIWE=
github.com/fluxcd/cli-utils v1.2.3/go.mod h1:RuUFNFaDCsyCWzX4ghxt54rjmyyhNWaUlUFVa/kQq2Q=
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=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/fxamacker/cbor/v2 v2.9.1 h1:2rWm8B193Ll4VdjsJY28jxs70IdDsHRWgQYAI80+rMQ=
github.com/fxamacker/cbor/v2 v2.9.1/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk=
github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
github.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs=
@ -117,21 +117,49 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ=
github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg=
github.com/go-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic=
github.com/go-openapi/jsonpointer v0.21.1/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk=
github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=
github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU=
github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0=
github.com/go-openapi/jsonpointer v1.0.0 h1:kR9tHqY0CtZaOPVFm622dPVNhrvYpwr4uCxgL3h1H8s=
github.com/go-openapi/jsonpointer v1.0.0/go.mod h1:Z3rw7dWu1p9IgitXCFamSlA5lmDiklEB6vkaxcNZW5Y=
github.com/go-openapi/jsonreference v1.0.0 h1:jlmTr6torcd1YgDQvSfNmRtKzYDO4FGBkrAdlAVWnpY=
github.com/go-openapi/jsonreference v1.0.0/go.mod h1:jtwdyGbJk0Xhe5Y+rwtglQP6Sb1WZST4rT32LWB+sv0=
github.com/go-openapi/swag v0.27.1 h1:VotvOLWW8q/EAxB0YdsBBGC8XYyeL1YwBj2ungAGPNg=
github.com/go-openapi/swag v0.27.1/go.mod h1:GTkJPwHfhJp6MWr4/rCh64HVI3Ofu+tcsbfjfHmTxpE=
github.com/go-openapi/swag/cmdutils v0.27.1 h1:I7sYqaWVl5mq0NEmNQkAmFDyNin9ufvMX/p2zwtQaOE=
github.com/go-openapi/swag/cmdutils v0.27.1/go.mod h1:Sm1MVFMkF6guJJ+pQqHnQA3N0j9qALV3NxzDSv6bETM=
github.com/go-openapi/swag/conv v0.27.1 h1:8wi9ZG+olmY1wXphl93EWniPtbSPkXM/feH7FgjsvrU=
github.com/go-openapi/swag/conv v0.27.1/go.mod h1:QbqMivkpKhC3g1B1GGGOJ6ANewI3S62dbzYu3Duowqs=
github.com/go-openapi/swag/fileutils v0.27.1 h1:QQqBSoi5mW4XpU85nS0mLcA+zAE6vLzrb0QkmLKf9oM=
github.com/go-openapi/swag/fileutils v0.27.1/go.mod h1:VvJFZLTZS0AI854gEQz5tk7dBESdLjiNUMSZ/th2ry8=
github.com/go-openapi/swag/jsonutils v0.27.1 h1:SVgK3i4USzCU5mibOOS/l4ea2h9UQXy7J7RNLTjuXjU=
github.com/go-openapi/swag/jsonutils v0.27.1/go.mod h1:tdlEpZqdcQ17uj6J4YdK9vd8It5qWMwjWXOs0tjpRlk=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.27.1 h1:mJu3COL9WEaZVp/Kf2PRMi7tPszPEJfSr/OO75ynCs8=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.27.1/go.mod h1:mofwUWx70wvskwESqRJ//k/9kURmCgyJl5m5Ppoh5kY=
github.com/go-openapi/swag/loading v0.27.1 h1:/DxUgDXKbBX4bcn7r9uEXfJyzN5XpiJmZplzQTjrRCY=
github.com/go-openapi/swag/loading v0.27.1/go.mod h1:jvGh3iA2+zyUUycB5fgJWzeHnhrpvGnJJM0RVE9ZShE=
github.com/go-openapi/swag/mangling v0.27.1 h1:yC9D0HyUE8gbP+BfmGx9+AA89ikwZTMjESK3OnnoaqA=
github.com/go-openapi/swag/mangling v0.27.1/go.mod h1:jtBE2+V+3pILxOR7Vgce+Cwp6A2PgZbvVqfNntbVs0w=
github.com/go-openapi/swag/netutils v0.27.1 h1:mICMFoS82F5TZ4Zy3cqmcQk+BFeCp3Uyq3Np7GI0/qU=
github.com/go-openapi/swag/netutils v0.27.1/go.mod h1:J+WYyFMLtvtCGqa6jLv+YNUmIKI3ZRQRrvfNDMoQoEQ=
github.com/go-openapi/swag/pools v0.27.1 h1:9LeadcMyb2GJCbXX5hVQDbZ2Lq9TL4dCs/nx1j5DO0E=
github.com/go-openapi/swag/pools v0.27.1/go.mod h1:kVQefhSK5RWuRe7BXsL8htgBPAMpN7HDGpGEknqugeE=
github.com/go-openapi/swag/stringutils v0.27.1 h1:ZXePZ0r2p1qSjo8tD3Un4vFj8+FqlCkczxDrJIhYUp8=
github.com/go-openapi/swag/stringutils v0.27.1/go.mod h1:lzRN95CxXmA03XcDWHLOb6nOMcxCqR5rGY0lOgsfRoM=
github.com/go-openapi/swag/typeutils v0.27.1 h1:KSTdFlfnse4r6dP9IrEnwMldjE+zs71UeEB3//PtVXc=
github.com/go-openapi/swag/typeutils v0.27.1/go.mod h1:Srm0xFNRZ1Y+vCxJclo5qzx8aj+1pAKda/YfFPrG0dQ=
github.com/go-openapi/swag/yamlutils v0.27.1 h1:ftxv6xvXb1E3zohUc+okZ9nSqNb9StQX/FXnKZ98sQA=
github.com/go-openapi/swag/yamlutils v0.27.1/go.mod h1:bnxFIB1qewGRiZHypXGZ3fNgf13/0HfRgnS/iZBDrOo=
github.com/go-openapi/testify/enable/yaml/v2 v2.6.0 h1:gGHwAJ0R/5jU8BEGDbfRNR3hL68dAVi84WuOApp29B0=
github.com/go-openapi/testify/enable/yaml/v2 v2.6.0/go.mod h1:tY+St1SGq4NFl0QIqdTY4aEdbChAHxhyB77XQi9iJCo=
github.com/go-openapi/testify/v2 v2.6.0 h1:5PKH2HE7YJ/LuRPQGvSxBRlFXNQhSetBLlGAgUEu3ug=
github.com/go-openapi/testify/v2 v2.6.0/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
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/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/gobwas/glob v1.0.0 h1:p+FKbLEIsK1yZ39/OINwFvqNb5oyPY4H8xcy6uYu8dg=
github.com/gobwas/glob v1.0.0/go.mod h1:oWCdo522i2P1n/hMXGNWs7yoV4wy/ciZuUIbvKj5rkc=
github.com/gofrs/flock v0.13.1 h1:jjREztyBeSKBZYAC+mgc1laB+xsgy4kYMf3FbKF2UBo=
github.com/gofrs/flock v0.13.1/go.mod h1:sf4BFiHwnvgxa25DlQoDqXQnwRMEOwqxRq37P6MzzmE=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@ -146,8 +174,8 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 h1:z2ogiKUYzX5Is6zr/vP9vJGqPwcdqsWjOt+V8J7+bTc=
github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI=
github.com/google/pprof v0.0.0-20260402051712-545e8a4df936 h1:EwtI+Al+DeppwYX2oXJCETMO23COyaKGP6fHVpkpWpg=
github.com/google/pprof v0.0.0-20260402051712-545e8a4df936/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE=
@ -156,8 +184,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/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/grpc-ecosystem/grpc-gateway/v2 v2.29.0 h1:5VipnvEpbqr2gA2VbM+nYVbkIF28c5ZQfqCBQ5g2xfk=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0/go.mod h1:Hyl3n6Twe1hvtd9XUXDec4pTvgMSEixRuQKPTMH2bNs=
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=
@ -170,16 +198,14 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
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.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/klauspost/compress v1.19.0 h1:sXLILfc9jV2QYWkzFOPWStmcUVH2RHEB1JCdY2oVvCQ=
github.com/klauspost/compress v1.19.0/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
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=
@ -197,8 +223,6 @@ 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=
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
@ -233,8 +257,8 @@ github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
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/ginkgo/v2 v2.32.0 h1:Hw7s2pVrQo/8Yz5N77qdnpHaoc+c6cC9WIV1Jce+J6E=
github.com/onsi/ginkgo/v2 v2.32.0/go.mod h1:+aXOY+vzZ5mu2iI2HpTZUPmM//oQfsNFX6gU9kNcA44=
github.com/onsi/gomega v1.42.1 h1:iN1rCUX+44NZ1Dc97MPoeFYbFR0vh8zxoxMFwKdyZ6I=
github.com/onsi/gomega v1.42.1/go.mod h1:REff/hsDsodHoKlWsP2mAPhu1+5/6hVYNf9rIEBpeSg=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
@ -252,23 +276,23 @@ github.com/poy/onpar v1.1.2/go.mod h1:6X8FLNoxyr9kkmnlqpK6LSoiOtrO6MICtWwEuWkLjz
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g=
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/client_golang v1.24.0 h1:5XStIklKuAtJSNpdD3s8XJj/Yv78IQmE1kbNk87JrAI=
github.com/prometheus/client_golang v1.24.0/go.mod h1:QcsNdotprC2nS4BTM2ucbcqxd2CeXTEa9jW7zHO9iDE=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc=
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
github.com/prometheus/common v0.70.0 h1:bcpru3tWPVnxGnETLgOV5jbp/JRXgYEyv65CuBLAMMI=
github.com/prometheus/common v0.70.0/go.mod h1:S/SFasQmgGiYH6C81LKCtYa8QACgthGg5zxL2udV7SY=
github.com/prometheus/otlptranslator v1.0.0 h1:s0LJW/iN9dkIH+EnhiD3BlkkP5QVIUVEoIwkU+A6qos=
github.com/prometheus/otlptranslator v1.0.0/go.mod h1:vRYWnXvI6aWGpsdY/mOT/cbeVRBlPWtBNDb7kGR3uKM=
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.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc=
github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
github.com/prometheus/procfs v0.21.1 h1:GljZCt+zSTS+NZq88cyQ1LjZ+RCHp3uVuabBWA5+OJI=
github.com/prometheus/procfs v0.21.1/go.mod h1:aB55Cww9pdSJVHk0hUf0inxWyyjPogFIjmHKYgMKmtY=
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=
@ -301,14 +325,14 @@ github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=
github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
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.12.0 h1:DuWcpNu/FzgEXgGBDp8J1Spc+CWOvvtvVyjKlaZopYU=
@ -326,8 +350,8 @@ go.opentelemetry.io/contrib/bridges/prometheus v0.67.0 h1:dkBzNEAIKADEaFnuESzcXv
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/contrib/instrumentation/net/http/otelhttp v0.69.0 h1:8tvICD4vSTOOsNrsI4Ljf6C+6UKvpTEH5XY3JMoyPoo=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0/go.mod h1:z9+yiacE0IHRqM4qFfkbt/JYlmYXgss8GY/jXoNuPJI=
go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.18.0 h1:deI9UQMoGFgrg5iLPgzueqFPHevDl+28YKfSpPTI6rY=
@ -338,10 +362,10 @@ go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.42.0 h1:MdK
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 v1.44.0 h1:4YsVu3B8+3qtWYYrsUYgn0OG78pN0rnNPRGX4SbokQI=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0/go.mod h1:+wnlSn0mD1ADVMe3v9Z/WIaiz6q6gL2J/ejaAmdmv80=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.44.0 h1:qazEJlUOQzhCpzQpFETGby7EdqjI1wsd0W+6Gg1SCTU=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.44.0/go.mod h1:fOD2Yefuxixkx3ahVNf0O/PERb6r4OlbxfATVnYvzCo=
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=
@ -376,8 +400,8 @@ 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.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/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
@ -468,12 +492,12 @@ golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
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-20260414002931-afd174a4e478 h1:yQugLulqltosq0B/f8l4w9VryjV+N/5gcW0jQ3N8Qec=
google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478/go.mod h1:C6ADNqOxbgdUUeRTU+LCHDPB9ttAMCTff6auwCVa4uc=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 h1:RmoJA1ujG+/lRGNfUnOMfhCy5EipVMyvUE+KNbPbTlw=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE=
google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA=
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa h1:Kjn0N0tCrDgiAFW+lGO4JZ3ck44CehvJQMAwj9QF0G8=
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:q4lMZS6kskjT5HvCPrnnypcDPVJqT/f4nfxmkE7gryY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.83.1 h1:HIO0+BEtBP6soyqvqC8sNUjZ7bTs+0hFQuFF+RAy++Y=
google.golang.org/grpc v1.83.1/go.mod h1:kDyl6SKsiHKt0uylY5gtn5cEjkrIOhQOGDgIc4JGwzQ=
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=
@ -490,28 +514,28 @@ 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.36.3 h1:NxB+05W2UGqXWFXcLO0RB5cnqnUPP5v5sVlaOH0Iz4w=
k8s.io/api v0.36.3/go.mod h1:JzLQKqRHC5+I8RVj/lS3lCg0mg6nWI9Fo/Sk3ElxHzg=
k8s.io/apiextensions-apiserver v0.36.3 h1:dPmOAPhwTtqb1bTxbFPsy18KHPhktQeO3WUPXunZIB0=
k8s.io/apiextensions-apiserver v0.36.3/go.mod h1:KTXFqgXiuw2pRoL+Wpmttqc+up9Xt/GohadPWeLLOa4=
k8s.io/apimachinery v0.36.3 h1:PkzMRBRG8joFD8EhCuQAtNPvJlxb82FwplP26HIzvAM=
k8s.io/apimachinery v0.36.3/go.mod h1:cTSjBWgPe/6CQyBKzY/hDIRWCQQQeK0mfLbml0UYFHE=
k8s.io/apiserver v0.36.3 h1:MGSg2SkdfuytiDEcRylT5mQFmmSsbx90XFUO67Y4bsQ=
k8s.io/apiserver v0.36.3/go.mod h1:fVH7zv9EUNUA7Fl7LtDKh8aB9W7u1VQPSGtWV5SjUxg=
k8s.io/cli-runtime v0.36.3 h1:g+eJ+M1sYpnNYp/q5fzaw2KejIL0Q7DH+xFl6YVoL4U=
k8s.io/cli-runtime v0.36.3/go.mod h1:hZpAqK8nSFXvvLaVCbzUPVp8e9TRLSTCfpNzMt7s3tE=
k8s.io/client-go v0.36.3 h1:M4JdVzXxYcZk4fGpfDdYnxSwhLKWCFoQsHW6t+z8Hfg=
k8s.io/client-go v0.36.3/go.mod h1:gcPwr0c87vjjG6HB6pWEqOeuYVoXSsREjzux2j6GF30=
k8s.io/component-base v0.36.3 h1:vc/UFvPCkW0irPz84LAodAL1j3f4xktPM6dDJIEheAY=
k8s.io/component-base v0.36.3/go.mod h1:hZbNFG+gCMl9EbykDGEu73feKP9/Cq6JsV4pTo9GTO8=
k8s.io/api v0.37.0 h1:Z//Vj9N7RA/yS2sDmxyeo7h+RR4zbUrd2vrd3Z0TbB4=
k8s.io/api v0.37.0/go.mod h1:LKXgcJWMc+f4OLbP5SFR8rulEg07zZhpi/zMULiBImk=
k8s.io/apiextensions-apiserver v0.37.0 h1:zRMQ3+/LIE5oZ0tVvXwYHC+dIkSP5cjNWju7AZU1LOI=
k8s.io/apiextensions-apiserver v0.37.0/go.mod h1:HU0PfSBwchHL5iDau6jjt9zU6ryWkDDlaVUiq91NK80=
k8s.io/apimachinery v0.37.0 h1:Np2AbDtf8x6RDHiD8T9LbKJ9gaegeVNa8yNm5FuGKm0=
k8s.io/apimachinery v0.37.0/go.mod h1:RN3nhprFSCxOi5Selxd7oMTXOe/c+ZbcE7Im+TS2zkE=
k8s.io/apiserver v0.37.0 h1:TXg7OxsOWrAH8J4Zi/gBAZuMw1Dfdd+6cca2h4qjRqo=
k8s.io/apiserver v0.37.0/go.mod h1:OddHDF4gy9qyIb8o/3+qaeP6S0vEObWLgOygVqXksv0=
k8s.io/cli-runtime v0.37.0 h1:U3XakUeirBQJMz5688r04z74SIHSE7V5SIZ6Ho5JyBM=
k8s.io/cli-runtime v0.37.0/go.mod h1:qiQMFkKwFFuPH6zy953On+nc3qfpEHAIDrJmAuRz5Vg=
k8s.io/client-go v0.37.0 h1:nsN31fy8wBySuZ+QRnKmrjRSQLOG2rvoGN0tKd12zhQ=
k8s.io/client-go v0.37.0/go.mod h1:FcGqw+Ll/gNQiq+nPGY1Oyt9y7SgDh1d3MW3RFDEbn0=
k8s.io/component-base v0.37.0 h1:3SdSa4+itMdFTDFTeR8CxKGmSTSMXFlKL4ky8OqjguM=
k8s.io/component-base v0.37.0/go.mod h1:LjOebp4R9y6LODWZQv102ZQxGheLcDO2ZJLAw6bbh4I=
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.3 h1:TesKp+XYQEjPYoFvuobcVnuvira2+/xAVlq//+kksaI=
k8s.io/kubectl v0.36.3/go.mod h1:W+NEb1CzBGmoaI1Nrpn2ETo9omNBl0AsyxnnMT40N6E=
k8s.io/utils v0.0.0-20260507154919-ff6756f316d2 h1:wU4tMEhLGgIbLvXQb1cfN+EcM0wf7zC6CPF+C79jroc=
k8s.io/utils v0.0.0-20260507154919-ff6756f316d2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk=
k8s.io/kube-openapi v0.0.0-20260721132016-d427ff9ee9ad h1:oXImqH8mQNk7PmvzKhmN3ddJoY6OnyM225MXwGHPm0A=
k8s.io/kube-openapi v0.0.0-20260721132016-d427ff9ee9ad/go.mod h1:0/mqHCVhlumdJ3BhCfnjSZQE037nAhNodh1/hK0T8/I=
k8s.io/kubectl v0.37.0 h1:cici6hiofx93ASldmprDmZF55SfhVt4o3HniltVLjTc=
k8s.io/kubectl v0.37.0/go.mod h1:RSeEl8e/yqDx6srG8Azr0uAtVPNIZljA0PNh9HCBcdg=
k8s.io/utils v0.0.0-20260626114624-be93311217bd h1:Ea7fgQ5we8Y9T0OX5o0dAHzQOBRI07D/dEYRaB9ZZEs=
k8s.io/utils v0.0.0-20260626114624-be93311217bd/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk=
oras.land/oras-go/v2 v2.6.2 h1:N04RXngAp1LJKTG6ifz3xHPipasEkWr+hFmInja5YKo=
oras.land/oras-go/v2 v2.6.2/go.mod h1:PlTtg4JTDJkDe8yVHpM2wz7/YDc00GVas+i4jAW2TZ4=
sigs.k8s.io/controller-runtime v0.24.1 h1:miPEwrmirImAvgME1L9qebGHrOnGJoVmVdtOU9fRfo4=
@ -524,7 +548,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.3 h1:u08YRbVUi59ri4YD6cg0UqNM4Dimn0sIl+wldcx5PYw=
sigs.k8s.io/structured-merge-diff/v6 v6.3.3/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
sigs.k8s.io/structured-merge-diff/v6 v6.4.2 h1:qdOxHwrl2Kaag1aQEarlYcOA9vSyGCp3CIki3aW8c4Q=
sigs.k8s.io/structured-merge-diff/v6 v6.4.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=

@ -141,7 +141,7 @@ func (ch *Chart) AppVersion() string {
// CRDs returns a list of File objects in the 'crds/' directory of a Helm chart.
//
// Deprecated: use CRDObjects()
// Deprecated: Use CRDObjects instead.
func (ch *Chart) CRDs() []*common.File {
files := []*common.File{}
// Find all resources in the crds/ directory

@ -43,6 +43,10 @@ func (l DirLoader) Load() (*chart.Chart, error) {
//
// This loads charts only from directories.
func LoadDir(dir string) (*chart.Chart, error) {
return loadDir(dir, archive.MaxDecompressedChartSize)
}
func loadDir(dir string, budget int64) (*chart.Chart, error) {
topdir, err := filepath.Abs(dir)
if err != nil {
return nil, err
@ -64,6 +68,7 @@ func LoadDir(dir string) (*chart.Chart, error) {
files := []*archive.BufferedFile{}
topdir += string(filepath.Separator)
budgetReader := archive.NewBudgetedReader(budget)
walk := func(name string, fi os.FileInfo, err error) error {
n := strings.TrimPrefix(name, topdir)
@ -100,11 +105,7 @@ func LoadDir(dir string) (*chart.Chart, error) {
return fmt.Errorf("cannot load irregular file %s as it has file mode type bits set", name)
}
if fi.Size() > archive.MaxDecompressedFileSize {
return fmt.Errorf("chart file %q is larger than the maximum file size %d", fi.Name(), archive.MaxDecompressedFileSize)
}
data, err := os.ReadFile(name)
data, err := budgetReader.ReadFileWithBudget(name, fi.Size())
if err != nil {
return fmt.Errorf("error reading %s: %w", n, err)
}

@ -123,7 +123,7 @@ func LoadFiles(files []*archive.BufferedFile) (*chart.Chart, error) {
}
fname := strings.TrimPrefix(f.Name, "charts/")
cname := strings.SplitN(fname, "/", 2)[0]
cname, _, _ := strings.Cut(fname, "/")
if slices.Index(subChartsKeys, cname) == -1 {
subChartsKeys = append(subChartsKeys, cname)
}
@ -183,8 +183,20 @@ func LoadFiles(files []*archive.BufferedFile) (*chart.Chart, error) {
// The reader is expected to contain one or more YAML documents, the values of which are merged.
// And the values can be either a chart's default values or user-supplied values.
func LoadValues(data io.Reader) (map[string]any, error) {
// Read fully first. YAMLReader/LineReader can drop a final unterminated
// line when its length is an exact multiple of bufio.Reader's default
// buffer (4096). Appending a trailing newline avoids that case.
// See https://github.com/helm/helm/issues/32506
b, err := io.ReadAll(data)
if err != nil {
return nil, err
}
if len(b) > 0 && b[len(b)-1] != '\n' {
b = append(b, '\n')
}
values := map[string]any{}
reader := utilyaml.NewYAMLReader(bufio.NewReader(data))
reader := utilyaml.NewYAMLReader(bufio.NewReader(bytes.NewReader(b)))
for {
currentMap := map[string]any{}
raw, err := reader.Read()

@ -21,6 +21,7 @@ import (
"bytes"
"compress/gzip"
"errors"
"fmt"
"io"
"log"
"os"
@ -49,6 +50,16 @@ func TestLoadDir(t *testing.T) {
verifyDependenciesLock(t, c)
}
func TestLoadDirExceedsBudget(t *testing.T) {
_, err := loadDir("testdata/frobnitz", 1)
if err == nil {
t.Fatal("expected error when chart directory exceeds budget")
}
if !strings.Contains(err.Error(), "chart exceeds maximum decompressed size") {
t.Fatalf("unexpected error: %v", err)
}
}
func TestLoadDirWithDevNull(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("test only works on unix systems with /dev/null present")
@ -419,6 +430,33 @@ foo:
}
}
func TestLoadValuesEOFBoundary(t *testing.T) {
// Reproduces #32506: a single logical line whose length is a multiple of
// bufio's default buffer (4096) and has no trailing newline used to be
// dropped entirely by YAMLReader, yielding empty values.
// Also cover 8192 (2x buffer) so we do not only hit the single-buffer case.
for _, size := range []int{4096, 8192} {
t.Run(fmt.Sprintf("size_%d", size), func(t *testing.T) {
prefix := []byte(`{"foo":"`)
suffix := []byte(`"}`)
pad := size - len(prefix) - len(suffix)
data := make([]byte, 0, size)
data = append(data, prefix...)
data = append(data, bytes.Repeat([]byte("x"), pad)...)
data = append(data, suffix...)
if len(data) != size {
t.Fatalf("test setup: want data length %d, got %d", size, len(data))
}
values, err := LoadValues(bytes.NewReader(data))
require.NoError(t, err)
assert.Equal(t, map[string]any{
"foo": string(bytes.Repeat([]byte("x"), pad)),
}, values)
})
}
}
func TestMergeValuesV3(t *testing.T) {
nestedMap := map[string]any{
"foo": "bar",

@ -18,7 +18,7 @@ package util
import (
"fmt"
"io"
"log/slog"
"os"
"path/filepath"
"regexp"
@ -642,12 +642,6 @@ spec:
restartPolicy: Never
`
// Stderr is an io.Writer to which error messages can be written
//
// In Helm 4, this will be replaced. It is needed in Helm 3 to preserve API backward
// compatibility.
var Stderr io.Writer = os.Stderr
// CreateFrom creates a new chart, but scaffolds it from the src chart.
func CreateFrom(chartfile *chart.Metadata, dest, src string) error {
schart, err := loader.Load(src)
@ -795,8 +789,7 @@ func Create(name, dir string) (string, error) {
for _, file := range files {
if _, err := os.Stat(file.path); err == nil {
// There is no handle to a preferred output stream here.
fmt.Fprintf(Stderr, "WARNING: File %q already exists. Overwriting.\n", file.path)
slog.Warn("Overwriting existing file", slog.String("file", file.path))
}
if err := writeFile(file.path, file.content); err != nil {
return cdir, err

@ -18,6 +18,7 @@ package util
import (
"bytes"
"log/slog"
"os"
"path/filepath"
"testing"
@ -98,7 +99,11 @@ func TestCreateFrom(t *testing.T) {
func TestCreate_Overwrite(t *testing.T) {
tdir := t.TempDir()
var errlog bytes.Buffer
prev := slog.Default()
t.Cleanup(func() { slog.SetDefault(prev) })
var logBuf bytes.Buffer
slog.SetDefault(slog.New(slog.NewJSONHandler(&logBuf, nil)))
_, err := Create("foo", tdir)
require.NoError(t, err)
@ -109,7 +114,6 @@ func TestCreate_Overwrite(t *testing.T) {
writeFile(tplname, []byte("FOO"))
// Now re-run the create
Stderr = &errlog
_, err = Create("foo", tdir)
require.NoError(t, err)
@ -118,7 +122,7 @@ func TestCreate_Overwrite(t *testing.T) {
require.NotEqual(t, "FOO", string(data), "File that should have been modified was not.")
assert.NotEqual(t, 0, errlog.Len(), "Expected warnings about overwriting files.")
assert.NotEqual(t, 0, logBuf.Len(), "Expected warnings about overwriting files.")
}
func TestValidateChartName(t *testing.T) {

@ -46,7 +46,6 @@ func processDependencyConditions(reqs []*chart.Dependency, cvals common.Values,
if c != "" {
// retrieve value
vv, err := cvals.PathValue(cpath + c)
var errNoValue common.ErrNoValue
if err == nil {
// if not bool, warn
if bv, ok := vv.(bool); ok {
@ -54,7 +53,7 @@ func processDependencyConditions(reqs []*chart.Dependency, cvals common.Values,
break
}
slog.Warn("returned non-bool value", "path", c, "chart", r.Name)
} else if errors.As(err, &errNoValue) {
} else if _, ok := errors.AsType[common.ErrNoValue](err); ok {
// this is a real error
slog.Warn("the method PathValue returned error", slog.Any("error", err))
}

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

@ -231,13 +231,10 @@ func TestSaveDir(t *testing.T) {
}
func TestRepeatableSave(t *testing.T) {
tmp := t.TempDir()
defer os.RemoveAll(tmp)
modTime := time.Date(2021, 9, 1, 20, 34, 58, 651387237, time.UTC)
tests := []struct {
name string
chart *chart.Chart
want string
}{
{
name: "Package 1 file",
@ -258,7 +255,6 @@ func TestRepeatableSave(t *testing.T) {
Schema: []byte("{\n \"title\": \"Values\"\n}"),
SchemaModTime: modTime,
},
want: "5bfea18cc3c8cbc265744bc32bffa9489a4dbe87d6b51b90f4255e4839d35e03",
},
{
name: "Package 2 files",
@ -280,20 +276,26 @@ func TestRepeatableSave(t *testing.T) {
Schema: []byte("{\n \"title\": \"Values\"\n}"),
SchemaModTime: modTime,
},
want: "a240365c21e0a2f4a57873132a9b686566a612d08bcb3f20c9446bfff005ccce",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
// create package
dest := path.Join(tmp, "newdir")
where, err := Save(test.chart, dest)
// Package the same chart twice, into separate directories so the
// second Save does not simply overwrite the first.
first, err := Save(test.chart, t.TempDir())
require.NoError(t, err, "Failed to save")
second, err := Save(test.chart, t.TempDir())
require.NoError(t, err, "Failed to save")
// get shasum for package
result, err := sha256Sum(where)
firstSum, err := sha256Sum(first)
require.NoError(t, err, "Failed to check shasum")
// assert that the package SHA is what we wanted.
assert.Equal(t, test.want, result, "FormatName() result = %v, want %v", result, test.want)
secondSum, err := sha256Sum(second)
require.NoError(t, err, "Failed to check shasum")
// Packaging a chart must be reproducible. The digest itself is not
// pinned here because the bytes depend on the compression
// implementation of the Go release Helm is built with.
assert.Equal(t, firstSum, secondSum, "Save() is not repeatable")
})
}
}

@ -1,110 +0,0 @@
/*
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 util
import (
"errors"
"fmt"
"regexp"
)
// validName is a regular expression for resource names.
//
// According to the Kubernetes help text, the regular expression it uses is:
//
// [a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*
//
// This follows the above regular expression (but requires a full string match, not partial).
//
// The Kubernetes documentation is here, though it is not entirely correct:
// https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
var validName = regexp.MustCompile(`^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$`)
var (
// errMissingName indicates that a release (name) was not provided.
errMissingName = errors.New("no name provided")
// errInvalidName indicates that an invalid release name was provided
errInvalidName = fmt.Errorf(
"invalid release name, must match regex %s and the length must not be longer than 53",
validName.String())
// errInvalidKubernetesName indicates that the name does not meet the Kubernetes
// restrictions on metadata names.
errInvalidKubernetesName = fmt.Errorf(
"invalid metadata name, must match regex %s and the length must not be longer than 253",
validName.String())
)
const (
// According to the Kubernetes docs (https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#rfc-1035-label-names)
// some resource names have a max length of 63 characters while others have a max
// length of 253 characters. As we cannot be sure the resources used in a chart, we
// therefore need to limit it to 63 chars and reserve 10 chars for additional part to name
// of the resource. The reason is that chart maintainers can use release name as part of
// the resource name (and some additional chars).
maxReleaseNameLen = 53
// maxMetadataNameLen is the maximum length Kubernetes allows for any name.
maxMetadataNameLen = 253
)
// ValidateReleaseName performs checks for an entry for a Helm release name
//
// For Helm to allow a name, it must be below a certain character count (53) and also match
// a regular expression.
//
// According to the Kubernetes help text, the regular expression it uses is:
//
// [a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*
//
// This follows the above regular expression (but requires a full string match, not partial).
//
// The Kubernetes documentation is here, though it is not entirely correct:
// https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
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
}
return nil
}
// ValidateMetadataName validates the name field of a Kubernetes metadata object.
//
// Empty strings, strings longer than 253 chars, or strings that don't match the regexp
// will fail.
//
// According to the Kubernetes help text, the regular expression it uses is:
//
// [a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*
//
// This follows the above regular expression (but requires a full string match, not partial).
//
// The Kubernetes documentation is here, though it is not entirely correct:
// https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
//
// Deprecated: remove in Helm 4. Name validation now uses rules defined in
// pkg/lint/rules.validateMetadataNameFunc()
func ValidateMetadataName(name string) error {
if name == "" || len(name) > maxMetadataNameLen || !validName.MatchString(name) {
return errInvalidKubernetesName
}
return nil
}

@ -1,93 +0,0 @@
/*
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 util
import (
"testing"
"github.com/stretchr/testify/assert"
)
// TestValidateReleaseName is a regression test for ValidateName
//
// Kubernetes has strict naming conventions for resource names. This test represents
// those conventions.
//
// See https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
//
// NOTE: At the time of this writing, the docs above say that names cannot begin with
// digits. However, `kubectl`'s regular expression explicit allows this, and
// Kubernetes (at least as of 1.18) also accepts resources whose names begin with digits.
func TestValidateReleaseName(t *testing.T) {
names := map[string]bool{
"": false,
"foo": true,
"foo.bar1234baz.seventyone": true,
"FOO": false,
"123baz": true,
"foo.BAR.baz": false,
"one-two": true,
"-two": false,
"one_two": false,
"a..b": false,
"%^&#$%*@^*@&#^": false,
"example:com": false,
"example%%com": false,
"a1111111111111111111111111111111111111111111111111111111111z": false,
}
for input, expectPass := range names {
err := ValidateReleaseName(input)
if expectPass {
assert.NoErrorf(t, err, "Expected release name %q to pass validation", input)
} else {
assert.Errorf(t, err, "Expected release name %q to fail validation, but it passed", input)
}
}
}
func TestValidateMetadataName(t *testing.T) {
names := map[string]bool{
"": false,
"foo": true,
"foo.bar1234baz.seventyone": true,
"FOO": false,
"123baz": true,
"foo.BAR.baz": false,
"one-two": true,
"-two": false,
"one_two": false,
"a..b": false,
"%^&#$%*@^*@&#^": false,
"example:com": false,
"example%%com": false,
"a1111111111111111111111111111111111111111111111111111111111z": true,
"a1111111111111111111111111111111111111111111111111111111111z" +
"a1111111111111111111111111111111111111111111111111111111111z" +
"a1111111111111111111111111111111111111111111111111111111111z" +
"a1111111111111111111111111111111111111111111111111111111111z" +
"a1111111111111111111111111111111111111111111111111111111111z" +
"a1111111111111111111111111111111111111111111111111111111111z": false,
}
for input, expectPass := range names {
err := ValidateMetadataName(input)
if expectPass {
assert.NoError(t, err, "Expected %q to succeed", input)
} else {
assert.Error(t, err, "Expected %q to fail", input)
}
}
}

@ -101,11 +101,15 @@ type ChartVersion struct {
// Search performs a search against the monocular search API
//
// Deprecated: Use SearchWithContext instead.
//
//go:fix inline
func (c *Client) Search(term string) ([]SearchResult, error) {
return c.SearchWithContext(context.Background(), term)
}
// SearchWithContext performs a search against the monocular search API
//
// TODO Helm v5: Rename this to Search (remove the current deprecated Search method) and 'go:fix inline' SearchWithContext method to call Search.
func (c *Client) SearchWithContext(ctx context.Context, term string) ([]SearchResult, error) {
// Create the URL to the search endpoint
// Note, this is currently an internal API for the Hub. This should be

@ -41,7 +41,7 @@ func TestUnmarshaConfig(t *testing.T) {
ShortHelp: "short help string",
LongHelp: "long help string",
IgnoreFlags: true,
}, *(config.(*schema.ConfigCLIV1)))
}, *config.(*schema.ConfigCLIV1))
}
// Test unmarshalling invalid config data

@ -48,7 +48,7 @@ var Extractors = map[string]Extractor{
// Convert a media type to an extractor extension.
//
// This should be refactored in Helm 4, combined with the extension-based mechanism.
// TODO Helm v5: refactor this, combined with the extension-based mechanism.
func mediaTypeToExtension(mt string) (string, bool) {
switch strings.ToLower(mt) {
case "application/gzip", "application/x-gzip", "application/x-tgz", "application/x-gtar":

@ -153,7 +153,7 @@ func NewForSource(source, version string) (installer Installer, err error) {
return installer, fmt.Errorf("cannot get information about plugin source %q (if it's a local directory, does it exist?), last error was: %w", source, err)
}
return
return installer, err
}
// FindSource determines the correct Installer for the given source.

@ -53,7 +53,7 @@ func detectPluginRoot(extractDir string) (string, error) {
// validatePluginName checks if the plugin directory name matches the plugin name
// from plugin.yaml when the plugin is in a subdirectory.
func validatePluginName(pluginRoot string, expectedName string) error {
func validatePluginName(pluginRoot, expectedName string) error {
// Only validate if plugin is in a subdirectory
dirName := filepath.Base(pluginRoot)
if dirName == expectedName {

@ -16,7 +16,7 @@ limitations under the License.
/*
This file contains a "registry" of supported plugin types.
It enables "dyanmic" operations on the go type associated with a given plugin type (see: `helm.sh/helm/v4/internal/plugin/schema` package)
It enables "dynamic" operations on the go type associated with a given plugin type (see: `helm.sh/helm/v4/internal/plugin/schema` package)
Examples:

@ -212,7 +212,7 @@ func (p *ExtismV1PluginRuntime) Invoke(ctx context.Context, input *Input) (*Outp
return output, nil
}
func buildManifest(pluginDir string, tmpDir string, rc *RuntimeConfigExtismV1) (extism.Manifest, error) {
func buildManifest(pluginDir, tmpDir string, rc *RuntimeConfigExtismV1) (extism.Manifest, error) {
wasmFile := filepath.Join(pluginDir, ExtismV1WasmBinaryFilename)
allowedHosts := rc.AllowedHosts

@ -47,7 +47,7 @@ type RuntimeConfigSubprocess struct {
PlatformHooks PlatformHooks `yaml:"platformHooks"`
// ProtocolCommands allows the plugin to specify protocol specific commands
//
// Obsolete/deprecated: This is a compatibility hangover from the old plugin downloader mechanism, which was extended
// Deprecated: This is an obsolete compatibility hangover from the old plugin downloader mechanism, which was extended
// to support multiple protocols in a given plugin. The command supplied in PlatformCommand should implement protocol
// specific logic by inspecting the download URL
ProtocolCommands []SubprocessProtocolCommand `yaml:"protocolCommands,omitempty"`
@ -74,7 +74,7 @@ func (r *RuntimeSubprocess) CreatePlugin(pluginDir string, metadata *Metadata) (
return &SubprocessPluginRuntime{
metadata: *metadata,
pluginDir: pluginDir,
RuntimeConfig: *(metadata.RuntimeConfig.(*RuntimeConfigSubprocess)),
RuntimeConfig: *metadata.RuntimeConfig.(*RuntimeConfigSubprocess),
EnvVars: maps.Clone(r.EnvVars),
}, nil
}
@ -112,7 +112,7 @@ func (r *SubprocessPluginRuntime) Invoke(ctx context.Context, input *Input) (*Ou
// InvokeWithEnv executes a plugin command with custom environment and I/O streams
// This method allows execution with different command/args than the plugin's default
func (r *SubprocessPluginRuntime) InvokeWithEnv(main string, argv []string, env []string, stdin io.Reader, stdout, stderr io.Writer) error {
func (r *SubprocessPluginRuntime) InvokeWithEnv(main string, argv, env []string, stdin io.Reader, stdout, stderr io.Writer) error {
mainCmdExp := os.ExpandEnv(main)
cmd := exec.CommandContext(context.Background(), mainCmdExp, argv...)
cmd.Env = slices.Clone(os.Environ())
@ -153,8 +153,7 @@ func (r *SubprocessPluginRuntime) InvokeHook(event string) error {
slog.Debug("executing plugin hook command", slog.String("pluginName", r.metadata.Name), slog.String("command", cmd.String()))
if err := cmd.Run(); err != nil {
var eerr *exec.ExitError
if errors.As(err, &eerr) {
if eerr, ok := errors.AsType[*exec.ExitError](err); ok {
os.Stderr.Write(eerr.Stderr)
return fmt.Errorf("plugin %s hook for %q exited with error", event, r.metadata.Name)
}
@ -168,8 +167,7 @@ func (r *SubprocessPluginRuntime) InvokeHook(event string) error {
// then replace the other three with a call to this func
func executeCmd(prog *exec.Cmd, pluginName string) error {
if err := prog.Run(); err != nil {
var eerr *exec.ExitError
if errors.As(err, &eerr) {
if eerr, ok := errors.AsType[*exec.ExitError](err); ok {
slog.Debug(
"plugin execution failed",
slog.String("pluginName", pluginName),

@ -42,7 +42,7 @@ func getProtocolCommand(commands []SubprocessProtocolCommand, protocol string) *
// TODO can we replace a lot of this func with RuntimeSubprocess.invokeWithEnv?
func (r *SubprocessPluginRuntime) runGetter(ctx context.Context, input *Input) (*Output, error) {
msg, ok := (input.Message).(schema.InputMessageGetterV1)
msg, ok := input.Message.(schema.InputMessageGetterV1)
if !ok {
return nil, fmt.Errorf("expected input type schema.InputMessageGetterV1, got %T", input)
}

@ -111,7 +111,7 @@ func GetPluginSigningInfo(metadata Metadata) (*SigningInfo, error) {
}, nil
}
func validateProvenanceHash(blockContent string, tarballPath string) bool {
func validateProvenanceHash(blockContent, tarballPath string) bool {
// Parse provenance to get the expected hash
_, sums, err := parsePluginMessageBlock([]byte(blockContent))
if err != nil {

@ -41,7 +41,7 @@ func (fn FilterFunc) Filter(rels []*v2.Release) (rets []*v2.Release) {
rets = append(rets, rel)
}
}
return
return rets
}
// Any returns a FilterFunc that filters a list of releases

@ -137,7 +137,7 @@ func sortHooksByKind(hooks []*release.Hook, ordering KindSortOrder) []*release.H
return h
}
func lessByKind(_ any, _ any, kindA string, kindB string, o KindSortOrder) bool {
func lessByKind(_, _ any, kindA, kindB string, o KindSortOrder) bool {
ordering := make(map[string]int, len(o))
for v, k := range o {
ordering[k] = v

@ -49,7 +49,7 @@ func AssertGoldenString(t TestingT, actual, filename string) {
}
// AssertGoldenFile asserts that the content of the actual file matches the contents of the expected file
func AssertGoldenFile(t TestingT, actualFileName string, expectedFilename string) {
func AssertGoldenFile(t TestingT, actualFileName, expectedFilename string) {
t.Helper()
actual, err := os.ReadFile(actualFileName)

@ -136,11 +136,11 @@ func IsRollingUpdate(deployment *apps.Deployment) bool {
// MaxUnavailable returns the maximum unavailable pods a rolling deployment can take.
func MaxUnavailable(deployment apps.Deployment) int32 {
if !IsRollingUpdate(&deployment) || *(deployment.Spec.Replicas) == 0 {
if !IsRollingUpdate(&deployment) || *deployment.Spec.Replicas == 0 {
return int32(0)
}
// Error caught by validation
_, maxUnavailable, _ := ResolveFenceposts(deployment.Spec.Strategy.RollingUpdate.MaxSurge, deployment.Spec.Strategy.RollingUpdate.MaxUnavailable, *(deployment.Spec.Replicas))
_, maxUnavailable, _ := ResolveFenceposts(deployment.Spec.Strategy.RollingUpdate.MaxSurge, deployment.Spec.Strategy.RollingUpdate.MaxUnavailable, *deployment.Spec.Replicas)
if maxUnavailable > *deployment.Spec.Replicas {
return *deployment.Spec.Replicas
}

@ -17,14 +17,20 @@ limitations under the License.
package version
import (
"strings"
"testing"
"github.com/stretchr/testify/require"
)
func TestK8sClientGoModVersion(t *testing.T) {
// Unfortunately, test builds don't include debug info / module info
// So we expect "K8sIOClientGoModVersion" to return error
_, err := K8sIOClientGoModVersion()
require.ErrorContains(t, err, "k8s.io/client-go not found in build info")
// Whether module info is embedded in a test binary depends on the Go
// release: builds before Go 1.27 omit it, so the lookup fails. Accept
// either outcome, but require that a successful lookup returns a version.
v, err := K8sIOClientGoModVersion()
if err != nil {
require.ErrorContains(t, err, "k8s.io/client-go not found in build info")
return
}
require.True(t, strings.HasPrefix(v, "v"), "expected a semver-like version, got %q", v)
}

@ -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.2"
version = "v4.3"
// metadata is extra build time data
metadata = ""

@ -508,8 +508,8 @@ func (cfg *Configuration) renderResources(ctx context.Context, ch *chart.Chart,
newDir = filepath.Join(outputDir, releaseName)
}
// NOTE: We do not have to worry about the post-renderer because
// output dir is only used by `helm template`. In the next major
// release, we should move this logic to template only as it is not
// output dir is only used by `helm template`.
// TODO Helm v5: move this logic to template only as it is not
// used by install or upgrade
err = writeToFile(newDir, m.Name, m.Content, fileWritten[m.Name])
if err != nil {

@ -77,12 +77,14 @@ func (d *Dependency) dependencyStatus(chartpath string, dep *chart.Dependency, p
filename := fmt.Sprintf("%s-%s.tgz", dep.Name, "*")
// If a chart is unpacked, this will check the unpacked chart's `charts/` directory for tarballs.
// Technically, this is COMPLETELY unnecessary, and should be removed in Helm 4. It is here
// to preserved backward compatibility. In Helm 2/3, there is a "difference" between
//
// Technically, this is COMPLETELY unnecessary. It is here to preserve backward
// compatibility. In Helm 2/3, there is a "difference" between
// the tgz version (which outputs "ok" if it unpacks) and the loaded version (which outputs
// "unpacked"). Early in Helm 2's history, this would have made a difference. But it no
// longer does. However, since this code shipped with Helm 3, the output must remain stable
// until Helm 4.
// longer does. However, since this code shipped with Helm 3, the output must remain stable.
//
// TODO Helm v5: remove this.
switch archives, err := filepath.Glob(filepath.Join(chartpath, "charts", filename)); {
case err != nil:
return "bad pattern"
@ -156,7 +158,9 @@ func (d *Dependency) dependencyStatus(chartpath string, dep *chart.Dependency, p
// stat an archive and return a message if the stat is successful
//
// This is a refactor of the code originally in dependencyStatus. It is here to
// support legacy behavior, and should be removed in Helm 4.
// support legacy behavior.
//
// TODO Helm v5: remove this.
func statArchiveForStatus(archive string, dep *chart.Dependency) string {
if _, err := os.Stat(archive); err == nil {
c, err := loader.Load(archive)

@ -489,7 +489,7 @@ func (i *Install) RunWithContext(ctx context.Context, ch ci.Charter, vals map[st
return rel, err
}
func (i *Install) performInstallCtx(ctx context.Context, rel *release.Release, toBeAdopted kube.ResourceList, resources kube.ResourceList) (*release.Release, error) {
func (i *Install) performInstallCtx(ctx context.Context, rel *release.Release, toBeAdopted, resources kube.ResourceList) (*release.Release, error) {
type Msg struct {
r *release.Release
e error
@ -516,7 +516,7 @@ func (i *Install) getGoroutineCount() int32 {
return i.goroutineCount.Load()
}
func (i *Install) performInstall(rel *release.Release, toBeAdopted kube.ResourceList, resources kube.ResourceList) (*release.Release, error) {
func (i *Install) performInstall(rel *release.Release, toBeAdopted, resources kube.ResourceList) (*release.Release, error) {
var err error
// pre-install hooks
if !i.DisableHooks {
@ -730,7 +730,7 @@ func (i *Install) replaceRelease(rel *release.Release) error {
}
// write the <data> to <output-dir>/<name>. <appendData> controls if the file is created or content will be appended
func writeToFile(outputDir string, name string, data string, appendData bool) error {
func writeToFile(outputDir, name, data string, appendData bool) error {
outfileName := outputDir + string(filepath.Separator) + name
err := ensureDirectoryForFile(outfileName)

@ -17,9 +17,14 @@ limitations under the License.
package action
import (
"archive/tar"
"compress/gzip"
"errors"
"io"
"os"
"path"
"testing"
"time"
"github.com/Masterminds/semver/v3"
"github.com/stretchr/testify/assert"
@ -144,3 +149,60 @@ func TestRun(t *testing.T) {
require.Equal(t, "empty-0.1.0.tgz", filename)
require.NoError(t, os.Remove(filename))
}
// TestRunWithSourceDateEpochStampsLockGenerated verifies that packaging a chart
// that has a Chart.lock stamps both the tar entry modtime and the marshaled
// generated: field in Chart.lock to the given epoch.
//
// This guards against the normalization regression where a caller supplying a
// local-timezone or sub-second time.Time would produce a non-reproducible
// generated: value even when the same SOURCE_DATE_EPOCH is used on different
// machines.
func TestRunWithSourceDateEpochStampsLockGenerated(t *testing.T) {
// Use a non-local, non-UTC timezone and sub-second precision to confirm
// normalization: without UTC().Truncate(time.Second) the generated: field
// would contain a timezone offset or fractional seconds.
loc := time.FixedZone("UTC+3", 3*60*60)
rawEpoch := time.Unix(1700000000, 123456789).In(loc)
epoch := rawEpoch.UTC().Truncate(time.Second)
client := NewPackage()
client.SourceDateEpoch = &rawEpoch
filename, err := client.Run("testdata/charts/chart-with-lock", nil)
require.NoError(t, err)
t.Cleanup(func() { os.Remove(filename) })
f, err := os.Open(filename)
require.NoError(t, err)
defer f.Close()
gr, err := gzip.NewReader(f)
require.NoError(t, err)
defer gr.Close()
const wantPath = "chart-with-lock/Chart.lock"
found := false
tr := tar.NewReader(gr)
for {
hdr, err := tr.Next()
if errors.Is(err, io.EOF) {
break
}
require.NoError(t, err)
if hdr.Name != wantPath {
continue
}
found = true
require.True(t, epoch.Equal(hdr.ModTime),
"Chart.lock tar modtime: got %v, want %v", hdr.ModTime, epoch)
raw, err := io.ReadAll(tr)
require.NoError(t, err)
wantGenerated := epoch.Format(time.RFC3339)
require.Contains(t, string(raw), wantGenerated,
"Chart.lock generated: field should contain normalized UTC timestamp")
break
}
require.True(t, found, "expected archive to contain %q entry", wantPath)
}

@ -90,7 +90,7 @@ func NewPushWithOpts(opts ...PushOpt) *Push {
}
// Run executes 'helm push' against the given chart archive.
func (p *Push) Run(chartRef string, remote string) (string, error) {
func (p *Push) Run(chartRef, remote string) (string, error) {
var out strings.Builder
c := uploader.ChartUploader{

@ -82,7 +82,7 @@ func NewRegistryLogin(cfg *Configuration) *RegistryLogin {
}
// Run executes the registry login operation
func (a *RegistryLogin) Run(_ io.Writer, hostname string, username string, password string, opts ...RegistryLoginOpt) error {
func (a *RegistryLogin) Run(_ io.Writer, hostname, username, password string, opts ...RegistryLoginOpt) error {
for _, opt := range opts {
if err := opt(a); err != nil {
return err

@ -0,0 +1,3 @@
dependencies: []
digest: sha256:0000000000000000000000000000000000000000000000000000000000000
generated: "2016-01-01T00:00:00Z"

@ -0,0 +1,4 @@
apiVersion: v2
name: chart-with-lock
version: 0.1.0
description: Test chart with a Chart.lock, used to verify SourceDateEpoch stamping

@ -467,7 +467,7 @@ func isReleaseApplyMethodClientSideApply(applyMethod string) bool {
return applyMethod == "" || applyMethod == string(release.ApplyMethodClientSideApply)
}
func (u *Upgrade) releasingUpgrade(c chan<- resultMessage, upgradedRelease *release.Release, current kube.ResourceList, target kube.ResourceList, originalRelease *release.Release, serverSideApply bool) {
func (u *Upgrade) releasingUpgrade(c chan<- resultMessage, upgradedRelease *release.Release, current, target kube.ResourceList, originalRelease *release.Release, serverSideApply bool) {
// pre-upgrade hooks
if !u.DisableHooks {
@ -678,7 +678,7 @@ func mergeCustomLabels(current, desired map[string]string) map[string]string {
return labels
}
func getUpgradeServerSideValue(serverSideOption string, releaseApplyMethod string) (bool, error) {
func getUpgradeServerSideValue(serverSideOption, releaseApplyMethod string) (bool, error) {
switch serverSideOption {
case "auto":
return releaseApplyMethod == "ssa", nil

@ -75,7 +75,7 @@ func newMissingDeployment(name, namespace string) *resource.Info {
return info
}
func newDeploymentWithOwner(name, namespace string, labels map[string]string, annotations map[string]string) *resource.Info {
func newDeploymentWithOwner(name, namespace string, labels, annotations map[string]string) *resource.Info {
obj := &appsv1.Deployment{
ObjectMeta: v1.ObjectMeta{
Name: name,

@ -87,7 +87,9 @@ func (kv *KubeVersion) String() string {
// GitVersion returns the full Kubernetes version string.
//
// Deprecated: use KubeVersion.Version.
// Deprecated: Use KubeVersion.Version.
//
//go:fix inline
func (kv *KubeVersion) GitVersion() string { return kv.Version }
// ParseKubeVersion parses kubernetes version from string

@ -46,7 +46,7 @@ func TestDefaultCapabilities(t *testing.T) {
assert.Equalf(t, "20", kv.Minor, "Expected default KubeVersion.Minor to be 20, got %q", kv.Minor)
hv := caps.HelmVersion
assert.Equalf(t, "v4.2", hv.Version, "Expected default HelmVersion to be v4.2, got %q", hv.Version)
assert.Equalf(t, "v4.3", hv.Version, "Expected default HelmVersion to be v4.3, got %q", hv.Version)
}
func TestParseKubeVersion(t *testing.T) {

@ -37,8 +37,10 @@ import (
// The default value is 100 MiB.
var MaxDecompressedChartSize int64 = 100 * 1024 * 1024 // Default 100 MiB
// MaxDecompressedFileSize is the size of the largest file that Helm will attempt to load.
// The size of the file is the decompressed version of it when it is stored in an archive.
// MaxDecompressedFileSize was the per-file size limit enforced during chart loading.
// It is no longer used internally; aggregate chart size is enforced via MaxDecompressedChartSize.
//
// Deprecated: Retained for backward compatibility with external callers. Will be removed in Helm v5.
var MaxDecompressedFileSize int64 = 5 * 1024 * 1024 // Default 5 MiB
var drivePathPattern = regexp.MustCompile(`^[a-zA-Z]:/`)
@ -128,10 +130,6 @@ func LoadArchiveFiles(in io.Reader) ([]*BufferedFile, error) {
return nil, fmt.Errorf("decompressed chart is larger than the maximum size %d", MaxDecompressedChartSize)
}
if hd.Size > MaxDecompressedFileSize {
return nil, fmt.Errorf("decompressed chart file %q is larger than the maximum file size %d", hd.Name, MaxDecompressedFileSize)
}
limitedReader := io.LimitReader(tr, remainingSize)
bytesWritten, err := io.Copy(b, limitedReader)

@ -0,0 +1,74 @@
/*
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 archive
import (
"fmt"
"io"
"math"
"os"
)
// BudgetedReader tracks cumulative file reads against a size limit.
type BudgetedReader struct {
max int64
remaining int64
}
// NewBudgetedReader creates a BudgetedReader with the given maximum total size.
// The remaining budget is initialized to the maximum.
func NewBudgetedReader(limit int64) *BudgetedReader {
return &BudgetedReader{
max: limit,
remaining: limit,
}
}
// ReadFileWithBudget reads a file and decrements the remaining budget by the bytes read.
// It returns an error if the total would exceed the configured maximum.
// The read is capped via io.LimitReader so a file that grows between stat
// and read cannot cause unbounded memory allocation.
func (r *BudgetedReader) ReadFileWithBudget(path string, size int64) ([]byte, error) {
if size > r.remaining {
return nil, fmt.Errorf("chart exceeds maximum decompressed size of %d bytes", r.max)
}
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
// Read at most r.remaining+1 bytes so we can detect over-budget without
// allocating unbounded memory if the file grew since stat.
// Clamp to avoid int64 overflow when r.remaining is near math.MaxInt64.
limit := r.remaining
if limit < math.MaxInt64 {
limit++
}
data, err := io.ReadAll(io.LimitReader(f, limit))
if err != nil {
return nil, err
}
if int64(len(data)) > r.remaining {
return nil, fmt.Errorf("chart exceeds maximum decompressed size of %d bytes", r.max)
}
r.remaining -= int64(len(data))
return data, nil
}

@ -0,0 +1,128 @@
/*
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 archive
import (
"fmt"
"os"
"path/filepath"
"testing"
)
func TestReadFileWithBudget(t *testing.T) {
dir := t.TempDir()
writeFile := func(t *testing.T, name string, size int) string {
t.Helper()
p := filepath.Join(dir, name)
if err := os.WriteFile(p, make([]byte, size), 0o644); err != nil {
t.Fatal(err)
}
return p
}
tcs := []struct {
name string
check func(t *testing.T)
}{
{
name: "reads file and decrements budget",
check: func(t *testing.T) {
t.Helper()
p := writeFile(t, "small.txt", 100)
fi, err := os.Stat(p)
if err != nil {
t.Fatalf("failed to stat %s: %v", p, err)
}
limit := int64(1000)
br := NewBudgetedReader(limit)
data, err := br.ReadFileWithBudget(p, fi.Size())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(data) != 100 {
t.Fatalf("expected 100 bytes, got %d", len(data))
}
if br.remaining != 900 {
t.Fatalf("expected remaining=900, got %d", br.remaining)
}
},
},
{
name: "rejects file exceeding budget",
check: func(t *testing.T) {
t.Helper()
p := writeFile(t, "big.txt", 500)
fi, err := os.Stat(p)
if err != nil {
t.Fatalf("failed to stat %s: %v", p, err)
}
limit := int64(100)
br := NewBudgetedReader(limit)
_, err = br.ReadFileWithBudget(p, fi.Size())
if err == nil {
t.Fatal("expected error for file exceeding budget")
}
expectedErr := fmt.Sprintf("chart exceeds maximum decompressed size of %d bytes", limit)
if err.Error() != expectedErr {
t.Fatalf("expected %q, got %q", expectedErr, err.Error())
}
if br.remaining != 100 {
t.Fatalf("budget should not change on rejection, got %d", br.remaining)
}
},
},
{
name: "tracks budget across multiple reads",
check: func(t *testing.T) {
t.Helper()
remaining := int64(250)
br := NewBudgetedReader(remaining)
for i := range 3 {
p := writeFile(t, fmt.Sprintf("f%d.txt", i), 80)
fi, err := os.Stat(p)
if err != nil {
t.Fatalf("failed to stat %s: %v", p, err)
}
if _, err := br.ReadFileWithBudget(p, fi.Size()); err != nil {
t.Fatalf("read %d: unexpected error: %v", i, err)
}
}
if br.remaining != 10 {
t.Fatalf("expected remaining=10, got %d", br.remaining)
}
p := writeFile(t, "over.txt", 20)
fi, err := os.Stat(p)
if err != nil {
t.Fatalf("failed to stat %s: %v", p, err)
}
_, err = br.ReadFileWithBudget(p, fi.Size())
if err == nil {
t.Fatal("expected error when cumulative reads exceed budget")
}
},
},
}
for _, tc := range tcs {
t.Run(tc.name, tc.check)
}
}

@ -144,7 +144,7 @@ func (ch *Chart) AppVersion() string {
// CRDs returns a list of File objects in the 'crds/' directory of a Helm chart.
//
// Deprecated: use CRDObjects()
// Deprecated: Use CRDObjects instead.
func (ch *Chart) CRDs() []*common.File {
files := []*common.File{}
// Find all resources in the crds/ directory
@ -177,34 +177,41 @@ func (ch *Chart) CRDObjects() []CRD {
return crds
}
// StampModTimes sets timestamps on the chart (and dependencies) to epoch.
// This is used for reproducible builds via SOURCE_DATE_EPOCH.
func (ch *Chart) StampModTimes(epoch time.Time) {
ch.ModTime = epoch
// StampModTimes sets timestamps on the chart (and dependencies) to t,
// normalized to UTC and truncated to whole seconds.
//
// Normalization is required because Chart.lock's generated: field is written
// by yaml.Marshal from Lock.Generated. Without UTC/truncate, a caller
// supplying a local-zone or sub-second time.Time produces a generated: value
// with a timezone offset or fractional seconds, making the lock file content
// non-reproducible across machines even when the same SOURCE_DATE_EPOCH is used.
func (ch *Chart) StampModTimes(t time.Time) {
t = t.UTC().Truncate(time.Second)
ch.ModTime = t
if len(ch.Schema) > 0 {
ch.SchemaModTime = epoch
ch.SchemaModTime = t
}
if ch.Lock != nil {
ch.Lock.Generated = epoch
ch.Lock.Generated = t
}
for _, f := range ch.Raw {
if f != nil {
f.ModTime = epoch
f.ModTime = t
}
}
for _, f := range ch.Templates {
if f != nil {
f.ModTime = epoch
f.ModTime = t
}
}
for _, f := range ch.Files {
if f != nil {
f.ModTime = epoch
f.ModTime = t
}
}
for _, dep := range ch.Dependencies() {
dep.StampModTimes(epoch)
dep.StampModTimes(t)
}
}

@ -43,6 +43,10 @@ func (l DirLoader) Load() (*chart.Chart, error) {
//
// This loads charts only from directories.
func LoadDir(dir string) (*chart.Chart, error) {
return loadDir(dir, archive.MaxDecompressedChartSize)
}
func loadDir(dir string, budget int64) (*chart.Chart, error) {
topdir, err := filepath.Abs(dir)
if err != nil {
return nil, err
@ -64,6 +68,7 @@ func LoadDir(dir string) (*chart.Chart, error) {
files := []*archive.BufferedFile{}
topdir += string(filepath.Separator)
budgetReader := archive.NewBudgetedReader(budget)
walk := func(name string, fi os.FileInfo, err error) error {
n := strings.TrimPrefix(name, topdir)
@ -100,11 +105,7 @@ func LoadDir(dir string) (*chart.Chart, error) {
return fmt.Errorf("cannot load irregular file %s as it has file mode type bits set", name)
}
if fi.Size() > archive.MaxDecompressedFileSize {
return fmt.Errorf("chart file %q is larger than the maximum file size %d", fi.Name(), archive.MaxDecompressedFileSize)
}
data, err := os.ReadFile(name)
data, err := budgetReader.ReadFileWithBudget(name, fi.Size())
if err != nil {
return fmt.Errorf("error reading %s: %w", n, err)
}

@ -113,8 +113,8 @@ func LoadFiles(files []*archive.BufferedFile) (*chart.Chart, error) {
c.Schema = f.Data
c.SchemaModTime = f.ModTime
// Deprecated: requirements.yaml is deprecated use Chart.yaml.
// We will handle it for you because we are nice people
// requirements.yaml is deprecated; dependencies are declared in Chart.yaml since
// apiVersion v2. Handled here for backwards compatibility.
case f.Name == "requirements.yaml":
if c.Metadata == nil {
c.Metadata = new(chart.Metadata)
@ -128,7 +128,8 @@ func LoadFiles(files []*archive.BufferedFile) (*chart.Chart, error) {
if c.Metadata.APIVersion == chart.APIVersionV1 {
c.Files = append(c.Files, &common.File{Name: f.Name, ModTime: f.ModTime, Data: f.Data})
}
// Deprecated: requirements.lock is deprecated use Chart.lock.
// requirements.lock is deprecated; use Chart.lock. Handled here for backwards
// compatibility.
case f.Name == "requirements.lock":
c.Lock = new(chart.Lock)
if err := yaml.Unmarshal(f.Data, &c.Lock); err != nil {
@ -153,7 +154,7 @@ func LoadFiles(files []*archive.BufferedFile) (*chart.Chart, error) {
}
fname := strings.TrimPrefix(f.Name, "charts/")
cname := strings.SplitN(fname, "/", 2)[0]
cname, _, _ := strings.Cut(fname, "/")
subcharts[cname] = append(subcharts[cname], &archive.BufferedFile{Name: fname, ModTime: f.ModTime, Data: f.Data})
default:
c.Files = append(c.Files, &common.File{Name: f.Name, ModTime: f.ModTime, Data: f.Data})
@ -210,8 +211,20 @@ func LoadFiles(files []*archive.BufferedFile) (*chart.Chart, error) {
// The reader is expected to contain one or more YAML documents, the values of which are merged.
// And the values can be either a chart's default values or user-supplied values.
func LoadValues(data io.Reader) (map[string]any, error) {
// Read fully first. YAMLReader/LineReader can drop a final unterminated
// line when its length is an exact multiple of bufio.Reader's default
// buffer (4096). Appending a trailing newline avoids that case.
// See https://github.com/helm/helm/issues/32506
b, err := io.ReadAll(data)
if err != nil {
return nil, err
}
if len(b) > 0 && b[len(b)-1] != '\n' {
b = append(b, '\n')
}
values := map[string]any{}
reader := utilyaml.NewYAMLReader(bufio.NewReader(data))
reader := utilyaml.NewYAMLReader(bufio.NewReader(bytes.NewReader(b)))
for {
currentMap := map[string]any{}
raw, err := reader.Read()

@ -21,6 +21,7 @@ import (
"bytes"
"compress/gzip"
"errors"
"fmt"
"io"
"log"
"os"
@ -49,6 +50,16 @@ func TestLoadDir(t *testing.T) {
verifyDependenciesLock(t, c)
}
func TestLoadDirExceedsBudget(t *testing.T) {
_, err := loadDir("testdata/frobnitz", 1)
if err == nil {
t.Fatal("expected error when chart directory exceeds budget")
}
if !strings.Contains(err.Error(), "chart exceeds maximum decompressed size") {
t.Fatalf("unexpected error: %v", err)
}
}
func TestLoadDirWithDevNull(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("test only works on unix systems with /dev/null present")
@ -463,6 +474,33 @@ foo:
}
}
func TestLoadValuesEOFBoundary(t *testing.T) {
// Reproduces #32506: a single logical line whose length is a multiple of
// bufio's default buffer (4096) and has no trailing newline used to be
// dropped entirely by YAMLReader, yielding empty values.
// Also cover 8192 (2x buffer) so we do not only hit the single-buffer case.
for _, size := range []int{4096, 8192} {
t.Run(fmt.Sprintf("size_%d", size), func(t *testing.T) {
prefix := []byte(`{"foo":"`)
suffix := []byte(`"}`)
pad := size - len(prefix) - len(suffix)
data := make([]byte, 0, size)
data = append(data, prefix...)
data = append(data, bytes.Repeat([]byte("x"), pad)...)
data = append(data, suffix...)
if len(data) != size {
t.Fatalf("test setup: want data length %d, got %d", size, len(data))
}
values, err := LoadValues(bytes.NewReader(data))
require.NoError(t, err)
assert.Equal(t, map[string]any{
"foo": string(bytes.Repeat([]byte("x"), pad)),
}, values)
})
}
}
func TestMergeValuesV2(t *testing.T) {
nestedMap := map[string]any{
"foo": "bar",

@ -643,7 +643,7 @@ spec:
// Stderr is an io.Writer to which error messages can be written
//
// In Helm 4, this will be replaced. It is needed in Helm 3 to preserve API backward
// TODO Helm v5: replace this. It is needed in Helm 3 to preserve API backward
// compatibility.
var Stderr io.Writer = os.Stderr

@ -46,7 +46,6 @@ func processDependencyConditions(reqs []*chart.Dependency, cvals common.Values,
if c != "" {
// retrieve value
vv, err := cvals.PathValue(cpath + c)
var errNoValue common.ErrNoValue
if err == nil {
// if not bool, warn
if bv, ok := vv.(bool); ok {
@ -54,7 +53,7 @@ func processDependencyConditions(reqs []*chart.Dependency, cvals common.Values,
break
}
slog.Warn("returned non-bool value", "path", c, "chart", r.Name)
} else if !errors.As(err, &errNoValue) {
} else if _, ok := errors.AsType[common.ErrNoValue](err); !ok {
// this is a real error
slog.Warn("the method PathValue returned error", slog.Any("error", err))
}

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

@ -183,7 +183,7 @@ func writeTarContents(out *tar.Writer, c *chart.Chart, prefix string) error {
}
// Save Chart.lock
// TODO: remove the APIVersion check when APIVersionV1 is not used anymore
// TODO Helm v5: remove the APIVersion check when APIVersionV1 is not used anymore
if c.Metadata.APIVersion == chart.APIVersionV2 {
if c.Lock != nil {
ldata, err := yaml.Marshal(c.Lock)

@ -298,13 +298,10 @@ func TestSaveDir(t *testing.T) {
}
func TestRepeatableSave(t *testing.T) {
tmp := t.TempDir()
defer os.RemoveAll(tmp)
modTime := time.Date(2021, 9, 1, 20, 34, 58, 651387237, time.UTC)
tests := []struct {
name string
chart *chart.Chart
want string
}{
{
name: "Package 1 file",
@ -325,7 +322,6 @@ func TestRepeatableSave(t *testing.T) {
Schema: []byte("{\n \"title\": \"Values\"\n}"),
SchemaModTime: modTime,
},
want: "63358874b93ea095c857cd66bcf5d0a4464840cf84a07547db744d81d6c5af59",
},
{
name: "Package 2 files",
@ -347,20 +343,26 @@ func TestRepeatableSave(t *testing.T) {
Schema: []byte("{\n \"title\": \"Values\"\n}"),
SchemaModTime: modTime,
},
want: "c2a43990053da788ad4e260d3b00d52a0b103ccc67ab9f48278a7b6dcfb2a4bd",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
// create package
dest := path.Join(tmp, "newdir")
where, err := Save(test.chart, dest)
// Package the same chart twice, into separate directories so the
// second Save does not simply overwrite the first.
first, err := Save(test.chart, t.TempDir())
require.NoError(t, err, "Failed to save")
second, err := Save(test.chart, t.TempDir())
require.NoError(t, err, "Failed to save")
// get shasum for package
result, err := sha256Sum(where)
firstSum, err := sha256Sum(first)
require.NoError(t, err, "Failed to check shasum")
// assert that the package SHA is what we wanted.
assert.Equal(t, test.want, result, "FormatName() result = %v, want %v", result, test.want)
secondSum, err := sha256Sum(second)
require.NoError(t, err, "Failed to check shasum")
// Packaging a chart must be reproducible. The digest itself is not
// pinned here because the bytes depend on the compression
// implementation of the Go release Helm is built with.
assert.Equal(t, firstSum, secondSum, "Save() is not repeatable")
})
}
}

@ -100,8 +100,11 @@ func ValidateReleaseName(name string) error {
// The Kubernetes documentation is here, though it is not entirely correct:
// https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
//
// Deprecated: remove in Helm 4. Name validation now uses rules defined in
// pkg/lint/rules.validateMetadataNameFunc()
// Deprecated: ValidateMetadataName is no longer used by Helm. Metadata name
// validation is now performed by the lint rule validateMetadataNameFunc (see
// pkg/chart/v2/lint/rules).
//
// TODO Helm v5: remove this function.
func ValidateMetadataName(name string) error {
if name == "" || len(name) > maxMetadataNameLen || !validName.MatchString(name) {
return errInvalidKubernetesName

@ -219,7 +219,7 @@ func envCSV(name string) (ls []string) {
if trimmed != "" {
ls = strings.Split(trimmed, ",")
}
return
return ls
}
func envColorMode() string {

@ -85,7 +85,7 @@ func ParseFormat(s string) (out Format, err error) {
default:
out, err = "", ErrInvalidFormatType
}
return
return out, err
}
// Writer is an interface that any type can implement to write supported formats

@ -104,7 +104,7 @@ func (o *createOptions) run(out io.Writer) error {
if !gates.ChartV3.IsEnabled() {
return gates.ChartV3.Error()
}
return o.createV3Chart(out)
return o.createV3Chart()
default:
return fmt.Errorf("unsupported chart API version: %s (supported: v2, v3)", o.chartAPIVersion)
}
@ -136,7 +136,7 @@ func (o *createOptions) createV2Chart(out io.Writer) error {
return err
}
func (o *createOptions) createV3Chart(out io.Writer) error {
func (o *createOptions) createV3Chart() error {
chartname := filepath.Base(o.name)
cfile := &chartv3.Metadata{
Name: chartname,
@ -157,7 +157,6 @@ func (o *createOptions) createV3Chart(out io.Writer) error {
return chartutilv3.CreateFrom(cfile, filepath.Dir(o.name), lstarter)
}
chartutilv3.Stderr = out
_, err := chartutilv3.Create(chartname, filepath.Dir(o.name))
return err
}

@ -19,6 +19,7 @@ import (
"errors"
"fmt"
"io"
"io/fs"
"os"
"path/filepath"
@ -82,8 +83,7 @@ func newDependencyBuildCmd(out io.Writer) *cobra.Command {
man.Verify = downloader.VerifyIfPossible
}
err = man.Build()
var e downloader.ErrRepoNotFound
if errors.As(err, &e) {
if e, ok := errors.AsType[downloader.ErrRepoNotFound](err); ok {
return fmt.Errorf("%s. Please add the missing repos via 'helm repo add'", e.Error())
}
return err
@ -97,9 +97,25 @@ func newDependencyBuildCmd(out io.Writer) *cobra.Command {
}
// defaultKeyring returns the expanded path to the default keyring.
//
// The legacy pubring.gpg file is preferred and treated as absent only when
// stat fails with "not exist" — any other stat error (e.g. permissions)
// keeps the legacy path so the real error surfaces when the file is opened.
// If the legacy file is absent, the file-backed pubring.kbx path takes over
// under the same rule. When neither exists, the legacy path is returned so
// error messages keep pointing at the traditional default.
func defaultKeyring() string {
gnupgHome := filepath.Join(homedir.HomeDir(), ".gnupg")
if v, ok := os.LookupEnv("GNUPGHOME"); ok {
return filepath.Join(v, "pubring.gpg")
gnupgHome = v
}
return filepath.Join(homedir.HomeDir(), ".gnupg", "pubring.gpg")
legacy := filepath.Join(gnupgHome, "pubring.gpg")
if _, err := os.Stat(legacy); !errors.Is(err, fs.ErrNotExist) {
return legacy
}
keybox := filepath.Join(gnupgHome, "pubring.kbx")
if _, err := os.Stat(keybox); !errors.Is(err, fs.ErrNotExist) {
return keybox
}
return legacy
}

@ -19,6 +19,7 @@ import (
"fmt"
"os"
"path/filepath"
"runtime"
"testing"
"github.com/stretchr/testify/assert"
@ -140,3 +141,68 @@ func TestDependencyBuildCmdWithHelmV2Hash(t *testing.T) {
t.Fatal(err)
}
}
func TestDefaultKeyring(t *testing.T) {
touch := func(t *testing.T, path string) {
t.Helper()
require.NoError(t, os.WriteFile(path, []byte("test"), 0o644))
}
tests := []struct {
name string
files []string
want string
}{
{"legacy keyring only", []string{"pubring.gpg"}, "pubring.gpg"},
{"keybox only", []string{"pubring.kbx"}, "pubring.kbx"},
{"legacy keyring preferred over keybox", []string{"pubring.gpg", "pubring.kbx"}, "pubring.gpg"},
{"neither present falls back to legacy path", nil, "pubring.gpg"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
dir := t.TempDir()
t.Setenv("GNUPGHOME", dir)
for _, f := range tt.files {
touch(t, filepath.Join(dir, f))
}
assert.Equal(t, filepath.Join(dir, tt.want), defaultKeyring())
})
}
t.Run("stat error other than not-exist keeps the legacy path", func(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("directory permissions are not enforced on Windows")
}
if os.Geteuid() == 0 {
t.Skip("root bypasses directory permissions")
}
parent := t.TempDir()
dir := filepath.Join(parent, ".gnupg")
require.NoError(t, os.MkdirAll(dir, 0o700))
touch(t, filepath.Join(dir, "pubring.kbx"))
t.Setenv("GNUPGHOME", dir)
// Make the directory unsearchable so stat on both keyrings fails
// with a permission error rather than "not exist".
require.NoError(t, os.Chmod(dir, 0o000))
t.Cleanup(func() { require.NoError(t, os.Chmod(dir, 0o700)) })
assert.Equal(t, filepath.Join(dir, "pubring.gpg"), defaultKeyring())
})
t.Run("no GNUPGHOME falls back to the home directory", func(t *testing.T) {
home := t.TempDir()
t.Setenv("HOME", home)
t.Setenv("GNUPGHOME", home) // register restoration before unsetting
require.NoError(t, os.Unsetenv("GNUPGHOME"))
gnupgDir := filepath.Join(home, ".gnupg")
require.NoError(t, os.MkdirAll(gnupgDir, 0o700))
assert.Equal(t, filepath.Join(gnupgDir, "pubring.gpg"), defaultKeyring())
touch(t, filepath.Join(gnupgDir, "pubring.kbx"))
assert.Equal(t, filepath.Join(gnupgDir, "pubring.kbx"), defaultKeyring())
})
}

@ -268,7 +268,7 @@ func (p *postRendererArgsSlice) GetSlice() []string {
return p.options.args
}
func compVersionFlag(chartRef string, _ string) ([]string, cobra.ShellCompDirective) {
func compVersionFlag(chartRef, _ string) ([]string, cobra.ShellCompDirective) {
chartInfo := strings.Split(chartRef, "/")
if len(chartInfo) != 2 {
return nil, cobra.ShellCompDirectiveNoFileComp

@ -214,7 +214,7 @@ func addInstallFlags(cmd *cobra.Command, f *pflag.FlagSet, client *action.Instal
// For `helm template`, these notes flags are legacy, unused, and should not show in help, but
// must remain accepted for backwards compatibility in Helm 4. Deprecate and hide them for now
// TODO remove these from template command in Helm 5
// TODO Helm v5: remove these from template command
if cmd.Name() == "template" {
if err := cmd.Flags().MarkDeprecated("hide-notes", "this flag has no effect for 'helm template' and will be removed in Helm 5"); err != nil {
log.Fatal(err)

@ -159,7 +159,7 @@ type releaseListWriter struct {
showSource bool
}
func newReleaseListWriter(releases []*release.Release, timeFormat string, noHeaders bool, noColor bool, showSource bool) *releaseListWriter {
func newReleaseListWriter(releases []*release.Release, timeFormat string, noHeaders, noColor, showSource bool) *releaseListWriter {
// Initialize the array so no results returns an empty array instead of null
elements := make([]releaseElement, 0, len(releases))
for _, r := range releases {

@ -268,7 +268,7 @@ func runPullTests(t *testing.T, tests []struct {
wantErrorMsg string
expectFile string
expectDir bool
}, outdir string, additionalFlags string,
}, outdir, additionalFlags string,
) {
t.Helper()
for _, tt := range tests {

@ -91,7 +91,7 @@ func newRegistryLoginCmd(cfg *action.Configuration, out io.Writer) *cobra.Comman
}
// Adapted from https://github.com/oras-project/oras
func getUsernamePassword(usernameOpt string, passwordOpt string, passwordFromStdinOpt bool) (string, string, error) {
func getUsernamePassword(usernameOpt, passwordOpt string, passwordFromStdinOpt bool) (string, string, error) {
var err error
username := usernameOpt
password := passwordOpt

@ -0,0 +1,109 @@
/*
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 cmd
import (
"bytes"
"fmt"
"io"
"path/filepath"
"testing"
"github.com/stretchr/testify/require"
"helm.sh/helm/v4/pkg/action"
"helm.sh/helm/v4/pkg/chart/common"
kubefake "helm.sh/helm/v4/pkg/kube/fake"
"helm.sh/helm/v4/pkg/repo/v1/repotest"
)
func TestTemplateOCIRegistryMessagesNotOnStdout(t *testing.T) {
defer resetEnv()()
stdout, stderr := runOCIChartCommand(t, func(ref, registryConfig, contentCache string) []string {
return []string{
"template", "release-name", ref,
"--version", "0.1.0",
"--plain-http",
"--registry-config", registryConfig,
"--content-cache", contentCache,
}
})
require.NotEmpty(t, stdout)
require.NotContains(t, stdout, "Pulled:")
require.NotContains(t, stdout, "Digest:")
require.Contains(t, stderr, "Pulled:")
require.Contains(t, stderr, "Digest:")
}
func TestShowOCIRegistryMessagesNotOnStdout(t *testing.T) {
defer resetEnv()()
stdout, stderr := runOCIChartCommand(t, func(ref, registryConfig, contentCache string) []string {
return []string{
"show", "chart", ref,
"--version", "0.1.0",
"--plain-http",
"--registry-config", registryConfig,
"--content-cache", contentCache,
}
})
require.NotEmpty(t, stdout)
require.Contains(t, stdout, "name: oci-dependent-chart")
require.NotContains(t, stdout, "Pulled:")
require.NotContains(t, stdout, "Digest:")
require.Contains(t, stderr, "Pulled:")
require.Contains(t, stderr, "Digest:")
}
func runOCIChartCommand(t *testing.T, argsFn func(ref, registryConfig, contentCache string) []string) (string, string) {
t.Helper()
srv := repotest.NewTempServer(
t,
repotest.WithChartSourceGlob("testdata/testcharts/*.tgz*"),
)
t.Cleanup(func() { srv.Stop() })
ociSrv, err := repotest.NewOCIServer(t, srv.Root())
require.NoError(t, err)
ociSrv.Run(t)
ref := fmt.Sprintf("oci://%s/u/ocitestuser/oci-dependent-chart", ociSrv.RegistryURL)
registryConfig := filepath.Join(srv.Root(), "config.json")
contentCache := t.TempDir()
args := argsFn(ref, registryConfig, contentCache)
stdout := &bytes.Buffer{}
stderr := &bytes.Buffer{}
actionConfig := &action.Configuration{
Releases: storageFixture(),
KubeClient: &kubefake.PrintingKubeClient{Out: io.Discard},
Capabilities: common.DefaultCapabilities,
}
root, err := newRootCmdWithConfig(actionConfig, stdout, args, SetupLogging)
require.NoError(t, err)
root.SetOut(stdout)
root.SetErr(stderr)
root.SetArgs(args)
require.NoError(t, root.Execute(), "stdout:\n%s\nstderr:\n%s", stdout.String(), stderr.String())
return stdout.String(), stderr.String()
}

@ -119,7 +119,7 @@ func TestRepoRemove(t *testing.T) {
}
}
func createCacheFiles(rootDir string, repoName string) (cacheIndexFile string, cacheChartsFile string) {
func createCacheFiles(rootDir, repoName string) (cacheIndexFile, cacheChartsFile string) {
cacheIndexFile = filepath.Join(rootDir, helmpath.CacheIndexFile(repoName))
mf, _ := os.Create(cacheIndexFile)
mf.Close()
@ -131,7 +131,7 @@ func createCacheFiles(rootDir string, repoName string) (cacheIndexFile string, c
return cacheIndexFile, cacheChartsFile
}
func testCacheFiles(t *testing.T, cacheIndexFile string, cacheChartsFile string, repoName string) {
func testCacheFiles(t *testing.T, cacheIndexFile, cacheChartsFile, repoName string) {
t.Helper()
_, err := os.Stat(cacheIndexFile)
require.Errorf(t, err, "Error cache index file was not removed for repository %s", repoName)

@ -268,7 +268,7 @@ func (r *repoSearchWriter) encodeByFormat(out io.Writer, format output.Format) e
}
// Provides the list of charts that are part of the specified repo, and that starts with 'prefix'.
func compListChartsOfRepo(repoName string, prefix string) []string {
func compListChartsOfRepo(repoName, prefix string) []string {
var charts []string
path := filepath.Join(settings.RepositoryCache, helmpath.CacheChartsFile(repoName))

@ -82,9 +82,9 @@ func newShowCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
Long: showAllDesc,
Args: require.ExactArgs(1),
ValidArgsFunction: validArgsFunc,
RunE: func(_ *cobra.Command, args []string) error {
RunE: func(cmd *cobra.Command, args []string) error {
client.OutputFormat = action.ShowAll
err := addRegistryClient(out, client)
err := addRegistryClient(cmd.ErrOrStderr(), client)
if err != nil {
return err
}
@ -103,9 +103,9 @@ func newShowCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
Long: showValuesDesc,
Args: require.ExactArgs(1),
ValidArgsFunction: validArgsFunc,
RunE: func(_ *cobra.Command, args []string) error {
RunE: func(cmd *cobra.Command, args []string) error {
client.OutputFormat = action.ShowValues
err := addRegistryClient(out, client)
err := addRegistryClient(cmd.ErrOrStderr(), client)
if err != nil {
return err
}
@ -124,9 +124,9 @@ func newShowCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
Long: showChartDesc,
Args: require.ExactArgs(1),
ValidArgsFunction: validArgsFunc,
RunE: func(_ *cobra.Command, args []string) error {
RunE: func(cmd *cobra.Command, args []string) error {
client.OutputFormat = action.ShowChart
err := addRegistryClient(out, client)
err := addRegistryClient(cmd.ErrOrStderr(), client)
if err != nil {
return err
}
@ -145,9 +145,9 @@ func newShowCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
Long: readmeChartDesc,
Args: require.ExactArgs(1),
ValidArgsFunction: validArgsFunc,
RunE: func(_ *cobra.Command, args []string) error {
RunE: func(cmd *cobra.Command, args []string) error {
client.OutputFormat = action.ShowReadme
err := addRegistryClient(out, client)
err := addRegistryClient(cmd.ErrOrStderr(), client)
if err != nil {
return err
}
@ -166,9 +166,9 @@ func newShowCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
Long: showCRDsDesc,
Args: require.ExactArgs(1),
ValidArgsFunction: validArgsFunc,
RunE: func(_ *cobra.Command, args []string) error {
RunE: func(cmd *cobra.Command, args []string) error {
client.OutputFormat = action.ShowCRDs
err := addRegistryClient(out, client)
err := addRegistryClient(cmd.ErrOrStderr(), client)
if err != nil {
return err
}
@ -224,8 +224,8 @@ func runShow(args []string, client *action.Show) (string, error) {
return client.Run(cp)
}
func addRegistryClient(out io.Writer, client *action.Show) error {
registryClient, err := newRegistryClient(out, client.CertFile, client.KeyFile, client.CaFile,
func addRegistryClient(registryOut io.Writer, client *action.Show) error {
registryClient, err := newRegistryClient(registryOut, client.CertFile, client.KeyFile, client.CaFile,
client.InsecureSkipTLSVerify, client.PlainHTTP, client.Username, client.Password)
if err != nil {
return fmt.Errorf("missing registry client: %w", err)

@ -85,7 +85,7 @@ func newTemplateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
client.KubeVersion = parsedKubeVersion
}
registryClient, err := newRegistryClient(out, client.CertFile, client.KeyFile, client.CaFile,
registryClient, err := newRegistryClient(cmd.ErrOrStderr(), client.CertFile, client.KeyFile, client.CaFile,
client.InsecureSkipTLSVerify, client.PlainHTTP, client.Username, client.Password)
if err != nil {
return fmt.Errorf("missing registry client: %w", err)
@ -238,7 +238,7 @@ func isTestHook(h *release.Hook) bool {
// bug introduced by #8156. As part of the todo to refactor renderResources
// this duplicate code should be removed. It is added here so that the API
// surface area is as minimally impacted as possible in fixing the issue.
func writeToFile(outputDir string, name string, data string, appendData bool) error {
func writeToFile(outputDir, name, data string, appendData bool) error {
outfileName := outputDir + string(filepath.Separator) + name
err := ensureDirectoryForFile(outfileName)

@ -1 +1 @@
Version: v4.2
Version: v4.3

@ -1 +1 @@
version.BuildInfo{Version:"v4.2", GitCommit:"", GitTreeState:"", GoVersion:"", KubeClientVersion:"v1.20"}
version.BuildInfo{Version:"v4.3", GitCommit:"", GitTreeState:"", GoVersion:"", KubeClientVersion:"v1.20"}

@ -290,7 +290,7 @@ func (c *ChartDownloader) DownloadToCache(ref, version string) (string, *provena
return pth, ver, err
}
body, err := g.Get(u.String() + ".prov")
body, err := g.Get(u.String()+".prov", c.Options...)
if err != nil {
if c.Verify == VerifyAlways {
return pth, ver, fmt.Errorf("failed to fetch provenance %q", u.String()+".prov")

@ -18,6 +18,8 @@ package downloader
import (
"crypto/sha256"
"encoding/hex"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
@ -430,6 +432,55 @@ func TestDownloadToCache(t *testing.T) {
})
}
func TestDownloadToCachePassesOptionsToProvenance(t *testing.T) {
chartData, err := os.ReadFile("testdata/signtest-0.1.0.tgz")
require.NoError(t, err)
provData, err := os.ReadFile("testdata/signtest-0.1.0.tgz.prov")
require.NoError(t, err)
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
username, password, ok := r.BasicAuth()
if !ok || username != "username" || password != "password" {
w.WriteHeader(http.StatusUnauthorized)
return
}
switch r.URL.Path {
case "/signtest-0.1.0.tgz":
_, _ = w.Write(chartData)
case "/signtest-0.1.0.tgz.prov":
_, _ = w.Write(provData)
default:
http.NotFound(w, r)
}
}))
t.Cleanup(srv.Close)
contentCache := t.TempDir()
c := ChartDownloader{
Out: os.Stderr,
Verify: VerifyLater,
RepositoryConfig: repoConfig,
RepositoryCache: repoCache,
Getters: getter.All(&cli.EnvSettings{
RepositoryConfig: repoConfig,
RepositoryCache: repoCache,
ContentCache: contentCache,
}),
Options: []getter.Option{
getter.WithBasicAuth("username", "password"),
},
Cache: &DiskCache{Root: contentCache},
}
_, _, err = c.DownloadToCache(srv.URL+"/signtest-0.1.0.tgz", "")
require.NoError(t, err)
digest := sha256.Sum256(chartData)
_, err = c.Cache.Get(digest, CacheProv)
require.NoError(t, err, "provenance file should be in cache")
}
func TestStripDigestAlgorithm(t *testing.T) {
tests := map[string]struct {
input string

@ -177,10 +177,11 @@ func (m *Manager) Update() error {
// For the repositories Helm is not configured to know about, ensure Helm
// has some information about them and, when possible, the index files
// locally.
// TODO(mattfarina): Repositories should be explicitly added by end users
// rather than automatic. In Helm v4 require users to add repositories. They
// should have to add them in order to make sure they are aware of the
// repositories and opt-in to any locations, for security.
//
// TODO Helm v5: require users to add repositories explicitly rather than
// adding them automatically. They should have to add them in order to make
// sure they are aware of the repositories and opt-in to any locations, for
// security.
repoNames, err = m.ensureMissingRepos(repoNames, req)
if err != nil {
return err
@ -733,22 +734,17 @@ func (m *Manager) findChartURL(name, version, repoURL string, repos map[string]*
var entry repo.ChartVersions
entry, err = findEntryByName(name, cr)
if err != nil {
// TODO: Where linting is skipped in this function we should
// refactor to remove naked returns while ensuring the same
// behavior
//nolint:nakedret
return
// TODO: Consider refactoring this function to reduce the number of returned values while preserving behavior.
return url, username, password, insecureSkipTLSVerify, passCredentialsAll, caFile, certFile, keyFile, err
}
var ve *repo.ChartVersion
ve, err = findVersionedEntry(version, entry)
if err != nil {
//nolint:nakedret
return
return url, username, password, insecureSkipTLSVerify, passCredentialsAll, caFile, certFile, keyFile, err
}
url, err = repo.ResolveReferenceURL(repoURL, ve.URLs[0])
if err != nil {
//nolint:nakedret
return
return url, username, password, insecureSkipTLSVerify, passCredentialsAll, caFile, certFile, keyFile, err
}
username = cr.Config.Username
password = cr.Config.Password
@ -757,8 +753,8 @@ func (m *Manager) findChartURL(name, version, repoURL string, repos map[string]*
caFile = cr.Config.CAFile
certFile = cr.Config.CertFile
keyFile = cr.Config.KeyFile
//nolint:nakedret
return
return url, username, password, insecureSkipTLSVerify, passCredentialsAll, caFile, certFile, keyFile, err
}
url, err = repo.FindChartInRepoURL(repoURL, name, m.Getters, repo.WithChartVersion(version), repo.WithClientTLS(certFile, keyFile, caFile))
if err == nil {

@ -79,6 +79,8 @@ func New(config *rest.Config) Engine {
// bar chart during render time.
//
// Deprecated: Use RenderWithContext instead.
//
//go:fix inline
func (e Engine) Render(chrt ci.Charter, values common.Values) (map[string]string, error) {
return e.RenderWithContext(context.Background(), chrt, values)
}
@ -102,6 +104,8 @@ func (e Engine) Render(chrt ci.Charter, values common.Values) (map[string]string
// that section of the values will be passed into the "foo" chart. And if that
// section contains a value named "bar", that value will be passed on to the
// bar chart during render time.
//
// TODO Helm v5: Rename this to Render (remove the current deprecated Render method) and 'go:fix inline' RenderWithContext method to call Render.
func (e Engine) RenderWithContext(ctx context.Context, chrt ci.Charter, values common.Values) (map[string]string, error) {
tmap := allTemplates(chrt, values)
return e.render(ctx, tmap)
@ -109,28 +113,38 @@ func (e Engine) RenderWithContext(ctx context.Context, chrt ci.Charter, values c
// Render takes a chart, optional values, and value overrides, and attempts to
// render the Go templates using the default options.
//
// Deprecated: Instantiate an Engine and call RenderWithContext instead.
//
// TODO Helm v5: Replace with a NewEngine function.
//
//go:fix inline
func Render(chrt ci.Charter, values common.Values) (map[string]string, error) {
return new(Engine).Render(chrt, values)
return new(Engine).RenderWithContext(context.Background(), chrt, values)
}
// RenderWithClient takes a chart, optional values, and value overrides, and attempts to
// render the Go templates using the default options. This engine is client aware and so can have template
// functions that interact with the client.
//
// TODO Helm v5: Replace with a NewEngine function that accepts a rest.Config option
func RenderWithClient(chrt ci.Charter, values common.Values, config *rest.Config) (map[string]string, error) {
var clientProvider ClientProvider = clientProviderFromConfig{config}
return Engine{
clientProvider: &clientProvider,
}.Render(chrt, values)
}.RenderWithContext(context.Background(), chrt, values)
}
// RenderWithClientProvider takes a chart, optional values, and value overrides, and attempts to
// render the Go templates using the default options. This engine is client aware and so can have template
// functions that interact with the client.
// This function differs from RenderWithClient in that it lets you customize the way a dynamic client is constructed.
//
// TODO Helm v5: Replace with a NewEngine function that accepts a ClientProvider option
func RenderWithClientProvider(chrt ci.Charter, values common.Values, clientProvider ClientProvider) (map[string]string, error) {
return Engine{
clientProvider: &clientProvider,
}.Render(chrt, values)
}.RenderWithContext(context.Background(), chrt, values)
}
// renderable is an object that can be rendered.
@ -413,7 +427,7 @@ func parseTemplateExecErrorString(s string) (TraceableError, bool) {
// Special case: "template: no template %q associated with template %q"
// Matches https://cs.opensource.google/go/go/+/refs/tags/go1.23.6:src/text/template/exec.go;l=191
func parseTemplateNoTemplateError(s string, remainder string) (TraceableError, bool) {
func parseTemplateNoTemplateError(s, remainder string) (TraceableError, bool) {
if strings.HasPrefix(remainder, "no template ") {
return TraceableError{message: s}, true
}
@ -482,8 +496,7 @@ func reformatExecErrorMsg(filename string, err error) error {
// If it can parse out details from that error message such as the line number, template it failed on,
// and error description, then it will construct a new error that displays these details in a structured way.
// If there are issues with parsing the error message, the err passed into the function should return instead.
var execError template.ExecError
if !errors.As(err, &execError) {
if _, ok := errors.AsType[template.ExecError](err); !ok {
return err
}

@ -447,7 +447,7 @@ func durationWeeks(v any) float64 {
//
// v and m accept the same forms as asDuration (e.g. "2h13m", "30s").
// On error, it returns time.Duration(0). If m is invalid, it returns v.
func durationRoundTo(v any, m any) time.Duration {
func durationRoundTo(v, m any) time.Duration {
d, err := asDuration(v)
if err != nil {
return 0
@ -463,7 +463,7 @@ func durationRoundTo(v any, m any) time.Duration {
// Returns a time.Duration.
//
// On error, it returns time.Duration(0). If m is invalid, it returns v.
func durationTruncateTo(v any, m any) time.Duration {
func durationTruncateTo(v, m any) time.Duration {
d, err := asDuration(v)
if err != nil {
return 0

@ -30,7 +30,7 @@ import (
"k8s.io/client-go/rest"
)
type lookupFunc = func(apiversion string, resource string, namespace string, name string) (map[string]any, error)
type lookupFunc = func(apiversion, resource, namespace, name string) (map[string]any, error)
// NewLookupFunction returns a function for looking up objects in the cluster.
//
@ -55,7 +55,7 @@ func (c clientProviderFromConfig) GetClientFor(apiVersion, kind string) (dynamic
}
func newLookupFunction(ctx context.Context, clientProvider ClientProvider) lookupFunc {
return func(apiversion string, kind string, namespace string, name string) (map[string]any, error) {
return func(apiversion, kind, namespace, name string) (map[string]any, error) {
var client dynamic.ResourceInterface
c, namespaced, err := clientProvider.GetClientFor(apiversion, kind)
if err != nil {
@ -105,7 +105,7 @@ func newLookupFunction(ctx context.Context, clientProvider ClientProvider) looku
}
// getDynamicClientOnKind returns a dynamic client on an Unstructured type. This client can be further namespaced.
func getDynamicClientOnKind(apiversion string, kind string, config *rest.Config) (dynamic.NamespaceableResourceInterface, bool, error) {
func getDynamicClientOnKind(apiversion, kind string, config *rest.Config) (dynamic.NamespaceableResourceInterface, bool, error) {
gvk := schema.FromAPIVersionAndKind(apiversion, kind)
apiRes, err := getAPIResourceForGVK(gvk, config)
if err != nil {

@ -447,7 +447,7 @@ func verifyInsecureSkipVerify(t *testing.T, g *HTTPGetter, caseName string, expe
require.NoError(t, err)
require.NotNil(t, returnVal, "Expected non nil value for http client")
transport := (returnVal.Transport).(*http.Transport)
transport := returnVal.Transport.(*http.Transport)
gotValue := false
if transport.TLSClientConfig != nil {
gotValue = transport.TLSClientConfig.InsecureSkipVerify
@ -465,7 +465,7 @@ func TestDefaultHTTPTransportReuse(t *testing.T) {
require.NotNil(t, httpClient1, "Expected non nil value for http client")
transport1 := (httpClient1.Transport).(*http.Transport)
transport1 := httpClient1.Transport.(*http.Transport)
httpClient2, err := g.httpClient(g.opts)
@ -473,7 +473,7 @@ func TestDefaultHTTPTransportReuse(t *testing.T) {
require.NotNil(t, httpClient2, "Expected non nil value for http client")
transport2 := (httpClient2.Transport).(*http.Transport)
transport2 := httpClient2.Transport.(*http.Transport)
require.Equal(t, transport2, transport1, "Expected default transport to be reused")
}
@ -489,7 +489,7 @@ func TestHTTPTransportOption(t *testing.T) {
require.NotNil(t, httpClient1, "Expected non nil value for http client")
transport1 := (httpClient1.Transport).(*http.Transport)
transport1 := httpClient1.Transport.(*http.Transport)
require.Equal(t, transport, transport1, "Expected transport option to be applied")
@ -499,7 +499,7 @@ func TestHTTPTransportOption(t *testing.T) {
require.NotNil(t, httpClient2, "Expected non nil value for http client")
transport2 := (httpClient2.Transport).(*http.Transport)
transport2 := httpClient2.Transport.(*http.Transport)
require.Equal(t, transport2, transport1, "Expected applied transport to be reused")

@ -1237,8 +1237,8 @@ func upgradeClientSideFieldManager(info *resource.Info, dryRun bool, fieldValida
return patched, err
}
// Patch reource using server-side apply
func patchResourceServerSide(target *resource.Info, dryRun bool, forceConflicts bool, fieldValidationDirective FieldValidationDirective) error {
// Patch resource using server-side apply
func patchResourceServerSide(target *resource.Info, dryRun, forceConflicts bool, fieldValidationDirective FieldValidationDirective) error {
helper := resource.NewHelper(
target.Client,
target.Mapping).

@ -79,7 +79,6 @@ func newPodWithStatus(name string, status v1.PodStatus, namespace string) v1.Pod
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: ns,
SelfLink: "/api/v1/namespaces/default/pods/" + name,
},
Spec: v1.PodSpec{
Containers: []v1.Container{{

@ -73,7 +73,7 @@ type Interface interface {
// by "\n---\n")
//
// Validates against OpenAPI schema if validate is true.
// TODO Helm 4: Integrate into Build with an argument
// TODO Helm v5: Integrate into Build with an argument
BuildTable(reader io.Reader, validate bool) (ResourceList, error)
}
@ -103,7 +103,7 @@ type Waiter interface {
// InterfaceWaitOptions defines an interface that extends Interface with
// methods that accept wait options.
//
// TODO Helm 5: Remove InterfaceWaitOptions and integrate its method(s) into the Interface.
// TODO Helm v5: Remove InterfaceWaitOptions and integrate its method(s) into the Interface.
type InterfaceWaitOptions interface {
// GetWaiter gets the Kube.Waiter with options.
GetWaiterWithOptions(ws WaitStrategy, opts ...WaitOption) (Waiter, error)

@ -386,7 +386,7 @@ func (c *ReadyChecker) crdReady(crd apiextv1.CustomResourceDefinition) bool {
func (c *ReadyChecker) statefulSetReady(sts *appsv1.StatefulSet) bool {
// Verify the generation observed by the statefulSet controller matches the spec generation
if sts.Status.ObservedGeneration != sts.Generation {
slog.Debug("StatefulSet is not ready, observedGeneration doest not match spec generation", "namespace", sts.GetNamespace(), "name", sts.GetName(), "actualGeneration", sts.Status.ObservedGeneration, "expectedGeneration", sts.Generation)
slog.Debug("StatefulSet is not ready, observedGeneration does not match spec generation", "namespace", sts.GetNamespace(), "name", sts.GetName(), "actualGeneration", sts.Status.ObservedGeneration, "expectedGeneration", sts.Generation)
return false
}
@ -440,7 +440,7 @@ func (c *ReadyChecker) statefulSetReady(sts *appsv1.StatefulSet) bool {
func (c *ReadyChecker) replicationControllerReady(rc *corev1.ReplicationController) bool {
// Verify the generation observed by the replicationController controller matches the spec generation
if rc.Status.ObservedGeneration != rc.Generation {
slog.Debug("ReplicationController is not ready, observedGeneration doest not match spec generation", "namespace", rc.GetNamespace(), "name", rc.GetName(), "actualGeneration", rc.Status.ObservedGeneration, "expectedGeneration", rc.Generation)
slog.Debug("ReplicationController is not ready, observedGeneration does not match spec generation", "namespace", rc.GetNamespace(), "name", rc.GetName(), "actualGeneration", rc.Status.ObservedGeneration, "expectedGeneration", rc.Generation)
return false
}
return true
@ -449,7 +449,7 @@ func (c *ReadyChecker) replicationControllerReady(rc *corev1.ReplicationControll
func (c *ReadyChecker) replicaSetReady(rs *appsv1.ReplicaSet) bool {
// Verify the generation observed by the replicaSet controller matches the spec generation
if rs.Status.ObservedGeneration != rs.Generation {
slog.Debug("ReplicaSet is not ready, observedGeneration doest not match spec generation", "namespace", rs.GetNamespace(), "name", rs.GetName(), "actualGeneration", rs.Status.ObservedGeneration, "expectedGeneration", rs.Generation)
slog.Debug("ReplicaSet is not ready, observedGeneration does not match spec generation", "namespace", rs.GetNamespace(), "name", rs.GetName(), "actualGeneration", rs.Status.ObservedGeneration, "expectedGeneration", rs.Generation)
return false
}
return true

@ -1444,7 +1444,7 @@ func newReplicationController(name string, generationInSync bool) *corev1.Replic
}
}
func newReplicaSet(name string, replicas int, readyReplicas int, generationInSync bool) *appsv1.ReplicaSet {
func newReplicaSet(name string, replicas, readyReplicas int, generationInSync bool) *appsv1.ReplicaSet {
d := newDeployment(name, replicas, 0, 0, generationInSync)
return &appsv1.ReplicaSet{
ObjectMeta: metav1.ObjectMeta{
@ -1503,7 +1503,7 @@ func newPersistentVolumeClaim(name string, phase corev1.PersistentVolumeClaimPha
}
}
func newJob(name string, backoffLimit int, completions *int32, succeeded int, failed int) *batchv1.Job {
func newJob(name string, backoffLimit int, completions *int32, succeeded, failed int) *batchv1.Job {
return &batchv1.Job{
ObjectMeta: metav1.ObjectMeta{
Name: name,

@ -0,0 +1,113 @@
/*
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 provenance
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
)
// GnuPG 2.1+ can store file-backed public keys in a "keybox" (pubring.kbx),
// a container that interleaves OpenPGP keyblocks with GnuPG-specific
// metadata. It is not an OpenPGP packet stream, so it cannot be handed to
// openpgp.ReadKeyRing directly. A keybox is a sequence of blobs, each starting
// with:
//
// byte 0..3 u32 blob length, big endian, including this header
// byte 4 u8 blob type (0 empty, 1 header, 2 OpenPGP, 3 X.509)
// byte 5 u8 blob version
//
// The first blob is a header carrying the "KBXf" magic at offset 8. OpenPGP
// blobs (type 2) record where the raw keyblock lives inside the blob:
//
// byte 8..11 u32 keyblock offset, relative to the blob start
// byte 12..15 u32 keyblock length
//
// Reference: kbx/keybox-blob.c in the GnuPG source tree.
const (
kbxBlobTypeHeader = 1
kbxBlobTypeOpenPGP = 2
// kbxBlobFlagEphemeral marks a blob GnuPG considers not (yet) part of
// the keyring, e.g. written during an interrupted keyserver operation
// (KEYBOX_FLAG_BLOB_EPHEMERAL in kbx/keybox.h). GnuPG skips such blobs
// on every normal read (kbx/keybox-search.c), and so do we.
kbxBlobFlagEphemeral = 0x0002
// kbxMinBlobLen covers the length and type fields present in every blob.
kbxMinBlobLen = 5
// kbxOpenPGPHeaderLen is how much of an OpenPGP blob header must be
// present for the flags, keyblock offset and keyblock length fields to
// be readable.
kbxOpenPGPHeaderLen = 16
)
// isKeybox reports whether data looks like a GnuPG keybox (pubring.kbx)
// image, identified by the "KBXf" magic in the mandatory first header blob.
func isKeybox(data []byte) bool {
return len(data) >= 12 && data[4] == kbxBlobTypeHeader && string(data[8:12]) == "KBXf"
}
// isArmored reports whether data looks like an ASCII-armored keyring, as
// produced by `gpg --export --armor`.
func isArmored(data []byte) bool {
return bytes.HasPrefix(bytes.TrimSpace(data), []byte("-----BEGIN PGP"))
}
// keyboxPublicKeys extracts the OpenPGP keyblocks embedded in a keybox image
// and returns them concatenated, ready for openpgp.ReadKeyRing. Blobs of any
// other type (header, X.509, empty) are skipped, as are blobs flagged
// ephemeral, which GnuPG itself ignores when reading the keyring. Malformed
// input yields an error, never a panic.
func keyboxPublicKeys(data []byte) ([]byte, error) {
var keyblocks bytes.Buffer
for offset := 0; offset < len(data); {
rest := data[offset:]
if len(rest) < kbxMinBlobLen {
return nil, fmt.Errorf("truncated blob header at offset %d", offset)
}
blobLen := binary.BigEndian.Uint32(rest)
if blobLen < kbxMinBlobLen {
return nil, fmt.Errorf("invalid blob length %d at offset %d", blobLen, offset)
}
if uint64(blobLen) > uint64(len(rest)) {
return nil, fmt.Errorf("blob at offset %d has length %d exceeding the %d remaining bytes", offset, blobLen, len(rest))
}
blob := rest[:blobLen]
if blob[4] == kbxBlobTypeOpenPGP {
if len(blob) < kbxOpenPGPHeaderLen {
return nil, fmt.Errorf("OpenPGP blob at offset %d is too short", offset)
}
flags := binary.BigEndian.Uint16(blob[6:])
keyblockOffset := binary.BigEndian.Uint32(blob[8:])
keyblockLen := binary.BigEndian.Uint32(blob[12:])
if uint64(keyblockOffset)+uint64(keyblockLen) > uint64(len(blob)) {
return nil, fmt.Errorf("OpenPGP blob at offset %d has an out-of-range keyblock", offset)
}
if flags&kbxBlobFlagEphemeral == 0 {
keyblocks.Write(blob[keyblockOffset : keyblockOffset+keyblockLen])
}
}
offset += int(blobLen)
}
if keyblocks.Len() == 0 {
return nil, errors.New("keybox contains no OpenPGP keys")
}
return keyblocks.Bytes(), nil
}

@ -0,0 +1,188 @@
/*
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 provenance
import (
"bytes"
"encoding/binary"
"os"
"testing"
"github.com/ProtonMail/go-crypto/openpgp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const (
// testKeybox is a GnuPG keybox (pubring.kbx) containing the helm-test
// public key. Regenerate with testdata/regen-keyring-formats.sh.
testKeybox = "testdata/helm-test-key.kbx"
// testMixedKeybox is a keybox containing the RSA and Ed25519 test keys.
testMixedKeybox = "testdata/helm-mixed-keyring.kbx"
// testArmoredPubfile is the ASCII-armored export of the helm-test key.
testArmoredPubfile = "testdata/helm-test-key.asc"
// testMultiBlockArmored is two concatenated single-key armored exports
// (cat key1.asc key2.asc), covering the RSA and Ed25519 test keys.
testMultiBlockArmored = "testdata/helm-mixed-keyring.asc"
)
func TestIsKeybox(t *testing.T) {
tests := []struct {
name string
file string
want bool
}{
{"keybox", testKeybox, true},
{"mixed keybox", testMixedKeybox, true},
{"legacy binary keyring", testPubfile, false},
{"armored keyring", testArmoredPubfile, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
data, err := os.ReadFile(tt.file)
require.NoError(t, err)
assert.Equal(t, tt.want, isKeybox(data))
})
}
t.Run("degenerate inputs", func(t *testing.T) {
assert.False(t, isKeybox(nil))
assert.False(t, isKeybox([]byte{}))
assert.False(t, isKeybox([]byte("KBXf")))
assert.False(t, isKeybox([]byte("garbage that is longer than twelve bytes")))
})
}
func TestIsArmored(t *testing.T) {
tests := []struct {
name string
file string
want bool
}{
{"armored keyring", testArmoredPubfile, true},
{"legacy binary keyring", testPubfile, false},
{"keybox", testKeybox, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
data, err := os.ReadFile(tt.file)
require.NoError(t, err)
assert.Equal(t, tt.want, isArmored(data))
})
}
t.Run("degenerate inputs", func(t *testing.T) {
assert.False(t, isArmored(nil))
assert.False(t, isArmored([]byte("not a key")))
assert.True(t, isArmored([]byte("\n\t -----BEGIN PGP PUBLIC KEY BLOCK-----")))
})
}
func TestKeyboxPublicKeys(t *testing.T) {
data, err := os.ReadFile(testKeybox)
require.NoError(t, err)
keys, err := keyboxPublicKeys(data)
require.NoError(t, err)
ring, err := openpgp.ReadKeyRing(bytes.NewReader(keys))
require.NoError(t, err)
require.Len(t, ring, 1)
_, ok := ring[0].Identities[testKeyName]
assert.True(t, ok, "expected keybox to contain %q", testKeyName)
}
func TestKeyboxPublicKeysEphemeral(t *testing.T) {
// GnuPG flags in-progress key material as ephemeral (bit 0x0002 of the
// blob flags at blob offset 6) and hides it from every normal read; the
// parser must do the same.
setFlags := func(t *testing.T, data []byte, blobStart int, flags uint16) []byte {
t.Helper()
require.Equal(t, byte(kbxBlobTypeOpenPGP), data[blobStart+4])
mutated := bytes.Clone(data)
binary.BigEndian.PutUint16(mutated[blobStart+6:], flags)
return mutated
}
t.Run("all blobs ephemeral means no keys", func(t *testing.T) {
valid, err := os.ReadFile(testKeybox)
require.NoError(t, err)
_, err = keyboxPublicKeys(setFlags(t, valid, 32, kbxBlobFlagEphemeral))
assert.ErrorContains(t, err, "no OpenPGP keys")
})
t.Run("ephemeral blob is skipped, others kept", func(t *testing.T) {
valid, err := os.ReadFile(testMixedKeybox)
require.NoError(t, err)
// Flag only the first OpenPGP blob (the RSA helm-test key).
keys, err := keyboxPublicKeys(setFlags(t, valid, 32, kbxBlobFlagEphemeral))
require.NoError(t, err)
ring, err := openpgp.ReadKeyRing(bytes.NewReader(keys))
require.NoError(t, err)
require.Len(t, ring, 1)
_, ok := ring[0].Identities[testKeyName]
assert.False(t, ok, "expected the ephemeral-flagged %q blob to be skipped", testKeyName)
})
}
func TestKeyboxPublicKeysMalformed(t *testing.T) {
valid, err := os.ReadFile(testKeybox)
require.NoError(t, err)
// The mutations below rely on the fixture layout: a 32-byte header blob
// followed by an OpenPGP blob.
const blobStart = 32
require.Greater(t, len(valid), blobStart+16)
require.Equal(t, byte(kbxBlobTypeOpenPGP), valid[blobStart+4])
mutate := func(offset int, value uint32) []byte {
data := bytes.Clone(valid)
binary.BigEndian.PutUint32(data[offset:], value)
return data
}
tests := []struct {
name string
data []byte
}{
{"header only, no keys", valid[:blobStart]},
{"truncated inside blob header", valid[:blobStart+2]},
{"truncated inside blob body", valid[:blobStart+16]},
{"zero blob length", mutate(blobStart, 0)},
{"blob length below minimum", mutate(blobStart, 4)},
{"blob length past end of data", mutate(blobStart, uint32(len(valid))+1)},
{"keyblock offset out of range", mutate(blobStart+8, uint32(len(valid)))},
{"keyblock length out of range", mutate(blobStart+12, uint32(len(valid)))},
{"keyblock offset overflow", mutate(blobStart+8, ^uint32(0))},
{"keyblock length overflow", mutate(blobStart+12, ^uint32(0))},
{"openpgp blob shorter than its header", append(bytes.Clone(valid[:blobStart]), 0, 0, 0, 8, kbxBlobTypeOpenPGP, 1, 0, 0)},
{"empty input", nil},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := keyboxPublicKeys(tt.data)
assert.Error(t, err)
})
}
}

@ -26,6 +26,7 @@ import (
"strings"
"github.com/ProtonMail/go-crypto/openpgp"
"github.com/ProtonMail/go-crypto/openpgp/armor"
"github.com/ProtonMail/go-crypto/openpgp/clearsign"
"github.com/ProtonMail/go-crypto/openpgp/packet"
"sigs.k8s.io/yaml"
@ -357,13 +358,64 @@ func loadKey(keypath string) (*openpgp.Entity, error) {
return openpgp.ReadEntity(pr)
}
// loadKeyRing loads a keyring from ringpath, auto-detecting its storage
// format. Three formats are supported:
//
// - the legacy binary OpenPGP packet stream (GnuPG's pubring.gpg)
// - a GnuPG file-backed keybox (pubring.kbx), introduced in GnuPG 2.1
// - an ASCII-armored keyring, as produced by `gpg --export --armor`
func loadKeyRing(ringpath string) (openpgp.EntityList, error) {
f, err := os.Open(ringpath)
data, err := os.ReadFile(ringpath)
if err != nil {
return nil, err
}
defer f.Close()
return openpgp.ReadKeyRing(f)
switch {
case isKeybox(data):
keys, err := keyboxPublicKeys(data)
if err != nil {
return nil, fmt.Errorf("reading keybox %s: %w", ringpath, err)
}
return openpgp.ReadKeyRing(bytes.NewReader(keys))
case isArmored(data):
ring, err := loadArmoredKeyRing(data)
if err != nil {
return nil, fmt.Errorf("reading armored keyring %s: %w", ringpath, err)
}
return ring, nil
default:
return openpgp.ReadKeyRing(bytes.NewReader(data))
}
}
// loadArmoredKeyRing reads every armored block in data and merges the keys
// into a single keyring. Unlike openpgp.ReadArmoredKeyRing, which silently
// ignores everything after the first block, this handles keyrings assembled
// by concatenating exports (cat key1.asc key2.asc > keyring.asc), the same
// way GnuPG imports them.
func loadArmoredKeyRing(data []byte) (openpgp.EntityList, error) {
var ring openpgp.EntityList
r := bytes.NewReader(data)
for {
block, err := armor.Decode(r)
if errors.Is(err, io.EOF) {
break
}
if err != nil {
return nil, err
}
if block.Type != openpgp.PublicKeyType && block.Type != openpgp.PrivateKeyType {
return nil, fmt.Errorf("expected a public or private key block, got %q", block.Type)
}
entities, err := openpgp.ReadKeyRing(block.Body)
if err != nil {
return nil, err
}
ring = append(ring, entities...)
}
if len(ring) == 0 {
return nil, errors.New("no keys found")
}
return ring, nil
}
// DigestFile calculates a SHA256 hash (like Docker) for a given file.

@ -16,6 +16,7 @@ limitations under the License.
package provenance
import (
"bytes"
"crypto"
"errors"
"io"
@ -24,6 +25,7 @@ import (
"strings"
"testing"
"github.com/ProtonMail/go-crypto/openpgp/armor"
pgperrors "github.com/ProtonMail/go-crypto/openpgp/errors"
"github.com/ProtonMail/go-crypto/openpgp/packet"
"github.com/stretchr/testify/assert"
@ -143,6 +145,72 @@ func TestLoadKeyRing(t *testing.T) {
}
}
func TestLoadKeyRingKeybox(t *testing.T) {
k, err := loadKeyRing(testKeybox)
require.NoError(t, err)
require.Len(t, k, 1)
_, ok := k[0].Identities[testKeyName]
assert.True(t, ok, "expected %q in keybox keyring", testKeyName)
}
func TestLoadKeyRingMixedKeybox(t *testing.T) {
k, err := loadKeyRing(testMixedKeybox)
require.NoError(t, err)
require.Len(t, k, 2)
names := make([]string, 0, len(k))
hasEdDSA := false
for _, e := range k {
for n := range e.Identities {
names = append(names, n)
}
if e.PrimaryKey != nil && e.PrimaryKey.PubKeyAlgo == packet.PubKeyAlgoEdDSA {
hasEdDSA = true
}
}
assert.Contains(t, names, testKeyName)
assert.True(t, hasEdDSA, "expected an Ed25519 key in %s", testMixedKeybox)
}
func TestLoadKeyRingArmored(t *testing.T) {
k, err := loadKeyRing(testArmoredPubfile)
require.NoError(t, err)
require.Len(t, k, 1)
_, ok := k[0].Identities[testKeyName]
assert.True(t, ok, "expected %q in armored keyring", testKeyName)
}
func TestLoadKeyRingArmoredMultiBlock(t *testing.T) {
// A keyring assembled by concatenating exports (cat a.asc b.asc) must
// load every block, the same way gpg --import does.
k, err := loadKeyRing(testMultiBlockArmored)
require.NoError(t, err)
require.Len(t, k, 2)
names := make([]string, 0, len(k))
for _, e := range k {
for n := range e.Identities {
names = append(names, n)
}
}
assert.Contains(t, names, testKeyName)
}
func TestLoadArmoredKeyRingRejectsNonKeyBlocks(t *testing.T) {
var buf bytes.Buffer
w, err := armor.Encode(&buf, "PGP MESSAGE", nil)
require.NoError(t, err)
_, err = w.Write([]byte("not a key"))
require.NoError(t, err)
require.NoError(t, w.Close())
_, err = loadArmoredKeyRing(buf.Bytes())
assert.ErrorContains(t, err, "expected a public or private key block")
}
func TestDigest(t *testing.T) {
f, err := os.Open(testChartfile)
require.NoError(t, err)
@ -318,6 +386,31 @@ func TestVerify(t *testing.T) {
}
}
// TestVerifyKeyboxKeyring mirrors TestVerify with the keyring loaded from a
// GnuPG keybox instead of the legacy binary format.
func TestVerifyKeyboxKeyring(t *testing.T) {
signer, err := NewFromKeyring(testKeybox, "")
require.NoError(t, err)
archiveData, err := os.ReadFile(testChartfile)
require.NoError(t, err)
sigData, err := os.ReadFile(testSigBlock)
require.NoError(t, err)
ver, err := signer.Verify(archiveData, sigData, filepath.Base(testChartfile))
require.NoError(t, err)
require.NotNil(t, ver.SignedBy)
_, ok := ver.SignedBy.Identities[testKeyName]
assert.True(t, ok, "expected chart to verify as signed by %q", testKeyName)
tamperedSigData, err := os.ReadFile(testTamperedSigBlock)
require.NoError(t, err)
_, err = signer.Verify(archiveData, tamperedSigData, filepath.Base(testChartfile))
assert.Error(t, err, "expected tampered signature to fail against keybox keyring")
}
// readSumFile reads a file containing a sum generated by the UNIX shasum tool.
func readSumFile(sumfile string) (string, error) {
data, err := os.ReadFile(sumfile)

@ -0,0 +1,40 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
mQENBFeWbZ4BCADIsgNRyVBSWJkbH6h3UWWXsA9ce1u+YKvaCYFSjIujKycTAqCC
P7qUV2Oj/4E2zUcOD1/8/meDwuTnNTLzdSw4ujtFlKRSw7zelQE6sxvID0KM0pQK
7AxDTXsm/7Afd/fg4WNW0/hcbeiNz4TVmSWAnbqeLg8o8eljR5QhTk47H6Glo4hV
raeLCKG77qm2qOQ/m38ec+L5n9iUpoZZu1S5RXPUIanV5pLlx2rQsooIQEdJbTRF
Iv3+2Nj/56PFVdrw7E5ARqCD4PpzzYV7uj8vKumOp+VivFj95Ze6DFENh1WWy336
jQEj0uTolgOYeQ6AdJobwSPUeYXGV6Sf2vwXABEBAAG0XUhlbG0gVGVzdGluZyAo
VGhpcyBrZXkgc2hvdWxkIG9ubHkgYmUgdXNlZCBmb3IgdGVzdGluZy4gRE8gTk9U
IFRSVVNULikgPGhlbG0tdGVzdGluZ0BoZWxtLnNoPokBNwQTAQoAIQUCV5ZtngIb
AwULCQgHAwUVCgkICwUWAgMBAAIeAQIXgAAKCRCEO7+YH8GHYolFCAC0+ejT5dIX
0juRl5AdG6XAlEf9IrehkVlo0s5bG1Ucea9jjNRafmmHyMravrxZVI9zGFhaNpxA
+McnnWAO8lx5yTgnGW269qvFBsj5n0ItPewILQZjTEgwLYf1oc2qCFpk+8ovBnxZ
Kaz3J5QKqU09zLO2yALuI5FTCCUupJ3OGIrKvLJ4H2jsRk/5jgqIgqJotW/4Baza
4zemFdQHj9FPvq9sVeW5lyxM48i638GtslIgc0y2Yd/bfiy0tfIkskWz/kS1U6c9
Utr3UPwpBqHjXnakmpK7GMGlp5eDXgr63BEdLKxJJWWxIys35fR19XZXcAELrwMh
zkTzB/dvI375uQENBFeWbZ4BCADB6a8oiF2royWbvwmujBxSmD68QWziNZbSCuRs
M4WM1iy69kspJHRwJEVIE5pkoVfV6tIcwQQ0VkJmq6Pf/pyKeMAXp0u8bp5K0mVE
Yio1/adiLM3kX1yleoXLNUb1gNvsk6Kp6BZCUNRmjKlbKxapJKHmL+opX8UoeLeE
KSgJNL8jvo3nRgg+wIPnVDoizz7y03F0k4SbgNGdoA/FtZs/vcSrEfzOLgI2RxQS
dj/ePkxg9TvQygUSYgAVbYlASw6XMmd/zTjkfD9qa0l3WSejTisn85b611WryDpA
iYkYC3GzK8g7S9VwkAbtr79cbG+1djlA4aHASpil+zR12yEhABEBAAGJAR8EGAEK
AAkFAleWbZ4CGwwACgkQhDu/mB/Bh2LbNQgAiOOMNzmKzY4A7/ra8ppaH9oL8XIa
WvInivOsx22K09PPuPVqi/ooBiRGyRqhdVS4ShPOoOTR5tOsdfPpEMTHnGQ1+jW+
Tw5MXv3oMckWw3YEYptnuvon3wT4bOXLr+eYlY4Z1ONs+pAXwMiQ2zXNuKHpA2fR
HsF8Wyw57rCVn7K5nQgZCbVxasYlzvFGnmt/itSC5w/AnIvICDOWcoGFKTieqhME
IkCyvu+DNrMAumnD2fqF2olsM4IzCEPSMEQqJIGzZTtQTseS8NSDioRGnG1AJQ94
BssQVmTh0/hlpoTqXY803lR5wb1fr3RRnkOu+lbEI6AAUj51j5TAAo/1dA==
=bvcV
-----END PGP PUBLIC KEY BLOCK-----
-----BEGIN PGP PUBLIC KEY BLOCK-----
mDMEaMzP2xYJKwYBBAHaRw8BAQdAXkL0KKTOZoMaM+6HsrkWv93zmLmnMiIoQvR2
F+bjx7G0KEhlbG0gRWQyNTUxOSBUZXN0IDxoZWxtLWVkMjU1MTlAaGVsbS5zaD6I
mQQTFgoAQRYhBEy+dUwlWAxsh43nEfuFaBLOL8tkBQJozM/bAhsDBQkFo5qABQsJ
CAcCAiICBhUKCQgLAgQWAgMBAh4HAheAAAoJEPuFaBLOL8tktBYA/2cclE0N+BZV
Wymk6XfwFMjErM6olWAfwA8DfAGQ55+lAQCqSa+Tkjd4dmToVHcb0QAm+zwklAY4
e6qWgGiivn77AA==
=wJ7K
-----END PGP PUBLIC KEY BLOCK-----

Binary file not shown.

@ -0,0 +1,30 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
mQENBFeWbZ4BCADIsgNRyVBSWJkbH6h3UWWXsA9ce1u+YKvaCYFSjIujKycTAqCC
P7qUV2Oj/4E2zUcOD1/8/meDwuTnNTLzdSw4ujtFlKRSw7zelQE6sxvID0KM0pQK
7AxDTXsm/7Afd/fg4WNW0/hcbeiNz4TVmSWAnbqeLg8o8eljR5QhTk47H6Glo4hV
raeLCKG77qm2qOQ/m38ec+L5n9iUpoZZu1S5RXPUIanV5pLlx2rQsooIQEdJbTRF
Iv3+2Nj/56PFVdrw7E5ARqCD4PpzzYV7uj8vKumOp+VivFj95Ze6DFENh1WWy336
jQEj0uTolgOYeQ6AdJobwSPUeYXGV6Sf2vwXABEBAAG0XUhlbG0gVGVzdGluZyAo
VGhpcyBrZXkgc2hvdWxkIG9ubHkgYmUgdXNlZCBmb3IgdGVzdGluZy4gRE8gTk9U
IFRSVVNULikgPGhlbG0tdGVzdGluZ0BoZWxtLnNoPokBNwQTAQoAIQUCV5ZtngIb
AwULCQgHAwUVCgkICwUWAgMBAAIeAQIXgAAKCRCEO7+YH8GHYolFCAC0+ejT5dIX
0juRl5AdG6XAlEf9IrehkVlo0s5bG1Ucea9jjNRafmmHyMravrxZVI9zGFhaNpxA
+McnnWAO8lx5yTgnGW269qvFBsj5n0ItPewILQZjTEgwLYf1oc2qCFpk+8ovBnxZ
Kaz3J5QKqU09zLO2yALuI5FTCCUupJ3OGIrKvLJ4H2jsRk/5jgqIgqJotW/4Baza
4zemFdQHj9FPvq9sVeW5lyxM48i638GtslIgc0y2Yd/bfiy0tfIkskWz/kS1U6c9
Utr3UPwpBqHjXnakmpK7GMGlp5eDXgr63BEdLKxJJWWxIys35fR19XZXcAELrwMh
zkTzB/dvI375uQENBFeWbZ4BCADB6a8oiF2royWbvwmujBxSmD68QWziNZbSCuRs
M4WM1iy69kspJHRwJEVIE5pkoVfV6tIcwQQ0VkJmq6Pf/pyKeMAXp0u8bp5K0mVE
Yio1/adiLM3kX1yleoXLNUb1gNvsk6Kp6BZCUNRmjKlbKxapJKHmL+opX8UoeLeE
KSgJNL8jvo3nRgg+wIPnVDoizz7y03F0k4SbgNGdoA/FtZs/vcSrEfzOLgI2RxQS
dj/ePkxg9TvQygUSYgAVbYlASw6XMmd/zTjkfD9qa0l3WSejTisn85b611WryDpA
iYkYC3GzK8g7S9VwkAbtr79cbG+1djlA4aHASpil+zR12yEhABEBAAGJAR8EGAEK
AAkFAleWbZ4CGwwACgkQhDu/mB/Bh2LbNQgAiOOMNzmKzY4A7/ra8ppaH9oL8XIa
WvInivOsx22K09PPuPVqi/ooBiRGyRqhdVS4ShPOoOTR5tOsdfPpEMTHnGQ1+jW+
Tw5MXv3oMckWw3YEYptnuvon3wT4bOXLr+eYlY4Z1ONs+pAXwMiQ2zXNuKHpA2fR
HsF8Wyw57rCVn7K5nQgZCbVxasYlzvFGnmt/itSC5w/AnIvICDOWcoGFKTieqhME
IkCyvu+DNrMAumnD2fqF2olsM4IzCEPSMEQqJIGzZTtQTseS8NSDioRGnG1AJQ94
BssQVmTh0/hlpoTqXY803lR5wb1fr3RRnkOu+lbEI6AAUj51j5TAAo/1dA==
=bvcV
-----END PGP PUBLIC KEY BLOCK-----

Binary file not shown.

@ -0,0 +1,26 @@
#!/bin/sh
# Regenerate the file-backed keyring fixtures from the committed binary
# keyrings. Requires GnuPG 2.1+ (keybox support).
#
# helm-test-key.kbx keybox (pubring.kbx) containing the helm-test key
# helm-mixed-keyring.kbx keybox containing the RSA and Ed25519 test keys
# helm-test-key.asc ASCII-armored export of the helm-test key
# helm-mixed-keyring.asc two concatenated single-key armored exports
set -e
GNUPGHOME=$(mktemp -d)
export GNUPGHOME
chmod 700 "$GNUPGHOME"
gpg --batch --no-tty --quiet --import helm-test-key.pub
cp "$GNUPGHOME/pubring.kbx" helm-test-key.kbx
gpg --batch --no-tty --export --armor helm-testing@helm.sh > helm-test-key.asc
rm -rf "$GNUPGHOME"
GNUPGHOME=$(mktemp -d)
export GNUPGHOME
chmod 700 "$GNUPGHOME"
gpg --batch --no-tty --quiet --import helm-mixed-keyring.pub
cp "$GNUPGHOME/pubring.kbx" helm-mixed-keyring.kbx
gpg --batch --no-tty --export --armor helm-testing@helm.sh > helm-mixed-keyring.asc
gpg --batch --no-tty --export --armor helm-ed25519@helm.sh >> helm-mixed-keyring.asc
rm -rf "$GNUPGHOME"

@ -108,7 +108,7 @@ func generateChartOCIAnnotations(meta *chart.Metadata, creationTime string) map[
}
// addToMap takes an existing map and adds an item if the value is not empty
func addToMap(inputMap map[string]string, newKey string, newValue string) map[string]string {
func addToMap(inputMap map[string]string, newKey, newValue string) map[string]string {
// Add item to map if its
if strings.TrimSpace(newValue) != "" {
inputMap[newKey] = newValue

@ -231,7 +231,7 @@ type (
// Returns true if the host contains a path component (i.e., contains a '/').
func warnIfHostHasPath(host string) bool {
if strings.Contains(host, "/") {
registryHost := strings.Split(host, "/")[0]
registryHost, _, _ := strings.Cut(host, "/")
slog.Warn("registry login currently only supports registry hostname, not a repository path", "host", host, "suggested", registryHost)
return true
}
@ -276,7 +276,7 @@ func (c *Client) Login(host string, options ...LoginOption) error {
}
// LoginOptBasicAuth returns a function that sets the username/password settings on login
func LoginOptBasicAuth(username string, password string) LoginOption {
func LoginOptBasicAuth(username, password string) LoginOption {
return func(o *loginOperation) {
o.client.username = username
o.client.password = password
@ -322,6 +322,11 @@ func ensureTLSConfig(client *auth.Client, setConfig *tls.Config) (*tls.Config, e
transport.TLSClientConfig = &tls.Config{}
}
// Idle connections were established under the previous TLS configuration.
// Drop them so the settings being applied here take effect on the next
// request instead of being bypassed by a pooled connection.
transport.CloseIdleConnections()
return transport.TLSClientConfig, nil
}

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

Loading…
Cancel
Save