pull from upstream to resolve conflict, change registry_test.go file to use token auth registry

kimsungmin1 9 months ago
commit 2dc1ee9496
No known key found for this signature in database
GPG Key ID: B0523CB3744439AD

@ -44,6 +44,7 @@ body:
label: Helm version
value: |
<details>
```console
$ helm version
# paste output here

@ -33,7 +33,7 @@ jobs:
persist-credentials: false
- name: "Run analysis"
uses: ossf/scorecard-action@05b42c624433fc40578a4040d5cf5e36ddca8cde # v2.4.2
uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3
with:
results_file: results.sarif
results_format: sarif

@ -7,7 +7,7 @@ jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@3a9db7e6a41a89f618792c92c0e97cc736e1b13f # v10.0.0
- uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
stale-issue-message: 'This issue has been marked as stale because it has been open for 90 days with no activity. This thread will be automatically closed in 30 days if no further activity occurs.'

2
.gitignore vendored

@ -2,7 +2,7 @@
*.swp
.DS_Store
.coverage/
.idea/
.idea
.vimrc
.vscode/
.devcontainer/

@ -43,9 +43,12 @@ linters:
- std-error-handling
rules:
- linters:
- revive
text: 'var-naming: avoid meaningless package names'
# This rule is triggered for packages like 'util'. When changes to those packages
# occur it triggers this rule. This exclusion enables making changes to existing
# packages.
- linters:
- revive
text: 'var-naming: avoid meaningless package names'
warn-unused: true

@ -10,8 +10,9 @@ require (
github.com/Masterminds/sprig/v3 v3.3.0
github.com/Masterminds/squirrel v1.5.4
github.com/Masterminds/vcs v1.13.3
github.com/ProtonMail/go-crypto v1.3.0
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2
github.com/cyphar/filepath-securejoin v0.4.1
github.com/cyphar/filepath-securejoin v0.5.0
github.com/distribution/distribution/v3 v3.0.0
github.com/evanphx/json-patch/v5 v5.9.11
github.com/extism/go-sdk v1.7.1
@ -19,7 +20,7 @@ require (
github.com/fluxcd/cli-utils v0.36.0-flux.14
github.com/foxcpp/go-mockdns v1.1.0
github.com/gobwas/glob v0.2.3
github.com/gofrs/flock v0.12.1
github.com/gofrs/flock v0.13.0
github.com/gosuri/uitable v0.0.4
github.com/jmoiron/sqlx v1.4.0
github.com/lib/pq v1.10.9
@ -31,24 +32,24 @@ require (
github.com/rubenv/sql-migrate v1.8.0
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2
github.com/spf13/cobra v1.10.1
github.com/spf13/pflag v1.0.9
github.com/spf13/pflag v1.0.10
github.com/stretchr/testify v1.11.1
github.com/tetratelabs/wazero v1.9.0
go.yaml.in/yaml/v3 v3.0.4
golang.org/x/crypto v0.41.0
golang.org/x/term v0.34.0
golang.org/x/text v0.28.0
golang.org/x/crypto v0.43.0
golang.org/x/term v0.36.0
golang.org/x/text v0.30.0
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/api v0.34.0
k8s.io/apiextensions-apiserver v0.34.0
k8s.io/apimachinery v0.34.0
k8s.io/apiserver v0.34.0
k8s.io/cli-runtime v0.34.0
k8s.io/client-go v0.34.0
k8s.io/api v0.34.1
k8s.io/apiextensions-apiserver v0.34.1
k8s.io/apimachinery v0.34.1
k8s.io/apiserver v0.34.1
k8s.io/cli-runtime v0.34.1
k8s.io/client-go v0.34.1
k8s.io/klog/v2 v2.130.1
k8s.io/kubectl v0.34.0
k8s.io/kubectl v0.34.1
oras.land/oras-go/v2 v2.6.0
sigs.k8s.io/controller-runtime v0.22.0
sigs.k8s.io/controller-runtime v0.22.3
sigs.k8s.io/kustomize/kyaml v0.20.1
sigs.k8s.io/yaml v1.6.0
)
@ -64,6 +65,7 @@ require (
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/chai2010/gettext-go v1.0.2 // indirect
github.com/cloudflare/circl v1.6.1 // indirect
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
@ -161,13 +163,13 @@ require (
go.opentelemetry.io/otel/trace v1.37.0 // indirect
go.opentelemetry.io/proto/otlp v1.5.0 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect
golang.org/x/mod v0.26.0 // indirect
golang.org/x/net v0.42.0 // indirect
golang.org/x/mod v0.28.0 // indirect
golang.org/x/net v0.45.0 // indirect
golang.org/x/oauth2 v0.30.0 // indirect
golang.org/x/sync v0.16.0 // indirect
golang.org/x/sys v0.35.0 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.37.0 // indirect
golang.org/x/time v0.12.0 // indirect
golang.org/x/tools v0.35.0 // indirect
golang.org/x/tools v0.37.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb // indirect
google.golang.org/grpc v1.72.1 // indirect
@ -175,7 +177,7 @@ require (
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
k8s.io/component-base v0.34.0 // indirect
k8s.io/component-base v0.34.1 // indirect
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b // indirect
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect

@ -22,6 +22,8 @@ github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8
github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10=
github.com/Masterminds/vcs v1.13.3 h1:IIA2aBdXvfbIM+yl/eTnL4hb1XwdpvuQLglAix1gweE=
github.com/Masterminds/vcs v1.13.3/go.mod h1:TiE7xuEjl1N4j016moRd6vezp6e6Lz23gypeXfzXeW8=
github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw=
github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
@ -49,14 +51,16 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chai2010/gettext-go v1.0.2 h1:1Lwwip6Q2QGsAdl/ZKPCwTe9fe0CjlUbqj5bFNSjIRk=
github.com/chai2010/gettext-go v1.0.2/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA=
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s=
github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
github.com/cyphar/filepath-securejoin v0.5.0 h1:hIAhkRBMQ8nIeuVwcAoymp7MY4oherZdAxD+m0u9zaw=
github.com/cyphar/filepath-securejoin v0.5.0/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
@ -127,8 +131,8 @@ github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZ
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E=
github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0=
github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=
github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
@ -304,8 +308,9 @@ github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w=
github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
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=
@ -393,16 +398,16 @@ 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.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
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.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg=
golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ=
golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@ -416,8 +421,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.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM=
golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -430,8 +435,8 @@ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -451,8 +456,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.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
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=
@ -460,8 +465,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.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
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=
@ -469,8 +474,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.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@ -481,8 +486,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.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0=
golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw=
golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@ -509,32 +514,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.34.0 h1:L+JtP2wDbEYPUeNGbeSa/5GwFtIA662EmT2YSLOkAVE=
k8s.io/api v0.34.0/go.mod h1:YzgkIzOOlhl9uwWCZNqpw6RJy9L2FK4dlJeayUoydug=
k8s.io/apiextensions-apiserver v0.34.0 h1:B3hiB32jV7BcyKcMU5fDaDxk882YrJ1KU+ZSkA9Qxoc=
k8s.io/apiextensions-apiserver v0.34.0/go.mod h1:hLI4GxE1BDBy9adJKxUxCEHBGZtGfIg98Q+JmTD7+g0=
k8s.io/apimachinery v0.34.0 h1:eR1WO5fo0HyoQZt1wdISpFDffnWOvFLOOeJ7MgIv4z0=
k8s.io/apimachinery v0.34.0/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw=
k8s.io/apiserver v0.34.0 h1:Z51fw1iGMqN7uJ1kEaynf2Aec1Y774PqU+FVWCFV3Jg=
k8s.io/apiserver v0.34.0/go.mod h1:52ti5YhxAvewmmpVRqlASvaqxt0gKJxvCeW7ZrwgazQ=
k8s.io/cli-runtime v0.34.0 h1:N2/rUlJg6TMEBgtQ3SDRJwa8XyKUizwjlOknT1mB2Cw=
k8s.io/cli-runtime v0.34.0/go.mod h1:t/skRecS73Piv+J+FmWIQA2N2/rDjdYSQzEE67LUUs8=
k8s.io/client-go v0.34.0 h1:YoWv5r7bsBfb0Hs2jh8SOvFbKzzxyNo0nSb0zC19KZo=
k8s.io/client-go v0.34.0/go.mod h1:ozgMnEKXkRjeMvBZdV1AijMHLTh3pbACPvK7zFR+QQY=
k8s.io/component-base v0.34.0 h1:bS8Ua3zlJzapklsB1dZgjEJuJEeHjj8yTu1gxE2zQX8=
k8s.io/component-base v0.34.0/go.mod h1:RSCqUdvIjjrEm81epPcjQ/DS+49fADvGSCkIP3IC6vg=
k8s.io/api v0.34.1 h1:jC+153630BMdlFukegoEL8E/yT7aLyQkIVuwhmwDgJM=
k8s.io/api v0.34.1/go.mod h1:SB80FxFtXn5/gwzCoN6QCtPD7Vbu5w2n1S0J5gFfTYk=
k8s.io/apiextensions-apiserver v0.34.1 h1:NNPBva8FNAPt1iSVwIE0FsdrVriRXMsaWFMqJbII2CI=
k8s.io/apiextensions-apiserver v0.34.1/go.mod h1:hP9Rld3zF5Ay2Of3BeEpLAToP+l4s5UlxiHfqRaRcMc=
k8s.io/apimachinery v0.34.1 h1:dTlxFls/eikpJxmAC7MVE8oOeP1zryV7iRyIjB0gky4=
k8s.io/apimachinery v0.34.1/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw=
k8s.io/apiserver v0.34.1 h1:U3JBGdgANK3dfFcyknWde1G6X1F4bg7PXuvlqt8lITA=
k8s.io/apiserver v0.34.1/go.mod h1:eOOc9nrVqlBI1AFCvVzsob0OxtPZUCPiUJL45JOTBG0=
k8s.io/cli-runtime v0.34.1 h1:btlgAgTrYd4sk8vJTRG6zVtqBKt9ZMDeQZo2PIzbL7M=
k8s.io/cli-runtime v0.34.1/go.mod h1:aVA65c+f0MZiMUPbseU/M9l1Wo2byeaGwUuQEQVVveE=
k8s.io/client-go v0.34.1 h1:ZUPJKgXsnKwVwmKKdPfw4tB58+7/Ik3CrjOEhsiZ7mY=
k8s.io/client-go v0.34.1/go.mod h1:kA8v0FP+tk6sZA0yKLRG67LWjqufAoSHA2xVGKw9Of8=
k8s.io/component-base v0.34.1 h1:v7xFgG+ONhytZNFpIz5/kecwD+sUhVE6HU7qQUiRM4A=
k8s.io/component-base v0.34.1/go.mod h1:mknCpLlTSKHzAQJJnnHVKqjxR7gBeHRv0rPXA7gdtQ0=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b h1:MloQ9/bdJyIu9lb1PzujOPolHyvO06MXG5TUIj2mNAA=
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts=
k8s.io/kubectl v0.34.0 h1:NcXz4TPTaUwhiX4LU+6r6udrlm0NsVnSkP3R9t0dmxs=
k8s.io/kubectl v0.34.0/go.mod h1:bmd0W5i+HuG7/p5sqicr0Li0rR2iIhXL0oUyLF3OjR4=
k8s.io/kubectl v0.34.1 h1:1qP1oqT5Xc93K+H8J7ecpBjaz511gan89KO9Vbsh/OI=
k8s.io/kubectl v0.34.1/go.mod h1:JRYlhJpGPyk3dEmJ+BuBiOB9/dAvnrALJEiY/C5qa6A=
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y=
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
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.22.0 h1:mTOfibb8Hxwpx3xEkR56i7xSjB+nH4hZG37SrlCY5e0=
sigs.k8s.io/controller-runtime v0.22.0/go.mod h1:FwiwRjkRPbiN+zp2QRp7wlTCzbUXxZ/D4OzuQUDwBHY=
sigs.k8s.io/controller-runtime v0.22.3 h1:I7mfqz/a/WdmDCEnXmSPm8/b/yRTy6JsKKENTijTq8Y=
sigs.k8s.io/controller-runtime v0.22.3/go.mod h1:+QX1XUpTXN4mLoblf4tqr5CQcyHPAki2HLXqQMY6vh8=
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE=
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
sigs.k8s.io/kustomize/api v0.20.1 h1:iWP1Ydh3/lmldBnH/S5RXgT98vWYMaTUL1ADcr+Sv7I=

@ -57,7 +57,7 @@ func RunAll(baseDir string, values map[string]interface{}, namespace string, opt
}
rules.Chartfile(&result)
rules.ValuesWithOverrides(&result, values)
rules.ValuesWithOverrides(&result, values, lo.SkipSchemaValidation)
rules.TemplatesWithSkipSchemaValidation(&result, values, namespace, lo.KubeVersion, lo.SkipSchemaValidation)
rules.Dependencies(&result)
rules.Crds(&result)

@ -27,8 +27,6 @@ import (
chartutil "helm.sh/helm/v4/internal/chart/v3/util"
)
var values map[string]interface{}
const namespace = "testNamespace"
const badChartDir = "rules/testdata/badchartfile"
@ -41,6 +39,7 @@ const malformedTemplate = "rules/testdata/malformed-template"
const invalidChartFileDir = "rules/testdata/invalidchartfile"
func TestBadChartV3(t *testing.T) {
var values map[string]any
m := RunAll(badChartDir, values, namespace).Messages
if len(m) != 8 {
t.Errorf("Number of errors %v", len(m))
@ -90,6 +89,7 @@ func TestBadChartV3(t *testing.T) {
}
func TestInvalidYaml(t *testing.T) {
var values map[string]any
m := RunAll(badYamlFileDir, values, namespace).Messages
if len(m) != 1 {
t.Fatalf("All didn't fail with expected errors, got %#v", m)
@ -100,6 +100,7 @@ func TestInvalidYaml(t *testing.T) {
}
func TestInvalidChartYamlV3(t *testing.T) {
var values map[string]any
m := RunAll(invalidChartFileDir, values, namespace).Messages
t.Log(m)
if len(m) != 3 {
@ -111,6 +112,7 @@ func TestInvalidChartYamlV3(t *testing.T) {
}
func TestBadValuesV3(t *testing.T) {
var values map[string]any
m := RunAll(badValuesFileDir, values, namespace).Messages
if len(m) < 1 {
t.Fatalf("All didn't fail with expected errors, got %#v", m)
@ -121,6 +123,7 @@ func TestBadValuesV3(t *testing.T) {
}
func TestBadCrdFileV3(t *testing.T) {
var values map[string]any
m := RunAll(badCrdFileDir, values, namespace).Messages
assert.Lenf(t, m, 2, "All didn't fail with expected errors, got %#v", m)
assert.ErrorContains(t, m[0].Err, "apiVersion is not in 'apiextensions.k8s.io'")
@ -128,6 +131,7 @@ func TestBadCrdFileV3(t *testing.T) {
}
func TestGoodChart(t *testing.T) {
var values map[string]any
m := RunAll(goodChartDir, values, namespace).Messages
if len(m) != 0 {
t.Error("All returned linter messages when it shouldn't have")
@ -141,6 +145,7 @@ func TestGoodChart(t *testing.T) {
//
// See https://github.com/helm/helm/issues/7923
func TestHelmCreateChart(t *testing.T) {
var values map[string]any
dir := t.TempDir()
createdChart, err := chartutil.Create("testhelmcreatepasseslint", dir)
@ -190,11 +195,11 @@ func TestHelmCreateChart_CheckDeprecatedWarnings(t *testing.T) {
// Add values to enable hpa, and ingress which are disabled by default.
// This is the equivalent of:
// helm lint checkdeprecatedwarnings --set 'autoscaling.enabled=true,ingress.enabled=true'
updatedValues := map[string]interface{}{
"autoscaling": map[string]interface{}{
updatedValues := map[string]any{
"autoscaling": map[string]any{
"enabled": true,
},
"ingress": map[string]interface{}{
"ingress": map[string]any{
"enabled": true,
},
}
@ -213,6 +218,7 @@ func TestHelmCreateChart_CheckDeprecatedWarnings(t *testing.T) {
// lint ignores import-values
// See https://github.com/helm/helm/issues/9658
func TestSubChartValuesChart(t *testing.T) {
var values map[string]any
m := RunAll(subChartValuesDir, values, namespace).Messages
if len(m) != 0 {
t.Error("All returned linter messages when it shouldn't have")
@ -225,6 +231,7 @@ func TestSubChartValuesChart(t *testing.T) {
// lint stuck with malformed template object
// See https://github.com/helm/helm/issues/11391
func TestMalformedTemplate(t *testing.T) {
var values map[string]any
c := time.After(3 * time.Second)
ch := make(chan int, 1)
var m []support.Message

@ -32,7 +32,7 @@ import (
// they are only tested for well-formedness.
//
// If additional values are supplied, they are coalesced into the values in values.yaml.
func ValuesWithOverrides(linter *support.Linter, valueOverrides map[string]interface{}) {
func ValuesWithOverrides(linter *support.Linter, valueOverrides map[string]interface{}, skipSchemaValidation bool) {
file := "values.yaml"
vf := filepath.Join(linter.ChartDir, file)
fileExists := linter.RunLinterRule(support.InfoSev, file, validateValuesFileExistence(vf))
@ -41,7 +41,7 @@ func ValuesWithOverrides(linter *support.Linter, valueOverrides map[string]inter
return
}
linter.RunLinterRule(support.ErrorSev, file, validateValuesFile(vf, valueOverrides))
linter.RunLinterRule(support.ErrorSev, file, validateValuesFile(vf, valueOverrides, skipSchemaValidation))
}
func validateValuesFileExistence(valuesPath string) error {
@ -52,7 +52,7 @@ func validateValuesFileExistence(valuesPath string) error {
return nil
}
func validateValuesFile(valuesPath string, overrides map[string]interface{}) error {
func validateValuesFile(valuesPath string, overrides map[string]interface{}, skipSchemaValidation bool) error {
values, err := common.ReadValuesFile(valuesPath)
if err != nil {
return fmt.Errorf("unable to parse YAML: %w", err)
@ -75,5 +75,10 @@ func validateValuesFile(valuesPath string, overrides map[string]interface{}) err
if err != nil {
return err
}
return util.ValidateAgainstSingleSchema(coalescedValues, schema)
if !skipSchemaValidation {
return util.ValidateAgainstSingleSchema(coalescedValues, schema)
}
return nil
}

@ -67,7 +67,7 @@ func TestValidateValuesFileWellFormed(t *testing.T) {
`
tmpdir := ensure.TempFile(t, "values.yaml", []byte(badYaml))
valfile := filepath.Join(tmpdir, "values.yaml")
if err := validateValuesFile(valfile, map[string]interface{}{}); err == nil {
if err := validateValuesFile(valfile, map[string]interface{}{}, false); err == nil {
t.Fatal("expected values file to fail parsing")
}
}
@ -78,7 +78,7 @@ func TestValidateValuesFileSchema(t *testing.T) {
createTestingSchema(t, tmpdir)
valfile := filepath.Join(tmpdir, "values.yaml")
if err := validateValuesFile(valfile, map[string]interface{}{}); err != nil {
if err := validateValuesFile(valfile, map[string]interface{}{}, false); err != nil {
t.Fatalf("Failed validation with %s", err)
}
}
@ -91,7 +91,7 @@ func TestValidateValuesFileSchemaFailure(t *testing.T) {
valfile := filepath.Join(tmpdir, "values.yaml")
err := validateValuesFile(valfile, map[string]interface{}{})
err := validateValuesFile(valfile, map[string]interface{}{}, false)
if err == nil {
t.Fatal("expected values file to fail parsing")
}
@ -99,6 +99,20 @@ func TestValidateValuesFileSchemaFailure(t *testing.T) {
assert.Contains(t, err.Error(), "- at '/username': got number, want string")
}
func TestValidateValuesFileSchemaFailureButWithSkipSchemaValidation(t *testing.T) {
// 1234 is an int, not a string. This should fail normally but pass with skipSchemaValidation.
yaml := "username: 1234\npassword: swordfish"
tmpdir := ensure.TempFile(t, "values.yaml", []byte(yaml))
createTestingSchema(t, tmpdir)
valfile := filepath.Join(tmpdir, "values.yaml")
err := validateValuesFile(valfile, map[string]interface{}{}, true)
if err != nil {
t.Fatal("expected values file to pass parsing because of skipSchemaValidation")
}
}
func TestValidateValuesFileSchemaOverrides(t *testing.T) {
yaml := "username: admin"
overrides := map[string]interface{}{
@ -108,7 +122,7 @@ func TestValidateValuesFileSchemaOverrides(t *testing.T) {
createTestingSchema(t, tmpdir)
valfile := filepath.Join(tmpdir, "values.yaml")
if err := validateValuesFile(valfile, overrides); err != nil {
if err := validateValuesFile(valfile, overrides, false); err != nil {
t.Fatalf("Failed validation with %s", err)
}
}
@ -145,7 +159,7 @@ func TestValidateValuesFile(t *testing.T) {
valfile := filepath.Join(tmpdir, "values.yaml")
err := validateValuesFile(valfile, tt.overrides)
err := validateValuesFile(valfile, tt.overrides, false)
switch {
case err != nil && tt.errorMessage == "":

@ -17,32 +17,16 @@ limitations under the License.
package loader
import (
"archive/tar"
"bytes"
"compress/gzip"
"errors"
"fmt"
"io"
"net/http"
"os"
"path"
"regexp"
"strings"
chart "helm.sh/helm/v4/internal/chart/v3"
"helm.sh/helm/v4/pkg/chart/loader/archive"
)
// MaxDecompressedChartSize is the maximum size of a chart archive that will be
// decompressed. This is the decompressed size of all the files.
// 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.
var MaxDecompressedFileSize int64 = 5 * 1024 * 1024 // Default 5 MiB
var drivePathPattern = regexp.MustCompile(`^[a-zA-Z]:/`)
// FileLoader loads a chart from a file
type FileLoader string
@ -65,7 +49,7 @@ func LoadFile(name string) (*chart.Chart, error) {
}
defer raw.Close()
err = ensureArchive(name, raw)
err = archive.EnsureArchive(name, raw)
if err != nil {
return nil, err
}
@ -79,153 +63,9 @@ func LoadFile(name string) (*chart.Chart, error) {
return c, err
}
// ensureArchive's job is to return an informative error if the file does not appear to be a gzipped archive.
//
// Sometimes users will provide a values.yaml for an argument where a chart is expected. One common occurrence
// of this is invoking `helm template values.yaml mychart` which would otherwise produce a confusing error
// if we didn't check for this.
func ensureArchive(name string, raw *os.File) error {
defer raw.Seek(0, 0) // reset read offset to allow archive loading to proceed.
// Check the file format to give us a chance to provide the user with more actionable feedback.
buffer := make([]byte, 512)
_, err := raw.Read(buffer)
if err != nil && err != io.EOF {
return fmt.Errorf("file '%s' cannot be read: %s", name, err)
}
// Helm may identify achieve of the application/x-gzip as application/vnd.ms-fontobject.
// Fix for: https://github.com/helm/helm/issues/12261
if contentType := http.DetectContentType(buffer); contentType != "application/x-gzip" && !isGZipApplication(buffer) {
// TODO: Is there a way to reliably test if a file content is YAML? ghodss/yaml accepts a wide
// variety of content (Makefile, .zshrc) as valid YAML without errors.
// Wrong content type. Let's check if it's yaml and give an extra hint?
if strings.HasSuffix(name, ".yml") || strings.HasSuffix(name, ".yaml") {
return fmt.Errorf("file '%s' seems to be a YAML file, but expected a gzipped archive", name)
}
return fmt.Errorf("file '%s' does not appear to be a gzipped archive; got '%s'", name, contentType)
}
return nil
}
// isGZipApplication checks whether the archive is of the application/x-gzip type.
func isGZipApplication(data []byte) bool {
sig := []byte("\x1F\x8B\x08")
return bytes.HasPrefix(data, sig)
}
// LoadArchiveFiles reads in files out of an archive into memory. This function
// performs important path security checks and should always be used before
// expanding a tarball
func LoadArchiveFiles(in io.Reader) ([]*BufferedFile, error) {
unzipped, err := gzip.NewReader(in)
if err != nil {
return nil, err
}
defer unzipped.Close()
files := []*BufferedFile{}
tr := tar.NewReader(unzipped)
remainingSize := MaxDecompressedChartSize
for {
b := bytes.NewBuffer(nil)
hd, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
return nil, err
}
if hd.FileInfo().IsDir() {
// Use this instead of hd.Typeflag because we don't have to do any
// inference chasing.
continue
}
switch hd.Typeflag {
// We don't want to process these extension header files.
case tar.TypeXGlobalHeader, tar.TypeXHeader:
continue
}
// Archive could contain \ if generated on Windows
delimiter := "/"
if strings.ContainsRune(hd.Name, '\\') {
delimiter = "\\"
}
parts := strings.Split(hd.Name, delimiter)
n := strings.Join(parts[1:], delimiter)
// Normalize the path to the / delimiter
n = strings.ReplaceAll(n, delimiter, "/")
if path.IsAbs(n) {
return nil, errors.New("chart illegally contains absolute paths")
}
n = path.Clean(n)
if n == "." {
// In this case, the original path was relative when it should have been absolute.
return nil, fmt.Errorf("chart illegally contains content outside the base directory: %q", hd.Name)
}
if strings.HasPrefix(n, "..") {
return nil, errors.New("chart illegally references parent directory")
}
// In some particularly arcane acts of path creativity, it is possible to intermix
// UNIX and Windows style paths in such a way that you produce a result of the form
// c:/foo even after all the built-in absolute path checks. So we explicitly check
// for this condition.
if drivePathPattern.MatchString(n) {
return nil, errors.New("chart contains illegally named files")
}
if parts[0] == "Chart.yaml" {
return nil, errors.New("chart yaml not in base directory")
}
if hd.Size > remainingSize {
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)
if err != nil {
return nil, err
}
remainingSize -= bytesWritten
// When the bytesWritten are less than the file size it means the limit reader ended
// copying early. Here we report that error. This is important if the last file extracted
// is the one that goes over the limit. It assumes the Size stored in the tar header
// is correct, something many applications do.
if bytesWritten < hd.Size || remainingSize <= 0 {
return nil, fmt.Errorf("decompressed chart is larger than the maximum size %d", MaxDecompressedChartSize)
}
data := bytes.TrimPrefix(b.Bytes(), utf8bom)
files = append(files, &BufferedFile{Name: n, Data: data})
b.Reset()
}
if len(files) == 0 {
return nil, errors.New("no files in chart archive")
}
return files, nil
}
// LoadArchive loads from a reader containing a compressed tar archive.
func LoadArchive(in io.Reader) (*chart.Chart, error) {
files, err := LoadArchiveFiles(in)
files, err := archive.LoadArchiveFiles(in)
if err != nil {
return nil, err
}

@ -25,6 +25,7 @@ import (
chart "helm.sh/helm/v4/internal/chart/v3"
"helm.sh/helm/v4/internal/sympath"
"helm.sh/helm/v4/pkg/chart/loader/archive"
"helm.sh/helm/v4/pkg/ignore"
)
@ -61,7 +62,7 @@ func LoadDir(dir string) (*chart.Chart, error) {
}
rules.AddDefaults()
files := []*BufferedFile{}
files := []*archive.BufferedFile{}
topdir += string(filepath.Separator)
walk := func(name string, fi os.FileInfo, err error) error {
@ -99,8 +100,8 @@ 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() > MaxDecompressedFileSize {
return fmt.Errorf("chart file %q is larger than the maximum file size %d", fi.Name(), MaxDecompressedFileSize)
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)
@ -110,7 +111,7 @@ func LoadDir(dir string) (*chart.Chart, error) {
data = bytes.TrimPrefix(data, utf8bom)
files = append(files, &BufferedFile{Name: n, Data: data})
files = append(files, &archive.BufferedFile{Name: n, Data: data})
return nil
}
if err = sympath.Walk(topdir, walk); err != nil {

@ -32,6 +32,7 @@ import (
chart "helm.sh/helm/v4/internal/chart/v3"
"helm.sh/helm/v4/pkg/chart/common"
"helm.sh/helm/v4/pkg/chart/loader/archive"
)
// ChartLoader loads a chart.
@ -66,16 +67,10 @@ func Load(name string) (*chart.Chart, error) {
return l.Load()
}
// BufferedFile represents an archive file buffered for later processing.
type BufferedFile struct {
Name string
Data []byte
}
// LoadFiles loads from in-memory files.
func LoadFiles(files []*BufferedFile) (*chart.Chart, error) {
func LoadFiles(files []*archive.BufferedFile) (*chart.Chart, error) {
c := new(chart.Chart)
subcharts := make(map[string][]*BufferedFile)
subcharts := make(map[string][]*archive.BufferedFile)
// do not rely on assumed ordering of files in the chart and crash
// if Chart.yaml was not coming early enough to initialize metadata
@ -125,7 +120,7 @@ func LoadFiles(files []*BufferedFile) (*chart.Chart, error) {
fname := strings.TrimPrefix(f.Name, "charts/")
cname := strings.SplitN(fname, "/", 2)[0]
subcharts[cname] = append(subcharts[cname], &BufferedFile{Name: fname, Data: f.Data})
subcharts[cname] = append(subcharts[cname], &archive.BufferedFile{Name: fname, Data: f.Data})
default:
c.Files = append(c.Files, &common.File{Name: f.Name, Data: f.Data})
}
@ -155,7 +150,7 @@ func LoadFiles(files []*BufferedFile) (*chart.Chart, error) {
default:
// We have to trim the prefix off of every file, and ignore any file
// that is in charts/, but isn't actually a chart.
buff := make([]*BufferedFile, 0, len(files))
buff := make([]*archive.BufferedFile, 0, len(files))
for _, f := range files {
parts := strings.SplitN(f.Name, "/", 2)
if len(parts) < 2 {

@ -32,6 +32,7 @@ import (
chart "helm.sh/helm/v4/internal/chart/v3"
"helm.sh/helm/v4/pkg/chart/common"
"helm.sh/helm/v4/pkg/chart/loader/archive"
)
func TestLoadDir(t *testing.T) {
@ -183,7 +184,7 @@ func TestLoadFile(t *testing.T) {
}
func TestLoadFiles(t *testing.T) {
goodFiles := []*BufferedFile{
goodFiles := []*archive.BufferedFile{
{
Name: "Chart.yaml",
Data: []byte(`apiVersion: v3
@ -248,7 +249,7 @@ icon: https://example.com/64x64.png
t.Errorf("Expected number of templates == 2, got %d", len(c.Templates))
}
if _, err = LoadFiles([]*BufferedFile{}); err == nil {
if _, err = LoadFiles([]*archive.BufferedFile{}); err == nil {
t.Fatal("Expected err to be non-nil")
}
if err.Error() != "Chart.yaml file is missing" {
@ -259,7 +260,7 @@ icon: https://example.com/64x64.png
// Test the order of file loading. The Chart.yaml file needs to come first for
// later comparison checks. See https://github.com/helm/helm/pull/8948
func TestLoadFilesOrder(t *testing.T) {
goodFiles := []*BufferedFile{
goodFiles := []*archive.BufferedFile{
{
Name: "requirements.yaml",
Data: []byte("dependencies:"),

@ -27,12 +27,12 @@ import (
"sigs.k8s.io/yaml"
chart "helm.sh/helm/v4/internal/chart/v3"
"helm.sh/helm/v4/internal/chart/v3/loader"
"helm.sh/helm/v4/pkg/chart/loader/archive"
)
// Expand uncompresses and extracts a chart into the specified directory.
func Expand(dir string, r io.Reader) error {
files, err := loader.LoadArchiveFiles(r)
files, err := archive.LoadArchiveFiles(r)
if err != nil {
return err
}

@ -19,24 +19,24 @@ package output
import (
"github.com/fatih/color"
release "helm.sh/helm/v4/pkg/release/v1"
"helm.sh/helm/v4/pkg/release/common"
)
// ColorizeStatus returns a colorized version of the status string based on the status value
func ColorizeStatus(status release.Status, noColor bool) string {
func ColorizeStatus(status common.Status, noColor bool) string {
// Disable color if requested
if noColor {
return status.String()
}
switch status {
case release.StatusDeployed:
case common.StatusDeployed:
return color.GreenString(status.String())
case release.StatusFailed:
case common.StatusFailed:
return color.RedString(status.String())
case release.StatusPendingInstall, release.StatusPendingUpgrade, release.StatusPendingRollback, release.StatusUninstalling:
case common.StatusPendingInstall, common.StatusPendingUpgrade, common.StatusPendingRollback, common.StatusUninstalling:
return color.YellowString(status.String())
case release.StatusUnknown:
case common.StatusUnknown:
return color.RedString(status.String())
default:
// For uninstalled, superseded, and any other status

@ -20,63 +20,63 @@ import (
"strings"
"testing"
release "helm.sh/helm/v4/pkg/release/v1"
"helm.sh/helm/v4/pkg/release/common"
)
func TestColorizeStatus(t *testing.T) {
tests := []struct {
name string
status release.Status
status common.Status
noColor bool
envNoColor string
wantColor bool // whether we expect color codes in output
}{
{
name: "deployed status with color",
status: release.StatusDeployed,
status: common.StatusDeployed,
noColor: false,
envNoColor: "",
wantColor: true,
},
{
name: "deployed status without color flag",
status: release.StatusDeployed,
status: common.StatusDeployed,
noColor: true,
envNoColor: "",
wantColor: false,
},
{
name: "deployed status with NO_COLOR env",
status: release.StatusDeployed,
status: common.StatusDeployed,
noColor: false,
envNoColor: "1",
wantColor: false,
},
{
name: "failed status with color",
status: release.StatusFailed,
status: common.StatusFailed,
noColor: false,
envNoColor: "",
wantColor: true,
},
{
name: "pending install status with color",
status: release.StatusPendingInstall,
status: common.StatusPendingInstall,
noColor: false,
envNoColor: "",
wantColor: true,
},
{
name: "unknown status with color",
status: release.StatusUnknown,
status: common.StatusUnknown,
noColor: false,
envNoColor: "",
wantColor: true,
},
{
name: "superseded status with color",
status: release.StatusSuperseded,
status: common.StatusSuperseded,
noColor: false,
envNoColor: "",
wantColor: false, // superseded doesn't get colored

@ -20,9 +20,12 @@ import (
"bytes"
"os"
"path/filepath"
"strings"
"testing"
)
// TestAtomicWriteFile tests the happy path of AtomicWriteFile function.
// It verifies that the function correctly writes content to a file with the specified mode.
func TestAtomicWriteFile(t *testing.T) {
dir := t.TempDir()
@ -55,3 +58,64 @@ func TestAtomicWriteFile(t *testing.T) {
mode, gotinfo.Mode())
}
}
// TestAtomicWriteFile_CreateTempError tests the error path when os.CreateTemp fails
func TestAtomicWriteFile_CreateTempError(t *testing.T) {
invalidPath := "/invalid/path/that/does/not/exist/testfile"
reader := bytes.NewReader([]byte("test content"))
mode := os.FileMode(0644)
err := AtomicWriteFile(invalidPath, reader, mode)
if err == nil {
t.Error("Expected error when CreateTemp fails, but got nil")
}
}
// TestAtomicWriteFile_EmptyContent tests with empty content
func TestAtomicWriteFile_EmptyContent(t *testing.T) {
dir := t.TempDir()
testpath := filepath.Join(dir, "empty_helm")
reader := bytes.NewReader([]byte(""))
mode := os.FileMode(0644)
err := AtomicWriteFile(testpath, reader, mode)
if err != nil {
t.Errorf("AtomicWriteFile error with empty content: %s", err)
}
got, err := os.ReadFile(testpath)
if err != nil {
t.Fatal(err)
}
if len(got) != 0 {
t.Fatalf("expected empty content, got: %s", string(got))
}
}
// TestAtomicWriteFile_LargeContent tests with large content
func TestAtomicWriteFile_LargeContent(t *testing.T) {
dir := t.TempDir()
testpath := filepath.Join(dir, "large_test")
// Create a large content string
largeContent := strings.Repeat("HELM", 1024*1024)
reader := bytes.NewReader([]byte(largeContent))
mode := os.FileMode(0644)
err := AtomicWriteFile(testpath, reader, mode)
if err != nil {
t.Errorf("AtomicWriteFile error with large content: %s", err)
}
got, err := os.ReadFile(testpath)
if err != nil {
t.Fatal(err)
}
if largeContent != string(got) {
t.Fatalf("expected large content to match, got different length: %d vs %d", len(largeContent), len(got))
}
}

@ -185,8 +185,8 @@ func (g *TarGzExtractor) Extract(buffer *bytes.Buffer, targetDir string) error {
func stripPluginName(name string) string {
var strippedName string
for suffix := range Extractors {
if strings.HasSuffix(name, suffix) {
strippedName = strings.TrimSuffix(name, suffix)
if before, ok := strings.CutSuffix(name, suffix); ok {
strippedName = before
break
}
}

@ -18,6 +18,7 @@ package installer
import (
"errors"
"fmt"
"log/slog"
"net/http"
"os"
"path/filepath"
@ -79,6 +80,7 @@ func InstallWithOptions(i Installer, opts Options) (*VerificationResult, error)
return nil, err
}
if _, pathErr := os.Stat(i.Path()); !os.IsNotExist(pathErr) {
slog.Warn("plugin already exists", "path", i.Path(), slog.Any("error", pathErr))
return nil, errors.New("plugin already exists")
}
@ -130,6 +132,7 @@ func InstallWithOptions(i Installer, opts Options) (*VerificationResult, error)
// Update updates a plugin.
func Update(i Installer) error {
if _, pathErr := os.Stat(i.Path()); os.IsNotExist(pathErr) {
slog.Warn("plugin does not exist", "path", i.Path(), slog.Any("error", pathErr))
return errors.New("plugin does not exist")
}
return i.Update()
@ -154,6 +157,7 @@ func NewForSource(source, version string) (Installer, error) {
func FindSource(location string) (Installer, error) {
installer, err := existingVCSRepo(location)
if err != nil && err.Error() == "Cannot detect VCS" {
slog.Warn("cannot get information about plugin source", "location", location, slog.Any("error", err))
return installer, errors.New("cannot get information about plugin source")
}
return installer, err

@ -29,7 +29,7 @@ import (
"helm.sh/helm/v4/internal/plugin/schema"
)
// SubprocessProtocolCommand maps a given protocol to the getter command used to retrieve artifacts for that protcol
// SubprocessProtocolCommand maps a given protocol to the getter command used to retrieve artifacts for that protocol
type SubprocessProtocolCommand struct {
// Protocols are the list of schemes from the charts URL.
Protocols []string `yaml:"protocols"`

@ -15,13 +15,10 @@ package schema
import (
"bytes"
"helm.sh/helm/v4/pkg/cli"
)
type InputMessageCLIV1 struct {
ExtraArgs []string `json:"extraArgs"`
Settings *cli.EnvSettings `json:"settings"`
ExtraArgs []string `json:"extraArgs"`
}
type OutputMessageCLIV1 struct {

@ -23,7 +23,7 @@ import (
"path/filepath"
"strings"
"golang.org/x/crypto/openpgp/clearsign" //nolint
"github.com/ProtonMail/go-crypto/openpgp/clearsign" //nolint
"helm.sh/helm/v4/pkg/helmpath"
)

@ -30,6 +30,7 @@ import (
"strings"
"sync"
"text/template"
"time"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/cli-runtime/pkg/genericclioptions"
@ -46,11 +47,11 @@ import (
"helm.sh/helm/v4/pkg/kube"
"helm.sh/helm/v4/pkg/postrenderer"
"helm.sh/helm/v4/pkg/registry"
ri "helm.sh/helm/v4/pkg/release"
release "helm.sh/helm/v4/pkg/release/v1"
releaseutil "helm.sh/helm/v4/pkg/release/v1/util"
"helm.sh/helm/v4/pkg/storage"
"helm.sh/helm/v4/pkg/storage/driver"
"helm.sh/helm/v4/pkg/time"
)
// Timestamper is a function capable of producing a timestamp.Timestamper.
@ -70,6 +71,21 @@ var (
errPending = errors.New("another operation (install/upgrade/rollback) is in progress")
)
type DryRunStrategy string
const (
// DryRunNone indicates the client will make all mutating calls
DryRunNone DryRunStrategy = "none"
// DryRunClient, or client-side dry-run, indicates the client will avoid
// making calls to the server
DryRunClient DryRunStrategy = "client"
// DryRunServer, or server-side dry-run, indicates the client will send
// calls to the APIServer with the dry-run parameter to prevent persisting changes
DryRunServer DryRunStrategy = "server"
)
// Configuration injects the dependencies that all actions share.
type Configuration struct {
// RESTClientGetter is an interface that loads Kubernetes clients.
@ -397,7 +413,7 @@ func (cfg *Configuration) Now() time.Time {
return Timestamper()
}
func (cfg *Configuration) releaseContent(name string, version int) (*release.Release, error) {
func (cfg *Configuration) releaseContent(name string, version int) (ri.Releaser, error) {
if err := chartutil.ValidateReleaseName(name); err != nil {
return nil, fmt.Errorf("releaseContent: Release name is invalid: %s", name)
}
@ -528,3 +544,13 @@ func determineReleaseSSApplyMethod(serverSideApply bool) release.ApplyMethod {
}
return release.ApplyMethodClientSideApply
}
// isDryRun returns true if the strategy is set to run as a DryRun
func isDryRun(strategy DryRunStrategy) bool {
return strategy == DryRunClient || strategy == DryRunServer
}
// interactWithServer determine whether or not to interact with a remote Kubernetes server
func interactWithServer(strategy DryRunStrategy) bool {
return strategy == DryRunNone || strategy == DryRunServer
}

@ -24,6 +24,7 @@ import (
"log/slog"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@ -35,10 +36,10 @@ import (
"helm.sh/helm/v4/pkg/kube"
kubefake "helm.sh/helm/v4/pkg/kube/fake"
"helm.sh/helm/v4/pkg/registry"
rcommon "helm.sh/helm/v4/pkg/release/common"
release "helm.sh/helm/v4/pkg/release/v1"
"helm.sh/helm/v4/pkg/storage"
"helm.sh/helm/v4/pkg/storage/driver"
"helm.sh/helm/v4/pkg/time"
)
var verbose = flag.Bool("test.log", false, "enable test logging (debug by default)")
@ -249,10 +250,10 @@ func withKube(version string) chartOption {
// releaseStub creates a release stub, complete with the chartStub as its chart.
func releaseStub() *release.Release {
return namedReleaseStub("angry-panda", release.StatusDeployed)
return namedReleaseStub("angry-panda", rcommon.StatusDeployed)
}
func namedReleaseStub(name string, status release.Status) *release.Release {
func namedReleaseStub(name string, status rcommon.Status) *release.Release {
now := time.Now()
return &release.Release{
Name: name,
@ -951,3 +952,15 @@ func TestDetermineReleaseSSAApplyMethod(t *testing.T) {
assert.Equal(t, release.ApplyMethodClientSideApply, determineReleaseSSApplyMethod(false))
assert.Equal(t, release.ApplyMethodServerSideApply, determineReleaseSSApplyMethod(true))
}
func TestIsDryRun(t *testing.T) {
assert.False(t, isDryRun(DryRunNone))
assert.True(t, isDryRun(DryRunClient))
assert.True(t, isDryRun(DryRunServer))
}
func TestInteractWithServer(t *testing.T) {
assert.True(t, interactWithServer(DryRunNone))
assert.False(t, interactWithServer(DryRunClient))
assert.True(t, interactWithServer(DryRunServer))
}

@ -17,7 +17,7 @@ limitations under the License.
package action
import (
release "helm.sh/helm/v4/pkg/release/v1"
release "helm.sh/helm/v4/pkg/release"
)
// Get is the action for checking a given release's information.
@ -38,7 +38,7 @@ func NewGet(cfg *Configuration) *Get {
}
// Run executes 'helm get' against the given release.
func (g *Get) Run(name string) (*release.Release, error) {
func (g *Get) Run(name string) (release.Releaser, error) {
if err := g.cfg.KubeClient.IsReachable(); err != nil {
return nil, err
}

@ -17,11 +17,15 @@ limitations under the License.
package action
import (
"errors"
"log/slog"
"sort"
"strings"
"time"
ci "helm.sh/helm/v4/pkg/chart"
chart "helm.sh/helm/v4/pkg/chart/v2"
"helm.sh/helm/v4/pkg/release"
)
// GetMetadata is the action for checking a given release's metadata.
@ -41,13 +45,13 @@ type Metadata struct {
// Annotations are fetched from the Chart.yaml file
Annotations map[string]string `json:"annotations,omitempty" yaml:"annotations,omitempty"`
// Labels of the release which are stored in driver metadata fields storage
Labels map[string]string `json:"labels,omitempty" yaml:"labels,omitempty"`
Dependencies []*chart.Dependency `json:"dependencies,omitempty" yaml:"dependencies,omitempty"`
Namespace string `json:"namespace" yaml:"namespace"`
Revision int `json:"revision" yaml:"revision"`
Status string `json:"status" yaml:"status"`
DeployedAt string `json:"deployedAt" yaml:"deployedAt"`
ApplyMethod string `json:"applyMethod,omitempty" yaml:"applyMethod,omitempty"`
Labels map[string]string `json:"labels,omitempty" yaml:"labels,omitempty"`
Dependencies []ci.Dependency `json:"dependencies,omitempty" yaml:"dependencies,omitempty"`
Namespace string `json:"namespace" yaml:"namespace"`
Revision int `json:"revision" yaml:"revision"`
Status string `json:"status" yaml:"status"`
DeployedAt string `json:"deployedAt" yaml:"deployedAt"`
ApplyMethod string `json:"applyMethod,omitempty" yaml:"applyMethod,omitempty"`
}
// NewGetMetadata creates a new GetMetadata object with the given configuration.
@ -68,19 +72,40 @@ func (g *GetMetadata) Run(name string) (*Metadata, error) {
return nil, err
}
rac, err := release.NewAccessor(rel)
if err != nil {
return nil, err
}
ac, err := ci.NewAccessor(rac.Chart())
if err != nil {
return nil, err
}
charti := rac.Chart()
var chrt *chart.Chart
switch c := charti.(type) {
case *chart.Chart:
chrt = c
case chart.Chart:
chrt = &c
default:
return nil, errors.New("invalid chart apiVersion")
}
return &Metadata{
Name: rel.Name,
Chart: rel.Chart.Metadata.Name,
Version: rel.Chart.Metadata.Version,
AppVersion: rel.Chart.Metadata.AppVersion,
Dependencies: rel.Chart.Metadata.Dependencies,
Annotations: rel.Chart.Metadata.Annotations,
Labels: rel.Labels,
Namespace: rel.Namespace,
Revision: rel.Version,
Status: rel.Info.Status.String(),
DeployedAt: rel.Info.LastDeployed.Format(time.RFC3339),
ApplyMethod: rel.ApplyMethod,
Name: rac.Name(),
Chart: chrt.Metadata.Name,
Version: chrt.Metadata.Version,
AppVersion: chrt.Metadata.AppVersion,
Dependencies: ac.MetaDependencies(),
Annotations: chrt.Metadata.Annotations,
Labels: rac.Labels(),
Namespace: rac.Namespace(),
Revision: rac.Version(),
Status: rac.Status(),
DeployedAt: rac.DeployedAt().Format(time.RFC3339),
ApplyMethod: rac.ApplyMethod(),
}, nil
}
@ -88,7 +113,13 @@ func (g *GetMetadata) Run(name string) (*Metadata, error) {
func (m *Metadata) FormattedDepNames() string {
depsNames := make([]string, 0, len(m.Dependencies))
for _, dep := range m.Dependencies {
depsNames = append(depsNames, dep.Name)
ac, err := ci.NewDependencyAccessor(dep)
if err != nil {
slog.Error("unable to access dependency metadata", "error", err)
continue
}
depsNames = append(depsNames, ac.Name())
}
sort.StringSlice(depsNames).Sort()

@ -25,21 +25,13 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
ci "helm.sh/helm/v4/pkg/chart"
chart "helm.sh/helm/v4/pkg/chart/v2"
kubefake "helm.sh/helm/v4/pkg/kube/fake"
"helm.sh/helm/v4/pkg/release/common"
release "helm.sh/helm/v4/pkg/release/v1"
helmtime "helm.sh/helm/v4/pkg/time"
)
// unreachableKubeClient is a test client that always returns an error for IsReachable
type unreachableKubeClient struct {
kubefake.PrintingKubeClient
}
func (u *unreachableKubeClient) IsReachable() error {
return errors.New("connection refused")
}
func TestNewGetMetadata(t *testing.T) {
cfg := actionConfigFixture(t)
client := NewGetMetadata(cfg)
@ -54,12 +46,12 @@ func TestGetMetadata_Run_BasicMetadata(t *testing.T) {
client := NewGetMetadata(cfg)
releaseName := "test-release"
deployedTime := helmtime.Now()
deployedTime := time.Now()
rel := &release.Release{
Name: releaseName,
Info: &release.Info{
Status: release.StatusDeployed,
Status: common.StatusDeployed,
LastDeployed: deployedTime,
},
Chart: &chart.Chart{
@ -73,7 +65,8 @@ func TestGetMetadata_Run_BasicMetadata(t *testing.T) {
Namespace: "default",
}
cfg.Releases.Create(rel)
err := cfg.Releases.Create(rel)
require.NoError(t, err)
result, err := client.Run(releaseName)
require.NoError(t, err)
@ -95,7 +88,7 @@ func TestGetMetadata_Run_WithDependencies(t *testing.T) {
client := NewGetMetadata(cfg)
releaseName := "test-release"
deployedTime := helmtime.Now()
deployedTime := time.Now()
dependencies := []*chart.Dependency{
{
@ -113,7 +106,7 @@ func TestGetMetadata_Run_WithDependencies(t *testing.T) {
rel := &release.Release{
Name: releaseName,
Info: &release.Info{
Status: release.StatusDeployed,
Status: common.StatusDeployed,
LastDeployed: deployedTime,
},
Chart: &chart.Chart{
@ -133,13 +126,18 @@ func TestGetMetadata_Run_WithDependencies(t *testing.T) {
result, err := client.Run(releaseName)
require.NoError(t, err)
dep0, err := ci.NewDependencyAccessor(result.Dependencies[0])
require.NoError(t, err)
dep1, err := ci.NewDependencyAccessor(result.Dependencies[1])
require.NoError(t, err)
assert.Equal(t, releaseName, result.Name)
assert.Equal(t, "test-chart", result.Chart)
assert.Equal(t, "1.0.0", result.Version)
assert.Equal(t, dependencies, result.Dependencies)
assert.Equal(t, convertDeps(dependencies), result.Dependencies)
assert.Len(t, result.Dependencies, 2)
assert.Equal(t, "mysql", result.Dependencies[0].Name)
assert.Equal(t, "redis", result.Dependencies[1].Name)
assert.Equal(t, "mysql", dep0.Name())
assert.Equal(t, "redis", dep1.Name())
}
func TestGetMetadata_Run_WithDependenciesAliases(t *testing.T) {
@ -147,7 +145,7 @@ func TestGetMetadata_Run_WithDependenciesAliases(t *testing.T) {
client := NewGetMetadata(cfg)
releaseName := "test-release"
deployedTime := helmtime.Now()
deployedTime := time.Now()
dependencies := []*chart.Dependency{
{
@ -167,7 +165,7 @@ func TestGetMetadata_Run_WithDependenciesAliases(t *testing.T) {
rel := &release.Release{
Name: releaseName,
Info: &release.Info{
Status: release.StatusDeployed,
Status: common.StatusDeployed,
LastDeployed: deployedTime,
},
Chart: &chart.Chart{
@ -187,15 +185,20 @@ func TestGetMetadata_Run_WithDependenciesAliases(t *testing.T) {
result, err := client.Run(releaseName)
require.NoError(t, err)
dep0, err := ci.NewDependencyAccessor(result.Dependencies[0])
require.NoError(t, err)
dep1, err := ci.NewDependencyAccessor(result.Dependencies[1])
require.NoError(t, err)
assert.Equal(t, releaseName, result.Name)
assert.Equal(t, "test-chart", result.Chart)
assert.Equal(t, "1.0.0", result.Version)
assert.Equal(t, dependencies, result.Dependencies)
assert.Equal(t, convertDeps(dependencies), result.Dependencies)
assert.Len(t, result.Dependencies, 2)
assert.Equal(t, "mysql", result.Dependencies[0].Name)
assert.Equal(t, "database", result.Dependencies[0].Alias)
assert.Equal(t, "redis", result.Dependencies[1].Name)
assert.Equal(t, "cache", result.Dependencies[1].Alias)
assert.Equal(t, "mysql", dep0.Name())
assert.Equal(t, "database", dep0.Alias())
assert.Equal(t, "redis", dep1.Name())
assert.Equal(t, "cache", dep1.Alias())
}
func TestGetMetadata_Run_WithMixedDependencies(t *testing.T) {
@ -203,7 +206,7 @@ func TestGetMetadata_Run_WithMixedDependencies(t *testing.T) {
client := NewGetMetadata(cfg)
releaseName := "test-release"
deployedTime := helmtime.Now()
deployedTime := time.Now()
dependencies := []*chart.Dependency{
{
@ -233,7 +236,7 @@ func TestGetMetadata_Run_WithMixedDependencies(t *testing.T) {
rel := &release.Release{
Name: releaseName,
Info: &release.Info{
Status: release.StatusDeployed,
Status: common.StatusDeployed,
LastDeployed: deployedTime,
},
Chart: &chart.Chart{
@ -253,23 +256,32 @@ func TestGetMetadata_Run_WithMixedDependencies(t *testing.T) {
result, err := client.Run(releaseName)
require.NoError(t, err)
dep0, err := ci.NewDependencyAccessor(result.Dependencies[0])
require.NoError(t, err)
dep1, err := ci.NewDependencyAccessor(result.Dependencies[1])
require.NoError(t, err)
dep2, err := ci.NewDependencyAccessor(result.Dependencies[2])
require.NoError(t, err)
dep3, err := ci.NewDependencyAccessor(result.Dependencies[3])
require.NoError(t, err)
assert.Equal(t, releaseName, result.Name)
assert.Equal(t, "test-chart", result.Chart)
assert.Equal(t, "1.0.0", result.Version)
assert.Equal(t, dependencies, result.Dependencies)
assert.Equal(t, convertDeps(dependencies), result.Dependencies)
assert.Len(t, result.Dependencies, 4)
// Verify dependencies with aliases
assert.Equal(t, "mysql", result.Dependencies[0].Name)
assert.Equal(t, "database", result.Dependencies[0].Alias)
assert.Equal(t, "redis", result.Dependencies[2].Name)
assert.Equal(t, "cache", result.Dependencies[2].Alias)
assert.Equal(t, "mysql", dep0.Name())
assert.Equal(t, "database", dep0.Alias())
assert.Equal(t, "redis", dep2.Name())
assert.Equal(t, "cache", dep2.Alias())
// Verify dependencies without aliases
assert.Equal(t, "nginx", result.Dependencies[1].Name)
assert.Equal(t, "", result.Dependencies[1].Alias)
assert.Equal(t, "postgresql", result.Dependencies[3].Name)
assert.Equal(t, "", result.Dependencies[3].Alias)
assert.Equal(t, "nginx", dep1.Name())
assert.Equal(t, "", dep1.Alias())
assert.Equal(t, "postgresql", dep3.Name())
assert.Equal(t, "", dep3.Alias())
}
func TestGetMetadata_Run_WithAnnotations(t *testing.T) {
@ -277,7 +289,7 @@ func TestGetMetadata_Run_WithAnnotations(t *testing.T) {
client := NewGetMetadata(cfg)
releaseName := "test-release"
deployedTime := helmtime.Now()
deployedTime := time.Now()
annotations := map[string]string{
"helm.sh/hook": "pre-install",
@ -288,7 +300,7 @@ func TestGetMetadata_Run_WithAnnotations(t *testing.T) {
rel := &release.Release{
Name: releaseName,
Info: &release.Info{
Status: release.StatusDeployed,
Status: common.StatusDeployed,
LastDeployed: deployedTime,
},
Chart: &chart.Chart{
@ -322,13 +334,13 @@ func TestGetMetadata_Run_SpecificVersion(t *testing.T) {
client.Version = 2
releaseName := "test-release"
deployedTime := helmtime.Now()
deployedTime := time.Now()
rel1 := &release.Release{
Name: releaseName,
Info: &release.Info{
Status: release.StatusSuperseded,
LastDeployed: helmtime.Time{Time: deployedTime.Time.Add(-time.Hour)},
Status: common.StatusSuperseded,
LastDeployed: deployedTime.Add(-time.Hour),
},
Chart: &chart.Chart{
Metadata: &chart.Metadata{
@ -344,7 +356,7 @@ func TestGetMetadata_Run_SpecificVersion(t *testing.T) {
rel2 := &release.Release{
Name: releaseName,
Info: &release.Info{
Status: release.StatusDeployed,
Status: common.StatusDeployed,
LastDeployed: deployedTime,
},
Chart: &chart.Chart{
@ -378,22 +390,22 @@ func TestGetMetadata_Run_DifferentStatuses(t *testing.T) {
testCases := []struct {
name string
status release.Status
status common.Status
expected string
}{
{"deployed", release.StatusDeployed, "deployed"},
{"failed", release.StatusFailed, "failed"},
{"uninstalled", release.StatusUninstalled, "uninstalled"},
{"pending-install", release.StatusPendingInstall, "pending-install"},
{"pending-upgrade", release.StatusPendingUpgrade, "pending-upgrade"},
{"pending-rollback", release.StatusPendingRollback, "pending-rollback"},
{"superseded", release.StatusSuperseded, "superseded"},
{"deployed", common.StatusDeployed, "deployed"},
{"failed", common.StatusFailed, "failed"},
{"uninstalled", common.StatusUninstalled, "uninstalled"},
{"pending-install", common.StatusPendingInstall, "pending-install"},
{"pending-upgrade", common.StatusPendingUpgrade, "pending-upgrade"},
{"pending-rollback", common.StatusPendingRollback, "pending-rollback"},
{"superseded", common.StatusSuperseded, "superseded"},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
releaseName := "test-release-" + tc.name
deployedTime := helmtime.Now()
deployedTime := time.Now()
rel := &release.Release{
Name: releaseName,
@ -424,9 +436,9 @@ func TestGetMetadata_Run_DifferentStatuses(t *testing.T) {
func TestGetMetadata_Run_UnreachableKubeClient(t *testing.T) {
cfg := actionConfigFixture(t)
cfg.KubeClient = &unreachableKubeClient{
PrintingKubeClient: kubefake.PrintingKubeClient{Out: io.Discard},
}
failingKubeClient := kubefake.FailingKubeClient{PrintingKubeClient: kubefake.PrintingKubeClient{Out: io.Discard}, DummyResources: nil}
failingKubeClient.ConnectionError = errors.New("connection refused")
cfg.KubeClient = &failingKubeClient
client := NewGetMetadata(cfg)
@ -449,12 +461,12 @@ func TestGetMetadata_Run_EmptyAppVersion(t *testing.T) {
client := NewGetMetadata(cfg)
releaseName := "test-release"
deployedTime := helmtime.Now()
deployedTime := time.Now()
rel := &release.Release{
Name: releaseName,
Info: &release.Info{
Status: release.StatusDeployed,
Status: common.StatusDeployed,
LastDeployed: deployedTime,
},
Chart: &chart.Chart{
@ -525,8 +537,9 @@ func TestMetadata_FormattedDepNames(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
deps := convertDeps(tc.dependencies)
metadata := &Metadata{
Dependencies: tc.dependencies,
Dependencies: deps,
}
result := metadata.FormattedDepNames()
@ -535,6 +548,14 @@ func TestMetadata_FormattedDepNames(t *testing.T) {
}
}
func convertDeps(deps []*chart.Dependency) []ci.Dependency {
var newDeps = make([]ci.Dependency, len(deps))
for i, c := range deps {
newDeps[i] = c
}
return newDeps
}
func TestMetadata_FormattedDepNames_WithComplexDependencies(t *testing.T) {
dependencies := []*chart.Dependency{
{
@ -556,8 +577,9 @@ func TestMetadata_FormattedDepNames_WithComplexDependencies(t *testing.T) {
},
}
deps := convertDeps(dependencies)
metadata := &Metadata{
Dependencies: dependencies,
Dependencies: deps,
}
result := metadata.FormattedDepNames()
@ -607,8 +629,9 @@ func TestMetadata_FormattedDepNames_WithAliases(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
deps := convertDeps(tc.dependencies)
metadata := &Metadata{
Dependencies: tc.dependencies,
Dependencies: deps,
}
result := metadata.FormattedDepNames()
@ -619,7 +642,7 @@ func TestMetadata_FormattedDepNames_WithAliases(t *testing.T) {
func TestGetMetadata_Labels(t *testing.T) {
rel := releaseStub()
rel.Info.Status = release.StatusDeployed
rel.Info.Status = common.StatusDeployed
customLabels := map[string]string{"key1": "value1", "key2": "value2"}
rel.Labels = customLabels

@ -16,7 +16,13 @@ limitations under the License.
package action
import "helm.sh/helm/v4/pkg/chart/common/util"
import (
"fmt"
"helm.sh/helm/v4/pkg/chart/common/util"
release "helm.sh/helm/v4/pkg/release"
rspb "helm.sh/helm/v4/pkg/release/v1"
)
// GetValues is the action for checking a given release's values.
//
@ -41,7 +47,12 @@ func (g *GetValues) Run(name string) (map[string]interface{}, error) {
return nil, err
}
rel, err := g.cfg.releaseContent(name, g.Version)
reli, err := g.cfg.releaseContent(name, g.Version)
if err != nil {
return nil, err
}
rel, err := releaserToV1Release(reli)
if err != nil {
return nil, err
}
@ -56,3 +67,18 @@ func (g *GetValues) Run(name string) (map[string]interface{}, error) {
}
return rel.Config, nil
}
// releaserToV1Release is a helper function to convert a v1 release passed by interface
// into the type object.
func releaserToV1Release(rel release.Releaser) (*rspb.Release, error) {
switch r := rel.(type) {
case rspb.Release:
return &r, nil
case *rspb.Release:
return r, nil
case nil:
return nil, nil
default:
return nil, fmt.Errorf("unsupported release type: %T", rel)
}
}

@ -17,6 +17,7 @@ limitations under the License.
package action
import (
"errors"
"io"
"testing"
@ -25,6 +26,7 @@ import (
chart "helm.sh/helm/v4/pkg/chart/v2"
kubefake "helm.sh/helm/v4/pkg/kube/fake"
"helm.sh/helm/v4/pkg/release/common"
release "helm.sh/helm/v4/pkg/release/v1"
)
@ -57,7 +59,7 @@ func TestGetValues_Run_UserConfigOnly(t *testing.T) {
rel := &release.Release{
Name: releaseName,
Info: &release.Info{
Status: release.StatusDeployed,
Status: common.StatusDeployed,
},
Chart: &chart.Chart{
Metadata: &chart.Metadata{
@ -111,7 +113,7 @@ func TestGetValues_Run_AllValues(t *testing.T) {
rel := &release.Release{
Name: releaseName,
Info: &release.Info{
Status: release.StatusDeployed,
Status: common.StatusDeployed,
},
Chart: &chart.Chart{
Metadata: &chart.Metadata{
@ -146,7 +148,7 @@ func TestGetValues_Run_EmptyValues(t *testing.T) {
rel := &release.Release{
Name: releaseName,
Info: &release.Info{
Status: release.StatusDeployed,
Status: common.StatusDeployed,
},
Chart: &chart.Chart{
Metadata: &chart.Metadata{
@ -168,9 +170,9 @@ func TestGetValues_Run_EmptyValues(t *testing.T) {
func TestGetValues_Run_UnreachableKubeClient(t *testing.T) {
cfg := actionConfigFixture(t)
cfg.KubeClient = &unreachableKubeClient{
PrintingKubeClient: kubefake.PrintingKubeClient{Out: io.Discard},
}
failingKubeClient := kubefake.FailingKubeClient{PrintingKubeClient: kubefake.PrintingKubeClient{Out: io.Discard}, DummyResources: nil}
failingKubeClient.ConnectionError = errors.New("connection refused")
cfg.KubeClient = &failingKubeClient
client := NewGetValues(cfg)
@ -197,7 +199,7 @@ func TestGetValues_Run_NilConfig(t *testing.T) {
rel := &release.Release{
Name: releaseName,
Info: &release.Info{
Status: release.StatusDeployed,
Status: common.StatusDeployed,
},
Chart: &chart.Chart{
Metadata: &chart.Metadata{

@ -22,7 +22,7 @@ import (
"fmt"
chartutil "helm.sh/helm/v4/pkg/chart/v2/util"
release "helm.sh/helm/v4/pkg/release/v1"
release "helm.sh/helm/v4/pkg/release"
)
// History is the action for checking the release's ledger.
@ -46,7 +46,7 @@ func NewHistory(cfg *Configuration) *History {
}
// Run executes 'helm history' against the given release.
func (h *History) Run(name string) ([]*release.Release, error) {
func (h *History) Run(name string) ([]release.Releaser, error) {
if err := h.cfg.KubeClient.IsReachable(); err != nil {
return nil, err
}

@ -29,7 +29,6 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
release "helm.sh/helm/v4/pkg/release/v1"
helmtime "helm.sh/helm/v4/pkg/time"
)
// execHook executes all of the hooks for the given hook event.
@ -62,7 +61,7 @@ func (cfg *Configuration) execHook(rl *release.Release, hook release.HookEvent,
// Record the time at which the hook was applied to the cluster
h.LastRun = release.HookExecution{
StartedAt: helmtime.Now(),
StartedAt: time.Now(),
Phase: release.HookPhaseRunning,
}
cfg.recordRelease(rl)
@ -76,7 +75,7 @@ func (cfg *Configuration) execHook(rl *release.Release, hook release.HookEvent,
if _, err := cfg.KubeClient.Create(
resources,
kube.ClientCreateOptionServerSideApply(serverSideApply, false)); err != nil {
h.LastRun.CompletedAt = helmtime.Now()
h.LastRun.CompletedAt = time.Now()
h.LastRun.Phase = release.HookPhaseFailed
return fmt.Errorf("warning: Hook %s %s failed: %w", hook, h.Path, err)
}
@ -88,7 +87,7 @@ func (cfg *Configuration) execHook(rl *release.Release, hook release.HookEvent,
// Watch hook resources until they have completed
err = waiter.WatchUntilReady(resources, timeout)
// Note the time of success/failure
h.LastRun.CompletedAt = helmtime.Now()
h.LastRun.CompletedAt = time.Now()
// Mark hook as succeeded or failed
if err != nil {
h.LastRun.Phase = release.HookPhaseFailed
@ -155,7 +154,7 @@ func (cfg *Configuration) deleteHookByPolicy(h *release.Hook, policy release.Hoo
if err != nil {
return fmt.Errorf("unable to build kubernetes object for deleting hook %s: %w", h.Path, err)
}
_, errs := cfg.KubeClient.Delete(resources)
_, errs := cfg.KubeClient.Delete(resources, metav1.DeletePropagationBackground)
if len(errs) > 0 {
return joinErrors(errs, "; ")
}
@ -224,16 +223,12 @@ func (cfg *Configuration) outputLogsByPolicy(h *release.Hook, releaseNamespace s
}
func (cfg *Configuration) outputContainerLogsForListOptions(namespace string, listOptions metav1.ListOptions) error {
// TODO Helm 4: Remove this check when GetPodList and OutputContainerLogsForPodList are moved from InterfaceLogs to Interface
if kubeClient, ok := cfg.KubeClient.(kube.InterfaceLogs); ok {
podList, err := kubeClient.GetPodList(namespace, listOptions)
if err != nil {
return err
}
err = kubeClient.OutputContainerLogsForPodList(podList, namespace, cfg.HookOutputFunc)
podList, err := cfg.KubeClient.GetPodList(namespace, listOptions)
if err != nil {
return err
}
return nil
return cfg.KubeClient.OutputContainerLogsForPodList(podList, namespace, cfg.HookOutputFunc)
}
func (cfg *Configuration) deriveNamespace(h *release.Hook, namespace string) (string, error) {

@ -21,17 +21,20 @@ import (
"fmt"
"io"
"reflect"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/yaml"
"k8s.io/cli-runtime/pkg/resource"
"helm.sh/helm/v4/pkg/chart/common"
"helm.sh/helm/v4/pkg/kube"
kubefake "helm.sh/helm/v4/pkg/kube/fake"
rcommon "helm.sh/helm/v4/pkg/release/common"
release "helm.sh/helm/v4/pkg/release/v1"
"helm.sh/helm/v4/pkg/storage"
"helm.sh/helm/v4/pkg/storage/driver"
@ -112,15 +115,15 @@ spec:
}
func convertHooksToCommaSeparated(hookDefinitions []release.HookOutputLogPolicy) string {
var commaSeparated string
var commaSeparated strings.Builder
for i, policy := range hookDefinitions {
if i+1 == len(hookDefinitions) {
commaSeparated += policy.String()
commaSeparated.WriteString(policy.String())
} else {
commaSeparated += policy.String() + ","
commaSeparated.WriteString(policy.String() + ",")
}
}
return commaSeparated
return commaSeparated.String()
}
func TestInstallRelease_HookOutputLogsOnFailure(t *testing.T) {
@ -183,10 +186,12 @@ func runInstallForHooksWithSuccess(t *testing.T, manifest, expectedNamespace str
}
vals := map[string]interface{}{}
res, err := instAction.Run(buildChartWithTemplates(templates), vals)
resi, err := instAction.Run(buildChartWithTemplates(templates), vals)
is.NoError(err)
res, err := releaserToV1Release(resi)
is.NoError(err)
is.Equal(expectedOutput, outBuffer.String())
is.Equal(release.StatusDeployed, res.Info.Status)
is.Equal(rcommon.StatusDeployed, res.Info.Status)
}
func runInstallForHooksWithFailure(t *testing.T, manifest, expectedNamespace string, shouldOutput bool) {
@ -210,11 +215,13 @@ func runInstallForHooksWithFailure(t *testing.T, manifest, expectedNamespace str
}
vals := map[string]interface{}{}
res, err := instAction.Run(buildChartWithTemplates(templates), vals)
resi, err := instAction.Run(buildChartWithTemplates(templates), vals)
is.Error(err)
res, err := releaserToV1Release(resi)
is.NoError(err)
is.Contains(res.Info.Description, "failed pre-install")
is.Equal(expectedOutput, outBuffer.String())
is.Equal(release.StatusFailed, res.Info.Status)
is.Equal(rcommon.StatusFailed, res.Info.Status)
}
type HookFailedError struct{}
@ -258,7 +265,7 @@ func (h *HookFailingKubeWaiter) WatchUntilReady(resources kube.ResourceList, _ t
return nil
}
func (h *HookFailingKubeClient) Delete(resources kube.ResourceList) (*kube.Result, []error) {
func (h *HookFailingKubeClient) Delete(resources kube.ResourceList, deletionPropagation metav1.DeletionPropagation) (*kube.Result, []error) {
for _, res := range resources {
h.deleteRecord = append(h.deleteRecord, resource.Info{
Name: res.Name,
@ -266,7 +273,7 @@ func (h *HookFailingKubeClient) Delete(resources kube.ResourceList) (*kube.Resul
})
}
return h.PrintingKubeClient.Delete(resources)
return h.PrintingKubeClient.Delete(resources, deletionPropagation)
}
func (h *HookFailingKubeClient) GetWaiter(strategy kube.WaitStrategy) (kube.Waiter, error) {

@ -26,7 +26,6 @@ import (
"log/slog"
"net/url"
"os"
"path"
"path/filepath"
"strings"
"sync"
@ -42,6 +41,7 @@ import (
"k8s.io/cli-runtime/pkg/resource"
"sigs.k8s.io/yaml"
ci "helm.sh/helm/v4/pkg/chart"
"helm.sh/helm/v4/pkg/chart/common"
"helm.sh/helm/v4/pkg/chart/common/util"
chart "helm.sh/helm/v4/pkg/chart/v2"
@ -53,6 +53,8 @@ import (
kubefake "helm.sh/helm/v4/pkg/kube/fake"
"helm.sh/helm/v4/pkg/postrenderer"
"helm.sh/helm/v4/pkg/registry"
ri "helm.sh/helm/v4/pkg/release"
rcommon "helm.sh/helm/v4/pkg/release/common"
release "helm.sh/helm/v4/pkg/release/v1"
releaseutil "helm.sh/helm/v4/pkg/release/v1/util"
"helm.sh/helm/v4/pkg/repo/v1"
@ -74,7 +76,6 @@ type Install struct {
ChartPathOptions
ClientOnly bool
// ForceReplace will, if set to `true`, ignore certain warnings and perform the install anyway.
//
// This should be used with caution.
@ -86,8 +87,8 @@ type Install struct {
// see: https://kubernetes.io/docs/reference/using-api/server-side-apply/
ServerSideApply bool
CreateNamespace bool
DryRun bool
DryRunOption string
// DryRunStrategy can be set to prepare, but not execute the operation and whether or not to interact with the remote cluster
DryRunStrategy DryRunStrategy
// HideSecret can be set to true when DryRun is enabled in order to hide
// Kubernetes Secrets in the output. It cannot be used outside of DryRun.
HideSecret bool
@ -158,6 +159,7 @@ func NewInstall(cfg *Configuration) *Install {
in := &Install{
cfg: cfg,
ServerSideApply: true,
DryRunStrategy: DryRunNone,
}
in.registryClient = cfg.RegistryClient
@ -243,7 +245,7 @@ func (i *Install) installCRDs(crds []chart.CRD) error {
//
// If DryRun is set to true, this will prepare the release, but not install it
func (i *Install) Run(chrt *chart.Chart, vals map[string]interface{}) (*release.Release, error) {
func (i *Install) Run(chrt ci.Charter, vals map[string]interface{}) (ri.Releaser, error) {
ctx := context.Background()
return i.RunWithContext(ctx, chrt, vals)
}
@ -252,9 +254,18 @@ func (i *Install) Run(chrt *chart.Chart, vals map[string]interface{}) (*release.
//
// When the task is cancelled through ctx, the function returns and the install
// proceeds in the background.
func (i *Install) RunWithContext(ctx context.Context, chrt *chart.Chart, vals map[string]interface{}) (*release.Release, error) {
// Check reachability of cluster unless in client-only mode (e.g. `helm template` without `--validate`)
if !i.ClientOnly {
func (i *Install) RunWithContext(ctx context.Context, ch ci.Charter, vals map[string]interface{}) (ri.Releaser, error) {
var chrt *chart.Chart
switch c := ch.(type) {
case *chart.Chart:
chrt = c
case chart.Chart:
chrt = &c
default:
return nil, errors.New("invalid chart apiVersion")
}
if interactWithServer(i.DryRunStrategy) {
if err := i.cfg.KubeClient.IsReachable(); err != nil {
slog.Error(fmt.Sprintf("cluster reachability check failed: %v", err))
return nil, fmt.Errorf("cluster reachability check failed: %w", err)
@ -262,7 +273,7 @@ func (i *Install) RunWithContext(ctx context.Context, chrt *chart.Chart, vals ma
}
// HideSecret must be used with dry run. Otherwise, return an error.
if !i.isDryRun() && i.HideSecret {
if !isDryRun(i.DryRunStrategy) && i.HideSecret {
slog.Error("hiding Kubernetes secrets requires a dry-run mode")
return nil, errors.New("hiding Kubernetes secrets requires a dry-run mode")
}
@ -277,23 +288,18 @@ func (i *Install) RunWithContext(ctx context.Context, chrt *chart.Chart, vals ma
return nil, fmt.Errorf("chart dependencies processing failed: %w", err)
}
var interactWithRemote bool
if !i.isDryRun() || i.DryRunOption == "server" || i.DryRunOption == "none" || i.DryRunOption == "false" {
interactWithRemote = true
}
// Pre-install anything in the crd/ directory. We do this before Helm
// contacts the upstream server and builds the capabilities object.
if crds := chrt.CRDObjects(); !i.ClientOnly && !i.SkipCRDs && len(crds) > 0 {
if crds := chrt.CRDObjects(); interactWithServer(i.DryRunStrategy) && !i.SkipCRDs && len(crds) > 0 {
// On dry run, bail here
if i.isDryRun() {
if isDryRun(i.DryRunStrategy) {
slog.Warn("This chart or one of its subcharts contains CRDs. Rendering may fail or contain inaccuracies.")
} else if err := i.installCRDs(crds); err != nil {
return nil, err
}
}
if i.ClientOnly {
if !interactWithServer(i.DryRunStrategy) {
// Add mock objects in here so it doesn't use Kube API server
// NOTE(bacongobbler): used for `helm template`
i.cfg.Capabilities = common.DefaultCapabilities.Copy()
@ -306,7 +312,7 @@ func (i *Install) RunWithContext(ctx context.Context, chrt *chart.Chart, vals ma
mem := driver.NewMemory()
mem.SetNamespace(i.Namespace)
i.cfg.Releases = storage.Init(mem)
} else if !i.ClientOnly && len(i.APIVersions) > 0 {
} else if interactWithServer(i.DryRunStrategy) && len(i.APIVersions) > 0 {
slog.Debug("API Version list given outside of client only mode, this list will be ignored")
}
@ -322,7 +328,7 @@ func (i *Install) RunWithContext(ctx context.Context, chrt *chart.Chart, vals ma
}
// special case for helm template --is-upgrade
isUpgrade := i.IsUpgrade && i.isDryRun()
isUpgrade := i.IsUpgrade && isDryRun(i.DryRunStrategy)
options := common.ReleaseOptions{
Name: i.ReleaseName,
Namespace: i.Namespace,
@ -342,20 +348,20 @@ func (i *Install) RunWithContext(ctx context.Context, chrt *chart.Chart, vals ma
rel := i.createRelease(chrt, vals, i.Labels)
var manifestDoc *bytes.Buffer
rel.Hooks, manifestDoc, rel.Info.Notes, err = i.cfg.renderResources(chrt, valuesToRender, i.ReleaseName, i.OutputDir, i.SubNotes, i.UseReleaseName, i.IncludeCRDs, i.PostRenderer, interactWithRemote, i.EnableDNS, i.HideSecret)
rel.Hooks, manifestDoc, rel.Info.Notes, err = i.cfg.renderResources(chrt, valuesToRender, i.ReleaseName, i.OutputDir, i.SubNotes, i.UseReleaseName, i.IncludeCRDs, i.PostRenderer, interactWithServer(i.DryRunStrategy), i.EnableDNS, i.HideSecret)
// Even for errors, attach this if available
if manifestDoc != nil {
rel.Manifest = manifestDoc.String()
}
// Check error from render
if err != nil {
rel.SetStatus(release.StatusFailed, fmt.Sprintf("failed to render resource: %s", err.Error()))
rel.SetStatus(rcommon.StatusFailed, fmt.Sprintf("failed to render resource: %s", err.Error()))
// Return a release with partial data so that the client can show debugging information.
return rel, err
}
// Mark this release as in-progress
rel.SetStatus(release.StatusPendingInstall, "Initial install underway")
rel.SetStatus(rcommon.StatusPendingInstall, "Initial install underway")
var toBeAdopted kube.ResourceList
resources, err := i.cfg.KubeClient.Build(bytes.NewBufferString(rel.Manifest), !i.DisableOpenAPIValidation)
@ -375,7 +381,7 @@ func (i *Install) RunWithContext(ctx context.Context, chrt *chart.Chart, vals ma
// we'll end up in a state where we will delete those resources upon
// deleting the release because the manifest will be pointing at that
// resource
if !i.ClientOnly && !isUpgrade && len(resources) > 0 {
if interactWithServer(i.DryRunStrategy) && !isUpgrade && len(resources) > 0 {
if i.TakeOwnership {
toBeAdopted, err = requireAdoption(resources)
} else {
@ -387,7 +393,7 @@ func (i *Install) RunWithContext(ctx context.Context, chrt *chart.Chart, vals ma
}
// Bail out here if it is a dry run
if i.isDryRun() {
if isDryRun(i.DryRunStrategy) {
rel.Info.Description = "Dry run complete"
return rel, nil
}
@ -469,14 +475,6 @@ func (i *Install) getGoroutineCount() int32 {
return i.goroutineCount.Load()
}
// isDryRun returns true if Upgrade is set to run as a DryRun
func (i *Install) isDryRun() bool {
if i.DryRun || i.DryRunOption == "client" || i.DryRunOption == "server" || i.DryRunOption == "true" {
return true
}
return false
}
func (i *Install) performInstall(rel *release.Release, toBeAdopted kube.ResourceList, resources kube.ResourceList) (*release.Release, error) {
var err error
// pre-install hooks
@ -528,9 +526,9 @@ func (i *Install) performInstall(rel *release.Release, toBeAdopted kube.Resource
}
if len(i.Description) > 0 {
rel.SetStatus(release.StatusDeployed, i.Description)
rel.SetStatus(rcommon.StatusDeployed, i.Description)
} else {
rel.SetStatus(release.StatusDeployed, "Install complete")
rel.SetStatus(rcommon.StatusDeployed, "Install complete")
}
// This is a tricky case. The release has been created, but the result
@ -548,7 +546,7 @@ func (i *Install) performInstall(rel *release.Release, toBeAdopted kube.Resource
}
func (i *Install) failRelease(rel *release.Release, err error) (*release.Release, error) {
rel.SetStatus(release.StatusFailed, fmt.Sprintf("Release %q failed: %s", i.ReleaseName, err.Error()))
rel.SetStatus(rcommon.StatusFailed, fmt.Sprintf("Release %q failed: %s", i.ReleaseName, err.Error()))
if i.RollbackOnFailure {
slog.Debug("install failed and rollback-on-failure is set, uninstalling release", "release", i.ReleaseName)
uninstall := NewUninstall(i.cfg)
@ -579,7 +577,7 @@ func (i *Install) availableName() error {
return fmt.Errorf("release name %q: %w", start, err)
}
// On dry run, bail here
if i.isDryRun() {
if isDryRun(i.DryRunStrategy) {
return nil
}
@ -587,15 +585,43 @@ func (i *Install) availableName() error {
if err != nil || len(h) < 1 {
return nil
}
releaseutil.Reverse(h, releaseutil.SortByRevision)
rel := h[0]
if st := rel.Info.Status; i.Replace && (st == release.StatusUninstalled || st == release.StatusFailed) {
hl, err := releaseListToV1List(h)
if err != nil {
return err
}
releaseutil.Reverse(hl, releaseutil.SortByRevision)
rel := hl[0]
if st := rel.Info.Status; i.Replace && (st == rcommon.StatusUninstalled || st == rcommon.StatusFailed) {
return nil
}
return errors.New("cannot reuse a name that is still in use")
}
func releaseListToV1List(ls []ri.Releaser) ([]*release.Release, error) {
rls := make([]*release.Release, 0, len(ls))
for _, val := range ls {
rel, err := releaserToV1Release(val)
if err != nil {
return nil, err
}
rls = append(rls, rel)
}
return rls, nil
}
func releaseV1ListToReleaserList(ls []*release.Release) ([]ri.Releaser, error) {
rls := make([]ri.Releaser, 0, len(ls))
for _, val := range ls {
rls = append(rls, val)
}
return rls, nil
}
// createRelease creates a new release object
func (i *Install) createRelease(chrt *chart.Chart, rawVals map[string]interface{}, labels map[string]string) *release.Release {
ts := i.cfg.Now()
@ -608,7 +634,7 @@ func (i *Install) createRelease(chrt *chart.Chart, rawVals map[string]interface{
Info: &release.Info{
FirstDeployed: ts,
LastDeployed: ts,
Status: release.StatusUnknown,
Status: rcommon.StatusUnknown,
},
Version: 1,
Labels: labels,
@ -634,20 +660,24 @@ func (i *Install) replaceRelease(rel *release.Release) error {
// No releases exist for this name, so we can return early
return nil
}
hl, err := releaseListToV1List(hist)
if err != nil {
return err
}
releaseutil.Reverse(hist, releaseutil.SortByRevision)
last := hist[0]
releaseutil.Reverse(hl, releaseutil.SortByRevision)
last := hl[0]
// Update version to the next available
rel.Version = last.Version + 1
// Do not change the status of a failed release.
if last.Info.Status == release.StatusFailed {
if last.Info.Status == rcommon.StatusFailed {
return nil
}
// For any other status, mark it as superseded and store the old record
last.SetStatus(release.StatusSuperseded, "superseded by new release")
last.SetStatus(rcommon.StatusSuperseded, "superseded by new release")
return i.recordRelease(last)
}
@ -686,7 +716,7 @@ func createOrOpenFile(filename string, appendData bool) (*os.File, error) {
// check if the directory exists to create file. creates if doesn't exist
func ensureDirectoryForFile(file string) error {
baseDir := path.Dir(file)
baseDir := filepath.Dir(file)
_, err := os.Stat(baseDir)
if err != nil && !errors.Is(err, fs.ErrNotExist) {
return err
@ -761,17 +791,30 @@ func TemplateName(nameTemplate string) (string, error) {
}
// CheckDependencies checks the dependencies for a chart.
func CheckDependencies(ch *chart.Chart, reqs []*chart.Dependency) error {
func CheckDependencies(ch ci.Charter, reqs []ci.Dependency) error {
ac, err := ci.NewAccessor(ch)
if err != nil {
return err
}
var missing []string
OUTER:
for _, r := range reqs {
for _, d := range ch.Dependencies() {
if d.Name() == r.Name {
rac, err := ci.NewDependencyAccessor(r)
if err != nil {
return err
}
for _, d := range ac.Dependencies() {
dac, err := ci.NewAccessor(d)
if err != nil {
return err
}
if dac.Name() == rac.Name() {
continue OUTER
}
}
missing = append(missing, r.Name)
missing = append(missing, rac.Name())
}
if len(missing) > 0 {
@ -804,7 +847,7 @@ func urlEqual(u1, u2 *url.URL) bool {
// This does not ensure that the chart is well-formed; only that the requested filename exists.
//
// Order of resolution:
// - relative to current working directory
// - relative to current working directory when --repo flag is not presented
// - if path is absolute or begins with '.', error out here
// - URL
//
@ -817,20 +860,22 @@ func (c *ChartPathOptions) LocateChart(name string, settings *cli.EnvSettings) (
name = strings.TrimSpace(name)
version := strings.TrimSpace(c.Version)
if _, err := os.Stat(name); err == nil {
abs, err := filepath.Abs(name)
if err != nil {
return abs, err
}
if c.Verify {
if _, err := downloader.VerifyChart(abs, abs+".prov", c.Keyring); err != nil {
return "", err
if c.RepoURL == "" {
if _, err := os.Stat(name); err == nil {
abs, err := filepath.Abs(name)
if err != nil {
return abs, err
}
if c.Verify {
if _, err := downloader.VerifyChart(abs, abs+".prov", c.Keyring); err != nil {
return "", err
}
}
return abs, nil
}
if filepath.IsAbs(name) || strings.HasPrefix(name, ".") {
return name, fmt.Errorf("path %q not found", name)
}
return abs, nil
}
if filepath.IsAbs(name) || strings.HasPrefix(name, ".") {
return name, fmt.Errorf("path %q not found", name)
}
dl := downloader.ChartDownloader{

@ -47,9 +47,9 @@ import (
"helm.sh/helm/v4/pkg/chart/common"
"helm.sh/helm/v4/pkg/kube"
kubefake "helm.sh/helm/v4/pkg/kube/fake"
rcommon "helm.sh/helm/v4/pkg/release/common"
release "helm.sh/helm/v4/pkg/release/v1"
"helm.sh/helm/v4/pkg/storage/driver"
helmtime "helm.sh/helm/v4/pkg/time"
)
type nameTemplateTestCase struct {
@ -131,14 +131,19 @@ func TestInstallRelease(t *testing.T) {
instAction := installAction(t)
vals := map[string]interface{}{}
ctx, done := context.WithCancel(t.Context())
res, err := instAction.RunWithContext(ctx, buildChart(), vals)
resi, err := instAction.RunWithContext(ctx, buildChart(), vals)
if err != nil {
t.Fatalf("Failed install: %s", err)
}
res, err := releaserToV1Release(resi)
is.NoError(err)
is.Equal(res.Name, "test-install-release", "Expected release name.")
is.Equal(res.Namespace, "spaced")
rel, err := instAction.cfg.Releases.Get(res.Name, res.Version)
r, err := instAction.cfg.Releases.Get(res.Name, res.Version)
is.NoError(err)
rel, err := releaserToV1Release(r)
is.NoError(err)
is.Len(rel.Hooks, 1)
@ -157,7 +162,9 @@ func TestInstallRelease(t *testing.T) {
time.Sleep(time.Millisecond * 100)
lastRelease, err := instAction.cfg.Releases.Last(rel.Name)
req.NoError(err)
is.Equal(lastRelease.Info.Status, release.StatusDeployed)
lrel, err := releaserToV1Release(lastRelease)
is.NoError(err)
is.Equal(lrel.Info.Status, rcommon.StatusDeployed)
}
func TestInstallReleaseWithTakeOwnership_ResourceNotOwned(t *testing.T) {
@ -176,12 +183,17 @@ func TestInstallReleaseWithTakeOwnership_ResourceNotOwned(t *testing.T) {
config := actionConfigFixtureWithDummyResources(t, createDummyResourceList(false))
instAction := installActionWithConfig(config)
instAction.TakeOwnership = true
res, err := instAction.Run(buildChart(), nil)
resi, err := instAction.Run(buildChart(), nil)
if err != nil {
t.Fatalf("Failed install: %s", err)
}
res, err := releaserToV1Release(resi)
is.NoError(err)
r, err := instAction.cfg.Releases.Get(res.Name, res.Version)
is.NoError(err)
rel, err := instAction.cfg.Releases.Get(res.Name, res.Version)
rel, err := releaserToV1Release(r)
is.NoError(err)
is.Equal(rel.Info.Description, "Install complete")
@ -194,11 +206,16 @@ func TestInstallReleaseWithTakeOwnership_ResourceOwned(t *testing.T) {
config := actionConfigFixtureWithDummyResources(t, createDummyResourceList(true))
instAction := installActionWithConfig(config)
instAction.TakeOwnership = false
res, err := instAction.Run(buildChart(), nil)
resi, err := instAction.Run(buildChart(), nil)
if err != nil {
t.Fatalf("Failed install: %s", err)
}
rel, err := instAction.cfg.Releases.Get(res.Name, res.Version)
res, err := releaserToV1Release(resi)
is.NoError(err)
r, err := instAction.cfg.Releases.Get(res.Name, res.Version)
is.NoError(err)
rel, err := releaserToV1Release(r)
is.NoError(err)
is.Equal(rel.Info.Description, "Install complete")
@ -228,14 +245,19 @@ func TestInstallReleaseWithValues(t *testing.T) {
"simpleKey": "simpleValue",
},
}
res, err := instAction.Run(buildChart(withSampleValues()), userVals)
resi, err := instAction.Run(buildChart(withSampleValues()), userVals)
if err != nil {
t.Fatalf("Failed install: %s", err)
}
res, err := releaserToV1Release(resi)
is.NoError(err)
is.Equal(res.Name, "test-install-release", "Expected release name.")
is.Equal(res.Namespace, "spaced")
rel, err := instAction.cfg.Releases.Get(res.Name, res.Version)
r, err := instAction.cfg.Releases.Get(res.Name, res.Version)
is.NoError(err)
rel, err := releaserToV1Release(r)
is.NoError(err)
is.Len(rel.Hooks, 1)
@ -250,16 +272,6 @@ func TestInstallReleaseWithValues(t *testing.T) {
is.Equal(expectedUserValues, rel.Config)
}
func TestInstallReleaseClientOnly(t *testing.T) {
is := assert.New(t)
instAction := installAction(t)
instAction.ClientOnly = true
instAction.Run(buildChart(), nil) // disregard output
is.Equal(instAction.cfg.Capabilities, common.DefaultCapabilities)
is.Equal(instAction.cfg.KubeClient, &kubefake.PrintingKubeClient{Out: io.Discard})
}
func TestInstallRelease_NoName(t *testing.T) {
instAction := installAction(t)
instAction.ReleaseName = ""
@ -276,15 +288,19 @@ func TestInstallRelease_WithNotes(t *testing.T) {
instAction := installAction(t)
instAction.ReleaseName = "with-notes"
vals := map[string]interface{}{}
res, err := instAction.Run(buildChart(withNotes("note here")), vals)
resi, err := instAction.Run(buildChart(withNotes("note here")), vals)
if err != nil {
t.Fatalf("Failed install: %s", err)
}
res, err := releaserToV1Release(resi)
is.NoError(err)
is.Equal(res.Name, "with-notes")
is.Equal(res.Namespace, "spaced")
rel, err := instAction.cfg.Releases.Get(res.Name, res.Version)
r, err := instAction.cfg.Releases.Get(res.Name, res.Version)
is.NoError(err)
rel, err := releaserToV1Release(r)
is.NoError(err)
is.Len(rel.Hooks, 1)
is.Equal(rel.Hooks[0].Manifest, manifestWithHook)
@ -303,12 +319,16 @@ func TestInstallRelease_WithNotesRendered(t *testing.T) {
instAction := installAction(t)
instAction.ReleaseName = "with-notes"
vals := map[string]interface{}{}
res, err := instAction.Run(buildChart(withNotes("got-{{.Release.Name}}")), vals)
resi, err := instAction.Run(buildChart(withNotes("got-{{.Release.Name}}")), vals)
if err != nil {
t.Fatalf("Failed install: %s", err)
}
res, err := releaserToV1Release(resi)
is.NoError(err)
rel, err := instAction.cfg.Releases.Get(res.Name, res.Version)
r, err := instAction.cfg.Releases.Get(res.Name, res.Version)
is.NoError(err)
rel, err := releaserToV1Release(r)
is.NoError(err)
expectedNotes := fmt.Sprintf("got-%s", res.Name)
@ -322,12 +342,16 @@ func TestInstallRelease_WithChartAndDependencyParentNotes(t *testing.T) {
instAction := installAction(t)
instAction.ReleaseName = "with-notes"
vals := map[string]interface{}{}
res, err := instAction.Run(buildChart(withNotes("parent"), withDependency(withNotes("child"))), vals)
resi, err := instAction.Run(buildChart(withNotes("parent"), withDependency(withNotes("child"))), vals)
if err != nil {
t.Fatalf("Failed install: %s", err)
}
res, err := releaserToV1Release(resi)
is.NoError(err)
rel, err := instAction.cfg.Releases.Get(res.Name, res.Version)
r, err := instAction.cfg.Releases.Get(res.Name, res.Version)
is.NoError(err)
rel, err := releaserToV1Release(r)
is.NoError(err)
is.Equal("with-notes", rel.Name)
is.Equal("parent", rel.Info.Notes)
@ -341,12 +365,16 @@ func TestInstallRelease_WithChartAndDependencyAllNotes(t *testing.T) {
instAction.ReleaseName = "with-notes"
instAction.SubNotes = true
vals := map[string]interface{}{}
res, err := instAction.Run(buildChart(withNotes("parent"), withDependency(withNotes("child"))), vals)
resi, err := instAction.Run(buildChart(withNotes("parent"), withDependency(withNotes("child"))), vals)
if err != nil {
t.Fatalf("Failed install: %s", err)
}
res, err := releaserToV1Release(resi)
is.NoError(err)
rel, err := instAction.cfg.Releases.Get(res.Name, res.Version)
r, err := instAction.cfg.Releases.Get(res.Name, res.Version)
is.NoError(err)
rel, err := releaserToV1Release(r)
is.NoError(err)
is.Equal("with-notes", rel.Name)
// test run can return as either 'parent\nchild' or 'child\nparent'
@ -356,27 +384,32 @@ func TestInstallRelease_WithChartAndDependencyAllNotes(t *testing.T) {
is.Equal(rel.Info.Description, "Install complete")
}
func TestInstallRelease_DryRun(t *testing.T) {
is := assert.New(t)
instAction := installAction(t)
instAction.DryRun = true
vals := map[string]interface{}{}
res, err := instAction.Run(buildChart(withSampleTemplates()), vals)
if err != nil {
t.Fatalf("Failed install: %s", err)
}
func TestInstallRelease_DryRunClient(t *testing.T) {
for _, dryRunStrategy := range []DryRunStrategy{DryRunClient, DryRunServer} {
is := assert.New(t)
instAction := installAction(t)
instAction.DryRunStrategy = dryRunStrategy
is.Contains(res.Manifest, "---\n# Source: hello/templates/hello\nhello: world")
is.Contains(res.Manifest, "---\n# Source: hello/templates/goodbye\ngoodbye: world")
is.Contains(res.Manifest, "hello: Earth")
is.NotContains(res.Manifest, "hello: {{ template \"_planet\" . }}")
is.NotContains(res.Manifest, "empty")
vals := map[string]interface{}{}
resi, err := instAction.Run(buildChart(withSampleTemplates()), vals)
if err != nil {
t.Fatalf("Failed install: %s", err)
}
res, err := releaserToV1Release(resi)
is.NoError(err)
_, err = instAction.cfg.Releases.Get(res.Name, res.Version)
is.Error(err)
is.Len(res.Hooks, 1)
is.True(res.Hooks[0].LastRun.CompletedAt.IsZero(), "expect hook to not be marked as run")
is.Equal(res.Info.Description, "Dry run complete")
is.Contains(res.Manifest, "---\n# Source: hello/templates/hello\nhello: world")
is.Contains(res.Manifest, "---\n# Source: hello/templates/goodbye\ngoodbye: world")
is.Contains(res.Manifest, "hello: Earth")
is.NotContains(res.Manifest, "hello: {{ template \"_planet\" . }}")
is.NotContains(res.Manifest, "empty")
_, err = instAction.cfg.Releases.Get(res.Name, res.Version)
is.Error(err)
is.Len(res.Hooks, 1)
is.True(res.Hooks[0].LastRun.CompletedAt.IsZero(), "expect hook to not be marked as run")
is.Equal(res.Info.Description, "Dry run complete")
}
}
func TestInstallRelease_DryRunHiddenSecret(t *testing.T) {
@ -384,12 +417,14 @@ func TestInstallRelease_DryRunHiddenSecret(t *testing.T) {
instAction := installAction(t)
// First perform a normal dry-run with the secret and confirm its presence.
instAction.DryRun = true
instAction.DryRunStrategy = DryRunClient
vals := map[string]interface{}{}
res, err := instAction.Run(buildChart(withSampleSecret(), withSampleTemplates()), vals)
resi, err := instAction.Run(buildChart(withSampleSecret(), withSampleTemplates()), vals)
if err != nil {
t.Fatalf("Failed install: %s", err)
}
res, err := releaserToV1Release(resi)
is.NoError(err)
is.Contains(res.Manifest, "---\n# Source: hello/templates/secret.yaml\napiVersion: v1\nkind: Secret")
_, err = instAction.cfg.Releases.Get(res.Name, res.Version)
@ -399,10 +434,12 @@ func TestInstallRelease_DryRunHiddenSecret(t *testing.T) {
// Perform a dry-run where the secret should not be present
instAction.HideSecret = true
vals = map[string]interface{}{}
res2, err := instAction.Run(buildChart(withSampleSecret(), withSampleTemplates()), vals)
res2i, err := instAction.Run(buildChart(withSampleSecret(), withSampleTemplates()), vals)
if err != nil {
t.Fatalf("Failed install: %s", err)
}
res2, err := releaserToV1Release(res2i)
is.NoError(err)
is.NotContains(res2.Manifest, "---\n# Source: hello/templates/secret.yaml\napiVersion: v1\nkind: Secret")
@ -411,7 +448,7 @@ func TestInstallRelease_DryRunHiddenSecret(t *testing.T) {
is.Equal(res2.Info.Description, "Dry run complete")
// Ensure there is an error when HideSecret True but not in a dry-run mode
instAction.DryRun = false
instAction.DryRunStrategy = DryRunNone
vals = map[string]interface{}{}
_, err = instAction.Run(buildChart(withSampleSecret(), withSampleTemplates()), vals)
if err == nil {
@ -423,7 +460,7 @@ func TestInstallRelease_DryRunHiddenSecret(t *testing.T) {
func TestInstallRelease_DryRun_Lookup(t *testing.T) {
is := assert.New(t)
instAction := installAction(t)
instAction.DryRun = true
instAction.DryRunStrategy = DryRunNone
vals := map[string]interface{}{}
mockChart := buildChart(withSampleTemplates())
@ -432,10 +469,12 @@ func TestInstallRelease_DryRun_Lookup(t *testing.T) {
Data: []byte(`goodbye: {{ lookup "v1" "Namespace" "" "___" }}`),
})
res, err := instAction.Run(mockChart, vals)
resi, err := instAction.Run(mockChart, vals)
if err != nil {
t.Fatalf("Failed install: %s", err)
}
res, err := releaserToV1Release(resi)
is.NoError(err)
is.Contains(res.Manifest, "goodbye: map[]")
}
@ -443,7 +482,7 @@ func TestInstallRelease_DryRun_Lookup(t *testing.T) {
func TestInstallReleaseIncorrectTemplate_DryRun(t *testing.T) {
is := assert.New(t)
instAction := installAction(t)
instAction.DryRun = true
instAction.DryRunStrategy = DryRunNone
vals := map[string]interface{}{}
_, err := instAction.Run(buildChart(withSampleIncludingIncorrectTemplates()), vals)
expectedErr := `hello/templates/incorrect:1:10
@ -463,10 +502,12 @@ func TestInstallRelease_NoHooks(t *testing.T) {
instAction.cfg.Releases.Create(releaseStub())
vals := map[string]interface{}{}
res, err := instAction.Run(buildChart(), vals)
resi, err := instAction.Run(buildChart(), vals)
if err != nil {
t.Fatalf("Failed install: %s", err)
}
res, err := releaserToV1Release(resi)
is.NoError(err)
is.True(res.Hooks[0].LastRun.CompletedAt.IsZero(), "hooks should not run with no-hooks")
}
@ -482,11 +523,13 @@ func TestInstallRelease_FailedHooks(t *testing.T) {
failer.PrintingKubeClient = kubefake.PrintingKubeClient{Out: io.Discard, LogOutput: outBuffer}
vals := map[string]interface{}{}
res, err := instAction.Run(buildChart(), vals)
resi, err := instAction.Run(buildChart(), vals)
is.Error(err)
res, err := releaserToV1Release(resi)
is.NoError(err)
is.Contains(res.Info.Description, "failed post-install")
is.Equal("", outBuffer.String())
is.Equal(release.StatusFailed, res.Info.Status)
is.Equal(rcommon.StatusFailed, res.Info.Status)
}
func TestInstallRelease_ReplaceRelease(t *testing.T) {
@ -495,21 +538,25 @@ func TestInstallRelease_ReplaceRelease(t *testing.T) {
instAction.Replace = true
rel := releaseStub()
rel.Info.Status = release.StatusUninstalled
rel.Info.Status = rcommon.StatusUninstalled
instAction.cfg.Releases.Create(rel)
instAction.ReleaseName = rel.Name
vals := map[string]interface{}{}
res, err := instAction.Run(buildChart(), vals)
resi, err := instAction.Run(buildChart(), vals)
is.NoError(err)
res, err := releaserToV1Release(resi)
is.NoError(err)
// This should have been auto-incremented
is.Equal(2, res.Version)
is.Equal(res.Name, rel.Name)
getres, err := instAction.cfg.Releases.Get(rel.Name, res.Version)
r, err := instAction.cfg.Releases.Get(rel.Name, res.Version)
is.NoError(err)
is.Equal(getres.Info.Status, release.StatusDeployed)
getres, err := releaserToV1Release(r)
is.NoError(err)
is.Equal(getres.Info.Status, rcommon.StatusDeployed)
}
func TestInstallRelease_KubeVersion(t *testing.T) {
@ -539,10 +586,12 @@ func TestInstallRelease_Wait(t *testing.T) {
goroutines := instAction.getGoroutineCount()
res, err := instAction.Run(buildChart(), vals)
resi, err := instAction.Run(buildChart(), vals)
is.Error(err)
res, err := releaserToV1Release(resi)
is.NoError(err)
is.Contains(res.Info.Description, "I timed out")
is.Equal(res.Info.Status, release.StatusFailed)
is.Equal(res.Info.Status, rcommon.StatusFailed)
is.Equal(goroutines, instAction.getGoroutineCount())
}
@ -580,10 +629,12 @@ func TestInstallRelease_WaitForJobs(t *testing.T) {
instAction.WaitForJobs = true
vals := map[string]interface{}{}
res, err := instAction.Run(buildChart(), vals)
resi, err := instAction.Run(buildChart(), vals)
is.Error(err)
res, err := releaserToV1Release(resi)
is.NoError(err)
is.Contains(res.Info.Description, "I timed out")
is.Equal(res.Info.Status, release.StatusFailed)
is.Equal(res.Info.Status, rcommon.StatusFailed)
}
func TestInstallRelease_RollbackOnFailure(t *testing.T) {
@ -601,11 +652,13 @@ func TestInstallRelease_RollbackOnFailure(t *testing.T) {
instAction.DisableHooks = true
vals := map[string]interface{}{}
res, err := instAction.Run(buildChart(), vals)
resi, err := instAction.Run(buildChart(), vals)
is.Error(err)
is.Contains(err.Error(), "I timed out")
is.Contains(err.Error(), "rollback-on-failure")
res, err := releaserToV1Release(resi)
is.NoError(err)
// Now make sure it isn't in storage anymore
_, err = instAction.cfg.Releases.Get(res.Name, res.Version)
is.Error(err)
@ -645,12 +698,14 @@ func TestInstallRelease_RollbackOnFailure_Interrupted(t *testing.T) {
goroutines := instAction.getGoroutineCount()
res, err := instAction.RunWithContext(ctx, buildChart(), vals)
resi, err := instAction.RunWithContext(ctx, buildChart(), vals)
is.Error(err)
is.Contains(err.Error(), "context canceled")
is.Contains(err.Error(), "rollback-on-failure")
is.Contains(err.Error(), "uninstalled")
res, err := releaserToV1Release(resi)
is.NoError(err)
// Now make sure it isn't in storage anymore
_, err = instAction.cfg.Releases.Get(res.Name, res.Version)
is.Error(err)
@ -856,32 +911,32 @@ func TestNameAndChartGenerateName(t *testing.T) {
{
"local filepath",
"./chart",
fmt.Sprintf("chart-%d", helmtime.Now().Unix()),
fmt.Sprintf("chart-%d", time.Now().Unix()),
},
{
"dot filepath",
".",
fmt.Sprintf("chart-%d", helmtime.Now().Unix()),
fmt.Sprintf("chart-%d", time.Now().Unix()),
},
{
"empty filepath",
"",
fmt.Sprintf("chart-%d", helmtime.Now().Unix()),
fmt.Sprintf("chart-%d", time.Now().Unix()),
},
{
"packaged chart",
"chart.tgz",
fmt.Sprintf("chart-%d", helmtime.Now().Unix()),
fmt.Sprintf("chart-%d", time.Now().Unix()),
},
{
"packaged chart with .tar.gz extension",
"chart.tar.gz",
fmt.Sprintf("chart-%d", helmtime.Now().Unix()),
fmt.Sprintf("chart-%d", time.Now().Unix()),
},
{
"packaged chart with local extension",
"./chart.tgz",
fmt.Sprintf("chart-%d", helmtime.Now().Unix()),
fmt.Sprintf("chart-%d", time.Now().Unix()),
},
}
@ -907,10 +962,12 @@ func TestInstallWithLabels(t *testing.T) {
"key1": "val1",
"key2": "val2",
}
res, err := instAction.Run(buildChart(), nil)
resi, err := instAction.Run(buildChart(), nil)
if err != nil {
t.Fatalf("Failed install: %s", err)
}
res, err := releaserToV1Release(resi)
is.NoError(err)
is.Equal(instAction.Labels, res.Labels)
}
@ -994,3 +1051,19 @@ func TestUrlEqual(t *testing.T) {
})
}
}
func TestInstallRun_UnreachableKubeClient(t *testing.T) {
config := actionConfigFixture(t)
failingKubeClient := kubefake.FailingKubeClient{PrintingKubeClient: kubefake.PrintingKubeClient{Out: io.Discard}, DummyResources: nil}
failingKubeClient.ConnectionError = errors.New("connection refused")
config.KubeClient = &failingKubeClient
instAction := NewInstall(config)
ctx, done := context.WithCancel(t.Context())
chrt := buildChart()
res, err := instAction.RunWithContext(ctx, chrt, nil)
done()
assert.Nil(t, res)
assert.ErrorContains(t, err, "connection refused")
}

@ -22,6 +22,7 @@ import (
"k8s.io/apimachinery/pkg/labels"
ri "helm.sh/helm/v4/pkg/release"
release "helm.sh/helm/v4/pkg/release/v1"
releaseutil "helm.sh/helm/v4/pkg/release/v1/util"
)
@ -139,13 +140,13 @@ type List struct {
// NewList constructs a new *List
func NewList(cfg *Configuration) *List {
return &List{
StateMask: ListDeployed | ListFailed,
StateMask: ListAll,
cfg: cfg,
}
}
// Run executes the list command, returning a set of matches.
func (l *List) Run() ([]*release.Release, error) {
func (l *List) Run() ([]ri.Releaser, error) {
if err := l.cfg.KubeClient.IsReachable(); err != nil {
return nil, err
}
@ -159,9 +160,13 @@ func (l *List) Run() ([]*release.Release, error) {
}
}
results, err := l.cfg.Releases.List(func(rel *release.Release) bool {
results, err := l.cfg.Releases.List(func(rel ri.Releaser) bool {
r, err := releaserToV1Release(rel)
if err != nil {
return false
}
// Skip anything that doesn't match the filter.
if filter != nil && !filter.MatchString(rel.Name) {
if filter != nil && !filter.MatchString(r.Name) {
return false
}
@ -176,30 +181,35 @@ func (l *List) Run() ([]*release.Release, error) {
return results, nil
}
rresults, err := releaseListToV1List(results)
if err != nil {
return nil, err
}
// by definition, superseded releases are never shown if
// only the latest releases are returned. so if requested statemask
// is _only_ ListSuperseded, skip the latest release filter
if l.StateMask != ListSuperseded {
results = filterLatestReleases(results)
rresults = filterLatestReleases(rresults)
}
// State mask application must occur after filtering to
// latest releases, otherwise outdated entries can be returned
results = l.filterStateMask(results)
rresults = l.filterStateMask(rresults)
// Skip anything that doesn't match the selector
selectorObj, err := labels.Parse(l.Selector)
if err != nil {
return nil, err
}
results = l.filterSelector(results, selectorObj)
rresults = l.filterSelector(rresults, selectorObj)
// Unfortunately, we have to sort before truncating, which can incur substantial overhead
l.sort(results)
l.sort(rresults)
// Guard on offset
if l.Offset >= len(results) {
return []*release.Release{}, nil
if l.Offset >= len(rresults) {
return releaseV1ListToReleaserList([]*release.Release{})
}
// Calculate the limit and offset, and then truncate results if necessary.
@ -208,12 +218,12 @@ func (l *List) Run() ([]*release.Release, error) {
limit = l.Limit
}
last := l.Offset + limit
if l := len(results); l < last {
if l := len(rresults); l < last {
last = l
}
results = results[l.Offset:last]
rresults = rresults[l.Offset:last]
return results, err
return releaseV1ListToReleaserList(rresults)
}
// sort is an in-place sort where order is based on the value of a.Sort
@ -317,7 +327,7 @@ func (l *List) SetStateMask() {
// Apply a default
if state == 0 {
state = ListDeployed | ListFailed
state = ListAll
}
l.StateMask = state

@ -17,10 +17,15 @@ limitations under the License.
package action
import (
"errors"
"io"
"testing"
"github.com/stretchr/testify/assert"
kubefake "helm.sh/helm/v4/pkg/kube/fake"
ri "helm.sh/helm/v4/pkg/release"
"helm.sh/helm/v4/pkg/release/common"
release "helm.sh/helm/v4/pkg/release/v1"
"helm.sh/helm/v4/pkg/storage"
)
@ -93,8 +98,11 @@ func TestList_Sort(t *testing.T) {
lister := newListFixture(t)
lister.Sort = ByNameDesc // Other sorts are tested elsewhere
makeMeSomeReleases(t, lister.cfg.Releases)
list, err := lister.Run()
l, err := lister.Run()
is.NoError(err)
list, err := releaseListToV1List(l)
is.NoError(err)
is.Len(list, 3)
is.Equal("two", list[0].Name)
is.Equal("three", list[1].Name)
@ -106,7 +114,9 @@ func TestList_Limit(t *testing.T) {
lister := newListFixture(t)
lister.Limit = 2
makeMeSomeReleases(t, lister.cfg.Releases)
list, err := lister.Run()
l, err := lister.Run()
is.NoError(err)
list, err := releaseListToV1List(l)
is.NoError(err)
is.Len(list, 2)
// Lex order means one, three, two
@ -119,7 +129,9 @@ func TestList_BigLimit(t *testing.T) {
lister := newListFixture(t)
lister.Limit = 20
makeMeSomeReleases(t, lister.cfg.Releases)
list, err := lister.Run()
l, err := lister.Run()
is.NoError(err)
list, err := releaseListToV1List(l)
is.NoError(err)
is.Len(list, 3)
@ -135,7 +147,9 @@ func TestList_LimitOffset(t *testing.T) {
lister.Limit = 2
lister.Offset = 1
makeMeSomeReleases(t, lister.cfg.Releases)
list, err := lister.Run()
l, err := lister.Run()
is.NoError(err)
list, err := releaseListToV1List(l)
is.NoError(err)
is.Len(list, 2)
@ -165,23 +179,45 @@ func TestList_StateMask(t *testing.T) {
is := assert.New(t)
lister := newListFixture(t)
makeMeSomeReleases(t, lister.cfg.Releases)
one, err := lister.cfg.Releases.Get("one", 1)
oner, err := lister.cfg.Releases.Get("one", 1)
is.NoError(err)
one.SetStatus(release.StatusUninstalled, "uninstalled")
var one release.Release
switch v := oner.(type) {
case release.Release:
one = v
case *release.Release:
one = *v
default:
t.Fatal("unsupported release type")
}
one.SetStatus(common.StatusUninstalled, "uninstalled")
err = lister.cfg.Releases.Update(one)
is.NoError(err)
res, err := lister.Run()
is.NoError(err)
is.Len(res, 2)
is.Equal("three", res[0].Name)
is.Equal("two", res[1].Name)
is.Len(res, 3)
ac0, err := ri.NewAccessor(res[0])
is.NoError(err)
ac1, err := ri.NewAccessor(res[1])
is.NoError(err)
ac2, err := ri.NewAccessor(res[2])
is.NoError(err)
is.Equal("one", ac0.Name())
is.Equal("three", ac1.Name())
is.Equal("two", ac2.Name())
lister.StateMask = ListUninstalled
res, err = lister.Run()
is.NoError(err)
is.Len(res, 1)
is.Equal("one", res[0].Name)
ac0, err = ri.NewAccessor(res[0])
is.NoError(err)
is.Equal("one", ac0.Name())
lister.StateMask |= ListDeployed
res, err = lister.Run()
@ -203,28 +239,30 @@ func TestList_StateMaskWithStaleRevisions(t *testing.T) {
// "dirty" release should _not_ be present as most recent
// release is deployed despite failed release in past
is.Equal("failed", res[0].Name)
ac0, err := ri.NewAccessor(res[0])
is.NoError(err)
is.Equal("failed", ac0.Name())
}
func makeMeSomeReleasesWithStaleFailure(t *testing.T, store *storage.Storage) {
t.Helper()
one := namedReleaseStub("clean", release.StatusDeployed)
one := namedReleaseStub("clean", common.StatusDeployed)
one.Namespace = "default"
one.Version = 1
two := namedReleaseStub("dirty", release.StatusDeployed)
two := namedReleaseStub("dirty", common.StatusDeployed)
two.Namespace = "default"
two.Version = 1
three := namedReleaseStub("dirty", release.StatusFailed)
three := namedReleaseStub("dirty", common.StatusFailed)
three.Namespace = "default"
three.Version = 2
four := namedReleaseStub("dirty", release.StatusDeployed)
four := namedReleaseStub("dirty", common.StatusDeployed)
four.Namespace = "default"
four.Version = 3
five := namedReleaseStub("failed", release.StatusFailed)
five := namedReleaseStub("failed", common.StatusFailed)
five.Namespace = "default"
five.Version = 1
@ -248,7 +286,9 @@ func TestList_Filter(t *testing.T) {
res, err := lister.Run()
is.NoError(err)
is.Len(res, 1)
is.Equal("three", res[0].Name)
ac0, err := ri.NewAccessor(res[0])
is.NoError(err)
is.Equal("three", ac0.Name())
}
func TestList_FilterFailsCompile(t *testing.T) {
@ -367,3 +407,16 @@ func TestSelectorList(t *testing.T) {
assert.ElementsMatch(t, expectedFilteredList, res)
})
}
func TestListRun_UnreachableKubeClient(t *testing.T) {
config := actionConfigFixture(t)
failingKubeClient := kubefake.FailingKubeClient{PrintingKubeClient: kubefake.PrintingKubeClient{Out: io.Discard}, DummyResources: nil}
failingKubeClient.ConnectionError = errors.New("connection refused")
config.KubeClient = &failingKubeClient
lister := NewList(config)
result, err := lister.Run()
assert.Nil(t, result)
assert.ErrorContains(t, err, "connection refused")
}

@ -28,7 +28,9 @@ import (
"golang.org/x/term"
"sigs.k8s.io/yaml"
"helm.sh/helm/v4/pkg/chart/v2/loader"
ci "helm.sh/helm/v4/pkg/chart"
"helm.sh/helm/v4/pkg/chart/loader"
chart "helm.sh/helm/v4/pkg/chart/v2"
chartutil "helm.sh/helm/v4/pkg/chart/v2/util"
"helm.sh/helm/v4/pkg/provenance"
)
@ -69,7 +71,21 @@ func NewPackage() *Package {
// Run executes 'helm package' against the given chart and returns the path to the packaged chart.
func (p *Package) Run(path string, _ map[string]interface{}) (string, error) {
ch, err := loader.LoadDir(path)
chrt, err := loader.LoadDir(path)
if err != nil {
return "", err
}
var ch *chart.Chart
switch c := chrt.(type) {
case *chart.Chart:
ch = c
case chart.Chart:
ch = &c
default:
return "", errors.New("invalid chart apiVersion")
}
ac, err := ci.NewAccessor(ch)
if err != nil {
return "", err
}
@ -87,7 +103,7 @@ func (p *Package) Run(path string, _ map[string]interface{}) (string, error) {
ch.Metadata.AppVersion = p.AppVersion
}
if reqs := ch.Metadata.Dependencies; reqs != nil {
if reqs := ac.MetaDependencies(); reqs != nil {
if err := CheckDependencies(ch, reqs); err != nil {
return "", err
}
@ -146,13 +162,22 @@ func (p *Package) Clearsign(filename string) error {
}
// Load the chart archive to extract metadata
chart, err := loader.LoadFile(filename)
chrt, err := loader.LoadFile(filename)
if err != nil {
return fmt.Errorf("failed to load chart for signing: %w", err)
}
var ch *chart.Chart
switch c := chrt.(type) {
case *chart.Chart:
ch = c
case chart.Chart:
ch = &c
default:
return errors.New("invalid chart apiVersion")
}
// Marshal chart metadata to YAML bytes
metadataBytes, err := yaml.Marshal(chart.Metadata)
metadataBytes, err := yaml.Marshal(ch.Metadata)
if err != nil {
return fmt.Errorf("failed to marshal chart metadata: %w", err)
}

@ -28,6 +28,7 @@ import (
chartutil "helm.sh/helm/v4/pkg/chart/v2/util"
"helm.sh/helm/v4/pkg/kube"
ri "helm.sh/helm/v4/pkg/release"
release "helm.sh/helm/v4/pkg/release/v1"
)
@ -45,7 +46,6 @@ type ReleaseTesting struct {
// Used for fetching logs from test pods
Namespace string
Filters map[string][]string
HideNotes bool
}
// NewReleaseTesting creates a new ReleaseTesting object with the given configuration.
@ -57,7 +57,7 @@ func NewReleaseTesting(cfg *Configuration) *ReleaseTesting {
}
// Run executes 'helm test' against the given release.
func (r *ReleaseTesting) Run(name string) (*release.Release, error) {
func (r *ReleaseTesting) Run(name string) (ri.Releaser, error) {
if err := r.cfg.KubeClient.IsReachable(); err != nil {
return nil, err
}
@ -67,7 +67,12 @@ func (r *ReleaseTesting) Run(name string) (*release.Release, error) {
}
// finds the non-deleted release with the given name
rel, err := r.cfg.Releases.Last(name)
reli, err := r.cfg.Releases.Last(name)
if err != nil {
return reli, err
}
rel, err := releaserToV1Release(reli)
if err != nil {
return rel, err
}

@ -23,10 +23,12 @@ import (
"strings"
"time"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
chartutil "helm.sh/helm/v4/pkg/chart/v2/util"
"helm.sh/helm/v4/pkg/kube"
"helm.sh/helm/v4/pkg/release/common"
release "helm.sh/helm/v4/pkg/release/v1"
helmtime "helm.sh/helm/v4/pkg/time"
)
// Rollback is the action for rolling back to a given release.
@ -40,7 +42,8 @@ type Rollback struct {
WaitStrategy kube.WaitStrategy
WaitForJobs bool
DisableHooks bool
DryRun bool
// DryRunStrategy can be set to prepare, but not execute the operation and whether or not to interact with the remote cluster
DryRunStrategy DryRunStrategy
// ForceReplace will, if set to `true`, ignore certain warnings and perform the rollback anyway.
//
// This should be used with caution.
@ -60,7 +63,8 @@ type Rollback struct {
// NewRollback creates a new Rollback object with the given configuration.
func NewRollback(cfg *Configuration) *Rollback {
return &Rollback{
cfg: cfg,
cfg: cfg,
DryRunStrategy: DryRunNone,
}
}
@ -78,7 +82,7 @@ func (r *Rollback) Run(name string) error {
return err
}
if !r.DryRun {
if !isDryRun(r.DryRunStrategy) {
slog.Debug("creating rolled back release", "name", name)
if err := r.cfg.Releases.Create(targetRelease); err != nil {
return err
@ -90,7 +94,7 @@ func (r *Rollback) Run(name string) error {
return err
}
if !r.DryRun {
if !isDryRun(r.DryRunStrategy) {
slog.Debug("updating status for rolled back release", "name", name)
if err := r.cfg.Releases.Update(targetRelease); err != nil {
return err
@ -110,7 +114,12 @@ func (r *Rollback) prepareRollback(name string) (*release.Release, *release.Rele
return nil, nil, false, errInvalidRevision
}
currentRelease, err := r.cfg.Releases.Last(name)
currentReleasei, err := r.cfg.Releases.Last(name)
if err != nil {
return nil, nil, false, err
}
currentRelease, err := releaserToV1Release(currentReleasei)
if err != nil {
return nil, nil, false, err
}
@ -127,7 +136,11 @@ func (r *Rollback) prepareRollback(name string) (*release.Release, *release.Rele
// Check if the history version to be rolled back exists
previousVersionExist := false
for _, historyRelease := range historyReleases {
for _, historyReleasei := range historyReleases {
historyRelease, err := releaserToV1Release(historyReleasei)
if err != nil {
return nil, nil, false, err
}
version := historyRelease.Version
if previousVersion == version {
previousVersionExist = true
@ -140,7 +153,11 @@ func (r *Rollback) prepareRollback(name string) (*release.Release, *release.Rele
slog.Debug("rolling back", "name", name, "currentVersion", currentRelease.Version, "targetVersion", previousVersion)
previousRelease, err := r.cfg.Releases.Get(name, previousVersion)
previousReleasei, err := r.cfg.Releases.Get(name, previousVersion)
if err != nil {
return nil, nil, false, err
}
previousRelease, err := releaserToV1Release(previousReleasei)
if err != nil {
return nil, nil, false, err
}
@ -158,8 +175,8 @@ func (r *Rollback) prepareRollback(name string) (*release.Release, *release.Rele
Config: previousRelease.Config,
Info: &release.Info{
FirstDeployed: currentRelease.Info.FirstDeployed,
LastDeployed: helmtime.Now(),
Status: release.StatusPendingRollback,
LastDeployed: time.Now(),
Status: common.StatusPendingRollback,
Notes: previousRelease.Info.Notes,
// Because we lose the reference to previous version elsewhere, we set the
// message here, and only override it later if we experience failure.
@ -176,7 +193,7 @@ func (r *Rollback) prepareRollback(name string) (*release.Release, *release.Rele
}
func (r *Rollback) performRollback(currentRelease, targetRelease *release.Release, serverSideApply bool) (*release.Release, error) {
if r.DryRun {
if isDryRun(r.DryRunStrategy) {
slog.Debug("dry run", "name", targetRelease.Name)
return targetRelease, nil
}
@ -216,14 +233,14 @@ func (r *Rollback) performRollback(currentRelease, targetRelease *release.Releas
if err != nil {
msg := fmt.Sprintf("Rollback %q failed: %s", targetRelease.Name, err)
slog.Warn(msg)
currentRelease.Info.Status = release.StatusSuperseded
targetRelease.Info.Status = release.StatusFailed
currentRelease.Info.Status = common.StatusSuperseded
targetRelease.Info.Status = common.StatusFailed
targetRelease.Info.Description = msg
r.cfg.recordRelease(currentRelease)
r.cfg.recordRelease(targetRelease)
if r.CleanupOnFail {
slog.Debug("cleanup on fail set, cleaning up resources", "count", len(results.Created))
_, errs := r.cfg.KubeClient.Delete(results.Created)
_, errs := r.cfg.KubeClient.Delete(results.Created, metav1.DeletePropagationBackground)
if errs != nil {
return targetRelease, fmt.Errorf(
"an error occurred while cleaning up resources. original rollback error: %w",
@ -240,14 +257,14 @@ func (r *Rollback) performRollback(currentRelease, targetRelease *release.Releas
}
if r.WaitForJobs {
if err := waiter.WaitWithJobs(target, r.Timeout); err != nil {
targetRelease.SetStatus(release.StatusFailed, fmt.Sprintf("Release %q failed: %s", targetRelease.Name, err.Error()))
targetRelease.SetStatus(common.StatusFailed, fmt.Sprintf("Release %q failed: %s", targetRelease.Name, err.Error()))
r.cfg.recordRelease(currentRelease)
r.cfg.recordRelease(targetRelease)
return targetRelease, fmt.Errorf("release %s failed: %w", targetRelease.Name, err)
}
} else {
if err := waiter.Wait(target, r.Timeout); err != nil {
targetRelease.SetStatus(release.StatusFailed, fmt.Sprintf("Release %q failed: %s", targetRelease.Name, err.Error()))
targetRelease.SetStatus(common.StatusFailed, fmt.Sprintf("Release %q failed: %s", targetRelease.Name, err.Error()))
r.cfg.recordRelease(currentRelease)
r.cfg.recordRelease(targetRelease)
return targetRelease, fmt.Errorf("release %s failed: %w", targetRelease.Name, err)
@ -266,13 +283,17 @@ func (r *Rollback) performRollback(currentRelease, targetRelease *release.Releas
return nil, err
}
// Supersede all previous deployments, see issue #2941.
for _, rel := range deployed {
for _, reli := range deployed {
rel, err := releaserToV1Release(reli)
if err != nil {
return nil, err
}
slog.Debug("superseding previous deployment", "version", rel.Version)
rel.Info.Status = release.StatusSuperseded
rel.Info.Status = common.StatusSuperseded
r.cfg.recordRelease(rel)
}
targetRelease.Info.Status = release.StatusDeployed
targetRelease.Info.Status = common.StatusDeployed
return targetRelease, nil
}

@ -18,10 +18,9 @@ package action
import (
"bytes"
"errors"
"helm.sh/helm/v4/pkg/kube"
release "helm.sh/helm/v4/pkg/release/v1"
ri "helm.sh/helm/v4/pkg/release"
)
// Status is the action for checking the deployment status of releases.
@ -45,38 +44,40 @@ func NewStatus(cfg *Configuration) *Status {
}
// Run executes 'helm status' against the given release.
func (s *Status) Run(name string) (*release.Release, error) {
func (s *Status) Run(name string) (ri.Releaser, error) {
if err := s.cfg.KubeClient.IsReachable(); err != nil {
return nil, err
}
rel, err := s.cfg.releaseContent(name, s.Version)
reli, err := s.cfg.releaseContent(name, s.Version)
if err != nil {
return nil, err
}
if kubeClient, ok := s.cfg.KubeClient.(kube.InterfaceResources); ok {
var resources kube.ResourceList
if s.ShowResourcesTable {
resources, err = kubeClient.BuildTable(bytes.NewBufferString(rel.Manifest), false)
if err != nil {
return nil, err
}
} else {
resources, err = s.cfg.KubeClient.Build(bytes.NewBufferString(rel.Manifest), false)
if err != nil {
return nil, err
}
}
rel, err := releaserToV1Release(reli)
if err != nil {
return nil, err
}
resp, err := kubeClient.Get(resources, true)
var resources kube.ResourceList
if s.ShowResourcesTable {
resources, err = s.cfg.KubeClient.BuildTable(bytes.NewBufferString(rel.Manifest), false)
if err != nil {
return nil, err
}
} else {
resources, err = s.cfg.KubeClient.Build(bytes.NewBufferString(rel.Manifest), false)
if err != nil {
return nil, err
}
}
rel.Info.Resources = resp
return rel, nil
resp, err := s.cfg.KubeClient.Get(resources, true)
if err != nil {
return nil, err
}
return nil, errors.New("unable to get kubeClient with interface InterfaceResources")
rel.Info.Resources = resp
return rel, nil
}

@ -27,10 +27,11 @@ import (
chartutil "helm.sh/helm/v4/pkg/chart/v2/util"
"helm.sh/helm/v4/pkg/kube"
releasei "helm.sh/helm/v4/pkg/release"
"helm.sh/helm/v4/pkg/release/common"
release "helm.sh/helm/v4/pkg/release/v1"
releaseutil "helm.sh/helm/v4/pkg/release/v1/util"
"helm.sh/helm/v4/pkg/storage/driver"
helmtime "helm.sh/helm/v4/pkg/time"
)
// Uninstall is the action for uninstalling releases.
@ -57,7 +58,7 @@ func NewUninstall(cfg *Configuration) *Uninstall {
}
// Run uninstalls the given release.
func (u *Uninstall) Run(name string) (*release.UninstallReleaseResponse, error) {
func (u *Uninstall) Run(name string) (*releasei.UninstallReleaseResponse, error) {
if err := u.cfg.KubeClient.IsReachable(); err != nil {
return nil, err
}
@ -68,51 +69,61 @@ func (u *Uninstall) Run(name string) (*release.UninstallReleaseResponse, error)
}
if u.DryRun {
r, err := u.cfg.releaseContent(name, 0)
ri, err := u.cfg.releaseContent(name, 0)
if err != nil {
if u.IgnoreNotFound && errors.Is(err, driver.ErrReleaseNotFound) {
return nil, nil
}
return &release.UninstallReleaseResponse{}, err
return &releasei.UninstallReleaseResponse{}, err
}
r, err := releaserToV1Release(ri)
if err != nil {
return nil, err
}
return &release.UninstallReleaseResponse{Release: r}, nil
return &releasei.UninstallReleaseResponse{Release: r}, nil
}
if err := chartutil.ValidateReleaseName(name); err != nil {
return nil, fmt.Errorf("uninstall: Release name is invalid: %s", name)
}
rels, err := u.cfg.Releases.History(name)
relsi, err := u.cfg.Releases.History(name)
if err != nil {
if u.IgnoreNotFound {
return nil, nil
}
return nil, fmt.Errorf("uninstall: Release not loaded: %s: %w", name, err)
}
if len(rels) < 1 {
if len(relsi) < 1 {
return nil, errMissingRelease
}
rels, err := releaseListToV1List(relsi)
if err != nil {
return nil, err
}
releaseutil.SortByRevision(rels)
rel := rels[len(rels)-1]
// TODO: Are there any cases where we want to force a delete even if it's
// already marked deleted?
if rel.Info.Status == release.StatusUninstalled {
if rel.Info.Status == common.StatusUninstalled {
if !u.KeepHistory {
if err := u.purgeReleases(rels...); err != nil {
return nil, fmt.Errorf("uninstall: Failed to purge the release: %w", err)
}
return &release.UninstallReleaseResponse{Release: rel}, nil
return &releasei.UninstallReleaseResponse{Release: rel}, nil
}
return nil, fmt.Errorf("the release named %q is already deleted", name)
}
slog.Debug("uninstall: deleting release", "name", name)
rel.Info.Status = release.StatusUninstalling
rel.Info.Deleted = helmtime.Now()
rel.Info.Status = common.StatusUninstalling
rel.Info.Deleted = time.Now()
rel.Info.Description = "Deletion in progress (or silently failed)"
res := &release.UninstallReleaseResponse{Release: rel}
res := &releasei.UninstallReleaseResponse{Release: rel}
if !u.DisableHooks {
serverSideApply := true
@ -151,7 +162,7 @@ func (u *Uninstall) Run(name string) (*release.UninstallReleaseResponse, error)
}
}
rel.Info.Status = release.StatusUninstalled
rel.Info.Status = common.StatusUninstalled
if len(u.Description) > 0 {
rel.Info.Description = u.Description
} else {
@ -246,11 +257,7 @@ func (u *Uninstall) deleteRelease(rel *release.Release) (kube.ResourceList, stri
return nil, "", []error{fmt.Errorf("unable to build kubernetes objects for delete: %w", err)}
}
if len(resources) > 0 {
if kubeClient, ok := u.cfg.KubeClient.(kube.InterfaceDeletionPropagation); ok {
_, errs = kubeClient.DeleteWithPropagationPolicy(resources, parseCascadingFlag(u.DeletionPropagation))
return resources, kept, errs
}
_, errs = u.cfg.KubeClient.Delete(resources)
_, errs = u.cfg.KubeClient.Delete(resources, parseCascadingFlag(u.DeletionPropagation))
}
return resources, kept, errs
}

@ -17,7 +17,9 @@ limitations under the License.
package action
import (
"errors"
"fmt"
"io"
"testing"
"github.com/stretchr/testify/assert"
@ -25,7 +27,7 @@ import (
"helm.sh/helm/v4/pkg/kube"
kubefake "helm.sh/helm/v4/pkg/kube/fake"
release "helm.sh/helm/v4/pkg/release/v1"
"helm.sh/helm/v4/pkg/release/common"
)
func uninstallAction(t *testing.T) *Uninstall {
@ -114,10 +116,12 @@ func TestUninstallRelease_Wait(t *testing.T) {
failer := unAction.cfg.KubeClient.(*kubefake.FailingKubeClient)
failer.WaitForDeleteError = fmt.Errorf("U timed out")
unAction.cfg.KubeClient = failer
res, err := unAction.Run(rel.Name)
resi, err := unAction.Run(rel.Name)
is.Error(err)
is.Contains(err.Error(), "U timed out")
is.Equal(res.Release.Info.Status, release.StatusUninstalled)
res, err := releaserToV1Release(resi.Release)
is.NoError(err)
is.Equal(res.Info.Status, common.StatusUninstalled)
}
func TestUninstallRelease_Cascade(t *testing.T) {
@ -144,10 +148,24 @@ func TestUninstallRelease_Cascade(t *testing.T) {
}`
unAction.cfg.Releases.Create(rel)
failer := unAction.cfg.KubeClient.(*kubefake.FailingKubeClient)
failer.DeleteWithPropagationError = fmt.Errorf("Uninstall with cascade failed")
failer.DeleteError = fmt.Errorf("Uninstall with cascade failed")
failer.BuildDummy = true
unAction.cfg.KubeClient = failer
_, err := unAction.Run(rel.Name)
require.Error(t, err)
is.Contains(err.Error(), "failed to delete release: come-fail-away")
}
func TestUninstallRun_UnreachableKubeClient(t *testing.T) {
t.Helper()
config := actionConfigFixture(t)
failingKubeClient := kubefake.FailingKubeClient{PrintingKubeClient: kubefake.PrintingKubeClient{Out: io.Discard}, DummyResources: nil}
failingKubeClient.ConnectionError = errors.New("connection refused")
config.KubeClient = &failingKubeClient
client := NewUninstall(config)
result, err := client.Run("")
assert.Nil(t, result)
assert.ErrorContains(t, err, "connection refused")
}

@ -26,15 +26,19 @@ import (
"sync"
"time"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/cli-runtime/pkg/resource"
"helm.sh/helm/v4/pkg/chart"
"helm.sh/helm/v4/pkg/chart/common"
"helm.sh/helm/v4/pkg/chart/common/util"
chart "helm.sh/helm/v4/pkg/chart/v2"
chartv2 "helm.sh/helm/v4/pkg/chart/v2"
chartutil "helm.sh/helm/v4/pkg/chart/v2/util"
"helm.sh/helm/v4/pkg/kube"
"helm.sh/helm/v4/pkg/postrenderer"
"helm.sh/helm/v4/pkg/registry"
ri "helm.sh/helm/v4/pkg/release"
rcommon "helm.sh/helm/v4/pkg/release/common"
release "helm.sh/helm/v4/pkg/release/v1"
releaseutil "helm.sh/helm/v4/pkg/release/v1/util"
"helm.sh/helm/v4/pkg/storage/driver"
@ -72,10 +76,8 @@ type Upgrade struct {
WaitForJobs bool
// DisableHooks disables hook processing if set to true.
DisableHooks bool
// DryRun controls whether the operation is prepared, but not executed.
DryRun bool
// DryRunOption controls whether the operation is prepared, but not executed with options on whether or not to interact with the remote cluster.
DryRunOption string
// DryRunStrategy can be set to prepare, but not execute the operation and whether or not to interact with the remote cluster
DryRunStrategy DryRunStrategy
// HideSecret can be set to true when DryRun is enabled in order to hide
// Kubernetes Secrets in the output. It cannot be used outside of DryRun.
HideSecret bool
@ -139,6 +141,7 @@ func NewUpgrade(cfg *Configuration) *Upgrade {
up := &Upgrade{
cfg: cfg,
ServerSideApply: "auto",
DryRunStrategy: DryRunNone,
}
up.registryClient = cfg.RegistryClient
@ -151,17 +154,27 @@ func (u *Upgrade) SetRegistryClient(client *registry.Client) {
}
// Run executes the upgrade on the given release.
func (u *Upgrade) Run(name string, chart *chart.Chart, vals map[string]interface{}) (*release.Release, error) {
func (u *Upgrade) Run(name string, chart chart.Charter, vals map[string]interface{}) (ri.Releaser, error) {
ctx := context.Background()
return u.RunWithContext(ctx, name, chart, vals)
}
// RunWithContext executes the upgrade on the given release with context.
func (u *Upgrade) RunWithContext(ctx context.Context, name string, chart *chart.Chart, vals map[string]interface{}) (*release.Release, error) {
func (u *Upgrade) RunWithContext(ctx context.Context, name string, ch chart.Charter, vals map[string]interface{}) (ri.Releaser, error) {
if err := u.cfg.KubeClient.IsReachable(); err != nil {
return nil, err
}
var chrt *chartv2.Chart
switch c := ch.(type) {
case *chartv2.Chart:
chrt = c
case chartv2.Chart:
chrt = &c
default:
return nil, errors.New("invalid chart apiVersion")
}
// Make sure wait is set if RollbackOnFailure. This makes it so
// the user doesn't have to specify both
if u.WaitStrategy == kube.HookOnlyStrategy && u.RollbackOnFailure {
@ -173,7 +186,7 @@ func (u *Upgrade) RunWithContext(ctx context.Context, name string, chart *chart.
}
slog.Debug("preparing upgrade", "name", name)
currentRelease, upgradedRelease, serverSideApply, err := u.prepareUpgrade(name, chart, vals)
currentRelease, upgradedRelease, serverSideApply, err := u.prepareUpgrade(name, chrt, vals)
if err != nil {
return nil, err
}
@ -187,7 +200,7 @@ func (u *Upgrade) RunWithContext(ctx context.Context, name string, chart *chart.
}
// Do not update for dry runs
if !u.isDryRun() {
if !isDryRun(u.DryRunStrategy) {
slog.Debug("updating status for upgraded release", "name", name)
if err := u.cfg.Releases.Update(upgradedRelease); err != nil {
return res, err
@ -197,27 +210,19 @@ func (u *Upgrade) RunWithContext(ctx context.Context, name string, chart *chart.
return res, nil
}
// isDryRun returns true if Upgrade is set to run as a DryRun
func (u *Upgrade) isDryRun() bool {
if u.DryRun || u.DryRunOption == "client" || u.DryRunOption == "server" || u.DryRunOption == "true" {
return true
}
return false
}
// prepareUpgrade builds an upgraded release for an upgrade operation.
func (u *Upgrade) prepareUpgrade(name string, chart *chart.Chart, vals map[string]interface{}) (*release.Release, *release.Release, bool, error) {
func (u *Upgrade) prepareUpgrade(name string, chart *chartv2.Chart, vals map[string]interface{}) (*release.Release, *release.Release, bool, error) {
if chart == nil {
return nil, nil, false, errMissingChart
}
// HideSecret must be used with dry run. Otherwise, return an error.
if !u.isDryRun() && u.HideSecret {
if !isDryRun(u.DryRunStrategy) && u.HideSecret {
return nil, nil, false, errors.New("hiding Kubernetes secrets requires a dry-run mode")
}
// finds the last non-deleted release with the given name
lastRelease, err := u.cfg.Releases.Last(name)
lastReleasei, err := u.cfg.Releases.Last(name)
if err != nil {
// to keep existing behavior of returning the "%q has no deployed releases" error when an existing release does not exist
if errors.Is(err, driver.ErrReleaseNotFound) {
@ -226,26 +231,37 @@ func (u *Upgrade) prepareUpgrade(name string, chart *chart.Chart, vals map[strin
return nil, nil, false, err
}
lastRelease, err := releaserToV1Release(lastReleasei)
if err != nil {
return nil, nil, false, err
}
// Concurrent `helm upgrade`s will either fail here with `errPending` or when creating the release with "already exists". This should act as a pessimistic lock.
if lastRelease.Info.Status.IsPending() {
return nil, nil, false, errPending
}
var currentRelease *release.Release
if lastRelease.Info.Status == release.StatusDeployed {
if lastRelease.Info.Status == rcommon.StatusDeployed {
// no need to retrieve the last deployed release from storage as the last release is deployed
currentRelease = lastRelease
} else {
// finds the deployed release with the given name
currentRelease, err = u.cfg.Releases.Deployed(name)
currentReleasei, err := u.cfg.Releases.Deployed(name)
var cerr error
currentRelease, cerr = releaserToV1Release(currentReleasei)
if cerr != nil {
return nil, nil, false, err
}
if err != nil {
if errors.Is(err, driver.ErrNoDeployedReleases) &&
(lastRelease.Info.Status == release.StatusFailed || lastRelease.Info.Status == release.StatusSuperseded) {
(lastRelease.Info.Status == rcommon.StatusFailed || lastRelease.Info.Status == rcommon.StatusSuperseded) {
currentRelease = lastRelease
} else {
return nil, nil, false, err
}
}
}
// determine if values will be reused
@ -278,13 +294,7 @@ func (u *Upgrade) prepareUpgrade(name string, chart *chart.Chart, vals map[strin
return nil, nil, false, err
}
// Determine whether or not to interact with remote
var interactWithRemote bool
if !u.isDryRun() || u.DryRunOption == "server" || u.DryRunOption == "none" || u.DryRunOption == "false" {
interactWithRemote = true
}
hooks, manifestDoc, notesTxt, err := u.cfg.renderResources(chart, valuesToRender, "", "", u.SubNotes, false, false, u.PostRenderer, interactWithRemote, u.EnableDNS, u.HideSecret)
hooks, manifestDoc, notesTxt, err := u.cfg.renderResources(chart, valuesToRender, "", "", u.SubNotes, false, false, u.PostRenderer, interactWithServer(u.DryRunStrategy), u.EnableDNS, u.HideSecret)
if err != nil {
return nil, nil, false, err
}
@ -309,7 +319,7 @@ func (u *Upgrade) prepareUpgrade(name string, chart *chart.Chart, vals map[strin
Info: &release.Info{
FirstDeployed: currentRelease.Info.FirstDeployed,
LastDeployed: Timestamper(),
Status: release.StatusPendingUpgrade,
Status: rcommon.StatusPendingUpgrade,
Description: "Preparing upgrade", // This should be overwritten later.
},
Version: revision,
@ -380,8 +390,7 @@ func (u *Upgrade) performUpgrade(ctx context.Context, originalRelease, upgradedR
return nil
})
// Run if it is a dry run
if u.isDryRun() {
if isDryRun(u.DryRunStrategy) {
slog.Debug("dry run for release", "name", upgradedRelease.Name)
if len(u.Description) > 0 {
upgradedRelease.Info.Description = u.Description
@ -492,10 +501,10 @@ func (u *Upgrade) releasingUpgrade(c chan<- resultMessage, upgradedRelease *rele
}
}
originalRelease.Info.Status = release.StatusSuperseded
originalRelease.Info.Status = rcommon.StatusSuperseded
u.cfg.recordRelease(originalRelease)
upgradedRelease.Info.Status = release.StatusDeployed
upgradedRelease.Info.Status = rcommon.StatusDeployed
if len(u.Description) > 0 {
upgradedRelease.Info.Description = u.Description
} else {
@ -508,12 +517,12 @@ func (u *Upgrade) failRelease(rel *release.Release, created kube.ResourceList, e
msg := fmt.Sprintf("Upgrade %q failed: %s", rel.Name, err)
slog.Warn("upgrade failed", "name", rel.Name, slog.Any("error", err))
rel.Info.Status = release.StatusFailed
rel.Info.Status = rcommon.StatusFailed
rel.Info.Description = msg
u.cfg.recordRelease(rel)
if u.CleanupOnFail && len(created) > 0 {
slog.Debug("cleanup on fail set", "cleaning_resources", len(created))
_, errs := u.cfg.KubeClient.Delete(created)
_, errs := u.cfg.KubeClient.Delete(created, metav1.DeletePropagationBackground)
if errs != nil {
return rel, fmt.Errorf(
"an error occurred while cleaning up resources. original upgrade error: %w: %w",
@ -538,12 +547,16 @@ func (u *Upgrade) failRelease(rel *release.Release, created kube.ResourceList, e
return rel, fmt.Errorf("an error occurred while finding last successful release. original upgrade error: %w: %w", err, herr)
}
fullHistoryV1, herr := releaseListToV1List(fullHistory)
if herr != nil {
return nil, herr
}
// There isn't a way to tell if a previous release was successful, but
// generally failed releases do not get superseded unless the next
// release is successful, so this should be relatively safe
filteredHistory := releaseutil.FilterFunc(func(r *release.Release) bool {
return r.Info.Status == release.StatusSuperseded || r.Info.Status == release.StatusDeployed
}).Filter(fullHistory)
return r.Info.Status == rcommon.StatusSuperseded || r.Info.Status == rcommon.StatusDeployed
}).Filter(fullHistoryV1)
if len(filteredHistory) == 0 {
return rel, fmt.Errorf("unable to find a previously successful release when attempting to rollback. original upgrade error: %w", err)
}
@ -578,7 +591,7 @@ func (u *Upgrade) failRelease(rel *release.Release, created kube.ResourceList, e
//
// This is skipped if the u.ResetValues flag is set, in which case the
// request values are not altered.
func (u *Upgrade) reuseValues(chart *chart.Chart, current *release.Release, newVals map[string]interface{}) (map[string]interface{}, error) {
func (u *Upgrade) reuseValues(chart *chartv2.Chart, current *release.Release, newVals map[string]interface{}) (map[string]interface{}, error) {
if u.ResetValues {
// If ResetValues is set, we completely ignore current.Config.
slog.Debug("resetting values to the chart's original version")

@ -18,7 +18,9 @@ package action
import (
"context"
"errors"
"fmt"
"io"
"reflect"
"testing"
"time"
@ -31,8 +33,8 @@ import (
"github.com/stretchr/testify/require"
kubefake "helm.sh/helm/v4/pkg/kube/fake"
"helm.sh/helm/v4/pkg/release/common"
release "helm.sh/helm/v4/pkg/release/v1"
helmtime "helm.sh/helm/v4/pkg/time"
)
func upgradeAction(t *testing.T) *Upgrade {
@ -51,24 +53,28 @@ func TestUpgradeRelease_Success(t *testing.T) {
upAction := upgradeAction(t)
rel := releaseStub()
rel.Name = "previous-release"
rel.Info.Status = release.StatusDeployed
rel.Info.Status = common.StatusDeployed
req.NoError(upAction.cfg.Releases.Create(rel))
upAction.WaitStrategy = kube.StatusWatcherStrategy
vals := map[string]interface{}{}
ctx, done := context.WithCancel(t.Context())
res, err := upAction.RunWithContext(ctx, rel.Name, buildChart(), vals)
done()
resi, err := upAction.RunWithContext(ctx, rel.Name, buildChart(), vals)
req.NoError(err)
is.Equal(res.Info.Status, release.StatusDeployed)
res, err := releaserToV1Release(resi)
is.NoError(err)
is.Equal(res.Info.Status, common.StatusDeployed)
done()
// Detecting previous bug where context termination after successful release
// caused release to fail.
time.Sleep(time.Millisecond * 100)
lastRelease, err := upAction.cfg.Releases.Last(rel.Name)
lastReleasei, err := upAction.cfg.Releases.Last(rel.Name)
req.NoError(err)
is.Equal(lastRelease.Info.Status, release.StatusDeployed)
lastRelease, err := releaserToV1Release(lastReleasei)
req.NoError(err)
is.Equal(lastRelease.Info.Status, common.StatusDeployed)
}
func TestUpgradeRelease_Wait(t *testing.T) {
@ -78,7 +84,7 @@ func TestUpgradeRelease_Wait(t *testing.T) {
upAction := upgradeAction(t)
rel := releaseStub()
rel.Name = "come-fail-away"
rel.Info.Status = release.StatusDeployed
rel.Info.Status = common.StatusDeployed
upAction.cfg.Releases.Create(rel)
failer := upAction.cfg.KubeClient.(*kubefake.FailingKubeClient)
@ -87,10 +93,12 @@ func TestUpgradeRelease_Wait(t *testing.T) {
upAction.WaitStrategy = kube.StatusWatcherStrategy
vals := map[string]interface{}{}
res, err := upAction.Run(rel.Name, buildChart(), vals)
resi, err := upAction.Run(rel.Name, buildChart(), vals)
req.Error(err)
res, err := releaserToV1Release(resi)
is.NoError(err)
is.Contains(res.Info.Description, "I timed out")
is.Equal(res.Info.Status, release.StatusFailed)
is.Equal(res.Info.Status, common.StatusFailed)
}
func TestUpgradeRelease_WaitForJobs(t *testing.T) {
@ -100,7 +108,7 @@ func TestUpgradeRelease_WaitForJobs(t *testing.T) {
upAction := upgradeAction(t)
rel := releaseStub()
rel.Name = "come-fail-away"
rel.Info.Status = release.StatusDeployed
rel.Info.Status = common.StatusDeployed
upAction.cfg.Releases.Create(rel)
failer := upAction.cfg.KubeClient.(*kubefake.FailingKubeClient)
@ -110,10 +118,12 @@ func TestUpgradeRelease_WaitForJobs(t *testing.T) {
upAction.WaitForJobs = true
vals := map[string]interface{}{}
res, err := upAction.Run(rel.Name, buildChart(), vals)
resi, err := upAction.Run(rel.Name, buildChart(), vals)
req.Error(err)
res, err := releaserToV1Release(resi)
is.NoError(err)
is.Contains(res.Info.Description, "I timed out")
is.Equal(res.Info.Status, release.StatusFailed)
is.Equal(res.Info.Status, common.StatusFailed)
}
func TestUpgradeRelease_CleanupOnFail(t *testing.T) {
@ -123,7 +133,7 @@ func TestUpgradeRelease_CleanupOnFail(t *testing.T) {
upAction := upgradeAction(t)
rel := releaseStub()
rel.Name = "come-fail-away"
rel.Info.Status = release.StatusDeployed
rel.Info.Status = common.StatusDeployed
upAction.cfg.Releases.Create(rel)
failer := upAction.cfg.KubeClient.(*kubefake.FailingKubeClient)
@ -134,11 +144,13 @@ func TestUpgradeRelease_CleanupOnFail(t *testing.T) {
upAction.CleanupOnFail = true
vals := map[string]interface{}{}
res, err := upAction.Run(rel.Name, buildChart(), vals)
resi, err := upAction.Run(rel.Name, buildChart(), vals)
req.Error(err)
is.NotContains(err.Error(), "unable to cleanup resources")
res, err := releaserToV1Release(resi)
is.NoError(err)
is.Contains(res.Info.Description, "I timed out")
is.Equal(res.Info.Status, release.StatusFailed)
is.Equal(res.Info.Status, common.StatusFailed)
}
func TestUpgradeRelease_RollbackOnFailure(t *testing.T) {
@ -150,7 +162,7 @@ func TestUpgradeRelease_RollbackOnFailure(t *testing.T) {
rel := releaseStub()
rel.Name = "nuketown"
rel.Info.Status = release.StatusDeployed
rel.Info.Status = common.StatusDeployed
upAction.cfg.Releases.Create(rel)
failer := upAction.cfg.KubeClient.(*kubefake.FailingKubeClient)
@ -160,23 +172,27 @@ func TestUpgradeRelease_RollbackOnFailure(t *testing.T) {
upAction.RollbackOnFailure = true
vals := map[string]interface{}{}
res, err := upAction.Run(rel.Name, buildChart(), vals)
resi, err := upAction.Run(rel.Name, buildChart(), vals)
req.Error(err)
is.Contains(err.Error(), "arming key removed")
is.Contains(err.Error(), "rollback-on-failure")
res, err := releaserToV1Release(resi)
is.NoError(err)
// Now make sure it is actually upgraded
updatedRes, err := upAction.cfg.Releases.Get(res.Name, 3)
updatedResi, err := upAction.cfg.Releases.Get(res.Name, 3)
is.NoError(err)
updatedRes, err := releaserToV1Release(updatedResi)
is.NoError(err)
// Should have rolled back to the previous
is.Equal(updatedRes.Info.Status, release.StatusDeployed)
is.Equal(updatedRes.Info.Status, common.StatusDeployed)
})
t.Run("rollback-on-failure uninstall fails", func(t *testing.T) {
upAction := upgradeAction(t)
rel := releaseStub()
rel.Name = "fallout"
rel.Info.Status = release.StatusDeployed
rel.Info.Status = common.StatusDeployed
upAction.cfg.Releases.Create(rel)
failer := upAction.cfg.KubeClient.(*kubefake.FailingKubeClient)
@ -217,7 +233,7 @@ func TestUpgradeRelease_ReuseValues(t *testing.T) {
rel := releaseStub()
rel.Name = "nuketown"
rel.Info.Status = release.StatusDeployed
rel.Info.Status = common.StatusDeployed
rel.Config = existingValues
err := upAction.cfg.Releases.Create(rel)
@ -225,18 +241,23 @@ func TestUpgradeRelease_ReuseValues(t *testing.T) {
upAction.ReuseValues = true
// setting newValues and upgrading
res, err := upAction.Run(rel.Name, buildChart(), newValues)
resi, err := upAction.Run(rel.Name, buildChart(), newValues)
is.NoError(err)
res, err := releaserToV1Release(resi)
is.NoError(err)
// Now make sure it is actually upgraded
updatedRes, err := upAction.cfg.Releases.Get(res.Name, 2)
updatedResi, err := upAction.cfg.Releases.Get(res.Name, 2)
is.NoError(err)
if updatedRes == nil {
if updatedResi == nil {
is.Fail("Updated Release is nil")
return
}
is.Equal(release.StatusDeployed, updatedRes.Info.Status)
updatedRes, err := releaserToV1Release(updatedResi)
is.NoError(err)
is.Equal(common.StatusDeployed, updatedRes.Info.Status)
is.Equal(expectedValues, updatedRes.Config)
})
@ -258,7 +279,7 @@ func TestUpgradeRelease_ReuseValues(t *testing.T) {
withValues(chartDefaultValues),
withMetadataDependency(dependency),
)
now := helmtime.Now()
now := time.Now()
existingValues := map[string]interface{}{
"subchart": map[string]interface{}{
"enabled": false,
@ -269,7 +290,7 @@ func TestUpgradeRelease_ReuseValues(t *testing.T) {
Info: &release.Info{
FirstDeployed: now,
LastDeployed: now,
Status: release.StatusDeployed,
Status: common.StatusDeployed,
Description: "Named Release Stub",
},
Chart: sampleChart,
@ -287,18 +308,23 @@ func TestUpgradeRelease_ReuseValues(t *testing.T) {
withMetadataDependency(dependency),
)
// reusing values and upgrading
res, err := upAction.Run(rel.Name, sampleChartWithSubChart, map[string]interface{}{})
resi, err := upAction.Run(rel.Name, sampleChartWithSubChart, map[string]interface{}{})
is.NoError(err)
res, err := releaserToV1Release(resi)
is.NoError(err)
// Now get the upgraded release
updatedRes, err := upAction.cfg.Releases.Get(res.Name, 2)
updatedResi, err := upAction.cfg.Releases.Get(res.Name, 2)
is.NoError(err)
if updatedRes == nil {
if updatedResi == nil {
is.Fail("Updated Release is nil")
return
}
is.Equal(release.StatusDeployed, updatedRes.Info.Status)
updatedRes, err := releaserToV1Release(updatedResi)
is.NoError(err)
is.Equal(common.StatusDeployed, updatedRes.Info.Status)
is.Equal(0, len(updatedRes.Chart.Dependencies()), "expected 0 dependencies")
expectedValues := map[string]interface{}{
@ -338,7 +364,7 @@ func TestUpgradeRelease_ResetThenReuseValues(t *testing.T) {
rel := releaseStub()
rel.Name = "nuketown"
rel.Info.Status = release.StatusDeployed
rel.Info.Status = common.StatusDeployed
rel.Config = existingValues
err := upAction.cfg.Releases.Create(rel)
@ -346,18 +372,23 @@ func TestUpgradeRelease_ResetThenReuseValues(t *testing.T) {
upAction.ResetThenReuseValues = true
// setting newValues and upgrading
res, err := upAction.Run(rel.Name, buildChart(withValues(newChartValues)), newValues)
resi, err := upAction.Run(rel.Name, buildChart(withValues(newChartValues)), newValues)
is.NoError(err)
res, err := releaserToV1Release(resi)
is.NoError(err)
// Now make sure it is actually upgraded
updatedRes, err := upAction.cfg.Releases.Get(res.Name, 2)
updatedResi, err := upAction.cfg.Releases.Get(res.Name, 2)
is.NoError(err)
if updatedRes == nil {
if updatedResi == nil {
is.Fail("Updated Release is nil")
return
}
is.Equal(release.StatusDeployed, updatedRes.Info.Status)
updatedRes, err := releaserToV1Release(updatedResi)
is.NoError(err)
is.Equal(common.StatusDeployed, updatedRes.Info.Status)
is.Equal(expectedValues, updatedRes.Config)
is.Equal(newChartValues, updatedRes.Chart.Values)
})
@ -369,11 +400,11 @@ func TestUpgradeRelease_Pending(t *testing.T) {
upAction := upgradeAction(t)
rel := releaseStub()
rel.Name = "come-fail-away"
rel.Info.Status = release.StatusDeployed
rel.Info.Status = common.StatusDeployed
upAction.cfg.Releases.Create(rel)
rel2 := releaseStub()
rel2.Name = "come-fail-away"
rel2.Info.Status = release.StatusPendingUpgrade
rel2.Info.Status = common.StatusPendingUpgrade
rel2.Version = 2
upAction.cfg.Releases.Create(rel2)
@ -390,7 +421,7 @@ func TestUpgradeRelease_Interrupted_Wait(t *testing.T) {
upAction := upgradeAction(t)
rel := releaseStub()
rel.Name = "interrupted-release"
rel.Info.Status = release.StatusDeployed
rel.Info.Status = common.StatusDeployed
upAction.cfg.Releases.Create(rel)
failer := upAction.cfg.KubeClient.(*kubefake.FailingKubeClient)
@ -402,11 +433,13 @@ func TestUpgradeRelease_Interrupted_Wait(t *testing.T) {
ctx, cancel := context.WithCancel(t.Context())
time.AfterFunc(time.Second, cancel)
res, err := upAction.RunWithContext(ctx, rel.Name, buildChart(), vals)
resi, err := upAction.RunWithContext(ctx, rel.Name, buildChart(), vals)
req.Error(err)
res, err := releaserToV1Release(resi)
is.NoError(err)
is.Contains(res.Info.Description, "Upgrade \"interrupted-release\" failed: context canceled")
is.Equal(res.Info.Status, release.StatusFailed)
is.Equal(res.Info.Status, common.StatusFailed)
}
func TestUpgradeRelease_Interrupted_RollbackOnFailure(t *testing.T) {
@ -417,7 +450,7 @@ func TestUpgradeRelease_Interrupted_RollbackOnFailure(t *testing.T) {
upAction := upgradeAction(t)
rel := releaseStub()
rel.Name = "interrupted-release"
rel.Info.Status = release.StatusDeployed
rel.Info.Status = common.StatusDeployed
upAction.cfg.Releases.Create(rel)
failer := upAction.cfg.KubeClient.(*kubefake.FailingKubeClient)
@ -429,16 +462,19 @@ func TestUpgradeRelease_Interrupted_RollbackOnFailure(t *testing.T) {
ctx, cancel := context.WithCancel(t.Context())
time.AfterFunc(time.Second, cancel)
res, err := upAction.RunWithContext(ctx, rel.Name, buildChart(), vals)
resi, err := upAction.RunWithContext(ctx, rel.Name, buildChart(), vals)
req.Error(err)
is.Contains(err.Error(), "release interrupted-release failed, and has been rolled back due to rollback-on-failure being set: context canceled")
res, err := releaserToV1Release(resi)
is.NoError(err)
// Now make sure it is actually upgraded
updatedRes, err := upAction.cfg.Releases.Get(res.Name, 3)
updatedResi, err := upAction.cfg.Releases.Get(res.Name, 3)
is.NoError(err)
updatedRes, err := releaserToV1Release(updatedResi)
is.NoError(err)
// Should have rolled back to the previous
is.Equal(updatedRes.Info.Status, release.StatusDeployed)
is.Equal(updatedRes.Info.Status, common.StatusDeployed)
}
func TestMergeCustomLabels(t *testing.T) {
@ -467,7 +503,7 @@ func TestUpgradeRelease_Labels(t *testing.T) {
"key1": "val1",
"key2": "val2.1",
}
rel.Info.Status = release.StatusDeployed
rel.Info.Status = common.StatusDeployed
err := upAction.cfg.Releases.Create(rel)
is.NoError(err)
@ -478,29 +514,35 @@ func TestUpgradeRelease_Labels(t *testing.T) {
"key3": "val3",
}
// setting newValues and upgrading
res, err := upAction.Run(rel.Name, buildChart(), nil)
resi, err := upAction.Run(rel.Name, buildChart(), nil)
is.NoError(err)
res, err := releaserToV1Release(resi)
is.NoError(err)
// Now make sure it is actually upgraded and labels were merged
updatedRes, err := upAction.cfg.Releases.Get(res.Name, 2)
updatedResi, err := upAction.cfg.Releases.Get(res.Name, 2)
is.NoError(err)
if updatedRes == nil {
if updatedResi == nil {
is.Fail("Updated Release is nil")
return
}
is.Equal(release.StatusDeployed, updatedRes.Info.Status)
updatedRes, err := releaserToV1Release(updatedResi)
is.NoError(err)
is.Equal(common.StatusDeployed, updatedRes.Info.Status)
is.Equal(mergeCustomLabels(rel.Labels, upAction.Labels), updatedRes.Labels)
// Now make sure it is suppressed release still contains original labels
initialRes, err := upAction.cfg.Releases.Get(res.Name, 1)
initialResi, err := upAction.cfg.Releases.Get(res.Name, 1)
is.NoError(err)
if initialRes == nil {
if initialResi == nil {
is.Fail("Updated Release is nil")
return
}
is.Equal(initialRes.Info.Status, release.StatusSuperseded)
initialRes, err := releaserToV1Release(initialResi)
is.NoError(err)
is.Equal(initialRes.Info.Status, common.StatusSuperseded)
is.Equal(initialRes.Labels, rel.Labels)
}
@ -515,7 +557,7 @@ func TestUpgradeRelease_SystemLabels(t *testing.T) {
"key1": "val1",
"key2": "val2.1",
}
rel.Info.Status = release.StatusDeployed
rel.Info.Status = common.StatusDeployed
err := upAction.cfg.Releases.Create(rel)
is.NoError(err)
@ -541,22 +583,26 @@ func TestUpgradeRelease_DryRun(t *testing.T) {
upAction := upgradeAction(t)
rel := releaseStub()
rel.Name = "previous-release"
rel.Info.Status = release.StatusDeployed
rel.Info.Status = common.StatusDeployed
req.NoError(upAction.cfg.Releases.Create(rel))
upAction.DryRun = true
upAction.DryRunStrategy = DryRunClient
vals := map[string]interface{}{}
ctx, done := context.WithCancel(t.Context())
res, err := upAction.RunWithContext(ctx, rel.Name, buildChart(withSampleSecret()), vals)
resi, err := upAction.RunWithContext(ctx, rel.Name, buildChart(withSampleSecret()), vals)
done()
req.NoError(err)
is.Equal(release.StatusPendingUpgrade, res.Info.Status)
res, err := releaserToV1Release(resi)
is.NoError(err)
is.Equal(common.StatusPendingUpgrade, res.Info.Status)
is.Contains(res.Manifest, "kind: Secret")
lastRelease, err := upAction.cfg.Releases.Last(rel.Name)
lastReleasei, err := upAction.cfg.Releases.Last(rel.Name)
req.NoError(err)
lastRelease, err := releaserToV1Release(lastReleasei)
req.NoError(err)
is.Equal(lastRelease.Info.Status, release.StatusDeployed)
is.Equal(lastRelease.Info.Status, common.StatusDeployed)
is.Equal(1, lastRelease.Version)
// Test the case for hiding the secret to ensure it is not displayed
@ -564,19 +610,23 @@ func TestUpgradeRelease_DryRun(t *testing.T) {
vals = map[string]interface{}{}
ctx, done = context.WithCancel(t.Context())
res, err = upAction.RunWithContext(ctx, rel.Name, buildChart(withSampleSecret()), vals)
resi, err = upAction.RunWithContext(ctx, rel.Name, buildChart(withSampleSecret()), vals)
done()
req.NoError(err)
is.Equal(release.StatusPendingUpgrade, res.Info.Status)
res, err = releaserToV1Release(resi)
is.NoError(err)
is.Equal(common.StatusPendingUpgrade, res.Info.Status)
is.NotContains(res.Manifest, "kind: Secret")
lastRelease, err = upAction.cfg.Releases.Last(rel.Name)
lastReleasei, err = upAction.cfg.Releases.Last(rel.Name)
req.NoError(err)
lastRelease, err = releaserToV1Release(lastReleasei)
req.NoError(err)
is.Equal(lastRelease.Info.Status, release.StatusDeployed)
is.Equal(lastRelease.Info.Status, common.StatusDeployed)
is.Equal(1, lastRelease.Version)
// Ensure in a dry run mode when using HideSecret
upAction.DryRun = false
upAction.DryRunStrategy = DryRunNone
vals = map[string]interface{}{}
ctx, done = context.WithCancel(t.Context())
@ -690,3 +740,18 @@ func TestGetUpgradeServerSideValue(t *testing.T) {
}
}
func TestUpgradeRun_UnreachableKubeClient(t *testing.T) {
t.Helper()
config := actionConfigFixture(t)
failingKubeClient := kubefake.FailingKubeClient{PrintingKubeClient: kubefake.PrintingKubeClient{Out: io.Discard}, DummyResources: nil}
failingKubeClient.ConnectionError = errors.New("connection refused")
config.KubeClient = &failingKubeClient
client := NewUpgrade(config)
vals := map[string]interface{}{}
result, err := client.Run("", buildChart(), vals)
assert.Nil(t, result)
assert.ErrorContains(t, err, "connection refused")
}

@ -28,7 +28,6 @@ import (
// It provides the implementation of 'helm verify'.
type Verify struct {
Keyring string
Out string
}
// NewVerify creates a new Verify object with the given configuration.
@ -37,23 +36,18 @@ func NewVerify() *Verify {
}
// Run executes 'helm verify'.
func (v *Verify) Run(chartfile string) error {
func (v *Verify) Run(chartfile string) (string, error) {
var out strings.Builder
p, err := downloader.VerifyChart(chartfile, chartfile+".prov", v.Keyring)
if err != nil {
return err
return "", err
}
for name := range p.SignedBy.Identities {
fmt.Fprintf(&out, "Signed by: %v\n", name)
_, _ = fmt.Fprintf(&out, "Signed by: %v\n", name)
}
fmt.Fprintf(&out, "Using Key With Fingerprint: %X\n", p.SignedBy.PrimaryKey.Fingerprint)
fmt.Fprintf(&out, "Chart Hash Verified: %s\n", p.FileHash)
_, _ = fmt.Fprintf(&out, "Using Key With Fingerprint: %X\n", p.SignedBy.PrimaryKey.Fingerprint)
_, _ = fmt.Fprintf(&out, "Chart Hash Verified: %s\n", p.FileHash)
// TODO(mattfarina): The output is set as a property rather than returned
// to maintain the Go API. In Helm v4 this function should return the out
// and the property on the struct can be removed.
v.Out = out.String()
return nil
return out.String(), err
}

@ -93,6 +93,14 @@ func (r *v2Accessor) Dependencies() []Charter {
return deps
}
func (r *v2Accessor) MetaDependencies() []Dependency {
var deps = make([]Dependency, len(r.chrt.Metadata.Dependencies))
for i, c := range r.chrt.Metadata.Dependencies {
deps[i] = c
}
return deps
}
func (r *v2Accessor) Values() map[string]interface{} {
return r.chrt.Values
}
@ -101,6 +109,10 @@ func (r *v2Accessor) Schema() []byte {
return r.chrt.Schema
}
func (r *v2Accessor) Deprecated() bool {
return r.chrt.Metadata.Deprecated
}
type v3Accessor struct {
chrt *v3chart.Chart
}
@ -150,6 +162,14 @@ func (r *v3Accessor) Dependencies() []Charter {
return deps
}
func (r *v3Accessor) MetaDependencies() []Dependency {
var deps = make([]Dependency, len(r.chrt.Dependencies()))
for i, c := range r.chrt.Metadata.Dependencies {
deps[i] = c
}
return deps
}
func (r *v3Accessor) Values() map[string]interface{} {
return r.chrt.Values
}
@ -158,6 +178,10 @@ func (r *v3Accessor) Schema() []byte {
return r.chrt.Schema
}
func (r *v3Accessor) Deprecated() bool {
return r.chrt.Metadata.Deprecated
}
func structToMap(obj interface{}) (map[string]interface{}, error) {
objValue := reflect.ValueOf(obj)

@ -0,0 +1,64 @@
/*
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 chart
import (
"errors"
v3chart "helm.sh/helm/v4/internal/chart/v3"
v2chart "helm.sh/helm/v4/pkg/chart/v2"
)
var NewDependencyAccessor func(dep Dependency) (DependencyAccessor, error) = NewDefaultDependencyAccessor //nolint:revive
func NewDefaultDependencyAccessor(dep Dependency) (DependencyAccessor, error) {
switch v := dep.(type) {
case v2chart.Dependency:
return &v2DependencyAccessor{&v}, nil
case *v2chart.Dependency:
return &v2DependencyAccessor{v}, nil
case v3chart.Dependency:
return &v3DependencyAccessor{&v}, nil
case *v3chart.Dependency:
return &v3DependencyAccessor{v}, nil
default:
return nil, errors.New("unsupported chart dependency type")
}
}
type v2DependencyAccessor struct {
dep *v2chart.Dependency
}
func (r *v2DependencyAccessor) Name() string {
return r.dep.Name
}
func (r *v2DependencyAccessor) Alias() string {
return r.dep.Alias
}
type v3DependencyAccessor struct {
dep *v3chart.Dependency
}
func (r *v3DependencyAccessor) Name() string {
return r.dep.Name
}
func (r *v3DependencyAccessor) Alias() string {
return r.dep.Alias
}

@ -21,6 +21,8 @@ import (
type Charter interface{}
type Dependency interface{}
type Accessor interface {
Name() string
IsRoot() bool
@ -30,6 +32,13 @@ type Accessor interface {
ChartFullPath() string
IsLibraryChart() bool
Dependencies() []Charter
MetaDependencies() []Dependency
Values() map[string]interface{}
Schema() []byte
Deprecated() bool
}
type DependencyAccessor interface {
Name() string
Alias() string
}

@ -0,0 +1,195 @@
/*
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.
*/
// archive provides utility functions for working with Helm chart archive files
package archive
import (
"archive/tar"
"bytes"
"compress/gzip"
"errors"
"fmt"
"io"
"net/http"
"os"
"path"
"regexp"
"strings"
)
// MaxDecompressedChartSize is the maximum size of a chart archive that will be
// decompressed. This is the decompressed size of all the files.
// 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.
var MaxDecompressedFileSize int64 = 5 * 1024 * 1024 // Default 5 MiB
var drivePathPattern = regexp.MustCompile(`^[a-zA-Z]:/`)
var utf8bom = []byte{0xEF, 0xBB, 0xBF}
// BufferedFile represents an archive file buffered for later processing.
type BufferedFile struct {
Name string
Data []byte
}
// LoadArchiveFiles reads in files out of an archive into memory. This function
// performs important path security checks and should always be used before
// expanding a tarball
func LoadArchiveFiles(in io.Reader) ([]*BufferedFile, error) {
unzipped, err := gzip.NewReader(in)
if err != nil {
return nil, err
}
defer unzipped.Close()
files := []*BufferedFile{}
tr := tar.NewReader(unzipped)
remainingSize := MaxDecompressedChartSize
for {
b := bytes.NewBuffer(nil)
hd, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
return nil, err
}
if hd.FileInfo().IsDir() {
// Use this instead of hd.Typeflag because we don't have to do any
// inference chasing.
continue
}
switch hd.Typeflag {
// We don't want to process these extension header files.
case tar.TypeXGlobalHeader, tar.TypeXHeader:
continue
}
// Archive could contain \ if generated on Windows
delimiter := "/"
if strings.ContainsRune(hd.Name, '\\') {
delimiter = "\\"
}
parts := strings.Split(hd.Name, delimiter)
n := strings.Join(parts[1:], delimiter)
// Normalize the path to the / delimiter
n = strings.ReplaceAll(n, delimiter, "/")
if path.IsAbs(n) {
return nil, errors.New("chart illegally contains absolute paths")
}
n = path.Clean(n)
if n == "." {
// In this case, the original path was relative when it should have been absolute.
return nil, fmt.Errorf("chart illegally contains content outside the base directory: %q", hd.Name)
}
if strings.HasPrefix(n, "..") {
return nil, errors.New("chart illegally references parent directory")
}
// In some particularly arcane acts of path creativity, it is possible to intermix
// UNIX and Windows style paths in such a way that you produce a result of the form
// c:/foo even after all the built-in absolute path checks. So we explicitly check
// for this condition.
if drivePathPattern.MatchString(n) {
return nil, errors.New("chart contains illegally named files")
}
if parts[0] == "Chart.yaml" {
return nil, errors.New("chart yaml not in base directory")
}
if hd.Size > remainingSize {
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)
if err != nil {
return nil, err
}
remainingSize -= bytesWritten
// When the bytesWritten are less than the file size it means the limit reader ended
// copying early. Here we report that error. This is important if the last file extracted
// is the one that goes over the limit. It assumes the Size stored in the tar header
// is correct, something many applications do.
if bytesWritten < hd.Size || remainingSize <= 0 {
return nil, fmt.Errorf("decompressed chart is larger than the maximum size %d", MaxDecompressedChartSize)
}
data := bytes.TrimPrefix(b.Bytes(), utf8bom)
files = append(files, &BufferedFile{Name: n, Data: data})
b.Reset()
}
if len(files) == 0 {
return nil, errors.New("no files in chart archive")
}
return files, nil
}
// ensureArchive's job is to return an informative error if the file does not appear to be a gzipped archive.
//
// Sometimes users will provide a values.yaml for an argument where a chart is expected. One common occurrence
// of this is invoking `helm template values.yaml mychart` which would otherwise produce a confusing error
// if we didn't check for this.
func EnsureArchive(name string, raw *os.File) error {
defer raw.Seek(0, 0) // reset read offset to allow archive loading to proceed.
// Check the file format to give us a chance to provide the user with more actionable feedback.
buffer := make([]byte, 512)
_, err := raw.Read(buffer)
if err != nil && err != io.EOF {
return fmt.Errorf("file '%s' cannot be read: %s", name, err)
}
// Helm may identify achieve of the application/x-gzip as application/vnd.ms-fontobject.
// Fix for: https://github.com/helm/helm/issues/12261
if contentType := http.DetectContentType(buffer); contentType != "application/x-gzip" && !isGZipApplication(buffer) {
// TODO: Is there a way to reliably test if a file content is YAML? ghodss/yaml accepts a wide
// variety of content (Makefile, .zshrc) as valid YAML without errors.
// Wrong content type. Let's check if it's yaml and give an extra hint?
if strings.HasSuffix(name, ".yml") || strings.HasSuffix(name, ".yaml") {
return fmt.Errorf("file '%s' seems to be a YAML file, but expected a gzipped archive", name)
}
return fmt.Errorf("file '%s' does not appear to be a gzipped archive; got '%s'", name, contentType)
}
return nil
}
// isGZipApplication checks whether the archive is of the application/x-gzip type.
func isGZipApplication(data []byte) bool {
sig := []byte("\x1F\x8B\x08")
return bytes.HasPrefix(data, sig)
}

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package loader
package archive
import (
"archive/tar"

@ -0,0 +1,163 @@
/*
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 loader
import (
"compress/gzip"
"errors"
"fmt"
"os"
"path/filepath"
"sigs.k8s.io/yaml"
c3 "helm.sh/helm/v4/internal/chart/v3"
c3load "helm.sh/helm/v4/internal/chart/v3/loader"
"helm.sh/helm/v4/pkg/chart"
"helm.sh/helm/v4/pkg/chart/loader/archive"
c2 "helm.sh/helm/v4/pkg/chart/v2"
c2load "helm.sh/helm/v4/pkg/chart/v2/loader"
)
// ChartLoader loads a chart.
type ChartLoader interface {
Load() (chart.Charter, error)
}
// Loader returns a new ChartLoader appropriate for the given chart name
func Loader(name string) (ChartLoader, error) {
fi, err := os.Stat(name)
if err != nil {
return nil, err
}
if fi.IsDir() {
return DirLoader(name), nil
}
return FileLoader(name), nil
}
// Load takes a string name, tries to resolve it to a file or directory, and then loads it.
//
// This is the preferred way to load a chart. It will discover the chart encoding
// and hand off to the appropriate chart reader.
//
// If a .helmignore file is present, the directory loader will skip loading any files
// matching it. But .helmignore is not evaluated when reading out of an archive.
func Load(name string) (chart.Charter, error) {
l, err := Loader(name)
if err != nil {
return nil, err
}
return l.Load()
}
// DirLoader loads a chart from a directory
type DirLoader string
// Load loads the chart
func (l DirLoader) Load() (chart.Charter, error) {
return LoadDir(string(l))
}
func LoadDir(dir string) (chart.Charter, error) {
topdir, err := filepath.Abs(dir)
if err != nil {
return nil, err
}
name := filepath.Join(topdir, "Chart.yaml")
data, err := os.ReadFile(name)
if err != nil {
return nil, fmt.Errorf("unable to detect chart at %s: %w", name, err)
}
c := new(chartBase)
err = yaml.Unmarshal(data, c)
if err != nil {
return nil, fmt.Errorf("cannot load Chart.yaml: %w", err)
}
switch c.APIVersion {
case c2.APIVersionV1, c2.APIVersionV2, "":
return c2load.Load(dir)
case c3.APIVersionV3:
return c3load.Load(dir)
default:
return nil, errors.New("unsupported chart version")
}
}
// FileLoader loads a chart from a file
type FileLoader string
// Load loads a chart
func (l FileLoader) Load() (chart.Charter, error) {
return LoadFile(string(l))
}
func LoadFile(name string) (chart.Charter, error) {
if fi, err := os.Stat(name); err != nil {
return nil, err
} else if fi.IsDir() {
return nil, errors.New("cannot load a directory")
}
raw, err := os.Open(name)
if err != nil {
return nil, err
}
defer raw.Close()
err = archive.EnsureArchive(name, raw)
if err != nil {
return nil, err
}
files, err := archive.LoadArchiveFiles(raw)
if err != nil {
if err == gzip.ErrHeader {
return nil, fmt.Errorf("file '%s' does not appear to be a valid chart file (details: %s)", name, err)
}
return nil, errors.New("unable to load chart archive")
}
for _, f := range files {
if f.Name == "Chart.yaml" {
c := new(chartBase)
if err := yaml.Unmarshal(f.Data, c); err != nil {
return c, fmt.Errorf("cannot load Chart.yaml: %w", err)
}
switch c.APIVersion {
case c2.APIVersionV1, c2.APIVersionV2, "":
return c2load.Load(name)
case c3.APIVersionV3:
return c3load.Load(name)
default:
return nil, errors.New("unsupported chart version")
}
}
}
return nil, errors.New("unable to detect chart version, no Chart.yaml found")
}
// chartBase is used to detect the API Version for the chart to run it through the
// loader for that type.
type chartBase struct {
APIVersion string `json:"apiVersion,omitempty"`
}

@ -57,8 +57,13 @@ func RunAll(baseDir string, values map[string]interface{}, namespace string, opt
}
rules.Chartfile(&result)
rules.ValuesWithOverrides(&result, values)
rules.TemplatesWithSkipSchemaValidation(&result, values, namespace, lo.KubeVersion, lo.SkipSchemaValidation)
rules.ValuesWithOverrides(&result, values, lo.SkipSchemaValidation)
rules.Templates(
&result,
namespace,
values,
rules.TemplateLinterKubeVersion(lo.KubeVersion),
rules.TemplateLinterSkipSchemaValidation(lo.SkipSchemaValidation))
rules.Dependencies(&result)
rules.Crds(&result)

@ -27,8 +27,6 @@ import (
chartutil "helm.sh/helm/v4/pkg/chart/v2/util"
)
var values map[string]interface{}
const namespace = "testNamespace"
const badChartDir = "rules/testdata/badchartfile"
@ -41,6 +39,7 @@ const malformedTemplate = "rules/testdata/malformed-template"
const invalidChartFileDir = "rules/testdata/invalidchartfile"
func TestBadChart(t *testing.T) {
var values map[string]any
m := RunAll(badChartDir, values, namespace).Messages
if len(m) != 9 {
t.Errorf("Number of errors %v", len(m))
@ -95,6 +94,7 @@ func TestBadChart(t *testing.T) {
}
func TestInvalidYaml(t *testing.T) {
var values map[string]any
m := RunAll(badYamlFileDir, values, namespace).Messages
if len(m) != 1 {
t.Fatalf("All didn't fail with expected errors, got %#v", m)
@ -105,6 +105,7 @@ func TestInvalidYaml(t *testing.T) {
}
func TestInvalidChartYaml(t *testing.T) {
var values map[string]any
m := RunAll(invalidChartFileDir, values, namespace).Messages
if len(m) != 2 {
t.Fatalf("All didn't fail with expected errors, got %#v", m)
@ -115,6 +116,7 @@ func TestInvalidChartYaml(t *testing.T) {
}
func TestBadValues(t *testing.T) {
var values map[string]any
m := RunAll(badValuesFileDir, values, namespace).Messages
if len(m) < 1 {
t.Fatalf("All didn't fail with expected errors, got %#v", m)
@ -125,6 +127,7 @@ func TestBadValues(t *testing.T) {
}
func TestBadCrdFile(t *testing.T) {
var values map[string]any
m := RunAll(badCrdFileDir, values, namespace).Messages
assert.Lenf(t, m, 2, "All didn't fail with expected errors, got %#v", m)
assert.ErrorContains(t, m[0].Err, "apiVersion is not in 'apiextensions.k8s.io'")
@ -132,6 +135,7 @@ func TestBadCrdFile(t *testing.T) {
}
func TestGoodChart(t *testing.T) {
var values map[string]any
m := RunAll(goodChartDir, values, namespace).Messages
if len(m) != 0 {
t.Error("All returned linter messages when it shouldn't have")
@ -145,6 +149,7 @@ func TestGoodChart(t *testing.T) {
//
// See https://github.com/helm/helm/issues/7923
func TestHelmCreateChart(t *testing.T) {
var values map[string]any
dir := t.TempDir()
createdChart, err := chartutil.Create("testhelmcreatepasseslint", dir)
@ -194,11 +199,11 @@ func TestHelmCreateChart_CheckDeprecatedWarnings(t *testing.T) {
// Add values to enable hpa, and ingress which are disabled by default.
// This is the equivalent of:
// helm lint checkdeprecatedwarnings --set 'autoscaling.enabled=true,ingress.enabled=true'
updatedValues := map[string]interface{}{
"autoscaling": map[string]interface{}{
updatedValues := map[string]any{
"autoscaling": map[string]any{
"enabled": true,
},
"ingress": map[string]interface{}{
"ingress": map[string]any{
"enabled": true,
},
}
@ -217,6 +222,7 @@ func TestHelmCreateChart_CheckDeprecatedWarnings(t *testing.T) {
// lint ignores import-values
// See https://github.com/helm/helm/issues/9658
func TestSubChartValuesChart(t *testing.T) {
var values map[string]any
m := RunAll(subChartValuesDir, values, namespace).Messages
if len(m) != 0 {
t.Error("All returned linter messages when it shouldn't have")
@ -229,6 +235,7 @@ func TestSubChartValuesChart(t *testing.T) {
// lint stuck with malformed template object
// See https://github.com/helm/helm/issues/11391
func TestMalformedTemplate(t *testing.T) {
var values map[string]any
c := time.After(3 * time.Second)
ch := make(chan int, 1)
var m []support.Message

@ -42,35 +42,66 @@ import (
)
// Templates lints the templates in the Linter.
func Templates(linter *support.Linter, values map[string]interface{}, namespace string, _ bool) {
TemplatesWithKubeVersion(linter, values, namespace, nil)
func Templates(linter *support.Linter, namespace string, values map[string]any, options ...TemplateLinterOption) {
templateLinter := newTemplateLinter(linter, namespace, values, options...)
templateLinter.Lint()
}
// TemplatesWithKubeVersion lints the templates in the Linter, allowing to specify the kubernetes version.
func TemplatesWithKubeVersion(linter *support.Linter, values map[string]interface{}, namespace string, kubeVersion *common.KubeVersion) {
TemplatesWithSkipSchemaValidation(linter, values, namespace, kubeVersion, false)
type TemplateLinterOption func(*templateLinter)
func TemplateLinterKubeVersion(kubeVersion *common.KubeVersion) TemplateLinterOption {
return func(tl *templateLinter) {
tl.kubeVersion = kubeVersion
}
}
func TemplateLinterSkipSchemaValidation(skipSchemaValidation bool) TemplateLinterOption {
return func(tl *templateLinter) {
tl.skipSchemaValidation = skipSchemaValidation
}
}
// TemplatesWithSkipSchemaValidation lints the templates in the Linter, allowing to specify the kubernetes version and if schema validation is enabled or not.
func TemplatesWithSkipSchemaValidation(linter *support.Linter, values map[string]interface{}, namespace string, kubeVersion *common.KubeVersion, skipSchemaValidation bool) {
fpath := "templates/"
templatesPath := filepath.Join(linter.ChartDir, fpath)
func newTemplateLinter(linter *support.Linter, namespace string, values map[string]any, options ...TemplateLinterOption) templateLinter {
result := templateLinter{
linter: linter,
values: values,
namespace: namespace,
}
// Templates directory is optional for now
templatesDirExists := linter.RunLinterRule(support.WarningSev, fpath, templatesDirExists(templatesPath))
for _, o := range options {
o(&result)
}
return result
}
type templateLinter struct {
linter *support.Linter
values map[string]any
namespace string
kubeVersion *common.KubeVersion
skipSchemaValidation bool
}
func (t *templateLinter) Lint() {
templatesDir := "templates/"
templatesPath := filepath.Join(t.linter.ChartDir, templatesDir)
templatesDirExists := t.linter.RunLinterRule(support.WarningSev, templatesDir, templatesDirExists(templatesPath))
if !templatesDirExists {
return
}
validTemplatesDir := linter.RunLinterRule(support.ErrorSev, fpath, validateTemplatesDir(templatesPath))
validTemplatesDir := t.linter.RunLinterRule(support.ErrorSev, templatesDir, validateTemplatesDir(templatesPath))
if !validTemplatesDir {
return
}
// Load chart and parse templates
chart, err := loader.Load(linter.ChartDir)
chart, err := loader.Load(t.linter.ChartDir)
chartLoaded := linter.RunLinterRule(support.ErrorSev, fpath, err)
chartLoaded := t.linter.RunLinterRule(support.ErrorSev, templatesDir, err)
if !chartLoaded {
return
@ -78,35 +109,35 @@ func TemplatesWithSkipSchemaValidation(linter *support.Linter, values map[string
options := common.ReleaseOptions{
Name: "test-release",
Namespace: namespace,
Namespace: t.namespace,
}
caps := common.DefaultCapabilities.Copy()
if kubeVersion != nil {
caps.KubeVersion = *kubeVersion
if t.kubeVersion != nil {
caps.KubeVersion = *t.kubeVersion
}
// lint ignores import-values
// See https://github.com/helm/helm/issues/9658
if err := chartutil.ProcessDependencies(chart, values); err != nil {
if err := chartutil.ProcessDependencies(chart, t.values); err != nil {
return
}
cvals, err := util.CoalesceValues(chart, values)
cvals, err := util.CoalesceValues(chart, t.values)
if err != nil {
return
}
valuesToRender, err := util.ToRenderValuesWithSchemaValidation(chart, cvals, options, caps, skipSchemaValidation)
valuesToRender, err := util.ToRenderValuesWithSchemaValidation(chart, cvals, options, caps, t.skipSchemaValidation)
if err != nil {
linter.RunLinterRule(support.ErrorSev, fpath, err)
t.linter.RunLinterRule(support.ErrorSev, templatesDir, err)
return
}
var e engine.Engine
e.LintMode = true
renderedContentMap, err := e.Render(chart, valuesToRender)
renderOk := linter.RunLinterRule(support.ErrorSev, fpath, err)
renderOk := t.linter.RunLinterRule(support.ErrorSev, templatesDir, err)
if !renderOk {
return
@ -121,9 +152,8 @@ func TemplatesWithSkipSchemaValidation(linter *support.Linter, values map[string
*/
for _, template := range chart.Templates {
fileName := template.Name
fpath = fileName
linter.RunLinterRule(support.ErrorSev, fpath, validateAllowedExtension(fileName))
t.linter.RunLinterRule(support.ErrorSev, fileName, validateAllowedExtension(fileName))
// We only apply the following lint rules to yaml files
if filepath.Ext(fileName) != ".yaml" || filepath.Ext(fileName) == ".yml" {
@ -139,7 +169,7 @@ func TemplatesWithSkipSchemaValidation(linter *support.Linter, values map[string
renderedContent := renderedContentMap[path.Join(chart.Name(), fileName)]
if strings.TrimSpace(renderedContent) != "" {
linter.RunLinterRule(support.WarningSev, fpath, validateTopIndentLevel(renderedContent))
t.linter.RunLinterRule(support.WarningSev, fileName, validateTopIndentLevel(renderedContent))
decoder := yaml.NewYAMLOrJSONDecoder(strings.NewReader(renderedContent), 4096)
@ -156,17 +186,17 @@ func TemplatesWithSkipSchemaValidation(linter *support.Linter, values map[string
// If YAML linting fails here, it will always fail in the next block as well, so we should return here.
// fix https://github.com/helm/helm/issues/11391
if !linter.RunLinterRule(support.ErrorSev, fpath, validateYamlContent(err)) {
if !t.linter.RunLinterRule(support.ErrorSev, fileName, validateYamlContent(err)) {
return
}
if yamlStruct != nil {
// NOTE: set to warnings to allow users to support out-of-date kubernetes
// Refs https://github.com/helm/helm/issues/8596
linter.RunLinterRule(support.WarningSev, fpath, validateMetadataName(yamlStruct))
linter.RunLinterRule(support.WarningSev, fpath, validateNoDeprecations(yamlStruct, kubeVersion))
t.linter.RunLinterRule(support.WarningSev, fileName, validateMetadataName(yamlStruct))
t.linter.RunLinterRule(support.WarningSev, fileName, validateNoDeprecations(yamlStruct, t.kubeVersion))
linter.RunLinterRule(support.ErrorSev, fpath, validateMatchSelector(yamlStruct, renderedContent))
linter.RunLinterRule(support.ErrorSev, fpath, validateListAnnotations(yamlStruct, renderedContent))
t.linter.RunLinterRule(support.ErrorSev, fileName, validateMatchSelector(yamlStruct, renderedContent))
t.linter.RunLinterRule(support.ErrorSev, fileName, validateListAnnotations(yamlStruct, renderedContent))
}
}
}
@ -234,6 +264,7 @@ func validateYamlContent(err error) error {
if err != nil {
return fmt.Errorf("unable to parse YAML: %w", err)
}
return nil
}

@ -51,11 +51,14 @@ func TestValidateAllowedExtension(t *testing.T) {
var values = map[string]interface{}{"nameOverride": "", "httpPort": 80}
const namespace = "testNamespace"
const strict = false
func TestTemplateParsing(t *testing.T) {
linter := support.Linter{ChartDir: templateTestBasedir}
Templates(&linter, values, namespace, strict)
Templates(
&linter,
namespace,
values,
TemplateLinterSkipSchemaValidation(false))
res := linter.Messages
if len(res) != 1 {
@ -78,7 +81,11 @@ func TestTemplateIntegrationHappyPath(t *testing.T) {
defer os.Rename(ignoredTemplatePath, wrongTemplatePath)
linter := support.Linter{ChartDir: templateTestBasedir}
Templates(&linter, values, namespace, strict)
Templates(
&linter,
namespace,
values,
TemplateLinterSkipSchemaValidation(false))
res := linter.Messages
if len(res) != 0 {
@ -88,7 +95,11 @@ func TestTemplateIntegrationHappyPath(t *testing.T) {
func TestMultiTemplateFail(t *testing.T) {
linter := support.Linter{ChartDir: "./testdata/multi-template-fail"}
Templates(&linter, values, namespace, strict)
Templates(
&linter,
namespace,
values,
TemplateLinterSkipSchemaValidation(false))
res := linter.Messages
if len(res) != 1 {
@ -208,7 +219,11 @@ func TestDeprecatedAPIFails(t *testing.T) {
}
linter := support.Linter{ChartDir: filepath.Join(tmpdir, mychart.Name())}
Templates(&linter, values, namespace, strict)
Templates(
&linter,
namespace,
values,
TemplateLinterSkipSchemaValidation(false))
if l := len(linter.Messages); l != 1 {
for i, msg := range linter.Messages {
t.Logf("Message %d: %s", i, msg)
@ -264,7 +279,11 @@ func TestStrictTemplateParsingMapError(t *testing.T) {
linter := &support.Linter{
ChartDir: filepath.Join(dir, ch.Metadata.Name),
}
Templates(linter, ch.Values, namespace, strict)
Templates(
linter,
namespace,
ch.Values,
TemplateLinterSkipSchemaValidation(false))
if len(linter.Messages) != 0 {
t.Errorf("expected zero messages, got %d", len(linter.Messages))
for i, msg := range linter.Messages {
@ -393,7 +412,11 @@ func TestEmptyWithCommentsManifests(t *testing.T) {
}
linter := support.Linter{ChartDir: filepath.Join(tmpdir, mychart.Name())}
Templates(&linter, values, namespace, strict)
Templates(
&linter,
namespace,
values,
TemplateLinterSkipSchemaValidation(false))
if l := len(linter.Messages); l > 0 {
for i, msg := range linter.Messages {
t.Logf("Message %d: %s", i, msg)

@ -32,7 +32,7 @@ import (
// they are only tested for well-formedness.
//
// If additional values are supplied, they are coalesced into the values in values.yaml.
func ValuesWithOverrides(linter *support.Linter, valueOverrides map[string]interface{}) {
func ValuesWithOverrides(linter *support.Linter, valueOverrides map[string]interface{}, skipSchemaValidation bool) {
file := "values.yaml"
vf := filepath.Join(linter.ChartDir, file)
fileExists := linter.RunLinterRule(support.InfoSev, file, validateValuesFileExistence(vf))
@ -41,7 +41,7 @@ func ValuesWithOverrides(linter *support.Linter, valueOverrides map[string]inter
return
}
linter.RunLinterRule(support.ErrorSev, file, validateValuesFile(vf, valueOverrides))
linter.RunLinterRule(support.ErrorSev, file, validateValuesFile(vf, valueOverrides, skipSchemaValidation))
}
func validateValuesFileExistence(valuesPath string) error {
@ -52,7 +52,7 @@ func validateValuesFileExistence(valuesPath string) error {
return nil
}
func validateValuesFile(valuesPath string, overrides map[string]interface{}) error {
func validateValuesFile(valuesPath string, overrides map[string]interface{}, skipSchemaValidation bool) error {
values, err := common.ReadValuesFile(valuesPath)
if err != nil {
return fmt.Errorf("unable to parse YAML: %w", err)
@ -75,5 +75,10 @@ func validateValuesFile(valuesPath string, overrides map[string]interface{}) err
if err != nil {
return err
}
return util.ValidateAgainstSingleSchema(coalescedValues, schema)
if !skipSchemaValidation {
return util.ValidateAgainstSingleSchema(coalescedValues, schema)
}
return nil
}

@ -67,7 +67,7 @@ func TestValidateValuesFileWellFormed(t *testing.T) {
`
tmpdir := ensure.TempFile(t, "values.yaml", []byte(badYaml))
valfile := filepath.Join(tmpdir, "values.yaml")
if err := validateValuesFile(valfile, map[string]interface{}{}); err == nil {
if err := validateValuesFile(valfile, map[string]interface{}{}, false); err == nil {
t.Fatal("expected values file to fail parsing")
}
}
@ -78,7 +78,7 @@ func TestValidateValuesFileSchema(t *testing.T) {
createTestingSchema(t, tmpdir)
valfile := filepath.Join(tmpdir, "values.yaml")
if err := validateValuesFile(valfile, map[string]interface{}{}); err != nil {
if err := validateValuesFile(valfile, map[string]interface{}{}, false); err != nil {
t.Fatalf("Failed validation with %s", err)
}
}
@ -91,7 +91,7 @@ func TestValidateValuesFileSchemaFailure(t *testing.T) {
valfile := filepath.Join(tmpdir, "values.yaml")
err := validateValuesFile(valfile, map[string]interface{}{})
err := validateValuesFile(valfile, map[string]interface{}{}, false)
if err == nil {
t.Fatal("expected values file to fail parsing")
}
@ -99,6 +99,20 @@ func TestValidateValuesFileSchemaFailure(t *testing.T) {
assert.Contains(t, err.Error(), "- at '/username': got number, want string")
}
func TestValidateValuesFileSchemaFailureButWithSkipSchemaValidation(t *testing.T) {
// 1234 is an int, not a string. This should fail normally but pass with skipSchemaValidation.
yaml := "username: 1234\npassword: swordfish"
tmpdir := ensure.TempFile(t, "values.yaml", []byte(yaml))
createTestingSchema(t, tmpdir)
valfile := filepath.Join(tmpdir, "values.yaml")
err := validateValuesFile(valfile, map[string]interface{}{}, true)
if err != nil {
t.Fatal("expected values file to pass parsing because of skipSchemaValidation")
}
}
func TestValidateValuesFileSchemaOverrides(t *testing.T) {
yaml := "username: admin"
overrides := map[string]interface{}{
@ -108,7 +122,7 @@ func TestValidateValuesFileSchemaOverrides(t *testing.T) {
createTestingSchema(t, tmpdir)
valfile := filepath.Join(tmpdir, "values.yaml")
if err := validateValuesFile(valfile, overrides); err != nil {
if err := validateValuesFile(valfile, overrides, false); err != nil {
t.Fatalf("Failed validation with %s", err)
}
}
@ -145,7 +159,7 @@ func TestValidateValuesFile(t *testing.T) {
valfile := filepath.Join(tmpdir, "values.yaml")
err := validateValuesFile(valfile, tt.overrides)
err := validateValuesFile(valfile, tt.overrides, false)
switch {
case err != nil && tt.errorMessage == "":

@ -17,32 +17,16 @@ limitations under the License.
package loader
import (
"archive/tar"
"bytes"
"compress/gzip"
"errors"
"fmt"
"io"
"net/http"
"os"
"path"
"regexp"
"strings"
"helm.sh/helm/v4/pkg/chart/loader/archive"
chart "helm.sh/helm/v4/pkg/chart/v2"
)
// MaxDecompressedChartSize is the maximum size of a chart archive that will be
// decompressed. This is the decompressed size of all the files.
// 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.
var MaxDecompressedFileSize int64 = 5 * 1024 * 1024 // Default 5 MiB
var drivePathPattern = regexp.MustCompile(`^[a-zA-Z]:/`)
// FileLoader loads a chart from a file
type FileLoader string
@ -65,7 +49,7 @@ func LoadFile(name string) (*chart.Chart, error) {
}
defer raw.Close()
err = ensureArchive(name, raw)
err = archive.EnsureArchive(name, raw)
if err != nil {
return nil, err
}
@ -79,153 +63,9 @@ func LoadFile(name string) (*chart.Chart, error) {
return c, err
}
// ensureArchive's job is to return an informative error if the file does not appear to be a gzipped archive.
//
// Sometimes users will provide a values.yaml for an argument where a chart is expected. One common occurrence
// of this is invoking `helm template values.yaml mychart` which would otherwise produce a confusing error
// if we didn't check for this.
func ensureArchive(name string, raw *os.File) error {
defer raw.Seek(0, 0) // reset read offset to allow archive loading to proceed.
// Check the file format to give us a chance to provide the user with more actionable feedback.
buffer := make([]byte, 512)
_, err := raw.Read(buffer)
if err != nil && err != io.EOF {
return fmt.Errorf("file '%s' cannot be read: %s", name, err)
}
// Helm may identify achieve of the application/x-gzip as application/vnd.ms-fontobject.
// Fix for: https://github.com/helm/helm/issues/12261
if contentType := http.DetectContentType(buffer); contentType != "application/x-gzip" && !isGZipApplication(buffer) {
// TODO: Is there a way to reliably test if a file content is YAML? ghodss/yaml accepts a wide
// variety of content (Makefile, .zshrc) as valid YAML without errors.
// Wrong content type. Let's check if it's yaml and give an extra hint?
if strings.HasSuffix(name, ".yml") || strings.HasSuffix(name, ".yaml") {
return fmt.Errorf("file '%s' seems to be a YAML file, but expected a gzipped archive", name)
}
return fmt.Errorf("file '%s' does not appear to be a gzipped archive; got '%s'", name, contentType)
}
return nil
}
// isGZipApplication checks whether the archive is of the application/x-gzip type.
func isGZipApplication(data []byte) bool {
sig := []byte("\x1F\x8B\x08")
return bytes.HasPrefix(data, sig)
}
// LoadArchiveFiles reads in files out of an archive into memory. This function
// performs important path security checks and should always be used before
// expanding a tarball
func LoadArchiveFiles(in io.Reader) ([]*BufferedFile, error) {
unzipped, err := gzip.NewReader(in)
if err != nil {
return nil, err
}
defer unzipped.Close()
files := []*BufferedFile{}
tr := tar.NewReader(unzipped)
remainingSize := MaxDecompressedChartSize
for {
b := bytes.NewBuffer(nil)
hd, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
return nil, err
}
if hd.FileInfo().IsDir() {
// Use this instead of hd.Typeflag because we don't have to do any
// inference chasing.
continue
}
switch hd.Typeflag {
// We don't want to process these extension header files.
case tar.TypeXGlobalHeader, tar.TypeXHeader:
continue
}
// Archive could contain \ if generated on Windows
delimiter := "/"
if strings.ContainsRune(hd.Name, '\\') {
delimiter = "\\"
}
parts := strings.Split(hd.Name, delimiter)
n := strings.Join(parts[1:], delimiter)
// Normalize the path to the / delimiter
n = strings.ReplaceAll(n, delimiter, "/")
if path.IsAbs(n) {
return nil, errors.New("chart illegally contains absolute paths")
}
n = path.Clean(n)
if n == "." {
// In this case, the original path was relative when it should have been absolute.
return nil, fmt.Errorf("chart illegally contains content outside the base directory: %q", hd.Name)
}
if strings.HasPrefix(n, "..") {
return nil, errors.New("chart illegally references parent directory")
}
// In some particularly arcane acts of path creativity, it is possible to intermix
// UNIX and Windows style paths in such a way that you produce a result of the form
// c:/foo even after all the built-in absolute path checks. So we explicitly check
// for this condition.
if drivePathPattern.MatchString(n) {
return nil, errors.New("chart contains illegally named files")
}
if parts[0] == "Chart.yaml" {
return nil, errors.New("chart yaml not in base directory")
}
if hd.Size > remainingSize {
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)
if err != nil {
return nil, err
}
remainingSize -= bytesWritten
// When the bytesWritten are less than the file size it means the limit reader ended
// copying early. Here we report that error. This is important if the last file extracted
// is the one that goes over the limit. It assumes the Size stored in the tar header
// is correct, something many applications do.
if bytesWritten < hd.Size || remainingSize <= 0 {
return nil, fmt.Errorf("decompressed chart is larger than the maximum size %d", MaxDecompressedChartSize)
}
data := bytes.TrimPrefix(b.Bytes(), utf8bom)
files = append(files, &BufferedFile{Name: n, Data: data})
b.Reset()
}
if len(files) == 0 {
return nil, errors.New("no files in chart archive")
}
return files, nil
}
// LoadArchive loads from a reader containing a compressed tar archive.
func LoadArchive(in io.Reader) (*chart.Chart, error) {
files, err := LoadArchiveFiles(in)
files, err := archive.LoadArchiveFiles(in)
if err != nil {
return nil, err
}

@ -1,92 +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 loader
import (
"archive/tar"
"bytes"
"compress/gzip"
"testing"
)
func TestLoadArchiveFiles(t *testing.T) {
tcs := []struct {
name string
generate func(w *tar.Writer)
check func(t *testing.T, files []*BufferedFile, err error)
}{
{
name: "empty input should return no files",
generate: func(_ *tar.Writer) {},
check: func(t *testing.T, _ []*BufferedFile, err error) {
t.Helper()
if err.Error() != "no files in chart archive" {
t.Fatalf(`expected "no files in chart archive", got [%#v]`, err)
}
},
},
{
name: "should ignore files with XGlobalHeader type",
generate: func(w *tar.Writer) {
// simulate the presence of a `pax_global_header` file like you would get when
// processing a GitHub release archive.
err := w.WriteHeader(&tar.Header{
Typeflag: tar.TypeXGlobalHeader,
Name: "pax_global_header",
})
if err != nil {
t.Fatal(err)
}
// we need to have at least one file, otherwise we'll get the "no files in chart archive" error
err = w.WriteHeader(&tar.Header{
Typeflag: tar.TypeReg,
Name: "dir/empty",
})
if err != nil {
t.Fatal(err)
}
},
check: func(t *testing.T, files []*BufferedFile, err error) {
t.Helper()
if err != nil {
t.Fatalf(`got unwanted error [%#v] for tar file with pax_global_header content`, err)
}
if len(files) != 1 {
t.Fatalf(`expected to get one file but got [%v]`, files)
}
},
},
}
for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {
buf := &bytes.Buffer{}
gzw := gzip.NewWriter(buf)
tw := tar.NewWriter(gzw)
tc.generate(tw)
_ = tw.Close()
_ = gzw.Close()
files, err := LoadArchiveFiles(buf)
tc.check(t, files, err)
})
}
}

@ -24,6 +24,7 @@ import (
"strings"
"helm.sh/helm/v4/internal/sympath"
"helm.sh/helm/v4/pkg/chart/loader/archive"
chart "helm.sh/helm/v4/pkg/chart/v2"
"helm.sh/helm/v4/pkg/ignore"
)
@ -61,7 +62,7 @@ func LoadDir(dir string) (*chart.Chart, error) {
}
rules.AddDefaults()
files := []*BufferedFile{}
files := []*archive.BufferedFile{}
topdir += string(filepath.Separator)
walk := func(name string, fi os.FileInfo, err error) error {
@ -99,8 +100,8 @@ 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() > MaxDecompressedFileSize {
return fmt.Errorf("chart file %q is larger than the maximum file size %d", fi.Name(), MaxDecompressedFileSize)
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)
@ -110,7 +111,7 @@ func LoadDir(dir string) (*chart.Chart, error) {
data = bytes.TrimPrefix(data, utf8bom)
files = append(files, &BufferedFile{Name: n, Data: data})
files = append(files, &archive.BufferedFile{Name: n, Data: data})
return nil
}
if err = sympath.Walk(topdir, walk); err != nil {

@ -32,6 +32,7 @@ import (
"sigs.k8s.io/yaml"
"helm.sh/helm/v4/pkg/chart/common"
"helm.sh/helm/v4/pkg/chart/loader/archive"
chart "helm.sh/helm/v4/pkg/chart/v2"
)
@ -67,16 +68,10 @@ func Load(name string) (*chart.Chart, error) {
return l.Load()
}
// BufferedFile represents an archive file buffered for later processing.
type BufferedFile struct {
Name string
Data []byte
}
// LoadFiles loads from in-memory files.
func LoadFiles(files []*BufferedFile) (*chart.Chart, error) {
func LoadFiles(files []*archive.BufferedFile) (*chart.Chart, error) {
c := new(chart.Chart)
subcharts := make(map[string][]*BufferedFile)
subcharts := make(map[string][]*archive.BufferedFile)
// do not rely on assumed ordering of files in the chart and crash
// if Chart.yaml was not coming early enough to initialize metadata
@ -157,7 +152,7 @@ func LoadFiles(files []*BufferedFile) (*chart.Chart, error) {
fname := strings.TrimPrefix(f.Name, "charts/")
cname := strings.SplitN(fname, "/", 2)[0]
subcharts[cname] = append(subcharts[cname], &BufferedFile{Name: fname, Data: f.Data})
subcharts[cname] = append(subcharts[cname], &archive.BufferedFile{Name: fname, Data: f.Data})
default:
c.Files = append(c.Files, &common.File{Name: f.Name, Data: f.Data})
}
@ -187,7 +182,7 @@ func LoadFiles(files []*BufferedFile) (*chart.Chart, error) {
default:
// We have to trim the prefix off of every file, and ignore any file
// that is in charts/, but isn't actually a chart.
buff := make([]*BufferedFile, 0, len(files))
buff := make([]*archive.BufferedFile, 0, len(files))
for _, f := range files {
parts := strings.SplitN(f.Name, "/", 2)
if len(parts) < 2 {

@ -31,6 +31,7 @@ import (
"time"
"helm.sh/helm/v4/pkg/chart/common"
"helm.sh/helm/v4/pkg/chart/loader/archive"
chart "helm.sh/helm/v4/pkg/chart/v2"
)
@ -211,12 +212,12 @@ func TestLoadFile(t *testing.T) {
func TestLoadFiles_BadCases(t *testing.T) {
for _, tt := range []struct {
name string
bufferedFiles []*BufferedFile
bufferedFiles []*archive.BufferedFile
expectError string
}{
{
name: "These files contain only requirements.lock",
bufferedFiles: []*BufferedFile{
bufferedFiles: []*archive.BufferedFile{
{
Name: "requirements.lock",
Data: []byte(""),
@ -235,7 +236,7 @@ func TestLoadFiles_BadCases(t *testing.T) {
}
func TestLoadFiles(t *testing.T) {
goodFiles := []*BufferedFile{
goodFiles := []*archive.BufferedFile{
{
Name: "Chart.yaml",
Data: []byte(`apiVersion: v1
@ -300,7 +301,7 @@ icon: https://example.com/64x64.png
t.Errorf("Expected number of templates == 2, got %d", len(c.Templates))
}
if _, err = LoadFiles([]*BufferedFile{}); err == nil {
if _, err = LoadFiles([]*archive.BufferedFile{}); err == nil {
t.Fatal("Expected err to be non-nil")
}
if err.Error() != "Chart.yaml file is missing" {
@ -311,7 +312,7 @@ icon: https://example.com/64x64.png
// Test the order of file loading. The Chart.yaml file needs to come first for
// later comparison checks. See https://github.com/helm/helm/pull/8948
func TestLoadFilesOrder(t *testing.T) {
goodFiles := []*BufferedFile{
goodFiles := []*archive.BufferedFile{
{
Name: "requirements.yaml",
Data: []byte("dependencies:"),

@ -26,13 +26,13 @@ import (
securejoin "github.com/cyphar/filepath-securejoin"
"sigs.k8s.io/yaml"
"helm.sh/helm/v4/pkg/chart/loader/archive"
chart "helm.sh/helm/v4/pkg/chart/v2"
"helm.sh/helm/v4/pkg/chart/v2/loader"
)
// Expand uncompresses and extracts a chart into the specified directory.
func Expand(dir string, r io.Reader) error {
files, err := loader.LoadArchiveFiles(r)
files, err := archive.LoadArchiveFiles(r)
if err != nil {
return err
}

@ -22,6 +22,7 @@ import (
"testing"
chart "helm.sh/helm/v4/pkg/chart/v2"
"helm.sh/helm/v4/pkg/release/common"
release "helm.sh/helm/v4/pkg/release/v1"
)
@ -31,7 +32,7 @@ func checkFileCompletion(t *testing.T, cmdName string, shouldBePerformed bool) {
storage := storageFixture()
storage.Create(&release.Release{
Name: "myrelease",
Info: &release.Info{Status: release.StatusDeployed},
Info: &release.Info{Status: common.StatusDeployed},
Chart: &chart.Chart{
Metadata: &chart.Metadata{
Name: "Myrelease-Chart",

@ -19,19 +19,20 @@ package cmd
import (
"fmt"
"testing"
"time"
"github.com/stretchr/testify/require"
"helm.sh/helm/v4/pkg/action"
chart "helm.sh/helm/v4/pkg/chart/v2"
"helm.sh/helm/v4/pkg/release/common"
release "helm.sh/helm/v4/pkg/release/v1"
helmtime "helm.sh/helm/v4/pkg/time"
)
func outputFlagCompletionTest(t *testing.T, cmdName string) {
t.Helper()
releasesMockWithStatus := func(info *release.Info, hooks ...*release.Hook) []*release.Release {
info.LastDeployed = helmtime.Unix(1452902400, 0).UTC()
info.LastDeployed = time.Unix(1452902400, 0).UTC()
return []*release.Release{{
Name: "athos",
Namespace: "default",
@ -64,35 +65,35 @@ func outputFlagCompletionTest(t *testing.T, cmdName string) {
cmd: fmt.Sprintf("__complete %s --output ''", cmdName),
golden: "output/output-comp.txt",
rels: releasesMockWithStatus(&release.Info{
Status: release.StatusDeployed,
Status: common.StatusDeployed,
}),
}, {
name: "completion for output flag long and after arg",
cmd: fmt.Sprintf("__complete %s aramis --output ''", cmdName),
golden: "output/output-comp.txt",
rels: releasesMockWithStatus(&release.Info{
Status: release.StatusDeployed,
Status: common.StatusDeployed,
}),
}, {
name: "completion for output flag short and before arg",
cmd: fmt.Sprintf("__complete %s -o ''", cmdName),
golden: "output/output-comp.txt",
rels: releasesMockWithStatus(&release.Info{
Status: release.StatusDeployed,
Status: common.StatusDeployed,
}),
}, {
name: "completion for output flag short and after arg",
cmd: fmt.Sprintf("__complete %s aramis -o ''", cmdName),
golden: "output/output-comp.txt",
rels: releasesMockWithStatus(&release.Info{
Status: release.StatusDeployed,
Status: common.StatusDeployed,
}),
}, {
name: "completion for output flag, no filter",
cmd: fmt.Sprintf("__complete %s --output jso", cmdName),
golden: "output/output-comp.txt",
rels: releasesMockWithStatus(&release.Info{
Status: release.StatusDeployed,
Status: common.StatusDeployed,
}),
}}
runTestCmd(t, tests)

@ -25,6 +25,7 @@ import (
"helm.sh/helm/v4/pkg/action"
"helm.sh/helm/v4/pkg/cmd/require"
"helm.sh/helm/v4/pkg/release"
)
const getHooksHelp = `
@ -52,8 +53,16 @@ func newGetHooksCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
if err != nil {
return err
}
for _, hook := range res.Hooks {
fmt.Fprintf(out, "---\n# Source: %s\n%s\n", hook.Path, hook.Manifest)
rac, err := release.NewAccessor(res)
if err != nil {
return err
}
for _, hook := range rac.Hooks() {
hac, err := release.NewHookAccessor(hook)
if err != nil {
return err
}
fmt.Fprintf(out, "---\n# Source: %s\n%s\n", hac.Path(), hac.Manifest())
}
return nil
},

@ -25,6 +25,7 @@ import (
"helm.sh/helm/v4/pkg/action"
"helm.sh/helm/v4/pkg/cmd/require"
"helm.sh/helm/v4/pkg/release"
)
var getManifestHelp = `
@ -54,7 +55,11 @@ func newGetManifestCmd(cfg *action.Configuration, out io.Writer) *cobra.Command
if err != nil {
return err
}
fmt.Fprintln(out, res.Manifest)
rac, err := release.NewAccessor(res)
if err != nil {
return err
}
fmt.Fprintln(out, rac.Manifest())
return nil
},
}

@ -25,6 +25,7 @@ import (
"helm.sh/helm/v4/pkg/action"
"helm.sh/helm/v4/pkg/cmd/require"
"helm.sh/helm/v4/pkg/release"
)
var getNotesHelp = `
@ -50,8 +51,12 @@ func newGetNotesCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
if err != nil {
return err
}
if len(res.Info.Notes) > 0 {
fmt.Fprintf(out, "NOTES:\n%s\n", res.Info.Notes)
rac, err := release.NewAccessor(res)
if err != nil {
return err
}
if len(rac.Notes()) > 0 {
fmt.Fprintf(out, "NOTES:\n%s\n", rac.Notes())
}
return nil
},

@ -0,0 +1,83 @@
/*
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 (
"fmt"
"log/slog"
"strconv"
"github.com/spf13/cobra"
"helm.sh/helm/v4/pkg/action"
)
func addDryRunFlag(cmd *cobra.Command) {
// --dry-run options with expected outcome:
// - Not set means no dry run and server is contacted.
// - Set with no value, a value of client, or a value of true and the server is not contacted
// - Set with a value of false, none, or false and the server is contacted
// The true/false part is meant to reflect some legacy behavior while none is equal to "".
f := cmd.Flags()
f.String(
"dry-run",
"none",
`simulates the operation without persisting changes. Must be one of: "none" (default), "client", or "server". '--dry-run=none' executes the operation normally and persists changes (no simulation). '--dry-run=client' simulates the operation client-side only and avoids cluster connections. '--dry-run=server' simulates the operation on the server, requiring cluster connectivity.`)
f.Lookup("dry-run").NoOptDefVal = "unset"
}
// Determine the `action.DryRunStrategy` given -dry-run=<value>` flag (or absence of)
// Legacy usage of the flag: boolean values, and `--dry-run` (without value) are supported, and log warnings emitted
func cmdGetDryRunFlagStrategy(cmd *cobra.Command, isTemplate bool) (action.DryRunStrategy, error) {
f := cmd.Flag("dry-run")
v := f.Value.String()
switch v {
case f.NoOptDefVal:
slog.Warn(`--dry-run is deprecated and should be replaced with '--dry-run=client'`)
return action.DryRunClient, nil
case string(action.DryRunClient):
return action.DryRunClient, nil
case string(action.DryRunServer):
return action.DryRunServer, nil
case string(action.DryRunNone):
if isTemplate {
// Special case hack for `helm template`, which is always a dry run
return action.DryRunNone, fmt.Errorf(`invalid dry-run value (%q). Must be "server" or "client"`, v)
}
return action.DryRunNone, nil
}
b, err := strconv.ParseBool(v)
if err != nil {
return action.DryRunNone, fmt.Errorf(`invalid dry-run value (%q). Must be "none", "server", or "client"`, v)
}
if isTemplate && !b {
// Special case for `helm template`, which is always a dry run
return action.DryRunNone, fmt.Errorf(`invalid dry-run value (%q). Must be "server" or "client"`, v)
}
result := action.DryRunNone
if b {
result = action.DryRunClient
}
slog.Warn(fmt.Sprintf(`boolean '--dry-run=%v' flag is deprecated and must be replaced with '--dry-run=%s'`, v, result))
return result, nil
}

@ -18,13 +18,18 @@ package cmd
import (
"bytes"
"encoding/json"
"io"
"log/slog"
"os"
"strings"
"testing"
"time"
shellwords "github.com/mattn/go-shellwords"
"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"helm.sh/helm/v4/internal/test"
"helm.sh/helm/v4/pkg/action"
@ -34,7 +39,6 @@ import (
release "helm.sh/helm/v4/pkg/release/v1"
"helm.sh/helm/v4/pkg/storage"
"helm.sh/helm/v4/pkg/storage/driver"
"helm.sh/helm/v4/pkg/time"
)
func testTimestamper() time.Time { return time.Unix(242085845, 0).UTC() }
@ -151,3 +155,155 @@ func resetEnv() func() {
settings = cli.New()
}
}
func TestCmdGetDryRunFlagStrategy(t *testing.T) {
type testCaseExpectedLog struct {
Level string
Msg string
}
testCases := map[string]struct {
DryRunFlagArg string
IsTemplate bool
ExpectedStrategy action.DryRunStrategy
ExpectedError bool
ExpectedLog *testCaseExpectedLog
}{
"unset_value": {
DryRunFlagArg: "--dry-run",
ExpectedStrategy: action.DryRunClient,
ExpectedLog: &testCaseExpectedLog{
Level: "WARN",
Msg: `--dry-run is deprecated and should be replaced with '--dry-run=client'`,
},
},
"unset_special": {
DryRunFlagArg: "--dry-run=unset", // Special value that matches cmd.Flags("dry-run").NoOptDefVal
ExpectedStrategy: action.DryRunClient,
ExpectedLog: &testCaseExpectedLog{
Level: "WARN",
Msg: `--dry-run is deprecated and should be replaced with '--dry-run=client'`,
},
},
"none": {
DryRunFlagArg: "--dry-run=none",
ExpectedStrategy: action.DryRunNone,
},
"client": {
DryRunFlagArg: "--dry-run=client",
ExpectedStrategy: action.DryRunClient,
},
"server": {
DryRunFlagArg: "--dry-run=server",
ExpectedStrategy: action.DryRunServer,
},
"bool_false": {
DryRunFlagArg: "--dry-run=false",
ExpectedStrategy: action.DryRunNone,
ExpectedLog: &testCaseExpectedLog{
Level: "WARN",
Msg: `boolean '--dry-run=false' flag is deprecated and must be replaced with '--dry-run=none'`,
},
},
"bool_true": {
DryRunFlagArg: "--dry-run=true",
ExpectedStrategy: action.DryRunClient,
ExpectedLog: &testCaseExpectedLog{
Level: "WARN",
Msg: `boolean '--dry-run=true' flag is deprecated and must be replaced with '--dry-run=client'`,
},
},
"bool_0": {
DryRunFlagArg: "--dry-run=0",
ExpectedStrategy: action.DryRunNone,
ExpectedLog: &testCaseExpectedLog{
Level: "WARN",
Msg: `boolean '--dry-run=0' flag is deprecated and must be replaced with '--dry-run=none'`,
},
},
"bool_1": {
DryRunFlagArg: "--dry-run=1",
ExpectedStrategy: action.DryRunClient,
ExpectedLog: &testCaseExpectedLog{
Level: "WARN",
Msg: `boolean '--dry-run=1' flag is deprecated and must be replaced with '--dry-run=client'`,
},
},
"invalid": {
DryRunFlagArg: "--dry-run=invalid",
ExpectedError: true,
},
"template_unset_value": {
DryRunFlagArg: "--dry-run",
IsTemplate: true,
ExpectedStrategy: action.DryRunClient,
ExpectedLog: &testCaseExpectedLog{
Level: "WARN",
Msg: `--dry-run is deprecated and should be replaced with '--dry-run=client'`,
},
},
"template_bool_false": {
DryRunFlagArg: "--dry-run=false",
IsTemplate: true,
ExpectedError: true,
},
"template_bool_template_true": {
DryRunFlagArg: "--dry-run=true",
IsTemplate: true,
ExpectedStrategy: action.DryRunClient,
ExpectedLog: &testCaseExpectedLog{
Level: "WARN",
Msg: `boolean '--dry-run=true' flag is deprecated and must be replaced with '--dry-run=client'`,
},
},
"template_none": {
DryRunFlagArg: "--dry-run=none",
IsTemplate: true,
ExpectedError: true,
},
"template_client": {
DryRunFlagArg: "--dry-run=client",
IsTemplate: true,
ExpectedStrategy: action.DryRunClient,
},
"template_server": {
DryRunFlagArg: "--dry-run=server",
IsTemplate: true,
ExpectedStrategy: action.DryRunServer,
},
}
for name, tc := range testCases {
logBuf := new(bytes.Buffer)
logger := slog.New(slog.NewJSONHandler(logBuf, nil))
slog.SetDefault(logger)
cmd := &cobra.Command{
Use: "helm",
}
addDryRunFlag(cmd)
cmd.Flags().Parse([]string{"helm", tc.DryRunFlagArg})
t.Run(name, func(t *testing.T) {
dryRunStrategy, err := cmdGetDryRunFlagStrategy(cmd, tc.IsTemplate)
if tc.ExpectedError {
assert.Error(t, err)
} else {
assert.Nil(t, err)
assert.Equal(t, tc.ExpectedStrategy, dryRunStrategy)
}
if tc.ExpectedLog != nil {
logResult := map[string]string{}
err = json.Unmarshal(logBuf.Bytes(), &logResult)
require.Nil(t, err)
assert.Equal(t, tc.ExpectedLog.Level, logResult["level"])
assert.Equal(t, tc.ExpectedLog.Msg, logResult["msg"])
} else {
assert.Equal(t, 0, logBuf.Len())
}
})
}
}

@ -17,6 +17,7 @@ limitations under the License.
package cmd
import (
"encoding/json"
"fmt"
"io"
"strconv"
@ -31,7 +32,6 @@ import (
"helm.sh/helm/v4/pkg/cmd/require"
release "helm.sh/helm/v4/pkg/release/v1"
releaseutil "helm.sh/helm/v4/pkg/release/v1/util"
helmtime "helm.sh/helm/v4/pkg/time"
)
var historyHelp = `
@ -84,12 +84,81 @@ func newHistoryCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
}
type releaseInfo struct {
Revision int `json:"revision"`
Updated helmtime.Time `json:"updated"`
Status string `json:"status"`
Chart string `json:"chart"`
AppVersion string `json:"app_version"`
Description string `json:"description"`
Revision int `json:"revision"`
Updated time.Time `json:"updated,omitzero"`
Status string `json:"status"`
Chart string `json:"chart"`
AppVersion string `json:"app_version"`
Description string `json:"description"`
}
// releaseInfoJSON is used for custom JSON marshaling/unmarshaling
type releaseInfoJSON struct {
Revision int `json:"revision"`
Updated *time.Time `json:"updated,omitempty"`
Status string `json:"status"`
Chart string `json:"chart"`
AppVersion string `json:"app_version"`
Description string `json:"description"`
}
// UnmarshalJSON implements the json.Unmarshaler interface.
// It handles empty string time fields by treating them as zero values.
func (r *releaseInfo) UnmarshalJSON(data []byte) error {
// First try to unmarshal into a map to handle empty string time fields
var raw map[string]interface{}
if err := json.Unmarshal(data, &raw); err != nil {
return err
}
// Replace empty string time fields with nil
if val, ok := raw["updated"]; ok {
if str, ok := val.(string); ok && str == "" {
raw["updated"] = nil
}
}
// Re-marshal with cleaned data
cleaned, err := json.Marshal(raw)
if err != nil {
return err
}
// Unmarshal into temporary struct with pointer time field
var tmp releaseInfoJSON
if err := json.Unmarshal(cleaned, &tmp); err != nil {
return err
}
// Copy values to releaseInfo struct
r.Revision = tmp.Revision
if tmp.Updated != nil {
r.Updated = *tmp.Updated
}
r.Status = tmp.Status
r.Chart = tmp.Chart
r.AppVersion = tmp.AppVersion
r.Description = tmp.Description
return nil
}
// MarshalJSON implements the json.Marshaler interface.
// It omits zero-value time fields from the JSON output.
func (r releaseInfo) MarshalJSON() ([]byte, error) {
tmp := releaseInfoJSON{
Revision: r.Revision,
Status: r.Status,
Chart: r.Chart,
AppVersion: r.AppVersion,
Description: r.Description,
}
if !r.Updated.IsZero() {
tmp.Updated = &r.Updated
}
return json.Marshal(tmp)
}
type releaseHistory []releaseInfo
@ -112,7 +181,11 @@ func (r releaseHistory) WriteTable(out io.Writer) error {
}
func getHistory(client *action.History, name string) (releaseHistory, error) {
hist, err := client.Run(name)
histi, err := client.Run(name)
if err != nil {
return nil, err
}
hist, err := releaseListToV1List(histi)
if err != nil {
return nil, err
}
@ -181,7 +254,11 @@ func compListRevisions(_ string, cfg *action.Configuration, releaseName string)
client := action.NewHistory(cfg)
var revisions []string
if hist, err := client.Run(releaseName); err == nil {
if histi, err := client.Run(releaseName); err == nil {
hist, err := releaseListToV1List(histi)
if err != nil {
return nil, cobra.ShellCompDirectiveError
}
for _, version := range hist {
appVersion := fmt.Sprintf("App: %s", version.Chart.Metadata.AppVersion)
chartDesc := fmt.Sprintf("Chart: %s-%s", version.Chart.Metadata.Name, version.Chart.Metadata.Version)

@ -17,14 +17,20 @@ limitations under the License.
package cmd
import (
"encoding/json"
"fmt"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"helm.sh/helm/v4/pkg/release/common"
release "helm.sh/helm/v4/pkg/release/v1"
)
func TestHistoryCmd(t *testing.T) {
mk := func(name string, vers int, status release.Status) *release.Release {
mk := func(name string, vers int, status common.Status) *release.Release {
return release.Mock(&release.MockReleaseOptions{
Name: name,
Version: vers,
@ -36,34 +42,34 @@ func TestHistoryCmd(t *testing.T) {
name: "get history for release",
cmd: "history angry-bird",
rels: []*release.Release{
mk("angry-bird", 4, release.StatusDeployed),
mk("angry-bird", 3, release.StatusSuperseded),
mk("angry-bird", 2, release.StatusSuperseded),
mk("angry-bird", 1, release.StatusSuperseded),
mk("angry-bird", 4, common.StatusDeployed),
mk("angry-bird", 3, common.StatusSuperseded),
mk("angry-bird", 2, common.StatusSuperseded),
mk("angry-bird", 1, common.StatusSuperseded),
},
golden: "output/history.txt",
}, {
name: "get history with max limit set",
cmd: "history angry-bird --max 2",
rels: []*release.Release{
mk("angry-bird", 4, release.StatusDeployed),
mk("angry-bird", 3, release.StatusSuperseded),
mk("angry-bird", 4, common.StatusDeployed),
mk("angry-bird", 3, common.StatusSuperseded),
},
golden: "output/history-limit.txt",
}, {
name: "get history with yaml output format",
cmd: "history angry-bird --output yaml",
rels: []*release.Release{
mk("angry-bird", 4, release.StatusDeployed),
mk("angry-bird", 3, release.StatusSuperseded),
mk("angry-bird", 4, common.StatusDeployed),
mk("angry-bird", 3, common.StatusSuperseded),
},
golden: "output/history.yaml",
}, {
name: "get history with json output format",
cmd: "history angry-bird --output json",
rels: []*release.Release{
mk("angry-bird", 4, release.StatusDeployed),
mk("angry-bird", 3, release.StatusSuperseded),
mk("angry-bird", 4, common.StatusDeployed),
mk("angry-bird", 3, common.StatusSuperseded),
},
golden: "output/history.json",
}}
@ -76,7 +82,7 @@ func TestHistoryOutputCompletion(t *testing.T) {
func revisionFlagCompletionTest(t *testing.T, cmdName string) {
t.Helper()
mk := func(name string, vers int, status release.Status) *release.Release {
mk := func(name string, vers int, status common.Status) *release.Release {
return release.Mock(&release.MockReleaseOptions{
Name: name,
Version: vers,
@ -85,10 +91,10 @@ func revisionFlagCompletionTest(t *testing.T, cmdName string) {
}
releases := []*release.Release{
mk("musketeers", 11, release.StatusDeployed),
mk("musketeers", 10, release.StatusSuperseded),
mk("musketeers", 9, release.StatusSuperseded),
mk("musketeers", 8, release.StatusSuperseded),
mk("musketeers", 11, common.StatusDeployed),
mk("musketeers", 10, common.StatusSuperseded),
mk("musketeers", 9, common.StatusSuperseded),
mk("musketeers", 8, common.StatusSuperseded),
}
tests := []cmdTestCase{{
@ -123,3 +129,205 @@ func TestHistoryFileCompletion(t *testing.T) {
checkFileCompletion(t, "history", false)
checkFileCompletion(t, "history myrelease", false)
}
func TestReleaseInfoMarshalJSON(t *testing.T) {
updated := time.Date(2025, 10, 8, 12, 0, 0, 0, time.UTC)
tests := []struct {
name string
info releaseInfo
expected string
}{
{
name: "all fields populated",
info: releaseInfo{
Revision: 1,
Updated: updated,
Status: "deployed",
Chart: "mychart-1.0.0",
AppVersion: "1.0.0",
Description: "Initial install",
},
expected: `{"revision":1,"updated":"2025-10-08T12:00:00Z","status":"deployed","chart":"mychart-1.0.0","app_version":"1.0.0","description":"Initial install"}`,
},
{
name: "without updated time",
info: releaseInfo{
Revision: 2,
Status: "superseded",
Chart: "mychart-1.0.1",
AppVersion: "1.0.1",
Description: "Upgraded",
},
expected: `{"revision":2,"status":"superseded","chart":"mychart-1.0.1","app_version":"1.0.1","description":"Upgraded"}`,
},
{
name: "with zero revision",
info: releaseInfo{
Revision: 0,
Updated: updated,
Status: "failed",
Chart: "mychart-1.0.0",
AppVersion: "1.0.0",
Description: "Install failed",
},
expected: `{"revision":0,"updated":"2025-10-08T12:00:00Z","status":"failed","chart":"mychart-1.0.0","app_version":"1.0.0","description":"Install failed"}`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
data, err := json.Marshal(&tt.info)
require.NoError(t, err)
assert.JSONEq(t, tt.expected, string(data))
})
}
}
func TestReleaseInfoUnmarshalJSON(t *testing.T) {
updated := time.Date(2025, 10, 8, 12, 0, 0, 0, time.UTC)
tests := []struct {
name string
input string
expected releaseInfo
wantErr bool
}{
{
name: "all fields populated",
input: `{"revision":1,"updated":"2025-10-08T12:00:00Z","status":"deployed","chart":"mychart-1.0.0","app_version":"1.0.0","description":"Initial install"}`,
expected: releaseInfo{
Revision: 1,
Updated: updated,
Status: "deployed",
Chart: "mychart-1.0.0",
AppVersion: "1.0.0",
Description: "Initial install",
},
},
{
name: "empty string updated field",
input: `{"revision":2,"updated":"","status":"superseded","chart":"mychart-1.0.1","app_version":"1.0.1","description":"Upgraded"}`,
expected: releaseInfo{
Revision: 2,
Status: "superseded",
Chart: "mychart-1.0.1",
AppVersion: "1.0.1",
Description: "Upgraded",
},
},
{
name: "missing updated field",
input: `{"revision":3,"status":"deployed","chart":"mychart-1.0.2","app_version":"1.0.2","description":"Upgraded"}`,
expected: releaseInfo{
Revision: 3,
Status: "deployed",
Chart: "mychart-1.0.2",
AppVersion: "1.0.2",
Description: "Upgraded",
},
},
{
name: "null updated field",
input: `{"revision":4,"updated":null,"status":"failed","chart":"mychart-1.0.3","app_version":"1.0.3","description":"Failed"}`,
expected: releaseInfo{
Revision: 4,
Status: "failed",
Chart: "mychart-1.0.3",
AppVersion: "1.0.3",
Description: "Failed",
},
},
{
name: "invalid time format",
input: `{"revision":5,"updated":"invalid-time","status":"deployed","chart":"mychart-1.0.4","app_version":"1.0.4","description":"Test"}`,
wantErr: true,
},
{
name: "zero revision",
input: `{"revision":0,"updated":"2025-10-08T12:00:00Z","status":"pending-install","chart":"mychart-1.0.0","app_version":"1.0.0","description":"Installing"}`,
expected: releaseInfo{
Revision: 0,
Updated: updated,
Status: "pending-install",
Chart: "mychart-1.0.0",
AppVersion: "1.0.0",
Description: "Installing",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var info releaseInfo
err := json.Unmarshal([]byte(tt.input), &info)
if tt.wantErr {
assert.Error(t, err)
return
}
require.NoError(t, err)
assert.Equal(t, tt.expected.Revision, info.Revision)
assert.Equal(t, tt.expected.Updated.Unix(), info.Updated.Unix())
assert.Equal(t, tt.expected.Status, info.Status)
assert.Equal(t, tt.expected.Chart, info.Chart)
assert.Equal(t, tt.expected.AppVersion, info.AppVersion)
assert.Equal(t, tt.expected.Description, info.Description)
})
}
}
func TestReleaseInfoRoundTrip(t *testing.T) {
updated := time.Date(2025, 10, 8, 12, 0, 0, 0, time.UTC)
original := releaseInfo{
Revision: 1,
Updated: updated,
Status: "deployed",
Chart: "mychart-1.0.0",
AppVersion: "1.0.0",
Description: "Initial install",
}
data, err := json.Marshal(&original)
require.NoError(t, err)
var decoded releaseInfo
err = json.Unmarshal(data, &decoded)
require.NoError(t, err)
assert.Equal(t, original.Revision, decoded.Revision)
assert.Equal(t, original.Updated.Unix(), decoded.Updated.Unix())
assert.Equal(t, original.Status, decoded.Status)
assert.Equal(t, original.Chart, decoded.Chart)
assert.Equal(t, original.AppVersion, decoded.AppVersion)
assert.Equal(t, original.Description, decoded.Description)
}
func TestReleaseInfoEmptyStringRoundTrip(t *testing.T) {
// This test specifically verifies that empty string time fields
// are handled correctly during parsing
input := `{"revision":1,"updated":"","status":"deployed","chart":"mychart-1.0.0","app_version":"1.0.0","description":"Test"}`
var info releaseInfo
err := json.Unmarshal([]byte(input), &info)
require.NoError(t, err)
// Verify time field is zero value
assert.True(t, info.Updated.IsZero())
assert.Equal(t, 1, info.Revision)
assert.Equal(t, "deployed", info.Status)
// Marshal back and verify empty time field is omitted
data, err := json.Marshal(&info)
require.NoError(t, err)
var result map[string]interface{}
err = json.Unmarshal(data, &result)
require.NoError(t, err)
// Zero time value should be omitted
assert.NotContains(t, result, "updated")
assert.Equal(t, float64(1), result["revision"])
assert.Equal(t, "deployed", result["status"])
assert.Equal(t, "mychart-1.0.0", result["chart"])
}

@ -18,14 +18,12 @@ package cmd
import (
"context"
"errors"
"fmt"
"io"
"log"
"log/slog"
"os"
"os/signal"
"slices"
"syscall"
"time"
@ -33,8 +31,8 @@ import (
"github.com/spf13/pflag"
"helm.sh/helm/v4/pkg/action"
chart "helm.sh/helm/v4/pkg/chart/v2"
"helm.sh/helm/v4/pkg/chart/v2/loader"
"helm.sh/helm/v4/pkg/chart"
"helm.sh/helm/v4/pkg/chart/loader"
"helm.sh/helm/v4/pkg/cli/output"
"helm.sh/helm/v4/pkg/cli/values"
"helm.sh/helm/v4/pkg/cmd/require"
@ -144,7 +142,7 @@ func newInstallCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
ValidArgsFunction: func(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return compInstall(args, toComplete, client)
},
RunE: func(_ *cobra.Command, args []string) error {
RunE: func(cmd *cobra.Command, args []string) error {
registryClient, err := newRegistryClient(client.CertFile, client.KeyFile, client.CaFile,
client.InsecureSkipTLSverify, client.PlainHTTP, client.Username, client.Password)
if err != nil {
@ -152,12 +150,12 @@ func newInstallCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
}
client.SetRegistryClient(registryClient)
// This is for the case where "" is specifically passed in as a
// value. When there is no value passed in NoOptDefVal will be used
// and it is set to client. See addInstallFlags.
if client.DryRunOption == "" {
client.DryRunOption = "none"
dryRunStrategy, err := cmdGetDryRunFlagStrategy(cmd, false)
if err != nil {
return err
}
client.DryRunStrategy = dryRunStrategy
rel, err := runInstall(args, client, valueOpts, out)
if err != nil {
return fmt.Errorf("INSTALLATION FAILED: %w", err)
@ -173,11 +171,12 @@ func newInstallCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
},
}
addInstallFlags(cmd, cmd.Flags(), client, valueOpts)
f := cmd.Flags()
addInstallFlags(cmd, f, client, valueOpts)
// hide-secret is not available in all places the install flags are used so
// it is added separately
f := cmd.Flags()
f.BoolVar(&client.HideSecret, "hide-secret", false, "hide Kubernetes Secrets when also using the --dry-run flag")
addDryRunFlag(cmd)
bindOutputFlag(cmd, &outfmt)
bindPostRenderFlag(cmd, &client.PostRenderer, settings)
@ -186,13 +185,6 @@ func newInstallCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
func addInstallFlags(cmd *cobra.Command, f *pflag.FlagSet, client *action.Install, valueOpts *values.Options) {
f.BoolVar(&client.CreateNamespace, "create-namespace", false, "create the release namespace if not present")
// --dry-run options with expected outcome:
// - Not set means no dry run and server is contacted.
// - Set with no value, a value of client, or a value of true and the server is not contacted
// - Set with a value of false, none, or false and the server is contacted
// The true/false part is meant to reflect some legacy behavior while none is equal to "".
f.StringVar(&client.DryRunOption, "dry-run", "", "simulate an install. If --dry-run is set with no option being specified or as '--dry-run=client', it will not attempt cluster connections. Setting '--dry-run=server' allows attempting cluster connections.")
f.Lookup("dry-run").NoOptDefVal = "client"
f.BoolVar(&client.ForceReplace, "force-replace", false, "force resource updates by replacement")
f.BoolVar(&client.ForceReplace, "force", false, "deprecated")
f.MarkDeprecated("force", "use --force-replace instead")
@ -270,15 +262,20 @@ func runInstall(args []string, client *action.Install, valueOpts *values.Options
return nil, err
}
if err := checkIfInstallable(chartRequested); err != nil {
ac, err := chart.NewAccessor(chartRequested)
if err != nil {
return nil, err
}
if err := checkIfInstallable(ac); err != nil {
return nil, err
}
if chartRequested.Metadata.Deprecated {
if ac.Deprecated() {
slog.Warn("this chart is deprecated")
}
if req := chartRequested.Metadata.Dependencies; req != nil {
if req := ac.MetaDependencies(); req != nil {
// If CheckDependencies returns an error, we have unfulfilled dependencies.
// As of Helm 2.4.0, this is treated as a stopping condition:
// https://github.com/helm/helm/issues/2209
@ -311,11 +308,6 @@ func runInstall(args []string, client *action.Install, valueOpts *values.Options
client.Namespace = settings.Namespace()
// Validate DryRunOption member is one of the allowed values
if err := validateDryRunOptionFlag(client.DryRunOption); err != nil {
return nil, err
}
// Create context and prepare the handle of SIGTERM
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
@ -331,18 +323,25 @@ func runInstall(args []string, client *action.Install, valueOpts *values.Options
cancel()
}()
return client.RunWithContext(ctx, chartRequested, vals)
ri, err := client.RunWithContext(ctx, chartRequested, vals)
rel, rerr := releaserToV1Release(ri)
if rerr != nil {
return nil, rerr
}
return rel, err
}
// checkIfInstallable validates if a chart can be installed
//
// Application chart type is only installable
func checkIfInstallable(ch *chart.Chart) error {
switch ch.Metadata.Type {
func checkIfInstallable(ch chart.Accessor) error {
meta := ch.MetadataAsMap()
switch meta["Type"] {
case "", "application":
return nil
}
return fmt.Errorf("%s charts are not installable", ch.Metadata.Type)
return fmt.Errorf("%s charts are not installable", meta["Type"])
}
// Provide dynamic auto-completion for the install and template commands
@ -356,13 +355,3 @@ func compInstall(args []string, toComplete string, client *action.Install) ([]st
}
return nil, cobra.ShellCompDirectiveNoFileComp
}
func validateDryRunOptionFlag(dryRunOptionFlagValue string) error {
// Validate dry-run flag value with a set of allowed value
allowedDryRunValues := []string{"false", "true", "none", "client", "server"}
isAllowed := slices.Contains(allowedDryRunValues, dryRunOptionFlagValue)
if !isAllowed {
return errors.New("invalid dry-run flag. Flag must one of the following: false, true, none, client, server")
}
return nil
}

@ -30,6 +30,7 @@ import (
"helm.sh/helm/v4/pkg/chart/common"
"helm.sh/helm/v4/pkg/chart/v2/lint/support"
"helm.sh/helm/v4/pkg/cli/values"
"helm.sh/helm/v4/pkg/cmd/require"
"helm.sh/helm/v4/pkg/getter"
)
@ -51,11 +52,9 @@ func newLintCmd(out io.Writer) *cobra.Command {
Use: "lint PATH",
Short: "examine a chart for possible issues",
Long: longLintHelp,
Args: require.MinimumNArgs(1),
RunE: func(_ *cobra.Command, args []string) error {
paths := []string{"."}
if len(args) > 0 {
paths = args
}
paths := args
if kubeVersion != "" {
parsedKubeVersion, err := common.ParseKubeVersion(kubeVersion)

@ -91,6 +91,15 @@ func TestLintCmdWithKubeVersionFlag(t *testing.T) {
runTestCmd(t, tests)
}
func TestLintCmdRequiresArgs(t *testing.T) {
tests := []cmdTestCase{{
name: "lint without arguments should fail",
cmd: "lint",
wantError: true,
}}
runTestCmd(t, tests)
}
func TestLintFileCompletion(t *testing.T) {
checkFileCompletion(t, "lint", true)
checkFileCompletion(t, "lint mypath", true) // Multiple paths can be given

@ -30,15 +30,17 @@ import (
"helm.sh/helm/v4/pkg/action"
"helm.sh/helm/v4/pkg/cli/output"
"helm.sh/helm/v4/pkg/cmd/require"
"helm.sh/helm/v4/pkg/release/common"
release "helm.sh/helm/v4/pkg/release/v1"
)
var listHelp = `
This command lists all of the releases for a specified namespace (uses current namespace context if namespace not specified).
By default, it lists only releases that are deployed or failed. Flags like
'--uninstalled' and '--all' will alter this behavior. Such flags can be combined:
'--uninstalled --failed'.
By default, it lists all releases in any status. Individual status filters like '--deployed', '--failed',
'--pending', '--uninstalled', '--superseded', and '--uninstalling' can be used
to show only releases in specific states. Such flags can be combined:
'--deployed --failed'.
By default, items are sorted alphabetically. Use the '-d' flag to sort by
release date.
@ -79,7 +81,11 @@ func newListCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
}
client.SetStateMask()
results, err := client.Run()
resultsi, err := client.Run()
if err != nil {
return err
}
results, err := releaseListToV1List(resultsi)
if err != nil {
return err
}
@ -117,11 +123,10 @@ func newListCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
f.StringVar(&client.TimeFormat, "time-format", "", `format time using golang time formatter. Example: --time-format "2006-01-02 15:04:05Z0700"`)
f.BoolVarP(&client.ByDate, "date", "d", false, "sort by release date")
f.BoolVarP(&client.SortReverse, "reverse", "r", false, "reverse the sort order")
f.BoolVarP(&client.All, "all", "a", false, "show all releases without any filter applied")
f.BoolVar(&client.Uninstalled, "uninstalled", false, "show uninstalled releases (if 'helm uninstall --keep-history' was used)")
f.BoolVar(&client.Superseded, "superseded", false, "show superseded releases")
f.BoolVar(&client.Uninstalling, "uninstalling", false, "show releases that are currently being uninstalled")
f.BoolVar(&client.Deployed, "deployed", false, "show deployed releases. If no other is specified, this will be automatically enabled")
f.BoolVar(&client.Deployed, "deployed", false, "show deployed releases")
f.BoolVar(&client.Failed, "failed", false, "show failed releases")
f.BoolVar(&client.Pending, "pending", false, "show pending releases")
f.BoolVarP(&client.AllNamespaces, "all-namespaces", "A", false, "list releases across all namespaces")
@ -193,28 +198,28 @@ func (w *releaseListWriter) WriteTable(out io.Writer) error {
}
for _, r := range w.releases {
// Parse the status string back to a release.Status to use color
var status release.Status
var status common.Status
switch r.Status {
case "deployed":
status = release.StatusDeployed
status = common.StatusDeployed
case "failed":
status = release.StatusFailed
status = common.StatusFailed
case "pending-install":
status = release.StatusPendingInstall
status = common.StatusPendingInstall
case "pending-upgrade":
status = release.StatusPendingUpgrade
status = common.StatusPendingUpgrade
case "pending-rollback":
status = release.StatusPendingRollback
status = common.StatusPendingRollback
case "uninstalling":
status = release.StatusUninstalling
status = common.StatusUninstalling
case "uninstalled":
status = release.StatusUninstalled
status = common.StatusUninstalled
case "superseded":
status = release.StatusSuperseded
status = common.StatusSuperseded
case "unknown":
status = release.StatusUnknown
status = common.StatusUnknown
default:
status = release.Status(r.Status)
status = common.Status(r.Status)
}
table.AddRow(r.Name, coloroutput.ColorizeNamespace(r.Namespace, w.noColor), r.Revision, r.Updated, coloroutput.ColorizeStatus(status, w.noColor), r.Chart, r.AppVersion)
}
@ -264,7 +269,11 @@ func compListReleases(toComplete string, ignoredReleaseNames []string, cfg *acti
// client.Filter = fmt.Sprintf("^%s", toComplete)
client.SetStateMask()
releases, err := client.Run()
releasesi, err := client.Run()
if err != nil {
return nil, cobra.ShellCompDirectiveDefault
}
releases, err := releaseListToV1List(releasesi)
if err != nil {
return nil, cobra.ShellCompDirectiveDefault
}

@ -18,10 +18,11 @@ package cmd
import (
"testing"
"time"
chart "helm.sh/helm/v4/pkg/chart/v2"
"helm.sh/helm/v4/pkg/release/common"
release "helm.sh/helm/v4/pkg/release/v1"
"helm.sh/helm/v4/pkg/time"
)
func TestListCmd(t *testing.T) {
@ -47,7 +48,7 @@ func TestListCmd(t *testing.T) {
Namespace: defaultNamespace,
Info: &release.Info{
LastDeployed: timestamp1,
Status: release.StatusSuperseded,
Status: common.StatusSuperseded,
},
Chart: chartInfo,
},
@ -57,7 +58,7 @@ func TestListCmd(t *testing.T) {
Namespace: defaultNamespace,
Info: &release.Info{
LastDeployed: timestamp1,
Status: release.StatusDeployed,
Status: common.StatusDeployed,
},
Chart: chartInfo,
},
@ -67,7 +68,7 @@ func TestListCmd(t *testing.T) {
Namespace: defaultNamespace,
Info: &release.Info{
LastDeployed: timestamp1,
Status: release.StatusUninstalled,
Status: common.StatusUninstalled,
},
Chart: chartInfo,
},
@ -77,7 +78,7 @@ func TestListCmd(t *testing.T) {
Namespace: defaultNamespace,
Info: &release.Info{
LastDeployed: timestamp1,
Status: release.StatusSuperseded,
Status: common.StatusSuperseded,
},
Chart: chartInfo,
},
@ -87,7 +88,7 @@ func TestListCmd(t *testing.T) {
Namespace: defaultNamespace,
Info: &release.Info{
LastDeployed: timestamp2,
Status: release.StatusFailed,
Status: common.StatusFailed,
},
Chart: chartInfo,
},
@ -97,7 +98,7 @@ func TestListCmd(t *testing.T) {
Namespace: defaultNamespace,
Info: &release.Info{
LastDeployed: timestamp1,
Status: release.StatusUninstalling,
Status: common.StatusUninstalling,
},
Chart: chartInfo,
},
@ -107,7 +108,7 @@ func TestListCmd(t *testing.T) {
Namespace: defaultNamespace,
Info: &release.Info{
LastDeployed: timestamp1,
Status: release.StatusPendingInstall,
Status: common.StatusPendingInstall,
},
Chart: chartInfo,
},
@ -117,7 +118,7 @@ func TestListCmd(t *testing.T) {
Namespace: defaultNamespace,
Info: &release.Info{
LastDeployed: timestamp3,
Status: release.StatusDeployed,
Status: common.StatusDeployed,
},
Chart: chartInfo,
},
@ -127,7 +128,7 @@ func TestListCmd(t *testing.T) {
Namespace: defaultNamespace,
Info: &release.Info{
LastDeployed: timestamp4,
Status: release.StatusDeployed,
Status: common.StatusDeployed,
},
Chart: chartInfo,
},
@ -137,7 +138,7 @@ func TestListCmd(t *testing.T) {
Namespace: "milano",
Info: &release.Info{
LastDeployed: timestamp1,
Status: release.StatusDeployed,
Status: common.StatusDeployed,
},
Chart: chartInfo,
},
@ -146,22 +147,17 @@ func TestListCmd(t *testing.T) {
tests := []cmdTestCase{{
name: "list releases",
cmd: "list",
golden: "output/list.txt",
golden: "output/list-all.txt",
rels: releaseFixture,
}, {
name: "list without headers",
cmd: "list --no-headers",
golden: "output/list-no-headers.txt",
rels: releaseFixture,
}, {
name: "list all releases",
cmd: "list --all",
golden: "output/list-all.txt",
golden: "output/list-all-no-headers.txt",
rels: releaseFixture,
}, {
name: "list releases sorted by release date",
cmd: "list --date",
golden: "output/list-date.txt",
golden: "output/list-all-date.txt",
rels: releaseFixture,
}, {
name: "list failed releases",
@ -171,17 +167,17 @@ func TestListCmd(t *testing.T) {
}, {
name: "list filtered releases",
cmd: "list --filter='.*'",
golden: "output/list-filter.txt",
golden: "output/list-all.txt",
rels: releaseFixture,
}, {
name: "list releases, limited to one release",
cmd: "list --max 1",
golden: "output/list-max.txt",
golden: "output/list-all-max.txt",
rels: releaseFixture,
}, {
name: "list releases, offset by one",
cmd: "list --offset 1",
golden: "output/list-offset.txt",
golden: "output/list-all-offset.txt",
rels: releaseFixture,
}, {
name: "list pending releases",
@ -191,27 +187,32 @@ func TestListCmd(t *testing.T) {
}, {
name: "list releases in reverse order",
cmd: "list --reverse",
golden: "output/list-reverse.txt",
golden: "output/list-all-reverse.txt",
rels: releaseFixture,
}, {
name: "list releases sorted by reversed release date",
cmd: "list --date --reverse",
golden: "output/list-date-reversed.txt",
golden: "output/list-all-date-reversed.txt",
rels: releaseFixture,
}, {
name: "list releases in short output format",
cmd: "list --short",
golden: "output/list-short.txt",
golden: "output/list-all-short.txt",
rels: releaseFixture,
}, {
name: "list releases in short output format",
cmd: "list --short --output yaml",
golden: "output/list-short-yaml.txt",
golden: "output/list-all-short-yaml.txt",
rels: releaseFixture,
}, {
name: "list releases in short output format",
cmd: "list --short --output json",
golden: "output/list-short-json.txt",
golden: "output/list-all-short-json.txt",
rels: releaseFixture,
}, {
name: "list deployed and failed releases only",
cmd: "list --deployed --failed",
golden: "output/list.txt",
rels: releaseFixture,
}, {
name: "list superseded releases",

@ -113,7 +113,6 @@ func loadCLIPlugins(baseCmd *cobra.Command, out io.Writer) {
input := &plugin.Input{
Message: schema.InputMessageCLIV1{
ExtraArgs: extraArgs,
Settings: settings,
},
Env: env,
Stdin: os.Stdin,

@ -65,19 +65,23 @@ func newReleaseTestCmd(cfg *action.Configuration, out io.Writer) *cobra.Command
client.Filters[action.ExcludeNameFilter] = append(client.Filters[action.ExcludeNameFilter], notName.ReplaceAllLiteralString(f, ""))
}
}
rel, runErr := client.Run(args[0])
reli, runErr := client.Run(args[0])
// We only return an error if we weren't even able to get the
// release, otherwise we keep going so we can print status and logs
// if requested
if runErr != nil && rel == nil {
if runErr != nil && reli == nil {
return runErr
}
rel, err := releaserToV1Release(reli)
if err != nil {
return err
}
if err := outfmt.Write(out, &statusPrinter{
release: rel,
debug: settings.Debug,
showMetadata: false,
hideNotes: client.HideNotes,
hideNotes: true,
noColor: settings.ShouldDisableColor(),
}); err != nil {
return err
@ -99,7 +103,6 @@ func newReleaseTestCmd(cfg *action.Configuration, out io.Writer) *cobra.Command
f.DurationVar(&client.Timeout, "timeout", 300*time.Second, "time to wait for any individual Kubernetes operation (like Jobs for hooks)")
f.BoolVar(&outputLogs, "logs", false, "dump the logs from test pods (this runs after all tests are complete, but before any cleanup)")
f.StringSliceVar(&filter, "filter", []string{}, "specify tests by attribute (currently \"name\") using attribute=value syntax or '!attribute=value' to exclude a test (can specify multiple or separate values with commas: name=test1,name=test2)")
f.BoolVar(&client.HideNotes, "hide-notes", false, "if set, do not show notes in test output. Does not affect presence in chart metadata")
return cmd
}

@ -17,7 +17,17 @@ limitations under the License.
package cmd
import (
"bytes"
"io"
"strings"
"testing"
"helm.sh/helm/v4/pkg/action"
"helm.sh/helm/v4/pkg/chart/common"
chart "helm.sh/helm/v4/pkg/chart/v2"
kubefake "helm.sh/helm/v4/pkg/kube/fake"
rcommon "helm.sh/helm/v4/pkg/release/common"
release "helm.sh/helm/v4/pkg/release/v1"
)
func TestReleaseTestingCompletion(t *testing.T) {
@ -28,3 +38,44 @@ func TestReleaseTestingFileCompletion(t *testing.T) {
checkFileCompletion(t, "test", false)
checkFileCompletion(t, "test myrelease", false)
}
func TestReleaseTestNotesHandling(t *testing.T) {
// Test that ensures notes behavior is correct for test command
// This is a simpler test that focuses on the core functionality
rel := &release.Release{
Name: "test-release",
Namespace: "default",
Info: &release.Info{
Status: rcommon.StatusDeployed,
Notes: "Some important notes that should be hidden by default",
},
Chart: &chart.Chart{Metadata: &chart.Metadata{Name: "test", Version: "1.0.0"}},
}
// Set up storage
store := storageFixture()
store.Create(rel)
// Set up action configuration properly
actionConfig := &action.Configuration{
Releases: store,
KubeClient: &kubefake.FailingKubeClient{PrintingKubeClient: kubefake.PrintingKubeClient{Out: io.Discard}},
Capabilities: common.DefaultCapabilities,
}
// Test the newReleaseTestCmd function directly
var buf1 bytes.Buffer
// Test 1: Default behavior (should hide notes)
cmd1 := newReleaseTestCmd(actionConfig, &buf1)
cmd1.SetArgs([]string{"test-release"})
err1 := cmd1.Execute()
if err1 != nil {
t.Fatalf("Unexpected error for default test: %v", err1)
}
output1 := buf1.String()
if strings.Contains(output1, "NOTES:") {
t.Errorf("Expected notes to be hidden by default, but found NOTES section in output: %s", output1)
}
}

@ -57,7 +57,7 @@ func newRollbackCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
return noMoreArgsComp()
},
RunE: func(_ *cobra.Command, args []string) error {
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) > 1 {
ver, err := strconv.Atoi(args[1])
if err != nil {
@ -66,6 +66,12 @@ func newRollbackCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
client.Version = ver
}
dryRunStrategy, err := cmdGetDryRunFlagStrategy(cmd, false)
if err != nil {
return err
}
client.DryRunStrategy = dryRunStrategy
if err := client.Run(args[0]); err != nil {
return err
}
@ -76,7 +82,6 @@ func newRollbackCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
}
f := cmd.Flags()
f.BoolVar(&client.DryRun, "dry-run", false, "simulate a rollback")
f.BoolVar(&client.ForceReplace, "force-replace", false, "force resource updates by replacement")
f.BoolVar(&client.ForceReplace, "force", false, "deprecated")
f.MarkDeprecated("force", "use --force-replace instead")
@ -87,6 +92,7 @@ func newRollbackCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
f.BoolVar(&client.WaitForJobs, "wait-for-jobs", false, "if set and --wait enabled, will wait until all Jobs have been completed before marking the release as successful. It will wait for as long as --timeout")
f.BoolVar(&client.CleanupOnFail, "cleanup-on-fail", false, "allow deletion of new resources created in this rollback when rollback fails")
f.IntVar(&client.MaxHistory, "history-max", settings.MaxHistory, "limit the maximum number of revisions saved per release. Use 0 for no limit")
addDryRunFlag(cmd)
AddWaitFlag(cmd, &client.WaitStrategy)
cmd.MarkFlagsMutuallyExclusive("force-replace", "force-conflicts")
cmd.MarkFlagsMutuallyExclusive("force", "force-conflicts")

@ -22,6 +22,7 @@ import (
"testing"
chart "helm.sh/helm/v4/pkg/chart/v2"
"helm.sh/helm/v4/pkg/release/common"
release "helm.sh/helm/v4/pkg/release/v1"
)
@ -29,13 +30,13 @@ func TestRollbackCmd(t *testing.T) {
rels := []*release.Release{
{
Name: "funny-honey",
Info: &release.Info{Status: release.StatusSuperseded},
Info: &release.Info{Status: common.StatusSuperseded},
Chart: &chart.Chart{},
Version: 1,
},
{
Name: "funny-honey",
Info: &release.Info{Status: release.StatusDeployed},
Info: &release.Info{Status: common.StatusDeployed},
Chart: &chart.Chart{},
Version: 2,
},
@ -83,7 +84,7 @@ func TestRollbackCmd(t *testing.T) {
}
func TestRollbackRevisionCompletion(t *testing.T) {
mk := func(name string, vers int, status release.Status) *release.Release {
mk := func(name string, vers int, status common.Status) *release.Release {
return release.Mock(&release.MockReleaseOptions{
Name: name,
Version: vers,
@ -92,11 +93,11 @@ func TestRollbackRevisionCompletion(t *testing.T) {
}
releases := []*release.Release{
mk("musketeers", 11, release.StatusDeployed),
mk("musketeers", 10, release.StatusSuperseded),
mk("musketeers", 9, release.StatusSuperseded),
mk("musketeers", 8, release.StatusSuperseded),
mk("carabins", 1, release.StatusSuperseded),
mk("musketeers", 11, common.StatusDeployed),
mk("musketeers", 10, common.StatusSuperseded),
mk("musketeers", 9, common.StatusSuperseded),
mk("musketeers", 8, common.StatusSuperseded),
mk("carabins", 1, common.StatusSuperseded),
}
tests := []cmdTestCase{{
@ -132,14 +133,14 @@ func TestRollbackWithLabels(t *testing.T) {
rels := []*release.Release{
{
Name: releaseName,
Info: &release.Info{Status: release.StatusSuperseded},
Info: &release.Info{Status: common.StatusSuperseded},
Chart: &chart.Chart{},
Version: 1,
Labels: labels1,
},
{
Name: releaseName,
Info: &release.Info{Status: release.StatusDeployed},
Info: &release.Info{Status: common.StatusDeployed},
Chart: &chart.Chart{},
Version: 2,
Labels: labels2,
@ -155,7 +156,11 @@ func TestRollbackWithLabels(t *testing.T) {
if err != nil {
t.Errorf("unexpected error, got '%v'", err)
}
updatedRel, err := storage.Get(releaseName, 3)
updatedReli, err := storage.Get(releaseName, 3)
if err != nil {
t.Errorf("unexpected error, got '%v'", err)
}
updatedRel, err := releaserToV1Release(updatedReli)
if err != nil {
t.Errorf("unexpected error, got '%v'", err)
}

@ -39,6 +39,7 @@ import (
"helm.sh/helm/v4/pkg/cli"
kubefake "helm.sh/helm/v4/pkg/kube/fake"
"helm.sh/helm/v4/pkg/registry"
ri "helm.sh/helm/v4/pkg/release"
release "helm.sh/helm/v4/pkg/release/v1"
"helm.sh/helm/v4/pkg/repo/v1"
"helm.sh/helm/v4/pkg/storage/driver"
@ -173,7 +174,7 @@ func newRootCmdWithConfig(actionConfig *action.Configuration, out io.Writer, arg
// those errors will be caught later during the call to cmd.Execution.
// This call is required to gather configuration information prior to
// execution.
flags.ParseErrorsWhitelist.UnknownFlags = true
flags.ParseErrorsAllowlist.UnknownFlags = true
flags.Parse(args)
logSetup(settings.Debug)
@ -465,3 +466,31 @@ type CommandError struct {
error
ExitCode int
}
// releaserToV1Release is a helper function to convert a v1 release passed by interface
// into the type object.
func releaserToV1Release(rel ri.Releaser) (*release.Release, error) {
switch r := rel.(type) {
case release.Release:
return &r, nil
case *release.Release:
return r, nil
case nil:
return nil, nil
default:
return nil, fmt.Errorf("unsupported release type: %T", rel)
}
}
func releaseListToV1List(ls []ri.Releaser) ([]*release.Release, error) {
rls := make([]*release.Release, 0, len(ls))
for _, val := range ls {
rel, err := releaserToV1Release(val)
if err != nil {
return nil, err
}
rls = append(rls, rel)
}
return rls, nil
}

@ -33,7 +33,8 @@ import (
"helm.sh/helm/v4/pkg/chart/common/util"
"helm.sh/helm/v4/pkg/cli/output"
"helm.sh/helm/v4/pkg/cmd/require"
release "helm.sh/helm/v4/pkg/release/v1"
"helm.sh/helm/v4/pkg/release"
releasev1 "helm.sh/helm/v4/pkg/release/v1"
)
// NOTE: Keep the list of statuses up-to-date with pkg/release/status.go.
@ -72,7 +73,11 @@ func newStatusCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
if outfmt == output.Table {
client.ShowResourcesTable = true
}
rel, err := client.Run(args[0])
reli, err := client.Run(args[0])
if err != nil {
return err
}
rel, err := releaserToV1Release(reli)
if err != nil {
return err
}
@ -110,54 +115,65 @@ func newStatusCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
}
type statusPrinter struct {
release *release.Release
release release.Releaser
debug bool
showMetadata bool
hideNotes bool
noColor bool
}
func (s statusPrinter) getV1Release() *releasev1.Release {
switch rel := s.release.(type) {
case releasev1.Release:
return &rel
case *releasev1.Release:
return rel
}
return &releasev1.Release{}
}
func (s statusPrinter) WriteJSON(out io.Writer) error {
return output.EncodeJSON(out, s.release)
return output.EncodeJSON(out, s.getV1Release())
}
func (s statusPrinter) WriteYAML(out io.Writer) error {
return output.EncodeYAML(out, s.release)
return output.EncodeYAML(out, s.getV1Release())
}
func (s statusPrinter) WriteTable(out io.Writer) error {
if s.release == nil {
return nil
}
_, _ = fmt.Fprintf(out, "NAME: %s\n", s.release.Name)
if !s.release.Info.LastDeployed.IsZero() {
_, _ = fmt.Fprintf(out, "LAST DEPLOYED: %s\n", s.release.Info.LastDeployed.Format(time.ANSIC))
rel := s.getV1Release()
_, _ = fmt.Fprintf(out, "NAME: %s\n", rel.Name)
if !rel.Info.LastDeployed.IsZero() {
_, _ = fmt.Fprintf(out, "LAST DEPLOYED: %s\n", rel.Info.LastDeployed.Format(time.ANSIC))
}
_, _ = fmt.Fprintf(out, "NAMESPACE: %s\n", coloroutput.ColorizeNamespace(s.release.Namespace, s.noColor))
_, _ = fmt.Fprintf(out, "STATUS: %s\n", coloroutput.ColorizeStatus(s.release.Info.Status, s.noColor))
_, _ = fmt.Fprintf(out, "REVISION: %d\n", s.release.Version)
_, _ = fmt.Fprintf(out, "NAMESPACE: %s\n", coloroutput.ColorizeNamespace(rel.Namespace, s.noColor))
_, _ = fmt.Fprintf(out, "STATUS: %s\n", coloroutput.ColorizeStatus(rel.Info.Status, s.noColor))
_, _ = fmt.Fprintf(out, "REVISION: %d\n", rel.Version)
if s.showMetadata {
_, _ = fmt.Fprintf(out, "CHART: %s\n", s.release.Chart.Metadata.Name)
_, _ = fmt.Fprintf(out, "VERSION: %s\n", s.release.Chart.Metadata.Version)
_, _ = fmt.Fprintf(out, "APP_VERSION: %s\n", s.release.Chart.Metadata.AppVersion)
_, _ = fmt.Fprintf(out, "CHART: %s\n", rel.Chart.Metadata.Name)
_, _ = fmt.Fprintf(out, "VERSION: %s\n", rel.Chart.Metadata.Version)
_, _ = fmt.Fprintf(out, "APP_VERSION: %s\n", rel.Chart.Metadata.AppVersion)
}
_, _ = fmt.Fprintf(out, "DESCRIPTION: %s\n", s.release.Info.Description)
_, _ = fmt.Fprintf(out, "DESCRIPTION: %s\n", rel.Info.Description)
if len(s.release.Info.Resources) > 0 {
if len(rel.Info.Resources) > 0 {
buf := new(bytes.Buffer)
printFlags := get.NewHumanPrintFlags()
typePrinter, _ := printFlags.ToPrinter("")
printer := &get.TablePrinter{Delegate: typePrinter}
var keys []string
for key := range s.release.Info.Resources {
for key := range rel.Info.Resources {
keys = append(keys, key)
}
for _, t := range keys {
_, _ = fmt.Fprintf(buf, "==> %s\n", t)
vk := s.release.Info.Resources[t]
vk := rel.Info.Resources[t]
for _, resource := range vk {
if err := printer.PrintObj(resource, buf); err != nil {
_, _ = fmt.Fprintf(buf, "failed to print object type %s: %v\n", t, err)
@ -170,8 +186,8 @@ func (s statusPrinter) WriteTable(out io.Writer) error {
_, _ = fmt.Fprintf(out, "RESOURCES:\n%s\n", buf.String())
}
executions := executionsByHookEvent(s.release)
if tests, ok := executions[release.HookTest]; !ok || len(tests) == 0 {
executions := executionsByHookEvent(rel)
if tests, ok := executions[releasev1.HookTest]; !ok || len(tests) == 0 {
_, _ = fmt.Fprintln(out, "TEST SUITE: None")
} else {
for _, h := range tests {
@ -190,14 +206,14 @@ func (s statusPrinter) WriteTable(out io.Writer) error {
if s.debug {
_, _ = fmt.Fprintln(out, "USER-SUPPLIED VALUES:")
err := output.EncodeYAML(out, s.release.Config)
err := output.EncodeYAML(out, rel.Config)
if err != nil {
return err
}
// Print an extra newline
_, _ = fmt.Fprintln(out)
cfg, err := util.CoalesceValues(s.release.Chart, s.release.Config)
cfg, err := util.CoalesceValues(rel.Chart, rel.Config)
if err != nil {
return err
}
@ -211,28 +227,28 @@ func (s statusPrinter) WriteTable(out io.Writer) error {
_, _ = fmt.Fprintln(out)
}
if strings.EqualFold(s.release.Info.Description, "Dry run complete") || s.debug {
if strings.EqualFold(rel.Info.Description, "Dry run complete") || s.debug {
_, _ = fmt.Fprintln(out, "HOOKS:")
for _, h := range s.release.Hooks {
for _, h := range rel.Hooks {
_, _ = fmt.Fprintf(out, "---\n# Source: %s\n%s\n", h.Path, h.Manifest)
}
_, _ = fmt.Fprintf(out, "MANIFEST:\n%s\n", s.release.Manifest)
_, _ = fmt.Fprintf(out, "MANIFEST:\n%s\n", rel.Manifest)
}
// Hide notes from output - option in install and upgrades
if !s.hideNotes && len(s.release.Info.Notes) > 0 {
_, _ = fmt.Fprintf(out, "NOTES:\n%s\n", strings.TrimSpace(s.release.Info.Notes))
if !s.hideNotes && len(rel.Info.Notes) > 0 {
_, _ = fmt.Fprintf(out, "NOTES:\n%s\n", strings.TrimSpace(rel.Info.Notes))
}
return nil
}
func executionsByHookEvent(rel *release.Release) map[release.HookEvent][]*release.Hook {
result := make(map[release.HookEvent][]*release.Hook)
func executionsByHookEvent(rel *releasev1.Release) map[releasev1.HookEvent][]*releasev1.Hook {
result := make(map[releasev1.HookEvent][]*releasev1.Hook)
for _, h := range rel.Hooks {
for _, e := range h.Events {
executions, ok := result[e]
if !ok {
executions = []*release.Hook{}
executions = []*releasev1.Hook{}
}
result[e] = append(executions, h)
}

@ -21,13 +21,13 @@ import (
"time"
chart "helm.sh/helm/v4/pkg/chart/v2"
"helm.sh/helm/v4/pkg/release/common"
release "helm.sh/helm/v4/pkg/release/v1"
helmtime "helm.sh/helm/v4/pkg/time"
)
func TestStatusCmd(t *testing.T) {
releasesMockWithStatus := func(info *release.Info, hooks ...*release.Hook) []*release.Release {
info.LastDeployed = helmtime.Unix(1452902400, 0).UTC()
info.LastDeployed = time.Unix(1452902400, 0).UTC()
return []*release.Release{{
Name: "flummoxed-chickadee",
Namespace: "default",
@ -42,14 +42,14 @@ func TestStatusCmd(t *testing.T) {
cmd: "status flummoxed-chickadee",
golden: "output/status.txt",
rels: releasesMockWithStatus(&release.Info{
Status: release.StatusDeployed,
Status: common.StatusDeployed,
}),
}, {
name: "get status of a deployed release, with desc",
cmd: "status flummoxed-chickadee",
golden: "output/status-with-desc.txt",
rels: releasesMockWithStatus(&release.Info{
Status: release.StatusDeployed,
Status: common.StatusDeployed,
Description: "Mock description",
}),
}, {
@ -57,7 +57,7 @@ func TestStatusCmd(t *testing.T) {
cmd: "status flummoxed-chickadee",
golden: "output/status-with-notes.txt",
rels: releasesMockWithStatus(&release.Info{
Status: release.StatusDeployed,
Status: common.StatusDeployed,
Notes: "release notes",
}),
}, {
@ -65,7 +65,7 @@ func TestStatusCmd(t *testing.T) {
cmd: "status flummoxed-chickadee -o json",
golden: "output/status.json",
rels: releasesMockWithStatus(&release.Info{
Status: release.StatusDeployed,
Status: common.StatusDeployed,
Notes: "release notes",
}),
}, {
@ -74,7 +74,7 @@ func TestStatusCmd(t *testing.T) {
golden: "output/status-with-resources.txt",
rels: releasesMockWithStatus(
&release.Info{
Status: release.StatusDeployed,
Status: common.StatusDeployed,
},
),
}, {
@ -83,7 +83,7 @@ func TestStatusCmd(t *testing.T) {
golden: "output/status-with-resources.json",
rels: releasesMockWithStatus(
&release.Info{
Status: release.StatusDeployed,
Status: common.StatusDeployed,
},
),
}, {
@ -92,7 +92,7 @@ func TestStatusCmd(t *testing.T) {
golden: "output/status-with-test-suite.txt",
rels: releasesMockWithStatus(
&release.Info{
Status: release.StatusDeployed,
Status: common.StatusDeployed,
},
&release.Hook{
Name: "never-run-test",
@ -130,8 +130,8 @@ func TestStatusCmd(t *testing.T) {
runTestCmd(t, tests)
}
func mustParseTime(t string) helmtime.Time {
res, _ := helmtime.Parse(time.RFC3339, t)
func mustParseTime(t string) time.Time {
res, _ := time.Parse(time.RFC3339, t)
return res
}
@ -141,7 +141,7 @@ func TestStatusCompletion(t *testing.T) {
Name: "athos",
Namespace: "default",
Info: &release.Info{
Status: release.StatusDeployed,
Status: common.StatusDeployed,
},
Chart: &chart.Chart{
Metadata: &chart.Metadata{
@ -153,7 +153,7 @@ func TestStatusCompletion(t *testing.T) {
Name: "porthos",
Namespace: "default",
Info: &release.Info{
Status: release.StatusFailed,
Status: common.StatusFailed,
},
Chart: &chart.Chart{
Metadata: &chart.Metadata{
@ -165,7 +165,7 @@ func TestStatusCompletion(t *testing.T) {
Name: "aramis",
Namespace: "default",
Info: &release.Info{
Status: release.StatusUninstalled,
Status: common.StatusUninstalled,
},
Chart: &chart.Chart{
Metadata: &chart.Metadata{
@ -177,7 +177,7 @@ func TestStatusCompletion(t *testing.T) {
Name: "dartagnan",
Namespace: "gascony",
Info: &release.Info{
Status: release.StatusUnknown,
Status: common.StatusUnknown,
},
Chart: &chart.Chart{
Metadata: &chart.Metadata{

@ -23,7 +23,6 @@ import (
"io"
"io/fs"
"os"
"path"
"path/filepath"
"regexp"
"slices"
@ -67,7 +66,7 @@ func newTemplateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
ValidArgsFunction: func(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return compInstall(args, toComplete, client)
},
RunE: func(_ *cobra.Command, args []string) error {
RunE: func(cmd *cobra.Command, args []string) error {
if kubeVersion != "" {
parsedKubeVersion, err := common.ParseKubeVersion(kubeVersion)
if err != nil {
@ -83,16 +82,17 @@ func newTemplateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
}
client.SetRegistryClient(registryClient)
// This is for the case where "" is specifically passed in as a
// value. When there is no value passed in NoOptDefVal will be used
// and it is set to client. See addInstallFlags.
if client.DryRunOption == "" {
client.DryRunOption = "true"
dryRunStrategy, err := cmdGetDryRunFlagStrategy(cmd, true)
if err != nil {
return err
}
if validate {
// Mimic deprecated --validate flag behavior by enabling server dry run
dryRunStrategy = action.DryRunServer
}
client.DryRun = true
client.DryRunStrategy = dryRunStrategy
client.ReleaseName = "release-name"
client.Replace = true // Skip the name check
client.ClientOnly = !validate
client.APIVersions = common.VersionSet(extraAPIs)
client.IncludeCRDs = includeCrds
rel, err := runInstall(args, client, valueOpts, out)
@ -196,14 +196,21 @@ func newTemplateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
addInstallFlags(cmd, f, client, valueOpts)
f.StringArrayVarP(&showFiles, "show-only", "s", []string{}, "only show manifests rendered from the given templates")
f.StringVar(&client.OutputDir, "output-dir", "", "writes the executed templates to files in output-dir instead of stdout")
f.BoolVar(&validate, "validate", false, "validate your manifests against the Kubernetes cluster you are currently pointing at. This is the same validation performed on an install")
f.BoolVar(&validate, "validate", false, "deprecated")
f.MarkDeprecated("validate", "use '--dry-run=server' instead")
f.BoolVar(&includeCrds, "include-crds", false, "include CRDs in the templated output")
f.BoolVar(&skipTests, "skip-tests", false, "skip tests from templated output")
f.BoolVar(&client.IsUpgrade, "is-upgrade", false, "set .Release.IsUpgrade instead of .Release.IsInstall")
f.StringVar(&kubeVersion, "kube-version", "", "Kubernetes version used for Capabilities.KubeVersion")
f.StringSliceVarP(&extraAPIs, "api-versions", "a", []string{}, "Kubernetes api versions used for Capabilities.APIVersions (multiple can be specified)")
f.BoolVar(&client.UseReleaseName, "release-name", false, "use release name in the output-dir path.")
f.String(
"dry-run",
"client",
`simulates the operation either client-side or server-side. Must be either: "client", or "server". '--dry-run=client simulates the operation client-side only and avoids cluster connections. '--dry-run=server' simulates/validates the operation on the server, requiring cluster connectivity.`)
f.Lookup("dry-run").NoOptDefVal = "unset"
bindPostRenderFlag(cmd, &client.PostRenderer, settings)
cmd.MarkFlagsMutuallyExclusive("validate", "dry-run")
return cmd
}
@ -250,7 +257,7 @@ func createOrOpenFile(filename string, appendData bool) (*os.File, error) {
}
func ensureDirectoryForFile(file string) error {
baseDir := path.Dir(file)
baseDir := filepath.Dir(file)
_, err := os.Stat(baseDir)
if err != nil && !errors.Is(err, fs.ErrNotExist) {
return err

@ -0,0 +1,9 @@
NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION
iguana default 2 2016-01-16 00:00:04 +0000 UTC deployed chickadee-1.0.0 0.0.1
hummingbird default 1 2016-01-16 00:00:03 +0000 UTC deployed chickadee-1.0.0 0.0.1
rocket default 1 2016-01-16 00:00:02 +0000 UTC failed chickadee-1.0.0 0.0.1
thanos default 1 2016-01-16 00:00:01 +0000 UTC pending-install chickadee-1.0.0 0.0.1
starlord default 2 2016-01-16 00:00:01 +0000 UTC deployed chickadee-1.0.0 0.0.1
groot default 1 2016-01-16 00:00:01 +0000 UTC uninstalled chickadee-1.0.0 0.0.1
gamora default 1 2016-01-16 00:00:01 +0000 UTC superseded chickadee-1.0.0 0.0.1
drax default 1 2016-01-16 00:00:01 +0000 UTC uninstalling chickadee-1.0.0 0.0.1

@ -0,0 +1,9 @@
NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION
drax default 1 2016-01-16 00:00:01 +0000 UTC uninstalling chickadee-1.0.0 0.0.1
gamora default 1 2016-01-16 00:00:01 +0000 UTC superseded chickadee-1.0.0 0.0.1
groot default 1 2016-01-16 00:00:01 +0000 UTC uninstalled chickadee-1.0.0 0.0.1
starlord default 2 2016-01-16 00:00:01 +0000 UTC deployed chickadee-1.0.0 0.0.1
thanos default 1 2016-01-16 00:00:01 +0000 UTC pending-install chickadee-1.0.0 0.0.1
rocket default 1 2016-01-16 00:00:02 +0000 UTC failed chickadee-1.0.0 0.0.1
hummingbird default 1 2016-01-16 00:00:03 +0000 UTC deployed chickadee-1.0.0 0.0.1
iguana default 2 2016-01-16 00:00:04 +0000 UTC deployed chickadee-1.0.0 0.0.1

@ -0,0 +1,2 @@
NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION
drax default 1 2016-01-16 00:00:01 +0000 UTC uninstalling chickadee-1.0.0 0.0.1

@ -0,0 +1,8 @@
drax default 1 2016-01-16 00:00:01 +0000 UTC uninstalling chickadee-1.0.0 0.0.1
gamora default 1 2016-01-16 00:00:01 +0000 UTC superseded chickadee-1.0.0 0.0.1
groot default 1 2016-01-16 00:00:01 +0000 UTC uninstalled chickadee-1.0.0 0.0.1
hummingbird default 1 2016-01-16 00:00:03 +0000 UTC deployed chickadee-1.0.0 0.0.1
iguana default 2 2016-01-16 00:00:04 +0000 UTC deployed chickadee-1.0.0 0.0.1
rocket default 1 2016-01-16 00:00:02 +0000 UTC failed chickadee-1.0.0 0.0.1
starlord default 2 2016-01-16 00:00:01 +0000 UTC deployed chickadee-1.0.0 0.0.1
thanos default 1 2016-01-16 00:00:01 +0000 UTC pending-install chickadee-1.0.0 0.0.1

@ -0,0 +1,8 @@
NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION
gamora default 1 2016-01-16 00:00:01 +0000 UTC superseded chickadee-1.0.0 0.0.1
groot default 1 2016-01-16 00:00:01 +0000 UTC uninstalled chickadee-1.0.0 0.0.1
hummingbird default 1 2016-01-16 00:00:03 +0000 UTC deployed chickadee-1.0.0 0.0.1
iguana default 2 2016-01-16 00:00:04 +0000 UTC deployed chickadee-1.0.0 0.0.1
rocket default 1 2016-01-16 00:00:02 +0000 UTC failed chickadee-1.0.0 0.0.1
starlord default 2 2016-01-16 00:00:01 +0000 UTC deployed chickadee-1.0.0 0.0.1
thanos default 1 2016-01-16 00:00:01 +0000 UTC pending-install chickadee-1.0.0 0.0.1

@ -0,0 +1,9 @@
NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION
thanos default 1 2016-01-16 00:00:01 +0000 UTC pending-install chickadee-1.0.0 0.0.1
starlord default 2 2016-01-16 00:00:01 +0000 UTC deployed chickadee-1.0.0 0.0.1
rocket default 1 2016-01-16 00:00:02 +0000 UTC failed chickadee-1.0.0 0.0.1
iguana default 2 2016-01-16 00:00:04 +0000 UTC deployed chickadee-1.0.0 0.0.1
hummingbird default 1 2016-01-16 00:00:03 +0000 UTC deployed chickadee-1.0.0 0.0.1
groot default 1 2016-01-16 00:00:01 +0000 UTC uninstalled chickadee-1.0.0 0.0.1
gamora default 1 2016-01-16 00:00:01 +0000 UTC superseded chickadee-1.0.0 0.0.1
drax default 1 2016-01-16 00:00:01 +0000 UTC uninstalling chickadee-1.0.0 0.0.1

@ -0,0 +1 @@
["drax","gamora","groot","hummingbird","iguana","rocket","starlord","thanos"]

@ -0,0 +1,8 @@
- drax
- gamora
- groot
- hummingbird
- iguana
- rocket
- starlord
- thanos

@ -0,0 +1,8 @@
drax
gamora
groot
hummingbird
iguana
rocket
starlord
thanos

@ -1 +1 @@
{"name":"flummoxed-chickadee","info":{"first_deployed":"","last_deployed":"2016-01-16T00:00:00Z","deleted":"","status":"deployed"},"namespace":"default"}
{"name":"flummoxed-chickadee","info":{"last_deployed":"2016-01-16T00:00:00Z","status":"deployed"},"namespace":"default"}

@ -1 +1 @@
{"name":"flummoxed-chickadee","info":{"first_deployed":"","last_deployed":"2016-01-16T00:00:00Z","deleted":"","status":"deployed","notes":"release notes"},"namespace":"default"}
{"name":"flummoxed-chickadee","info":{"last_deployed":"2016-01-16T00:00:00Z","status":"deployed","notes":"release notes"},"namespace":"default"}

@ -1 +0,0 @@
version.BuildInfo{Version:"v4.0", GitCommit:"", GitTreeState:"", GoVersion:""}

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

Loading…
Cancel
Save