Merge branch 'helm:main' into feature/rollback-description-flag

MrJack 1 month ago committed by GitHub
commit d0e17b8f61
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -48,7 +48,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@68bde559dea0fdcac2102bfdf6230c5f70eb485e # pinv4.35.4
uses: github/codeql-action/init@9e0d7b8d25671d64c341c19c0152d693099fb5ba # pinv4.35.5
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@68bde559dea0fdcac2102bfdf6230c5f70eb485e # pinv4.35.4
uses: github/codeql-action/autobuild@9e0d7b8d25671d64c341c19c0152d693099fb5ba # pinv4.35.5
# 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@68bde559dea0fdcac2102bfdf6230c5f70eb485e # pinv4.35.4
uses: github/codeql-action/analyze@9e0d7b8d25671d64c341c19c0152d693099fb5ba # pinv4.35.5

@ -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@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4
uses: github/codeql-action/upload-sarif@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5
with:
sarif_file: results.sarif

@ -17,7 +17,7 @@ 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.0
github.com/fluxcd/cli-utils v1.2.1
github.com/foxcpp/go-mockdns v1.2.0
github.com/gobwas/glob v0.2.3
github.com/gofrs/flock v0.13.0
@ -35,20 +35,20 @@ require (
github.com/stretchr/testify v1.11.1
github.com/tetratelabs/wazero v1.11.0
go.yaml.in/yaml/v3 v3.0.4
golang.org/x/crypto v0.50.0
golang.org/x/term v0.42.0
golang.org/x/text v0.36.0
golang.org/x/crypto v0.51.0
golang.org/x/term v0.43.0
golang.org/x/text v0.37.0
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/api v0.36.0
k8s.io/apiextensions-apiserver v0.36.0
k8s.io/apimachinery v0.36.0
k8s.io/apiserver v0.36.0
k8s.io/cli-runtime v0.36.0
k8s.io/client-go v0.36.0
k8s.io/api v0.36.1
k8s.io/apiextensions-apiserver v0.36.1
k8s.io/apimachinery v0.36.1
k8s.io/apiserver v0.36.1
k8s.io/cli-runtime v0.36.1
k8s.io/client-go v0.36.1
k8s.io/klog/v2 v2.140.0
k8s.io/kubectl v0.36.0
k8s.io/kubectl v0.36.1
oras.land/oras-go/v2 v2.6.0
sigs.k8s.io/controller-runtime v0.24.0
sigs.k8s.io/controller-runtime v0.24.1
sigs.k8s.io/kustomize/kyaml v0.21.1
sigs.k8s.io/yaml v1.6.0
)
@ -157,13 +157,13 @@ require (
go.opentelemetry.io/otel/trace v1.43.0 // indirect
go.opentelemetry.io/proto/otlp v1.10.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
golang.org/x/mod v0.34.0 // indirect
golang.org/x/net v0.52.0 // indirect
golang.org/x/mod v0.35.0 // indirect
golang.org/x/net v0.53.0 // indirect
golang.org/x/oauth2 v0.36.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.43.0 // indirect
golang.org/x/sys v0.44.0 // indirect
golang.org/x/time v0.15.0 // indirect
golang.org/x/tools v0.43.0 // indirect
golang.org/x/tools v0.44.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect
google.golang.org/grpc v1.80.0 // indirect
@ -171,7 +171,7 @@ require (
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.0 // indirect
k8s.io/component-base v0.36.1 // indirect
k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a // indirect
k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 // indirect
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect

@ -93,8 +93,8 @@ github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fluxcd/cli-utils v1.2.0 h1:1o07pXTMxJ/XJ1GpAbLtjdXwfCUMq4Ku1OcnvJHLohI=
github.com/fluxcd/cli-utils v1.2.0/go.mod h1:d5HdTDdR5sCbsIbgtOQ7x7srKYwYeZORU6CD2yn4j/M=
github.com/fluxcd/cli-utils v1.2.1 h1:ug9CicKW7H9QXnvNDapTSKuryZvWcu4Nw7pRvQa6jDY=
github.com/fluxcd/cli-utils v1.2.1/go.mod h1:cky6M6eHvTQkoPtsuFYLIgAMYdpTCSLoor4IA6vueSw=
github.com/foxcpp/go-mockdns v1.2.0 h1:omK3OrHRD1IWJz1FuFBCFquhXslXoF17OvBS6JPzZF0=
github.com/foxcpp/go-mockdns v1.2.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
@ -382,14 +382,14 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g=
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@ -400,8 +400,8 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ=
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -430,8 +430,8 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@ -439,8 +439,8 @@ golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
golang.org/x/term v0.14.0/go.mod h1:TySc+nGkYR6qt8km8wUhuFRTVSMIX3XPR58y2lC8vww=
golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY=
golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY=
golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4=
golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
@ -448,8 +448,8 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@ -458,8 +458,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.15.0/go.mod h1:hpksKq4dtpQWS1uQ61JkdqWM3LscIS6Slf+VVkm+wQk=
golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
@ -485,32 +485,32 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
k8s.io/api v0.36.0 h1:SgqDhZzHdOtMk40xVSvCXkP9ME0H05hPM3p9AB1kL80=
k8s.io/api v0.36.0/go.mod h1:m1LVrGPNYax5NBHdO+QuAedXyuzTt4RryI/qnmNvs34=
k8s.io/apiextensions-apiserver v0.36.0 h1:Wt7E8J+VBCbj4FjiBfDTK/neXDDjyJVJc7xfuOHImZ0=
k8s.io/apiextensions-apiserver v0.36.0/go.mod h1:kGDjH0msuiIB3tgsYRV0kS9GqpMYMUsQ3GHv7TApyug=
k8s.io/apimachinery v0.36.0 h1:jZyPzhd5Z+3h9vJLt0z9XdzW9VzNzWAUw+P1xZ9PXtQ=
k8s.io/apimachinery v0.36.0/go.mod h1:FklypaRJt6n5wUIwWXIP6GJlIpUizTgfo1T/As+Tyxc=
k8s.io/apiserver v0.36.0 h1:Jg5OFAENUACByUCg15CmhZAYrr5ZyJ+jodyA1mHl3YE=
k8s.io/apiserver v0.36.0/go.mod h1:mHvwdHf+qKEm+1/hYm756SV+oREOKSPnsjagOpx6Vho=
k8s.io/cli-runtime v0.36.0 h1:HNxciQpQMMOKS0/GiUXcKDyA6J2FDILJj9NmP2BZrTg=
k8s.io/cli-runtime v0.36.0/go.mod h1:KObkknK9Ro5LYX+1RdiKc7C8CvGg4aX+V/Zv+E8WPHA=
k8s.io/client-go v0.36.0 h1:pOYi7C4RHChYjMiHpZSpSbIM6ZxVbRXBy7CuiIwqA3c=
k8s.io/client-go v0.36.0/go.mod h1:ZKKcpwF0aLYfkHFCjillCKaTK/yBkEDHTDXCFY6AS9Y=
k8s.io/component-base v0.36.0 h1:hFjEktssxiJhrK1zfybkH4kJOi8iZuF+mIDCqS5+jRo=
k8s.io/component-base v0.36.0/go.mod h1:JZvIfcNHk+uck+8LhJzhSBtydWXaZNQwX2OdL+Mnwsk=
k8s.io/api v0.36.1 h1:XbL/EMj8K2aJpJtePmqUyQMsM0D4QI2pvl7YKJ20FTY=
k8s.io/api v0.36.1/go.mod h1:KOWo4ey3TINlXjeHVuwB3i+tXXnu+UcwFBHlI/9dvEo=
k8s.io/apiextensions-apiserver v0.36.1 h1:6JfYmPUsuUIHuN+3QxutXYWj492RqF5fBSx67GYK5Ks=
k8s.io/apiextensions-apiserver v0.36.1/go.mod h1:pLzZin90riwisdzKwv/GoTwENooytoIx5zWJb4Hkby8=
k8s.io/apimachinery v0.36.1 h1:G63Gjx2W+q0YD+72Vo8oY0nDnePVwnuzTmmy5ENrVSA=
k8s.io/apimachinery v0.36.1/go.mod h1:ibYOR00vW/I1kzvi5SF0dRuJ52BvKtfvRdOn35GPQ+8=
k8s.io/apiserver v0.36.1 h1:iMS5V+rPUertv5P9RaqJgmHHTuh4quWpoxchvMUY+JY=
k8s.io/apiserver v0.36.1/go.mod h1:Cby1PbLWztu0GDOxoO6iFOyyqIsziHNEW+w9zVQ22Kw=
k8s.io/cli-runtime v0.36.1 h1:yuC/BGnnj1YYPh6D1P+pZnzinCs6DvMq86yAeNqoqzM=
k8s.io/cli-runtime v0.36.1/go.mod h1:ZQWHGt8xAF7KnviB79vX0lYNyUUqKIpU+LQg7exuFAw=
k8s.io/client-go v0.36.1 h1:FN/K8QIT2CEDt+2WB2HnWrUANZ50AP5GII43/SP2JR0=
k8s.io/client-go v0.36.1/go.mod h1:s6rAnCtTGYDQnpNjEhSaISV+2O8jwruZ6m3QOYBFbtU=
k8s.io/component-base v0.36.1 h1:iG6GsELftXqTNG9HG6kiVjatSgAw1sf5pJ6R5a6N0kA=
k8s.io/component-base v0.36.1/go.mod h1:nf9XPlntRdqO6WMeEWAA5F93Y4ICZQdeT9GeqLDB3JI=
k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc=
k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0=
k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a h1:xCeOEAOoGYl2jnJoHkC3hkbPJgdATINPMAxaynU2Ovg=
k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a/go.mod h1:uGBT7iTA6c6MvqUvSXIaYZo9ukscABYi2btjhvgKGZ0=
k8s.io/kubectl v0.36.0 h1:hEGr8NvIm2Wjqs2Xy48Uzmvo6lpHdGKlLyMvau2gTms=
k8s.io/kubectl v0.36.0/go.mod h1:iDe8aV5BEi45W8k+5n71I2pJ/nwE0PHDu+/2cejzYoo=
k8s.io/kubectl v0.36.1 h1:96HqS9twIdHM0MlJLTwbo14b9kUKPkOzZ4tlRDLv4qI=
k8s.io/kubectl v0.36.1/go.mod h1:/DGPAIewKsFWF9VFgGvkPhao2Ev4SNuE3BioZo8yPbk=
k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 h1:AZYQSJemyQB5eRxqcPky+/7EdBj0xi3g0ZcxxJ7vbWU=
k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk=
oras.land/oras-go/v2 v2.6.0 h1:X4ELRsiGkrbeox69+9tzTu492FMUu7zJQW6eJU+I2oc=
oras.land/oras-go/v2 v2.6.0/go.mod h1:magiQDfG6H1O9APp+rOsvCPcW1GD2MM7vgnKY0Y+u1o=
sigs.k8s.io/controller-runtime v0.24.0 h1:Ck6N2LdS8Lovy1o25BB4r1xjvLEKUl1s2o9kU+KWDE4=
sigs.k8s.io/controller-runtime v0.24.0/go.mod h1:vFkfY5fGt5xAC/sKb8IBFKgWPNKG9OUG29dR8Y2wImw=
sigs.k8s.io/controller-runtime v0.24.1 h1:miPEwrmirImAvgME1L9qebGHrOnGJoVmVdtOU9fRfo4=
sigs.k8s.io/controller-runtime v0.24.1/go.mod h1:vFkfY5fGt5xAC/sKb8IBFKgWPNKG9OUG29dR8Y2wImw=
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
sigs.k8s.io/kustomize/api v0.21.1 h1:lzqbzvz2CSvsjIUZUBNFKtIMsEw7hVLJp0JeSIVmuJs=

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

@ -150,8 +150,8 @@ func (cfg *Configuration) execHookWithDelayedShutdown(rl *release.Release, hook
return func() error {
// If all hooks are successful, check the annotation of each hook to determine whether the hook should be deleted
// or output should be logged under succeeded condition. If so, then clear the corresponding resource object in each hook
for i := len(executingHooks) - 1; i >= 0; i-- {
h := executingHooks[i]
for _, v := range slices.Backward(executingHooks) {
h := v
if err := cfg.outputLogsByPolicy(h, rl.Namespace, release.HookOutputOnSucceeded); err != nil {
// We log here as we still want to attempt hook resource deletion even if output logging fails.
log.Printf("error outputting logs for hook failure: %v", err)

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

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

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

@ -20,6 +20,7 @@ import (
"encoding/json"
"fmt"
"io"
"slices"
"strconv"
"time"
@ -207,8 +208,8 @@ func getHistory(client *action.History, name string) (releaseHistory, error) {
}
func getReleaseHistory(rls []*release.Release) (history releaseHistory) {
for i := len(rls) - 1; i >= 0; i-- {
r := rls[i]
for _, v := range slices.Backward(rls) {
r := v
c := formatChartName(r.Chart)
s := r.Info.Status.String()
v := r.Version

@ -19,9 +19,15 @@ package engine
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"maps"
"math"
"reflect"
"strconv"
"strings"
"text/template"
"time"
"github.com/BurntSushi/toml"
"github.com/Masterminds/sprig/v3"
@ -62,6 +68,19 @@ func funcMap() template.FuncMap {
"fromJson": fromJSON,
"fromJsonArray": fromJSONArray,
// Duration helpers
"mustToDuration": mustToDuration,
"durationSeconds": durationSeconds,
"durationMilliseconds": durationMilliseconds,
"durationMicroseconds": durationMicroseconds,
"durationNanoseconds": durationNanoseconds,
"durationMinutes": durationMinutes,
"durationHours": durationHours,
"durationDays": durationDays,
"durationWeeks": durationWeeks,
"durationRoundTo": durationRoundTo,
"durationTruncateTo": durationTruncateTo,
// This is a placeholder for the "include" function, which is
// late-bound to a template. By declaring it here, we preserve the
// integrity of the linter.
@ -249,3 +268,210 @@ func fromJSONArray(str string) []any {
}
return a
}
// -----------------------------------------------------------------------------
// Duration helpers (numeric and time.Duration returns)
// -----------------------------------------------------------------------------
const (
maxDurationSeconds = int64(math.MaxInt64 / int64(time.Second))
minDurationSeconds = int64(math.MinInt64 / int64(time.Second))
maxDurationSecondsFloat = float64(math.MaxInt64) / float64(time.Second)
minDurationSecondsFloat = float64(math.MinInt64) / float64(time.Second)
)
func durationFromSecondsInt(seconds int64) (time.Duration, error) {
if seconds > maxDurationSeconds || seconds < minDurationSeconds {
return 0, fmt.Errorf("duration seconds overflow: %d", seconds)
}
return time.Duration(seconds) * time.Second, nil
}
func durationFromSecondsUint(seconds uint64) (time.Duration, error) {
if seconds > uint64(maxDurationSeconds) {
return 0, fmt.Errorf("duration seconds overflow: %d", seconds)
}
return time.Duration(int64(seconds)) * time.Second, nil
}
func durationFromSecondsFloat(seconds float64) (time.Duration, error) {
if math.IsNaN(seconds) || math.IsInf(seconds, 0) {
return 0, fmt.Errorf("invalid duration seconds: %v", seconds)
}
if seconds > maxDurationSecondsFloat || seconds < minDurationSecondsFloat {
return 0, fmt.Errorf("duration seconds overflow: %v", seconds)
}
nanos := seconds * float64(time.Second)
if nanos > float64(math.MaxInt64) || nanos < float64(math.MinInt64) {
return 0, fmt.Errorf("duration nanoseconds overflow: %v", nanos)
}
return time.Duration(nanos), nil
}
// asDuration converts common template values into a time.Duration.
//
// Supported inputs:
// - time.Duration
// - string duration values parsed by time.ParseDuration (e.g. "1h2m3s")
// - numeric strings treated as seconds (e.g. "2.5")
// - ints and uints treated as seconds
// - floats treated as seconds
func asDuration(v any) (time.Duration, error) {
switch x := v.(type) {
case time.Duration:
return x, nil
case string:
s := strings.TrimSpace(x)
if s == "" {
return 0, errors.New("empty duration")
}
if d, err := time.ParseDuration(s); err == nil {
return d, nil
}
if f, err := strconv.ParseFloat(s, 64); err == nil {
return durationFromSecondsFloat(f)
}
return 0, fmt.Errorf("could not parse duration %q", x)
case nil:
return 0, errors.New("invalid duration")
}
rv := reflect.ValueOf(v)
switch rv.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return durationFromSecondsInt(rv.Int())
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
return durationFromSecondsUint(rv.Uint())
case reflect.Float32, reflect.Float64:
return durationFromSecondsFloat(rv.Float())
default:
return 0, fmt.Errorf("unsupported duration type %T", v)
}
}
// mustToDuration takes anything and attempts to parse as a duration returning a time.Duration.
//
// This is designed to be called from a template when need to ensure that a
// duration is valid.
func mustToDuration(v any) time.Duration {
d, err := asDuration(v)
if err != nil {
panic(err)
}
return d
}
// durationSeconds converts a duration to seconds (float64).
// On error it returns 0.
func durationSeconds(v any) float64 {
d, err := asDuration(v)
if err != nil {
return 0
}
return d.Seconds()
}
// durationMilliseconds converts a duration to milliseconds (int64).
// On error it returns 0.
func durationMilliseconds(v any) int64 {
d, err := asDuration(v)
if err != nil {
return 0
}
return d.Milliseconds()
}
// durationMicroseconds converts a duration to microseconds (int64).
// On error it returns 0.
func durationMicroseconds(v any) int64 {
d, err := asDuration(v)
if err != nil {
return 0
}
return d.Microseconds()
}
// durationNanoseconds converts a duration to nanoseconds (int64).
// On error it returns 0.
func durationNanoseconds(v any) int64 {
d, err := asDuration(v)
if err != nil {
return 0
}
return d.Nanoseconds()
}
// durationMinutes converts a duration to minutes (float64).
// On error it returns 0.
func durationMinutes(v any) float64 {
d, err := asDuration(v)
if err != nil {
return 0
}
return d.Minutes()
}
// durationHours converts a duration to hours (float64).
// On error it returns 0.
func durationHours(v any) float64 {
d, err := asDuration(v)
if err != nil {
return 0
}
return d.Hours()
}
// durationDays converts a duration to days (float64). (Not in Go's stdlib; handy in templates.)
// On error it returns 0.
func durationDays(v any) float64 {
d, err := asDuration(v)
if err != nil {
return 0
}
return d.Hours() / 24.0
}
// durationWeeks converts a duration to weeks (float64). (Not in Go's stdlib; handy in templates.)
// On error it returns 0.
func durationWeeks(v any) float64 {
d, err := asDuration(v)
if err != nil {
return 0
}
return d.Hours() / 24.0 / 7.0
}
// durationRoundTo rounds v to the nearest multiple of m.
// Returns a time.Duration.
//
// 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 {
d, err := asDuration(v)
if err != nil {
return 0
}
mul, err := asDuration(m)
if err != nil {
return d
}
return d.Round(mul)
}
// durationTruncateTo truncates v toward zero to a multiple of m.
// 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 {
d, err := asDuration(v)
if err != nil {
return 0
}
mul, err := asDuration(m)
if err != nil {
return d
}
return d.Truncate(mul)
}

@ -17,11 +17,14 @@ limitations under the License.
package engine
import (
"math"
"strings"
"testing"
"text/template"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestFuncs(t *testing.T) {
@ -151,6 +154,17 @@ keyInElement1 = "valueInElement1"`,
}, {
tpl: `{{ mustToJson . }}`,
vars: loopMap,
}, {
tpl: `{{ mustToDuration 30 }}`,
expect: `30s`,
vars: nil,
}, {
tpl: `{{ mustToDuration "1m30s" }}`,
expect: `1m30s`,
vars: nil,
}, {
tpl: `{{ mustToDuration "foo" }}`,
vars: nil,
}, {
tpl: `{{ toYaml . }}`,
expect: "", // should return empty string and swallow error
@ -181,6 +195,239 @@ keyInElement1 = "valueInElement1"`,
}
}
func TestDurationHelpers(t *testing.T) {
tests := []struct {
name string
tpl string
vars any
expect string
}{{
name: "durationSeconds parses duration string",
tpl: `{{ durationSeconds "1m30s" }}`,
expect: `90`,
}, {
name: "durationSeconds parses numeric string as seconds",
tpl: `{{ durationSeconds "2.5" }}`,
expect: `2.5`,
}, {
name: "durationSeconds trims whitespace around numeric string",
tpl: `{{ durationSeconds " 2.5 " }}`,
expect: `2.5`,
}, {
name: "durationSeconds int treated as seconds",
tpl: `{{ durationSeconds 2 }}`,
expect: `2`,
}, {
name: "durationSeconds float treated as seconds",
tpl: `{{ durationSeconds 2.5 }}`,
expect: `2.5`,
}, {
name: "durationSeconds uint treated as seconds",
tpl: `{{ durationSeconds . }}`,
vars: uint(2),
expect: `2`,
}, {
name: "durationSeconds time.Duration passthrough",
tpl: `{{ durationSeconds . }}`,
vars: 1500 * time.Millisecond,
expect: `1.5`,
}, {
name: "invalid duration string returns 0",
tpl: `{{ durationSeconds "nope" }}`,
expect: `0`,
}, {
name: "empty duration string returns 0",
tpl: `{{ durationSeconds "" }}`,
expect: `0`,
}, {
name: "whitespace-only duration string returns 0",
tpl: `{{ durationSeconds " " }}`,
expect: `0`,
}, {
name: "nil returns 0",
tpl: `{{ durationSeconds . }}`,
vars: nil,
expect: `0`,
}, {
name: "durationSeconds uint overflow returns 0",
tpl: `{{ durationSeconds . }}`,
vars: uint64(math.MaxInt64) + 1,
expect: `0`,
}, {
name: "durationSeconds int overflow returns 0",
tpl: `{{ durationSeconds . }}`,
vars: maxDurationSeconds + 1,
expect: `0`,
}, {
name: "durationSeconds int underflow returns 0",
tpl: `{{ durationSeconds . }}`,
vars: minDurationSeconds - 1,
expect: `0`,
}, {
name: "durationSeconds float overflow returns 0",
tpl: `{{ durationSeconds . }}`,
vars: maxDurationSecondsFloat + 0.5,
expect: `0`,
}, {
name: "durationSeconds float underflow returns 0",
tpl: `{{ durationSeconds . }}`,
vars: minDurationSecondsFloat - 0.5,
expect: `0`,
}, {
name: "durationSeconds NaN returns 0",
tpl: `{{ durationSeconds . }}`,
vars: math.NaN(),
expect: `0`,
}, {
name: "durationSeconds Inf returns 0",
tpl: `{{ durationSeconds . }}`,
vars: math.Inf(1),
expect: `0`,
}, {
name: "durationMilliseconds int seconds",
tpl: `{{ durationMilliseconds 2 }}`,
expect: `2000`,
}, {
name: "durationMilliseconds float seconds",
tpl: `{{ durationMilliseconds 1.5 }}`,
expect: `1500`,
}, {
name: "durationMicroseconds int seconds",
tpl: `{{ durationMicroseconds 2 }}`,
expect: `2000000`,
}, {
name: "durationNanoseconds int seconds",
tpl: `{{ durationNanoseconds 2 }}`,
expect: `2000000000`,
}, {
name: "durationMinutes parses duration string",
tpl: `{{ durationMinutes "90s" }}`,
expect: `1.5`,
}, {
name: "durationHours parses duration string",
tpl: `{{ durationHours "90m" }}`,
expect: `1.5`,
}, {
name: "durationDays parses duration string",
tpl: `{{ durationDays "36h" }}`,
expect: `1.5`,
}, {
name: "durationDays numeric seconds",
tpl: `{{ durationDays 86400 }}`,
expect: `1`,
}, {
name: "durationWeeks parses duration string",
tpl: `{{ durationWeeks "168h" }}`,
expect: `1`,
}, {
name: "durationWeeks parses fractional weeks",
tpl: `{{ durationWeeks "252h" }}`,
expect: `1.5`,
}, {
name: "durationRoundTo numeric seconds",
tpl: `{{ durationRoundTo 93 60 }}`, // 93s rounded to 60s = 120s
expect: `2m0s`,
}, {
name: "durationTruncateTo numeric seconds",
tpl: `{{ durationTruncateTo 93 60 }}`, // 93s truncated to 60s = 60s
expect: `1m0s`,
}, {
name: "durationRoundTo accepts duration-string multiplier",
tpl: `{{ durationRoundTo "93s" "1m" }}`,
expect: `2m0s`,
}, {
name: "durationTruncateTo accepts duration-string multiplier",
tpl: `{{ durationTruncateTo "93s" "1m" }}`,
expect: `1m0s`,
}, {
name: "durationRoundTo invalid m returns v unchanged",
tpl: `{{ durationRoundTo "93s" "nope" }}`,
expect: `1m33s`,
}, {
name: "durationTruncateTo invalid m returns v unchanged",
tpl: `{{ durationTruncateTo "93s" "nope" }}`,
expect: `1m33s`,
}, {
name: "durationRoundTo zero m returns v unchanged",
tpl: `{{ durationRoundTo "93s" 0 }}`,
expect: `1m33s`,
}, {
name: "durationTruncateTo negative m returns v unchanged",
tpl: `{{ durationTruncateTo "93s" -1 }}`,
expect: `1m33s`,
}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var b strings.Builder
err := template.Must(template.New("test").Funcs(funcMap()).Parse(tt.tpl)).Execute(&b, tt.vars)
require.NoError(t, err, tt.tpl)
assert.Equal(t, tt.expect, b.String(), tt.tpl)
})
}
mustErrTests := []struct {
name string
tpl string
vars any
}{{
name: "mustToDuration invalid string",
tpl: `{{ mustToDuration "nope" }}`,
}, {
name: "mustToDuration empty string",
tpl: `{{ mustToDuration "" }}`,
}, {
name: "mustToDuration whitespace string",
tpl: `{{ mustToDuration " " }}`,
}, {
name: "mustToDuration unsupported type",
tpl: `{{ mustToDuration . }}`,
vars: []int{1, 2, 3},
}, {
name: "mustToDuration uint overflow",
tpl: `{{ mustToDuration . }}`,
vars: uint64(math.MaxInt64) + 1,
}, {
name: "mustToDuration int overflow",
tpl: `{{ mustToDuration . }}`,
vars: maxDurationSeconds + 1,
}, {
name: "mustToDuration int underflow",
tpl: `{{ mustToDuration . }}`,
vars: minDurationSeconds - 1,
}, {
name: "mustToDuration float overflow",
tpl: `{{ mustToDuration . }}`,
vars: maxDurationSecondsFloat + 0.5,
}, {
name: "mustToDuration float underflow",
tpl: `{{ mustToDuration . }}`,
vars: minDurationSecondsFloat - 0.5,
}, {
name: "mustToDuration NaN",
tpl: `{{ mustToDuration . }}`,
vars: math.NaN(),
}, {
name: "mustToDuration Inf",
tpl: `{{ mustToDuration . }}`,
vars: math.Inf(-1),
},
}
for _, tt := range mustErrTests {
t.Run(tt.name, func(t *testing.T) {
var b strings.Builder
tmpl := template.Must(
template.New("test").
Funcs(funcMap()).
Parse(tt.tpl),
)
err := tmpl.Execute(&b, tt.vars)
require.Error(t, err, tt.tpl)
})
}
}
// This test to check a function provided by sprig is due to a change in a
// dependency of sprig. mergo in v0.3.9 changed the way it merges and only does
// public fields (i.e. those starting with a capital letter). This test, from

@ -52,22 +52,10 @@ type LoggingTransport struct {
// NewTransport creates and returns a new instance of LoggingTransport
func NewTransport(debug bool) *retry.Transport {
type cloner[T any] interface {
Clone() T
}
// try to copy (clone) the http.DefaultTransport so any mutations we
// perform on it (e.g. TLS config) are not reflected globally
// follow https://github.com/golang/go/issues/39299 for a more elegant
// solution in the future
// clone http.DefaultTransport so mutations (e.g. TLS config) are not
// reflected globally
transport := http.DefaultTransport
if t, ok := transport.(cloner[*http.Transport]); ok {
transport = t.Clone()
} else if t, ok := transport.(cloner[http.RoundTripper]); ok {
// this branch will not be used with go 1.20, it was added
// optimistically to try to clone if the http.DefaultTransport
// implementation changes, still the Clone method in that case
// might not return http.RoundTripper...
if t, ok := transport.(*http.Transport); ok {
transport = t.Clone()
}
if debug {

@ -25,6 +25,7 @@ import (
"os"
"path"
"path/filepath"
"slices"
"sort"
"strings"
"time"
@ -153,7 +154,7 @@ func (i IndexFile) MustAdd(md *chart.Metadata, filename, baseURL, digest string)
// Deprecated: Use index.MustAdd instead.
func (i IndexFile) Add(md *chart.Metadata, filename, baseURL, digest string) {
if err := i.MustAdd(md, filename, baseURL, digest); err != nil {
slog.Error("skipping loading invalid entry for chart %q %q from %s: %s", md.Name, md.Version, filename, err)
slog.Error("skipping loading invalid entry for chart", "name", md.Name, "version", md.Version, "file", filename, "error", err)
}
}
@ -356,21 +357,21 @@ func loadIndex(data []byte, source string) (*IndexFile, error) {
}
for name, cvs := range i.Entries {
for idx := len(cvs) - 1; idx >= 0; idx-- {
if cvs[idx] == nil {
slog.Warn(fmt.Sprintf("skipping loading invalid entry for chart %q from %s: empty entry", name, source))
for idx, v := range slices.Backward(cvs) {
if v == nil {
slog.Warn("skipping loading invalid entry for chart: empty entry", "name", name, "source", source)
cvs = append(cvs[:idx], cvs[idx+1:]...)
continue
}
// When metadata section missing, initialize with no data
if cvs[idx].Metadata == nil {
cvs[idx].Metadata = &chart.Metadata{}
if v.Metadata == nil {
v.Metadata = &chart.Metadata{}
}
if cvs[idx].APIVersion == "" {
cvs[idx].APIVersion = chart.APIVersionV1
if v.APIVersion == "" {
v.APIVersion = chart.APIVersionV1
}
if err := cvs[idx].Validate(); ignoreSkippableChartValidationError(err) != nil {
slog.Warn(fmt.Sprintf("skipping loading invalid entry for chart %q %q from %s: %s", name, cvs[idx].Version, source, err))
if err := v.Validate(); ignoreSkippableChartValidationError(err) != nil {
slog.Warn("skipping loading invalid entry for chart", "name", name, "version", v.Version, "source", source, "error", err)
cvs = append(cvs[:idx], cvs[idx+1:]...)
}
}

Loading…
Cancel
Save