Merge branch 'main' into overwrite-31291

pull/31331/head
Siew Kam Onn 1 year ago
commit 3a1719e4e1

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

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

@ -7,7 +7,7 @@ jobs:
stale: stale:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/stale@3a9db7e6a41a89f618792c92c0e97cc736e1b13f # v10.0.0 - uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
with: with:
repo-token: ${{ secrets.GITHUB_TOKEN }} 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.' 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.'

@ -42,6 +42,14 @@ linters:
- legacy - legacy
- std-error-handling - std-error-handling
rules:
# 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 warn-unused: true
settings: settings:

@ -10,8 +10,9 @@ require (
github.com/Masterminds/sprig/v3 v3.3.0 github.com/Masterminds/sprig/v3 v3.3.0
github.com/Masterminds/squirrel v1.5.4 github.com/Masterminds/squirrel v1.5.4
github.com/Masterminds/vcs v1.13.3 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/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/distribution/distribution/v3 v3.0.0
github.com/evanphx/json-patch/v5 v5.9.11 github.com/evanphx/json-patch/v5 v5.9.11
github.com/extism/go-sdk v1.7.1 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/fluxcd/cli-utils v0.36.0-flux.14
github.com/foxcpp/go-mockdns v1.1.0 github.com/foxcpp/go-mockdns v1.1.0
github.com/gobwas/glob v0.2.3 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/gosuri/uitable v0.0.4
github.com/jmoiron/sqlx v1.4.0 github.com/jmoiron/sqlx v1.4.0
github.com/lib/pq v1.10.9 github.com/lib/pq v1.10.9
@ -35,9 +36,9 @@ require (
github.com/stretchr/testify v1.11.1 github.com/stretchr/testify v1.11.1
github.com/tetratelabs/wazero v1.9.0 github.com/tetratelabs/wazero v1.9.0
go.yaml.in/yaml/v3 v3.0.4 go.yaml.in/yaml/v3 v3.0.4
golang.org/x/crypto v0.42.0 golang.org/x/crypto v0.43.0
golang.org/x/term v0.35.0 golang.org/x/term v0.36.0
golang.org/x/text v0.29.0 golang.org/x/text v0.30.0
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/api v0.34.1 k8s.io/api v0.34.1
k8s.io/apiextensions-apiserver v0.34.1 k8s.io/apiextensions-apiserver v0.34.1
@ -48,7 +49,7 @@ require (
k8s.io/klog/v2 v2.130.1 k8s.io/klog/v2 v2.130.1
k8s.io/kubectl v0.34.1 k8s.io/kubectl v0.34.1
oras.land/oras-go/v2 v2.6.0 oras.land/oras-go/v2 v2.6.0
sigs.k8s.io/controller-runtime v0.22.1 sigs.k8s.io/controller-runtime v0.22.2
sigs.k8s.io/kustomize/kyaml v0.20.1 sigs.k8s.io/kustomize/kyaml v0.20.1
sigs.k8s.io/yaml v1.6.0 sigs.k8s.io/yaml v1.6.0
) )
@ -64,6 +65,7 @@ require (
github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/chai2010/gettext-go v1.0.2 // 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/coreos/go-systemd/v22 v22.5.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
@ -160,13 +162,13 @@ require (
go.opentelemetry.io/otel/trace v1.37.0 // indirect go.opentelemetry.io/otel/trace v1.37.0 // indirect
go.opentelemetry.io/proto/otlp v1.5.0 // indirect go.opentelemetry.io/proto/otlp v1.5.0 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect
golang.org/x/mod v0.27.0 // indirect golang.org/x/mod v0.28.0 // indirect
golang.org/x/net v0.43.0 // indirect golang.org/x/net v0.45.0 // indirect
golang.org/x/oauth2 v0.30.0 // indirect golang.org/x/oauth2 v0.30.0 // indirect
golang.org/x/sync v0.17.0 // indirect golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.36.0 // indirect golang.org/x/sys v0.37.0 // indirect
golang.org/x/time v0.12.0 // indirect golang.org/x/time v0.12.0 // indirect
golang.org/x/tools v0.36.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/api v0.0.0-20250303144028-a0af3efb3deb // indirect
google.golang.org/genproto/googleapis/rpc 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 google.golang.org/grpc v1.72.1 // 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/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 h1:IIA2aBdXvfbIM+yl/eTnL4hb1XwdpvuQLglAix1gweE=
github.com/Masterminds/vcs v1.13.3/go.mod h1:TiE7xuEjl1N4j016moRd6vezp6e6Lz23gypeXfzXeW8= 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/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/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= 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/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 h1:1Lwwip6Q2QGsAdl/ZKPCwTe9fe0CjlUbqj5bFNSjIRk=
github.com/chai2010/gettext-go v1.0.2/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA= 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 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= 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 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= 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 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= 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.5.0 h1:hIAhkRBMQ8nIeuVwcAoymp7MY4oherZdAxD+m0u9zaw=
github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= 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.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.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
@ -125,8 +129,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 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= 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/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.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=
github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= 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.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 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
@ -392,16 +396,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.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= 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.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g=
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= 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.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.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.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.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.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.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= 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-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-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@ -415,8 +419,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.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= 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.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ=
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM=
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= 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 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= 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= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -450,8 +454,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.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.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.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 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-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.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@ -459,8 +463,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.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= 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.14.0/go.mod h1:TySc+nGkYR6qt8km8wUhuFRTVSMIX3XPR58y2lC8vww=
golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ= golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA= 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.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.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
@ -468,8 +472,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.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.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.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= 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 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= 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= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@ -480,8 +484,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.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= 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.15.0/go.mod h1:hpksKq4dtpQWS1uQ61JkdqWM3LscIS6Slf+VVkm+wQk=
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= 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-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-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@ -532,8 +536,8 @@ k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= 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 h1:X4ELRsiGkrbeox69+9tzTu492FMUu7zJQW6eJU+I2oc=
oras.land/oras-go/v2 v2.6.0/go.mod h1:magiQDfG6H1O9APp+rOsvCPcW1GD2MM7vgnKY0Y+u1o= oras.land/oras-go/v2 v2.6.0/go.mod h1:magiQDfG6H1O9APp+rOsvCPcW1GD2MM7vgnKY0Y+u1o=
sigs.k8s.io/controller-runtime v0.22.1 h1:Ah1T7I+0A7ize291nJZdS1CabF/lB4E++WizgV24Eqg= sigs.k8s.io/controller-runtime v0.22.2 h1:cK2l8BGWsSWkXz09tcS4rJh95iOLney5eawcK5A33r4=
sigs.k8s.io/controller-runtime v0.22.1/go.mod h1:FwiwRjkRPbiN+zp2QRp7wlTCzbUXxZ/D4OzuQUDwBHY= sigs.k8s.io/controller-runtime v0.22.2/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 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE=
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= 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= sigs.k8s.io/kustomize/api v0.20.1 h1:iWP1Ydh3/lmldBnH/S5RXgT98vWYMaTUL1ADcr+Sv7I=

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

@ -19,24 +19,24 @@ package output
import ( import (
"github.com/fatih/color" "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 // 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 // Disable color if requested
if noColor { if noColor {
return status.String() return status.String()
} }
switch status { switch status {
case release.StatusDeployed: case common.StatusDeployed:
return color.GreenString(status.String()) return color.GreenString(status.String())
case release.StatusFailed: case common.StatusFailed:
return color.RedString(status.String()) 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()) return color.YellowString(status.String())
case release.StatusUnknown: case common.StatusUnknown:
return color.RedString(status.String()) return color.RedString(status.String())
default: default:
// For uninstalled, superseded, and any other status // For uninstalled, superseded, and any other status

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

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

@ -29,7 +29,7 @@ import (
"helm.sh/helm/v4/internal/plugin/schema" "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 { type SubprocessProtocolCommand struct {
// Protocols are the list of schemes from the charts URL. // Protocols are the list of schemes from the charts URL.
Protocols []string `yaml:"protocols"` Protocols []string `yaml:"protocols"`

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

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

@ -47,6 +47,7 @@ import (
"helm.sh/helm/v4/pkg/kube" "helm.sh/helm/v4/pkg/kube"
"helm.sh/helm/v4/pkg/postrenderer" "helm.sh/helm/v4/pkg/postrenderer"
"helm.sh/helm/v4/pkg/registry" "helm.sh/helm/v4/pkg/registry"
ri "helm.sh/helm/v4/pkg/release"
release "helm.sh/helm/v4/pkg/release/v1" release "helm.sh/helm/v4/pkg/release/v1"
releaseutil "helm.sh/helm/v4/pkg/release/v1/util" releaseutil "helm.sh/helm/v4/pkg/release/v1/util"
"helm.sh/helm/v4/pkg/storage" "helm.sh/helm/v4/pkg/storage"
@ -70,6 +71,21 @@ var (
errPending = errors.New("another operation (install/upgrade/rollback) is in progress") 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. // Configuration injects the dependencies that all actions share.
type Configuration struct { type Configuration struct {
// RESTClientGetter is an interface that loads Kubernetes clients. // RESTClientGetter is an interface that loads Kubernetes clients.
@ -397,7 +413,7 @@ func (cfg *Configuration) Now() time.Time {
return Timestamper() 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 { if err := chartutil.ValidateReleaseName(name); err != nil {
return nil, fmt.Errorf("releaseContent: Release name is invalid: %s", name) return nil, fmt.Errorf("releaseContent: Release name is invalid: %s", name)
} }
@ -528,3 +544,13 @@ func determineReleaseSSApplyMethod(serverSideApply bool) release.ApplyMethod {
} }
return release.ApplyMethodClientSideApply 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
}

@ -36,6 +36,7 @@ import (
"helm.sh/helm/v4/pkg/kube" "helm.sh/helm/v4/pkg/kube"
kubefake "helm.sh/helm/v4/pkg/kube/fake" kubefake "helm.sh/helm/v4/pkg/kube/fake"
"helm.sh/helm/v4/pkg/registry" "helm.sh/helm/v4/pkg/registry"
rcommon "helm.sh/helm/v4/pkg/release/common"
release "helm.sh/helm/v4/pkg/release/v1" release "helm.sh/helm/v4/pkg/release/v1"
"helm.sh/helm/v4/pkg/storage" "helm.sh/helm/v4/pkg/storage"
"helm.sh/helm/v4/pkg/storage/driver" "helm.sh/helm/v4/pkg/storage/driver"
@ -249,10 +250,10 @@ func withKube(version string) chartOption {
// releaseStub creates a release stub, complete with the chartStub as its chart. // releaseStub creates a release stub, complete with the chartStub as its chart.
func releaseStub() *release.Release { 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() now := time.Now()
return &release.Release{ return &release.Release{
Name: name, Name: name,
@ -951,3 +952,15 @@ func TestDetermineReleaseSSAApplyMethod(t *testing.T) {
assert.Equal(t, release.ApplyMethodClientSideApply, determineReleaseSSApplyMethod(false)) assert.Equal(t, release.ApplyMethodClientSideApply, determineReleaseSSApplyMethod(false))
assert.Equal(t, release.ApplyMethodServerSideApply, determineReleaseSSApplyMethod(true)) 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 package action
import ( 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. // 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. // 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 { if err := g.cfg.KubeClient.IsReachable(); err != nil {
return nil, err return nil, err
} }

@ -17,11 +17,15 @@ limitations under the License.
package action package action
import ( import (
"errors"
"log/slog"
"sort" "sort"
"strings" "strings"
"time" "time"
ci "helm.sh/helm/v4/pkg/chart"
chart "helm.sh/helm/v4/pkg/chart/v2" 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. // 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 are fetched from the Chart.yaml file
Annotations map[string]string `json:"annotations,omitempty" yaml:"annotations,omitempty"` Annotations map[string]string `json:"annotations,omitempty" yaml:"annotations,omitempty"`
// Labels of the release which are stored in driver metadata fields storage // Labels of the release which are stored in driver metadata fields storage
Labels map[string]string `json:"labels,omitempty" yaml:"labels,omitempty"` Labels map[string]string `json:"labels,omitempty" yaml:"labels,omitempty"`
Dependencies []*chart.Dependency `json:"dependencies,omitempty" yaml:"dependencies,omitempty"` Dependencies []ci.Dependency `json:"dependencies,omitempty" yaml:"dependencies,omitempty"`
Namespace string `json:"namespace" yaml:"namespace"` Namespace string `json:"namespace" yaml:"namespace"`
Revision int `json:"revision" yaml:"revision"` Revision int `json:"revision" yaml:"revision"`
Status string `json:"status" yaml:"status"` Status string `json:"status" yaml:"status"`
DeployedAt string `json:"deployedAt" yaml:"deployedAt"` DeployedAt string `json:"deployedAt" yaml:"deployedAt"`
ApplyMethod string `json:"applyMethod,omitempty" yaml:"applyMethod,omitempty"` ApplyMethod string `json:"applyMethod,omitempty" yaml:"applyMethod,omitempty"`
} }
// NewGetMetadata creates a new GetMetadata object with the given configuration. // 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 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{ return &Metadata{
Name: rel.Name, Name: rac.Name(),
Chart: rel.Chart.Metadata.Name, Chart: chrt.Metadata.Name,
Version: rel.Chart.Metadata.Version, Version: chrt.Metadata.Version,
AppVersion: rel.Chart.Metadata.AppVersion, AppVersion: chrt.Metadata.AppVersion,
Dependencies: rel.Chart.Metadata.Dependencies, Dependencies: ac.MetaDependencies(),
Annotations: rel.Chart.Metadata.Annotations, Annotations: chrt.Metadata.Annotations,
Labels: rel.Labels, Labels: rac.Labels(),
Namespace: rel.Namespace, Namespace: rac.Namespace(),
Revision: rel.Version, Revision: rac.Version(),
Status: rel.Info.Status.String(), Status: rac.Status(),
DeployedAt: rel.Info.LastDeployed.Format(time.RFC3339), DeployedAt: rac.DeployedAt().Format(time.RFC3339),
ApplyMethod: rel.ApplyMethod, ApplyMethod: rac.ApplyMethod(),
}, nil }, nil
} }
@ -88,7 +113,13 @@ func (g *GetMetadata) Run(name string) (*Metadata, error) {
func (m *Metadata) FormattedDepNames() string { func (m *Metadata) FormattedDepNames() string {
depsNames := make([]string, 0, len(m.Dependencies)) depsNames := make([]string, 0, len(m.Dependencies))
for _, dep := range 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() sort.StringSlice(depsNames).Sort()

@ -25,8 +25,10 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
ci "helm.sh/helm/v4/pkg/chart"
chart "helm.sh/helm/v4/pkg/chart/v2" chart "helm.sh/helm/v4/pkg/chart/v2"
kubefake "helm.sh/helm/v4/pkg/kube/fake" kubefake "helm.sh/helm/v4/pkg/kube/fake"
"helm.sh/helm/v4/pkg/release/common"
release "helm.sh/helm/v4/pkg/release/v1" release "helm.sh/helm/v4/pkg/release/v1"
) )
@ -49,7 +51,7 @@ func TestGetMetadata_Run_BasicMetadata(t *testing.T) {
rel := &release.Release{ rel := &release.Release{
Name: releaseName, Name: releaseName,
Info: &release.Info{ Info: &release.Info{
Status: release.StatusDeployed, Status: common.StatusDeployed,
LastDeployed: deployedTime, LastDeployed: deployedTime,
}, },
Chart: &chart.Chart{ Chart: &chart.Chart{
@ -63,7 +65,8 @@ func TestGetMetadata_Run_BasicMetadata(t *testing.T) {
Namespace: "default", Namespace: "default",
} }
cfg.Releases.Create(rel) err := cfg.Releases.Create(rel)
require.NoError(t, err)
result, err := client.Run(releaseName) result, err := client.Run(releaseName)
require.NoError(t, err) require.NoError(t, err)
@ -103,7 +106,7 @@ func TestGetMetadata_Run_WithDependencies(t *testing.T) {
rel := &release.Release{ rel := &release.Release{
Name: releaseName, Name: releaseName,
Info: &release.Info{ Info: &release.Info{
Status: release.StatusDeployed, Status: common.StatusDeployed,
LastDeployed: deployedTime, LastDeployed: deployedTime,
}, },
Chart: &chart.Chart{ Chart: &chart.Chart{
@ -123,13 +126,18 @@ func TestGetMetadata_Run_WithDependencies(t *testing.T) {
result, err := client.Run(releaseName) result, err := client.Run(releaseName)
require.NoError(t, err) 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, releaseName, result.Name)
assert.Equal(t, "test-chart", result.Chart) assert.Equal(t, "test-chart", result.Chart)
assert.Equal(t, "1.0.0", result.Version) 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.Len(t, result.Dependencies, 2)
assert.Equal(t, "mysql", result.Dependencies[0].Name) assert.Equal(t, "mysql", dep0.Name())
assert.Equal(t, "redis", result.Dependencies[1].Name) assert.Equal(t, "redis", dep1.Name())
} }
func TestGetMetadata_Run_WithDependenciesAliases(t *testing.T) { func TestGetMetadata_Run_WithDependenciesAliases(t *testing.T) {
@ -157,7 +165,7 @@ func TestGetMetadata_Run_WithDependenciesAliases(t *testing.T) {
rel := &release.Release{ rel := &release.Release{
Name: releaseName, Name: releaseName,
Info: &release.Info{ Info: &release.Info{
Status: release.StatusDeployed, Status: common.StatusDeployed,
LastDeployed: deployedTime, LastDeployed: deployedTime,
}, },
Chart: &chart.Chart{ Chart: &chart.Chart{
@ -177,15 +185,20 @@ func TestGetMetadata_Run_WithDependenciesAliases(t *testing.T) {
result, err := client.Run(releaseName) result, err := client.Run(releaseName)
require.NoError(t, err) 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, releaseName, result.Name)
assert.Equal(t, "test-chart", result.Chart) assert.Equal(t, "test-chart", result.Chart)
assert.Equal(t, "1.0.0", result.Version) 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.Len(t, result.Dependencies, 2)
assert.Equal(t, "mysql", result.Dependencies[0].Name) assert.Equal(t, "mysql", dep0.Name())
assert.Equal(t, "database", result.Dependencies[0].Alias) assert.Equal(t, "database", dep0.Alias())
assert.Equal(t, "redis", result.Dependencies[1].Name) assert.Equal(t, "redis", dep1.Name())
assert.Equal(t, "cache", result.Dependencies[1].Alias) assert.Equal(t, "cache", dep1.Alias())
} }
func TestGetMetadata_Run_WithMixedDependencies(t *testing.T) { func TestGetMetadata_Run_WithMixedDependencies(t *testing.T) {
@ -223,7 +236,7 @@ func TestGetMetadata_Run_WithMixedDependencies(t *testing.T) {
rel := &release.Release{ rel := &release.Release{
Name: releaseName, Name: releaseName,
Info: &release.Info{ Info: &release.Info{
Status: release.StatusDeployed, Status: common.StatusDeployed,
LastDeployed: deployedTime, LastDeployed: deployedTime,
}, },
Chart: &chart.Chart{ Chart: &chart.Chart{
@ -243,23 +256,32 @@ func TestGetMetadata_Run_WithMixedDependencies(t *testing.T) {
result, err := client.Run(releaseName) result, err := client.Run(releaseName)
require.NoError(t, err) 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, releaseName, result.Name)
assert.Equal(t, "test-chart", result.Chart) assert.Equal(t, "test-chart", result.Chart)
assert.Equal(t, "1.0.0", result.Version) 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) assert.Len(t, result.Dependencies, 4)
// Verify dependencies with aliases // Verify dependencies with aliases
assert.Equal(t, "mysql", result.Dependencies[0].Name) assert.Equal(t, "mysql", dep0.Name())
assert.Equal(t, "database", result.Dependencies[0].Alias) assert.Equal(t, "database", dep0.Alias())
assert.Equal(t, "redis", result.Dependencies[2].Name) assert.Equal(t, "redis", dep2.Name())
assert.Equal(t, "cache", result.Dependencies[2].Alias) assert.Equal(t, "cache", dep2.Alias())
// Verify dependencies without aliases // Verify dependencies without aliases
assert.Equal(t, "nginx", result.Dependencies[1].Name) assert.Equal(t, "nginx", dep1.Name())
assert.Equal(t, "", result.Dependencies[1].Alias) assert.Equal(t, "", dep1.Alias())
assert.Equal(t, "postgresql", result.Dependencies[3].Name) assert.Equal(t, "postgresql", dep3.Name())
assert.Equal(t, "", result.Dependencies[3].Alias) assert.Equal(t, "", dep3.Alias())
} }
func TestGetMetadata_Run_WithAnnotations(t *testing.T) { func TestGetMetadata_Run_WithAnnotations(t *testing.T) {
@ -278,7 +300,7 @@ func TestGetMetadata_Run_WithAnnotations(t *testing.T) {
rel := &release.Release{ rel := &release.Release{
Name: releaseName, Name: releaseName,
Info: &release.Info{ Info: &release.Info{
Status: release.StatusDeployed, Status: common.StatusDeployed,
LastDeployed: deployedTime, LastDeployed: deployedTime,
}, },
Chart: &chart.Chart{ Chart: &chart.Chart{
@ -317,7 +339,7 @@ func TestGetMetadata_Run_SpecificVersion(t *testing.T) {
rel1 := &release.Release{ rel1 := &release.Release{
Name: releaseName, Name: releaseName,
Info: &release.Info{ Info: &release.Info{
Status: release.StatusSuperseded, Status: common.StatusSuperseded,
LastDeployed: deployedTime.Add(-time.Hour), LastDeployed: deployedTime.Add(-time.Hour),
}, },
Chart: &chart.Chart{ Chart: &chart.Chart{
@ -334,7 +356,7 @@ func TestGetMetadata_Run_SpecificVersion(t *testing.T) {
rel2 := &release.Release{ rel2 := &release.Release{
Name: releaseName, Name: releaseName,
Info: &release.Info{ Info: &release.Info{
Status: release.StatusDeployed, Status: common.StatusDeployed,
LastDeployed: deployedTime, LastDeployed: deployedTime,
}, },
Chart: &chart.Chart{ Chart: &chart.Chart{
@ -368,16 +390,16 @@ func TestGetMetadata_Run_DifferentStatuses(t *testing.T) {
testCases := []struct { testCases := []struct {
name string name string
status release.Status status common.Status
expected string expected string
}{ }{
{"deployed", release.StatusDeployed, "deployed"}, {"deployed", common.StatusDeployed, "deployed"},
{"failed", release.StatusFailed, "failed"}, {"failed", common.StatusFailed, "failed"},
{"uninstalled", release.StatusUninstalled, "uninstalled"}, {"uninstalled", common.StatusUninstalled, "uninstalled"},
{"pending-install", release.StatusPendingInstall, "pending-install"}, {"pending-install", common.StatusPendingInstall, "pending-install"},
{"pending-upgrade", release.StatusPendingUpgrade, "pending-upgrade"}, {"pending-upgrade", common.StatusPendingUpgrade, "pending-upgrade"},
{"pending-rollback", release.StatusPendingRollback, "pending-rollback"}, {"pending-rollback", common.StatusPendingRollback, "pending-rollback"},
{"superseded", release.StatusSuperseded, "superseded"}, {"superseded", common.StatusSuperseded, "superseded"},
} }
for _, tc := range testCases { for _, tc := range testCases {
@ -444,7 +466,7 @@ func TestGetMetadata_Run_EmptyAppVersion(t *testing.T) {
rel := &release.Release{ rel := &release.Release{
Name: releaseName, Name: releaseName,
Info: &release.Info{ Info: &release.Info{
Status: release.StatusDeployed, Status: common.StatusDeployed,
LastDeployed: deployedTime, LastDeployed: deployedTime,
}, },
Chart: &chart.Chart{ Chart: &chart.Chart{
@ -515,8 +537,9 @@ func TestMetadata_FormattedDepNames(t *testing.T) {
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
deps := convertDeps(tc.dependencies)
metadata := &Metadata{ metadata := &Metadata{
Dependencies: tc.dependencies, Dependencies: deps,
} }
result := metadata.FormattedDepNames() result := metadata.FormattedDepNames()
@ -525,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) { func TestMetadata_FormattedDepNames_WithComplexDependencies(t *testing.T) {
dependencies := []*chart.Dependency{ dependencies := []*chart.Dependency{
{ {
@ -546,8 +577,9 @@ func TestMetadata_FormattedDepNames_WithComplexDependencies(t *testing.T) {
}, },
} }
deps := convertDeps(dependencies)
metadata := &Metadata{ metadata := &Metadata{
Dependencies: dependencies, Dependencies: deps,
} }
result := metadata.FormattedDepNames() result := metadata.FormattedDepNames()
@ -597,8 +629,9 @@ func TestMetadata_FormattedDepNames_WithAliases(t *testing.T) {
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
deps := convertDeps(tc.dependencies)
metadata := &Metadata{ metadata := &Metadata{
Dependencies: tc.dependencies, Dependencies: deps,
} }
result := metadata.FormattedDepNames() result := metadata.FormattedDepNames()
@ -609,7 +642,7 @@ func TestMetadata_FormattedDepNames_WithAliases(t *testing.T) {
func TestGetMetadata_Labels(t *testing.T) { func TestGetMetadata_Labels(t *testing.T) {
rel := releaseStub() rel := releaseStub()
rel.Info.Status = release.StatusDeployed rel.Info.Status = common.StatusDeployed
customLabels := map[string]string{"key1": "value1", "key2": "value2"} customLabels := map[string]string{"key1": "value1", "key2": "value2"}
rel.Labels = customLabels rel.Labels = customLabels

@ -16,7 +16,13 @@ limitations under the License.
package action 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. // 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 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 { if err != nil {
return nil, err return nil, err
} }
@ -56,3 +67,18 @@ func (g *GetValues) Run(name string) (map[string]interface{}, error) {
} }
return rel.Config, nil 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)
}
}

@ -26,6 +26,7 @@ import (
chart "helm.sh/helm/v4/pkg/chart/v2" chart "helm.sh/helm/v4/pkg/chart/v2"
kubefake "helm.sh/helm/v4/pkg/kube/fake" kubefake "helm.sh/helm/v4/pkg/kube/fake"
"helm.sh/helm/v4/pkg/release/common"
release "helm.sh/helm/v4/pkg/release/v1" release "helm.sh/helm/v4/pkg/release/v1"
) )
@ -58,7 +59,7 @@ func TestGetValues_Run_UserConfigOnly(t *testing.T) {
rel := &release.Release{ rel := &release.Release{
Name: releaseName, Name: releaseName,
Info: &release.Info{ Info: &release.Info{
Status: release.StatusDeployed, Status: common.StatusDeployed,
}, },
Chart: &chart.Chart{ Chart: &chart.Chart{
Metadata: &chart.Metadata{ Metadata: &chart.Metadata{
@ -112,7 +113,7 @@ func TestGetValues_Run_AllValues(t *testing.T) {
rel := &release.Release{ rel := &release.Release{
Name: releaseName, Name: releaseName,
Info: &release.Info{ Info: &release.Info{
Status: release.StatusDeployed, Status: common.StatusDeployed,
}, },
Chart: &chart.Chart{ Chart: &chart.Chart{
Metadata: &chart.Metadata{ Metadata: &chart.Metadata{
@ -147,7 +148,7 @@ func TestGetValues_Run_EmptyValues(t *testing.T) {
rel := &release.Release{ rel := &release.Release{
Name: releaseName, Name: releaseName,
Info: &release.Info{ Info: &release.Info{
Status: release.StatusDeployed, Status: common.StatusDeployed,
}, },
Chart: &chart.Chart{ Chart: &chart.Chart{
Metadata: &chart.Metadata{ Metadata: &chart.Metadata{
@ -198,7 +199,7 @@ func TestGetValues_Run_NilConfig(t *testing.T) {
rel := &release.Release{ rel := &release.Release{
Name: releaseName, Name: releaseName,
Info: &release.Info{ Info: &release.Info{
Status: release.StatusDeployed, Status: common.StatusDeployed,
}, },
Chart: &chart.Chart{ Chart: &chart.Chart{
Metadata: &chart.Metadata{ Metadata: &chart.Metadata{

@ -22,7 +22,7 @@ import (
"fmt" "fmt"
chartutil "helm.sh/helm/v4/pkg/chart/v2/util" 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. // 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. // 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 { if err := h.cfg.KubeClient.IsReachable(); err != nil {
return nil, err return nil, err
} }

@ -154,7 +154,7 @@ func (cfg *Configuration) deleteHookByPolicy(h *release.Hook, policy release.Hoo
if err != nil { if err != nil {
return fmt.Errorf("unable to build kubernetes object for deleting hook %s: %w", h.Path, err) 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 { if len(errs) > 0 {
return joinErrors(errs, "; ") return joinErrors(errs, "; ")
} }
@ -223,16 +223,12 @@ func (cfg *Configuration) outputLogsByPolicy(h *release.Hook, releaseNamespace s
} }
func (cfg *Configuration) outputContainerLogsForListOptions(namespace string, listOptions metav1.ListOptions) error { 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 podList, err := cfg.KubeClient.GetPodList(namespace, listOptions)
if kubeClient, ok := cfg.KubeClient.(kube.InterfaceLogs); ok { if err != nil {
podList, err := kubeClient.GetPodList(namespace, listOptions)
if err != nil {
return err
}
err = kubeClient.OutputContainerLogsForPodList(podList, namespace, cfg.HookOutputFunc)
return err return err
} }
return nil
return cfg.KubeClient.OutputContainerLogsForPodList(podList, namespace, cfg.HookOutputFunc)
} }
func (cfg *Configuration) deriveNamespace(h *release.Hook, namespace string) (string, error) { func (cfg *Configuration) deriveNamespace(h *release.Hook, namespace string) (string, error) {

@ -27,12 +27,14 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
v1 "k8s.io/api/core/v1" v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/yaml" "k8s.io/apimachinery/pkg/util/yaml"
"k8s.io/cli-runtime/pkg/resource" "k8s.io/cli-runtime/pkg/resource"
"helm.sh/helm/v4/pkg/chart/common" "helm.sh/helm/v4/pkg/chart/common"
"helm.sh/helm/v4/pkg/kube" "helm.sh/helm/v4/pkg/kube"
kubefake "helm.sh/helm/v4/pkg/kube/fake" kubefake "helm.sh/helm/v4/pkg/kube/fake"
rcommon "helm.sh/helm/v4/pkg/release/common"
release "helm.sh/helm/v4/pkg/release/v1" release "helm.sh/helm/v4/pkg/release/v1"
"helm.sh/helm/v4/pkg/storage" "helm.sh/helm/v4/pkg/storage"
"helm.sh/helm/v4/pkg/storage/driver" "helm.sh/helm/v4/pkg/storage/driver"
@ -184,10 +186,12 @@ func runInstallForHooksWithSuccess(t *testing.T, manifest, expectedNamespace str
} }
vals := map[string]interface{}{} 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.NoError(err)
is.Equal(expectedOutput, outBuffer.String()) 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) { func runInstallForHooksWithFailure(t *testing.T, manifest, expectedNamespace string, shouldOutput bool) {
@ -211,11 +215,13 @@ func runInstallForHooksWithFailure(t *testing.T, manifest, expectedNamespace str
} }
vals := map[string]interface{}{} vals := map[string]interface{}{}
res, err := instAction.Run(buildChartWithTemplates(templates), vals) resi, err := instAction.Run(buildChartWithTemplates(templates), vals)
is.Error(err) is.Error(err)
res, err := releaserToV1Release(resi)
is.NoError(err)
is.Contains(res.Info.Description, "failed pre-install") is.Contains(res.Info.Description, "failed pre-install")
is.Equal(expectedOutput, outBuffer.String()) is.Equal(expectedOutput, outBuffer.String())
is.Equal(release.StatusFailed, res.Info.Status) is.Equal(rcommon.StatusFailed, res.Info.Status)
} }
type HookFailedError struct{} type HookFailedError struct{}
@ -259,7 +265,7 @@ func (h *HookFailingKubeWaiter) WatchUntilReady(resources kube.ResourceList, _ t
return nil 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 { for _, res := range resources {
h.deleteRecord = append(h.deleteRecord, resource.Info{ h.deleteRecord = append(h.deleteRecord, resource.Info{
Name: res.Name, Name: res.Name,
@ -267,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) { func (h *HookFailingKubeClient) GetWaiter(strategy kube.WaitStrategy) (kube.Waiter, error) {

@ -26,7 +26,6 @@ import (
"log/slog" "log/slog"
"net/url" "net/url"
"os" "os"
"path"
"path/filepath" "path/filepath"
"strings" "strings"
"sync" "sync"
@ -54,6 +53,8 @@ import (
kubefake "helm.sh/helm/v4/pkg/kube/fake" kubefake "helm.sh/helm/v4/pkg/kube/fake"
"helm.sh/helm/v4/pkg/postrenderer" "helm.sh/helm/v4/pkg/postrenderer"
"helm.sh/helm/v4/pkg/registry" "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" release "helm.sh/helm/v4/pkg/release/v1"
releaseutil "helm.sh/helm/v4/pkg/release/v1/util" releaseutil "helm.sh/helm/v4/pkg/release/v1/util"
"helm.sh/helm/v4/pkg/repo/v1" "helm.sh/helm/v4/pkg/repo/v1"
@ -75,7 +76,6 @@ type Install struct {
ChartPathOptions ChartPathOptions
ClientOnly bool
// ForceReplace will, if set to `true`, ignore certain warnings and perform the install anyway. // ForceReplace will, if set to `true`, ignore certain warnings and perform the install anyway.
// //
// This should be used with caution. // This should be used with caution.
@ -87,8 +87,8 @@ type Install struct {
// see: https://kubernetes.io/docs/reference/using-api/server-side-apply/ // see: https://kubernetes.io/docs/reference/using-api/server-side-apply/
ServerSideApply bool ServerSideApply bool
CreateNamespace bool CreateNamespace bool
DryRun bool // DryRunStrategy can be set to prepare, but not execute the operation and whether or not to interact with the remote cluster
DryRunOption string DryRunStrategy DryRunStrategy
// HideSecret can be set to true when DryRun is enabled in order to hide // 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. // Kubernetes Secrets in the output. It cannot be used outside of DryRun.
HideSecret bool HideSecret bool
@ -159,6 +159,7 @@ func NewInstall(cfg *Configuration) *Install {
in := &Install{ in := &Install{
cfg: cfg, cfg: cfg,
ServerSideApply: true, ServerSideApply: true,
DryRunStrategy: DryRunNone,
} }
in.registryClient = cfg.RegistryClient in.registryClient = cfg.RegistryClient
@ -244,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 // If DryRun is set to true, this will prepare the release, but not install it
func (i *Install) Run(chrt ci.Charter, vals map[string]interface{}) (*release.Release, error) { func (i *Install) Run(chrt ci.Charter, vals map[string]interface{}) (ri.Releaser, error) {
ctx := context.Background() ctx := context.Background()
return i.RunWithContext(ctx, chrt, vals) return i.RunWithContext(ctx, chrt, vals)
} }
@ -253,7 +254,7 @@ func (i *Install) Run(chrt ci.Charter, vals map[string]interface{}) (*release.Re
// //
// When the task is cancelled through ctx, the function returns and the install // When the task is cancelled through ctx, the function returns and the install
// proceeds in the background. // proceeds in the background.
func (i *Install) RunWithContext(ctx context.Context, ch ci.Charter, vals map[string]interface{}) (*release.Release, error) { func (i *Install) RunWithContext(ctx context.Context, ch ci.Charter, vals map[string]interface{}) (ri.Releaser, error) {
var chrt *chart.Chart var chrt *chart.Chart
switch c := ch.(type) { switch c := ch.(type) {
case *chart.Chart: case *chart.Chart:
@ -264,8 +265,7 @@ func (i *Install) RunWithContext(ctx context.Context, ch ci.Charter, vals map[st
return nil, errors.New("invalid chart apiVersion") return nil, errors.New("invalid chart apiVersion")
} }
// Check reachability of cluster unless in client-only mode (e.g. `helm template` without `--validate`) if interactWithServer(i.DryRunStrategy) {
if !i.ClientOnly {
if err := i.cfg.KubeClient.IsReachable(); err != nil { if err := i.cfg.KubeClient.IsReachable(); err != nil {
slog.Error(fmt.Sprintf("cluster reachability check failed: %v", err)) slog.Error(fmt.Sprintf("cluster reachability check failed: %v", err))
return nil, fmt.Errorf("cluster reachability check failed: %w", err) return nil, fmt.Errorf("cluster reachability check failed: %w", err)
@ -273,7 +273,7 @@ func (i *Install) RunWithContext(ctx context.Context, ch ci.Charter, vals map[st
} }
// HideSecret must be used with dry run. Otherwise, return an error. // 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") slog.Error("hiding Kubernetes secrets requires a dry-run mode")
return nil, errors.New("hiding Kubernetes secrets requires a dry-run mode") return nil, errors.New("hiding Kubernetes secrets requires a dry-run mode")
} }
@ -288,23 +288,18 @@ func (i *Install) RunWithContext(ctx context.Context, ch ci.Charter, vals map[st
return nil, fmt.Errorf("chart dependencies processing failed: %w", err) 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 // Pre-install anything in the crd/ directory. We do this before Helm
// contacts the upstream server and builds the capabilities object. // 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 // 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.") 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 { } else if err := i.installCRDs(crds); err != nil {
return nil, err return nil, err
} }
} }
if i.ClientOnly { if !interactWithServer(i.DryRunStrategy) {
// Add mock objects in here so it doesn't use Kube API server // Add mock objects in here so it doesn't use Kube API server
// NOTE(bacongobbler): used for `helm template` // NOTE(bacongobbler): used for `helm template`
i.cfg.Capabilities = common.DefaultCapabilities.Copy() i.cfg.Capabilities = common.DefaultCapabilities.Copy()
@ -317,7 +312,7 @@ func (i *Install) RunWithContext(ctx context.Context, ch ci.Charter, vals map[st
mem := driver.NewMemory() mem := driver.NewMemory()
mem.SetNamespace(i.Namespace) mem.SetNamespace(i.Namespace)
i.cfg.Releases = storage.Init(mem) 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") slog.Debug("API Version list given outside of client only mode, this list will be ignored")
} }
@ -333,7 +328,7 @@ func (i *Install) RunWithContext(ctx context.Context, ch ci.Charter, vals map[st
} }
// special case for helm template --is-upgrade // special case for helm template --is-upgrade
isUpgrade := i.IsUpgrade && i.isDryRun() isUpgrade := i.IsUpgrade && isDryRun(i.DryRunStrategy)
options := common.ReleaseOptions{ options := common.ReleaseOptions{
Name: i.ReleaseName, Name: i.ReleaseName,
Namespace: i.Namespace, Namespace: i.Namespace,
@ -353,20 +348,20 @@ func (i *Install) RunWithContext(ctx context.Context, ch ci.Charter, vals map[st
rel := i.createRelease(chrt, vals, i.Labels) rel := i.createRelease(chrt, vals, i.Labels)
var manifestDoc *bytes.Buffer 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 // Even for errors, attach this if available
if manifestDoc != nil { if manifestDoc != nil {
rel.Manifest = manifestDoc.String() rel.Manifest = manifestDoc.String()
} }
// Check error from render // Check error from render
if err != nil { 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 a release with partial data so that the client can show debugging information.
return rel, err return rel, err
} }
// Mark this release as in-progress // Mark this release as in-progress
rel.SetStatus(release.StatusPendingInstall, "Initial install underway") rel.SetStatus(rcommon.StatusPendingInstall, "Initial install underway")
var toBeAdopted kube.ResourceList var toBeAdopted kube.ResourceList
resources, err := i.cfg.KubeClient.Build(bytes.NewBufferString(rel.Manifest), !i.DisableOpenAPIValidation) resources, err := i.cfg.KubeClient.Build(bytes.NewBufferString(rel.Manifest), !i.DisableOpenAPIValidation)
@ -386,7 +381,7 @@ func (i *Install) RunWithContext(ctx context.Context, ch ci.Charter, vals map[st
// we'll end up in a state where we will delete those resources upon // 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 // deleting the release because the manifest will be pointing at that
// resource // resource
if !i.ClientOnly && !isUpgrade && len(resources) > 0 { if interactWithServer(i.DryRunStrategy) && !isUpgrade && len(resources) > 0 {
if i.TakeOwnership { if i.TakeOwnership {
toBeAdopted, err = requireAdoption(resources) toBeAdopted, err = requireAdoption(resources)
} else { } else {
@ -398,7 +393,7 @@ func (i *Install) RunWithContext(ctx context.Context, ch ci.Charter, vals map[st
} }
// Bail out here if it is a dry run // Bail out here if it is a dry run
if i.isDryRun() { if isDryRun(i.DryRunStrategy) {
rel.Info.Description = "Dry run complete" rel.Info.Description = "Dry run complete"
return rel, nil return rel, nil
} }
@ -480,14 +475,6 @@ func (i *Install) getGoroutineCount() int32 {
return i.goroutineCount.Load() 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) { func (i *Install) performInstall(rel *release.Release, toBeAdopted kube.ResourceList, resources kube.ResourceList) (*release.Release, error) {
var err error var err error
// pre-install hooks // pre-install hooks
@ -539,9 +526,9 @@ func (i *Install) performInstall(rel *release.Release, toBeAdopted kube.Resource
} }
if len(i.Description) > 0 { if len(i.Description) > 0 {
rel.SetStatus(release.StatusDeployed, i.Description) rel.SetStatus(rcommon.StatusDeployed, i.Description)
} else { } 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 // This is a tricky case. The release has been created, but the result
@ -559,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) { 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 { if i.RollbackOnFailure {
slog.Debug("install failed and rollback-on-failure is set, uninstalling release", "release", i.ReleaseName) slog.Debug("install failed and rollback-on-failure is set, uninstalling release", "release", i.ReleaseName)
uninstall := NewUninstall(i.cfg) uninstall := NewUninstall(i.cfg)
@ -590,7 +577,7 @@ func (i *Install) availableName() error {
return fmt.Errorf("release name %q: %w", start, err) return fmt.Errorf("release name %q: %w", start, err)
} }
// On dry run, bail here // On dry run, bail here
if i.isDryRun() { if isDryRun(i.DryRunStrategy) {
return nil return nil
} }
@ -598,15 +585,43 @@ func (i *Install) availableName() error {
if err != nil || len(h) < 1 { if err != nil || len(h) < 1 {
return nil 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 nil
} }
return errors.New("cannot reuse a name that is still in use") 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 // createRelease creates a new release object
func (i *Install) createRelease(chrt *chart.Chart, rawVals map[string]interface{}, labels map[string]string) *release.Release { func (i *Install) createRelease(chrt *chart.Chart, rawVals map[string]interface{}, labels map[string]string) *release.Release {
ts := i.cfg.Now() ts := i.cfg.Now()
@ -619,7 +634,7 @@ func (i *Install) createRelease(chrt *chart.Chart, rawVals map[string]interface{
Info: &release.Info{ Info: &release.Info{
FirstDeployed: ts, FirstDeployed: ts,
LastDeployed: ts, LastDeployed: ts,
Status: release.StatusUnknown, Status: rcommon.StatusUnknown,
}, },
Version: 1, Version: 1,
Labels: labels, Labels: labels,
@ -645,20 +660,24 @@ func (i *Install) replaceRelease(rel *release.Release) error {
// No releases exist for this name, so we can return early // No releases exist for this name, so we can return early
return nil return nil
} }
hl, err := releaseListToV1List(hist)
if err != nil {
return err
}
releaseutil.Reverse(hist, releaseutil.SortByRevision) releaseutil.Reverse(hl, releaseutil.SortByRevision)
last := hist[0] last := hl[0]
// Update version to the next available // Update version to the next available
rel.Version = last.Version + 1 rel.Version = last.Version + 1
// Do not change the status of a failed release. // Do not change the status of a failed release.
if last.Info.Status == release.StatusFailed { if last.Info.Status == rcommon.StatusFailed {
return nil return nil
} }
// For any other status, mark it as superseded and store the old record // 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) return i.recordRelease(last)
} }
@ -697,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 // check if the directory exists to create file. creates if doesn't exist
func ensureDirectoryForFile(file string) error { func ensureDirectoryForFile(file string) error {
baseDir := path.Dir(file) baseDir := filepath.Dir(file)
_, err := os.Stat(baseDir) _, err := os.Stat(baseDir)
if err != nil && !errors.Is(err, fs.ErrNotExist) { if err != nil && !errors.Is(err, fs.ErrNotExist) {
return err return err
@ -828,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. // This does not ensure that the chart is well-formed; only that the requested filename exists.
// //
// Order of resolution: // 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 // - if path is absolute or begins with '.', error out here
// - URL // - URL
// //
@ -841,20 +860,22 @@ func (c *ChartPathOptions) LocateChart(name string, settings *cli.EnvSettings) (
name = strings.TrimSpace(name) name = strings.TrimSpace(name)
version := strings.TrimSpace(c.Version) version := strings.TrimSpace(c.Version)
if _, err := os.Stat(name); err == nil { if c.RepoURL == "" {
abs, err := filepath.Abs(name) if _, err := os.Stat(name); err == nil {
if err != nil { abs, err := filepath.Abs(name)
return abs, err if err != nil {
} return abs, err
if c.Verify {
if _, err := downloader.VerifyChart(abs, abs+".prov", c.Keyring); err != nil {
return "", 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{ dl := downloader.ChartDownloader{

@ -47,6 +47,7 @@ import (
"helm.sh/helm/v4/pkg/chart/common" "helm.sh/helm/v4/pkg/chart/common"
"helm.sh/helm/v4/pkg/kube" "helm.sh/helm/v4/pkg/kube"
kubefake "helm.sh/helm/v4/pkg/kube/fake" kubefake "helm.sh/helm/v4/pkg/kube/fake"
rcommon "helm.sh/helm/v4/pkg/release/common"
release "helm.sh/helm/v4/pkg/release/v1" release "helm.sh/helm/v4/pkg/release/v1"
"helm.sh/helm/v4/pkg/storage/driver" "helm.sh/helm/v4/pkg/storage/driver"
) )
@ -130,14 +131,19 @@ func TestInstallRelease(t *testing.T) {
instAction := installAction(t) instAction := installAction(t)
vals := map[string]interface{}{} vals := map[string]interface{}{}
ctx, done := context.WithCancel(t.Context()) ctx, done := context.WithCancel(t.Context())
res, err := instAction.RunWithContext(ctx, buildChart(), vals) resi, err := instAction.RunWithContext(ctx, buildChart(), vals)
if err != nil { if err != nil {
t.Fatalf("Failed install: %s", err) 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.Name, "test-install-release", "Expected release name.")
is.Equal(res.Namespace, "spaced") 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.NoError(err)
is.Len(rel.Hooks, 1) is.Len(rel.Hooks, 1)
@ -156,7 +162,9 @@ func TestInstallRelease(t *testing.T) {
time.Sleep(time.Millisecond * 100) time.Sleep(time.Millisecond * 100)
lastRelease, err := instAction.cfg.Releases.Last(rel.Name) lastRelease, err := instAction.cfg.Releases.Last(rel.Name)
req.NoError(err) 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) { func TestInstallReleaseWithTakeOwnership_ResourceNotOwned(t *testing.T) {
@ -175,12 +183,17 @@ func TestInstallReleaseWithTakeOwnership_ResourceNotOwned(t *testing.T) {
config := actionConfigFixtureWithDummyResources(t, createDummyResourceList(false)) config := actionConfigFixtureWithDummyResources(t, createDummyResourceList(false))
instAction := installActionWithConfig(config) instAction := installActionWithConfig(config)
instAction.TakeOwnership = true instAction.TakeOwnership = true
res, err := instAction.Run(buildChart(), nil) resi, err := instAction.Run(buildChart(), nil)
if err != nil { if err != nil {
t.Fatalf("Failed install: %s", err) 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.NoError(err)
is.Equal(rel.Info.Description, "Install complete") is.Equal(rel.Info.Description, "Install complete")
@ -193,11 +206,16 @@ func TestInstallReleaseWithTakeOwnership_ResourceOwned(t *testing.T) {
config := actionConfigFixtureWithDummyResources(t, createDummyResourceList(true)) config := actionConfigFixtureWithDummyResources(t, createDummyResourceList(true))
instAction := installActionWithConfig(config) instAction := installActionWithConfig(config)
instAction.TakeOwnership = false instAction.TakeOwnership = false
res, err := instAction.Run(buildChart(), nil) resi, err := instAction.Run(buildChart(), nil)
if err != nil { if err != nil {
t.Fatalf("Failed install: %s", err) 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.NoError(err)
is.Equal(rel.Info.Description, "Install complete") is.Equal(rel.Info.Description, "Install complete")
@ -227,14 +245,19 @@ func TestInstallReleaseWithValues(t *testing.T) {
"simpleKey": "simpleValue", "simpleKey": "simpleValue",
}, },
} }
res, err := instAction.Run(buildChart(withSampleValues()), userVals) resi, err := instAction.Run(buildChart(withSampleValues()), userVals)
if err != nil { if err != nil {
t.Fatalf("Failed install: %s", err) 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.Name, "test-install-release", "Expected release name.")
is.Equal(res.Namespace, "spaced") 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.NoError(err)
is.Len(rel.Hooks, 1) is.Len(rel.Hooks, 1)
@ -249,16 +272,6 @@ func TestInstallReleaseWithValues(t *testing.T) {
is.Equal(expectedUserValues, rel.Config) 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) { func TestInstallRelease_NoName(t *testing.T) {
instAction := installAction(t) instAction := installAction(t)
instAction.ReleaseName = "" instAction.ReleaseName = ""
@ -275,15 +288,19 @@ func TestInstallRelease_WithNotes(t *testing.T) {
instAction := installAction(t) instAction := installAction(t)
instAction.ReleaseName = "with-notes" instAction.ReleaseName = "with-notes"
vals := map[string]interface{}{} 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 { if err != nil {
t.Fatalf("Failed install: %s", err) t.Fatalf("Failed install: %s", err)
} }
res, err := releaserToV1Release(resi)
is.NoError(err)
is.Equal(res.Name, "with-notes") is.Equal(res.Name, "with-notes")
is.Equal(res.Namespace, "spaced") 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.NoError(err)
is.Len(rel.Hooks, 1) is.Len(rel.Hooks, 1)
is.Equal(rel.Hooks[0].Manifest, manifestWithHook) is.Equal(rel.Hooks[0].Manifest, manifestWithHook)
@ -302,12 +319,16 @@ func TestInstallRelease_WithNotesRendered(t *testing.T) {
instAction := installAction(t) instAction := installAction(t)
instAction.ReleaseName = "with-notes" instAction.ReleaseName = "with-notes"
vals := map[string]interface{}{} 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 { if err != nil {
t.Fatalf("Failed install: %s", err) 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.NoError(err)
expectedNotes := fmt.Sprintf("got-%s", res.Name) expectedNotes := fmt.Sprintf("got-%s", res.Name)
@ -321,12 +342,16 @@ func TestInstallRelease_WithChartAndDependencyParentNotes(t *testing.T) {
instAction := installAction(t) instAction := installAction(t)
instAction.ReleaseName = "with-notes" instAction.ReleaseName = "with-notes"
vals := map[string]interface{}{} 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 { if err != nil {
t.Fatalf("Failed install: %s", err) 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.NoError(err)
is.Equal("with-notes", rel.Name) is.Equal("with-notes", rel.Name)
is.Equal("parent", rel.Info.Notes) is.Equal("parent", rel.Info.Notes)
@ -340,12 +365,16 @@ func TestInstallRelease_WithChartAndDependencyAllNotes(t *testing.T) {
instAction.ReleaseName = "with-notes" instAction.ReleaseName = "with-notes"
instAction.SubNotes = true instAction.SubNotes = true
vals := map[string]interface{}{} 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 { if err != nil {
t.Fatalf("Failed install: %s", err) 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.NoError(err)
is.Equal("with-notes", rel.Name) is.Equal("with-notes", rel.Name)
// test run can return as either 'parent\nchild' or 'child\nparent' // test run can return as either 'parent\nchild' or 'child\nparent'
@ -355,27 +384,32 @@ func TestInstallRelease_WithChartAndDependencyAllNotes(t *testing.T) {
is.Equal(rel.Info.Description, "Install complete") is.Equal(rel.Info.Description, "Install complete")
} }
func TestInstallRelease_DryRun(t *testing.T) { func TestInstallRelease_DryRunClient(t *testing.T) {
is := assert.New(t) for _, dryRunStrategy := range []DryRunStrategy{DryRunClient, DryRunServer} {
instAction := installAction(t) is := assert.New(t)
instAction.DryRun = true instAction := installAction(t)
vals := map[string]interface{}{} instAction.DryRunStrategy = dryRunStrategy
res, err := instAction.Run(buildChart(withSampleTemplates()), vals)
if err != nil { vals := map[string]interface{}{}
t.Fatalf("Failed install: %s", err) resi, err := instAction.Run(buildChart(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/hello\nhello: world") 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, "---\n# Source: hello/templates/goodbye\ngoodbye: world")
is.Contains(res.Manifest, "hello: Earth") is.Contains(res.Manifest, "hello: Earth")
is.NotContains(res.Manifest, "hello: {{ template \"_planet\" . }}") is.NotContains(res.Manifest, "hello: {{ template \"_planet\" . }}")
is.NotContains(res.Manifest, "empty") is.NotContains(res.Manifest, "empty")
_, err = instAction.cfg.Releases.Get(res.Name, res.Version) _, err = instAction.cfg.Releases.Get(res.Name, res.Version)
is.Error(err) is.Error(err)
is.Len(res.Hooks, 1) is.Len(res.Hooks, 1)
is.True(res.Hooks[0].LastRun.CompletedAt.IsZero(), "expect hook to not be marked as run") 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.Equal(res.Info.Description, "Dry run complete")
}
} }
func TestInstallRelease_DryRunHiddenSecret(t *testing.T) { func TestInstallRelease_DryRunHiddenSecret(t *testing.T) {
@ -383,12 +417,14 @@ func TestInstallRelease_DryRunHiddenSecret(t *testing.T) {
instAction := installAction(t) instAction := installAction(t)
// First perform a normal dry-run with the secret and confirm its presence. // First perform a normal dry-run with the secret and confirm its presence.
instAction.DryRun = true instAction.DryRunStrategy = DryRunClient
vals := map[string]interface{}{} vals := map[string]interface{}{}
res, err := instAction.Run(buildChart(withSampleSecret(), withSampleTemplates()), vals) resi, err := instAction.Run(buildChart(withSampleSecret(), withSampleTemplates()), vals)
if err != nil { if err != nil {
t.Fatalf("Failed install: %s", err) 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") is.Contains(res.Manifest, "---\n# Source: hello/templates/secret.yaml\napiVersion: v1\nkind: Secret")
_, err = instAction.cfg.Releases.Get(res.Name, res.Version) _, err = instAction.cfg.Releases.Get(res.Name, res.Version)
@ -398,10 +434,12 @@ func TestInstallRelease_DryRunHiddenSecret(t *testing.T) {
// Perform a dry-run where the secret should not be present // Perform a dry-run where the secret should not be present
instAction.HideSecret = true instAction.HideSecret = true
vals = map[string]interface{}{} vals = map[string]interface{}{}
res2, err := instAction.Run(buildChart(withSampleSecret(), withSampleTemplates()), vals) res2i, err := instAction.Run(buildChart(withSampleSecret(), withSampleTemplates()), vals)
if err != nil { if err != nil {
t.Fatalf("Failed install: %s", err) 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") is.NotContains(res2.Manifest, "---\n# Source: hello/templates/secret.yaml\napiVersion: v1\nkind: Secret")
@ -410,7 +448,7 @@ func TestInstallRelease_DryRunHiddenSecret(t *testing.T) {
is.Equal(res2.Info.Description, "Dry run complete") is.Equal(res2.Info.Description, "Dry run complete")
// Ensure there is an error when HideSecret True but not in a dry-run mode // 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{}{} vals = map[string]interface{}{}
_, err = instAction.Run(buildChart(withSampleSecret(), withSampleTemplates()), vals) _, err = instAction.Run(buildChart(withSampleSecret(), withSampleTemplates()), vals)
if err == nil { if err == nil {
@ -422,7 +460,7 @@ func TestInstallRelease_DryRunHiddenSecret(t *testing.T) {
func TestInstallRelease_DryRun_Lookup(t *testing.T) { func TestInstallRelease_DryRun_Lookup(t *testing.T) {
is := assert.New(t) is := assert.New(t)
instAction := installAction(t) instAction := installAction(t)
instAction.DryRun = true instAction.DryRunStrategy = DryRunNone
vals := map[string]interface{}{} vals := map[string]interface{}{}
mockChart := buildChart(withSampleTemplates()) mockChart := buildChart(withSampleTemplates())
@ -431,10 +469,12 @@ func TestInstallRelease_DryRun_Lookup(t *testing.T) {
Data: []byte(`goodbye: {{ lookup "v1" "Namespace" "" "___" }}`), Data: []byte(`goodbye: {{ lookup "v1" "Namespace" "" "___" }}`),
}) })
res, err := instAction.Run(mockChart, vals) resi, err := instAction.Run(mockChart, vals)
if err != nil { if err != nil {
t.Fatalf("Failed install: %s", err) t.Fatalf("Failed install: %s", err)
} }
res, err := releaserToV1Release(resi)
is.NoError(err)
is.Contains(res.Manifest, "goodbye: map[]") is.Contains(res.Manifest, "goodbye: map[]")
} }
@ -442,7 +482,7 @@ func TestInstallRelease_DryRun_Lookup(t *testing.T) {
func TestInstallReleaseIncorrectTemplate_DryRun(t *testing.T) { func TestInstallReleaseIncorrectTemplate_DryRun(t *testing.T) {
is := assert.New(t) is := assert.New(t)
instAction := installAction(t) instAction := installAction(t)
instAction.DryRun = true instAction.DryRunStrategy = DryRunNone
vals := map[string]interface{}{} vals := map[string]interface{}{}
_, err := instAction.Run(buildChart(withSampleIncludingIncorrectTemplates()), vals) _, err := instAction.Run(buildChart(withSampleIncludingIncorrectTemplates()), vals)
expectedErr := `hello/templates/incorrect:1:10 expectedErr := `hello/templates/incorrect:1:10
@ -462,10 +502,12 @@ func TestInstallRelease_NoHooks(t *testing.T) {
instAction.cfg.Releases.Create(releaseStub()) instAction.cfg.Releases.Create(releaseStub())
vals := map[string]interface{}{} vals := map[string]interface{}{}
res, err := instAction.Run(buildChart(), vals) resi, err := instAction.Run(buildChart(), vals)
if err != nil { if err != nil {
t.Fatalf("Failed install: %s", err) 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") is.True(res.Hooks[0].LastRun.CompletedAt.IsZero(), "hooks should not run with no-hooks")
} }
@ -481,11 +523,13 @@ func TestInstallRelease_FailedHooks(t *testing.T) {
failer.PrintingKubeClient = kubefake.PrintingKubeClient{Out: io.Discard, LogOutput: outBuffer} failer.PrintingKubeClient = kubefake.PrintingKubeClient{Out: io.Discard, LogOutput: outBuffer}
vals := map[string]interface{}{} vals := map[string]interface{}{}
res, err := instAction.Run(buildChart(), vals) resi, err := instAction.Run(buildChart(), vals)
is.Error(err) is.Error(err)
res, err := releaserToV1Release(resi)
is.NoError(err)
is.Contains(res.Info.Description, "failed post-install") is.Contains(res.Info.Description, "failed post-install")
is.Equal("", outBuffer.String()) is.Equal("", outBuffer.String())
is.Equal(release.StatusFailed, res.Info.Status) is.Equal(rcommon.StatusFailed, res.Info.Status)
} }
func TestInstallRelease_ReplaceRelease(t *testing.T) { func TestInstallRelease_ReplaceRelease(t *testing.T) {
@ -494,21 +538,25 @@ func TestInstallRelease_ReplaceRelease(t *testing.T) {
instAction.Replace = true instAction.Replace = true
rel := releaseStub() rel := releaseStub()
rel.Info.Status = release.StatusUninstalled rel.Info.Status = rcommon.StatusUninstalled
instAction.cfg.Releases.Create(rel) instAction.cfg.Releases.Create(rel)
instAction.ReleaseName = rel.Name instAction.ReleaseName = rel.Name
vals := map[string]interface{}{} 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) is.NoError(err)
// This should have been auto-incremented // This should have been auto-incremented
is.Equal(2, res.Version) is.Equal(2, res.Version)
is.Equal(res.Name, rel.Name) 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)
getres, err := releaserToV1Release(r)
is.NoError(err) is.NoError(err)
is.Equal(getres.Info.Status, release.StatusDeployed) is.Equal(getres.Info.Status, rcommon.StatusDeployed)
} }
func TestInstallRelease_KubeVersion(t *testing.T) { func TestInstallRelease_KubeVersion(t *testing.T) {
@ -538,10 +586,12 @@ func TestInstallRelease_Wait(t *testing.T) {
goroutines := instAction.getGoroutineCount() goroutines := instAction.getGoroutineCount()
res, err := instAction.Run(buildChart(), vals) resi, err := instAction.Run(buildChart(), vals)
is.Error(err) is.Error(err)
res, err := releaserToV1Release(resi)
is.NoError(err)
is.Contains(res.Info.Description, "I timed out") 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()) is.Equal(goroutines, instAction.getGoroutineCount())
} }
@ -579,10 +629,12 @@ func TestInstallRelease_WaitForJobs(t *testing.T) {
instAction.WaitForJobs = true instAction.WaitForJobs = true
vals := map[string]interface{}{} vals := map[string]interface{}{}
res, err := instAction.Run(buildChart(), vals) resi, err := instAction.Run(buildChart(), vals)
is.Error(err) is.Error(err)
res, err := releaserToV1Release(resi)
is.NoError(err)
is.Contains(res.Info.Description, "I timed out") 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) { func TestInstallRelease_RollbackOnFailure(t *testing.T) {
@ -600,11 +652,13 @@ func TestInstallRelease_RollbackOnFailure(t *testing.T) {
instAction.DisableHooks = true instAction.DisableHooks = true
vals := map[string]interface{}{} vals := map[string]interface{}{}
res, err := instAction.Run(buildChart(), vals) resi, err := instAction.Run(buildChart(), vals)
is.Error(err) is.Error(err)
is.Contains(err.Error(), "I timed out") is.Contains(err.Error(), "I timed out")
is.Contains(err.Error(), "rollback-on-failure") is.Contains(err.Error(), "rollback-on-failure")
res, err := releaserToV1Release(resi)
is.NoError(err)
// Now make sure it isn't in storage anymore // Now make sure it isn't in storage anymore
_, err = instAction.cfg.Releases.Get(res.Name, res.Version) _, err = instAction.cfg.Releases.Get(res.Name, res.Version)
is.Error(err) is.Error(err)
@ -644,12 +698,14 @@ func TestInstallRelease_RollbackOnFailure_Interrupted(t *testing.T) {
goroutines := instAction.getGoroutineCount() goroutines := instAction.getGoroutineCount()
res, err := instAction.RunWithContext(ctx, buildChart(), vals) resi, err := instAction.RunWithContext(ctx, buildChart(), vals)
is.Error(err) is.Error(err)
is.Contains(err.Error(), "context canceled") is.Contains(err.Error(), "context canceled")
is.Contains(err.Error(), "rollback-on-failure") is.Contains(err.Error(), "rollback-on-failure")
is.Contains(err.Error(), "uninstalled") is.Contains(err.Error(), "uninstalled")
res, err := releaserToV1Release(resi)
is.NoError(err)
// Now make sure it isn't in storage anymore // Now make sure it isn't in storage anymore
_, err = instAction.cfg.Releases.Get(res.Name, res.Version) _, err = instAction.cfg.Releases.Get(res.Name, res.Version)
is.Error(err) is.Error(err)
@ -906,10 +962,12 @@ func TestInstallWithLabels(t *testing.T) {
"key1": "val1", "key1": "val1",
"key2": "val2", "key2": "val2",
} }
res, err := instAction.Run(buildChart(), nil) resi, err := instAction.Run(buildChart(), nil)
if err != nil { if err != nil {
t.Fatalf("Failed install: %s", err) t.Fatalf("Failed install: %s", err)
} }
res, err := releaserToV1Release(resi)
is.NoError(err)
is.Equal(instAction.Labels, res.Labels) is.Equal(instAction.Labels, res.Labels)
} }
@ -1001,7 +1059,6 @@ func TestInstallRun_UnreachableKubeClient(t *testing.T) {
config.KubeClient = &failingKubeClient config.KubeClient = &failingKubeClient
instAction := NewInstall(config) instAction := NewInstall(config)
instAction.ClientOnly = false
ctx, done := context.WithCancel(t.Context()) ctx, done := context.WithCancel(t.Context())
chrt := buildChart() chrt := buildChart()
res, err := instAction.RunWithContext(ctx, chrt, nil) res, err := instAction.RunWithContext(ctx, chrt, nil)

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

@ -24,6 +24,8 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
kubefake "helm.sh/helm/v4/pkg/kube/fake" 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" release "helm.sh/helm/v4/pkg/release/v1"
"helm.sh/helm/v4/pkg/storage" "helm.sh/helm/v4/pkg/storage"
) )
@ -96,8 +98,11 @@ func TestList_Sort(t *testing.T) {
lister := newListFixture(t) lister := newListFixture(t)
lister.Sort = ByNameDesc // Other sorts are tested elsewhere lister.Sort = ByNameDesc // Other sorts are tested elsewhere
makeMeSomeReleases(t, lister.cfg.Releases) makeMeSomeReleases(t, lister.cfg.Releases)
list, err := lister.Run() l, err := lister.Run()
is.NoError(err)
list, err := releaseListToV1List(l)
is.NoError(err) is.NoError(err)
is.Len(list, 3) is.Len(list, 3)
is.Equal("two", list[0].Name) is.Equal("two", list[0].Name)
is.Equal("three", list[1].Name) is.Equal("three", list[1].Name)
@ -109,7 +114,9 @@ func TestList_Limit(t *testing.T) {
lister := newListFixture(t) lister := newListFixture(t)
lister.Limit = 2 lister.Limit = 2
makeMeSomeReleases(t, lister.cfg.Releases) makeMeSomeReleases(t, lister.cfg.Releases)
list, err := lister.Run() l, err := lister.Run()
is.NoError(err)
list, err := releaseListToV1List(l)
is.NoError(err) is.NoError(err)
is.Len(list, 2) is.Len(list, 2)
// Lex order means one, three, two // Lex order means one, three, two
@ -122,7 +129,9 @@ func TestList_BigLimit(t *testing.T) {
lister := newListFixture(t) lister := newListFixture(t)
lister.Limit = 20 lister.Limit = 20
makeMeSomeReleases(t, lister.cfg.Releases) makeMeSomeReleases(t, lister.cfg.Releases)
list, err := lister.Run() l, err := lister.Run()
is.NoError(err)
list, err := releaseListToV1List(l)
is.NoError(err) is.NoError(err)
is.Len(list, 3) is.Len(list, 3)
@ -138,7 +147,9 @@ func TestList_LimitOffset(t *testing.T) {
lister.Limit = 2 lister.Limit = 2
lister.Offset = 1 lister.Offset = 1
makeMeSomeReleases(t, lister.cfg.Releases) makeMeSomeReleases(t, lister.cfg.Releases)
list, err := lister.Run() l, err := lister.Run()
is.NoError(err)
list, err := releaseListToV1List(l)
is.NoError(err) is.NoError(err)
is.Len(list, 2) is.Len(list, 2)
@ -168,23 +179,42 @@ func TestList_StateMask(t *testing.T) {
is := assert.New(t) is := assert.New(t)
lister := newListFixture(t) lister := newListFixture(t)
makeMeSomeReleases(t, lister.cfg.Releases) makeMeSomeReleases(t, lister.cfg.Releases)
one, err := lister.cfg.Releases.Get("one", 1) oner, err := lister.cfg.Releases.Get("one", 1)
is.NoError(err) 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) err = lister.cfg.Releases.Update(one)
is.NoError(err) is.NoError(err)
res, err := lister.Run() res, err := lister.Run()
is.NoError(err) is.NoError(err)
is.Len(res, 2) is.Len(res, 2)
is.Equal("three", res[0].Name)
is.Equal("two", res[1].Name) ac0, err := ri.NewAccessor(res[0])
is.NoError(err)
ac1, err := ri.NewAccessor(res[1])
is.NoError(err)
is.Equal("three", ac0.Name())
is.Equal("two", ac1.Name())
lister.StateMask = ListUninstalled lister.StateMask = ListUninstalled
res, err = lister.Run() res, err = lister.Run()
is.NoError(err) is.NoError(err)
is.Len(res, 1) 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 lister.StateMask |= ListDeployed
res, err = lister.Run() res, err = lister.Run()
@ -206,28 +236,30 @@ func TestList_StateMaskWithStaleRevisions(t *testing.T) {
// "dirty" release should _not_ be present as most recent // "dirty" release should _not_ be present as most recent
// release is deployed despite failed release in past // 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) { func makeMeSomeReleasesWithStaleFailure(t *testing.T, store *storage.Storage) {
t.Helper() t.Helper()
one := namedReleaseStub("clean", release.StatusDeployed) one := namedReleaseStub("clean", common.StatusDeployed)
one.Namespace = "default" one.Namespace = "default"
one.Version = 1 one.Version = 1
two := namedReleaseStub("dirty", release.StatusDeployed) two := namedReleaseStub("dirty", common.StatusDeployed)
two.Namespace = "default" two.Namespace = "default"
two.Version = 1 two.Version = 1
three := namedReleaseStub("dirty", release.StatusFailed) three := namedReleaseStub("dirty", common.StatusFailed)
three.Namespace = "default" three.Namespace = "default"
three.Version = 2 three.Version = 2
four := namedReleaseStub("dirty", release.StatusDeployed) four := namedReleaseStub("dirty", common.StatusDeployed)
four.Namespace = "default" four.Namespace = "default"
four.Version = 3 four.Version = 3
five := namedReleaseStub("failed", release.StatusFailed) five := namedReleaseStub("failed", common.StatusFailed)
five.Namespace = "default" five.Namespace = "default"
five.Version = 1 five.Version = 1
@ -251,7 +283,9 @@ func TestList_Filter(t *testing.T) {
res, err := lister.Run() res, err := lister.Run()
is.NoError(err) is.NoError(err)
is.Len(res, 1) 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) { func TestList_FilterFailsCompile(t *testing.T) {

@ -28,6 +28,7 @@ import (
chartutil "helm.sh/helm/v4/pkg/chart/v2/util" chartutil "helm.sh/helm/v4/pkg/chart/v2/util"
"helm.sh/helm/v4/pkg/kube" "helm.sh/helm/v4/pkg/kube"
ri "helm.sh/helm/v4/pkg/release"
release "helm.sh/helm/v4/pkg/release/v1" release "helm.sh/helm/v4/pkg/release/v1"
) )
@ -56,7 +57,7 @@ func NewReleaseTesting(cfg *Configuration) *ReleaseTesting {
} }
// Run executes 'helm test' against the given release. // 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 { if err := r.cfg.KubeClient.IsReachable(); err != nil {
return nil, err return nil, err
} }
@ -66,7 +67,12 @@ func (r *ReleaseTesting) Run(name string) (*release.Release, error) {
} }
// finds the non-deleted release with the given name // 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 { if err != nil {
return rel, err return rel, err
} }

@ -23,8 +23,11 @@ import (
"strings" "strings"
"time" "time"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
chartutil "helm.sh/helm/v4/pkg/chart/v2/util" chartutil "helm.sh/helm/v4/pkg/chart/v2/util"
"helm.sh/helm/v4/pkg/kube" "helm.sh/helm/v4/pkg/kube"
"helm.sh/helm/v4/pkg/release/common"
release "helm.sh/helm/v4/pkg/release/v1" release "helm.sh/helm/v4/pkg/release/v1"
) )
@ -39,7 +42,8 @@ type Rollback struct {
WaitStrategy kube.WaitStrategy WaitStrategy kube.WaitStrategy
WaitForJobs bool WaitForJobs bool
DisableHooks 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. // ForceReplace will, if set to `true`, ignore certain warnings and perform the rollback anyway.
// //
// This should be used with caution. // This should be used with caution.
@ -59,7 +63,8 @@ type Rollback struct {
// NewRollback creates a new Rollback object with the given configuration. // NewRollback creates a new Rollback object with the given configuration.
func NewRollback(cfg *Configuration) *Rollback { func NewRollback(cfg *Configuration) *Rollback {
return &Rollback{ return &Rollback{
cfg: cfg, cfg: cfg,
DryRunStrategy: DryRunNone,
} }
} }
@ -77,7 +82,7 @@ func (r *Rollback) Run(name string) error {
return err return err
} }
if !r.DryRun { if !isDryRun(r.DryRunStrategy) {
slog.Debug("creating rolled back release", "name", name) slog.Debug("creating rolled back release", "name", name)
if err := r.cfg.Releases.Create(targetRelease); err != nil { if err := r.cfg.Releases.Create(targetRelease); err != nil {
return err return err
@ -89,7 +94,7 @@ func (r *Rollback) Run(name string) error {
return err return err
} }
if !r.DryRun { if !isDryRun(r.DryRunStrategy) {
slog.Debug("updating status for rolled back release", "name", name) slog.Debug("updating status for rolled back release", "name", name)
if err := r.cfg.Releases.Update(targetRelease); err != nil { if err := r.cfg.Releases.Update(targetRelease); err != nil {
return err return err
@ -109,7 +114,12 @@ func (r *Rollback) prepareRollback(name string) (*release.Release, *release.Rele
return nil, nil, false, errInvalidRevision 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 { if err != nil {
return nil, nil, false, err return nil, nil, false, err
} }
@ -126,7 +136,11 @@ func (r *Rollback) prepareRollback(name string) (*release.Release, *release.Rele
// Check if the history version to be rolled back exists // Check if the history version to be rolled back exists
previousVersionExist := false 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 version := historyRelease.Version
if previousVersion == version { if previousVersion == version {
previousVersionExist = true previousVersionExist = true
@ -139,7 +153,11 @@ func (r *Rollback) prepareRollback(name string) (*release.Release, *release.Rele
slog.Debug("rolling back", "name", name, "currentVersion", currentRelease.Version, "targetVersion", previousVersion) 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 { if err != nil {
return nil, nil, false, err return nil, nil, false, err
} }
@ -158,7 +176,7 @@ func (r *Rollback) prepareRollback(name string) (*release.Release, *release.Rele
Info: &release.Info{ Info: &release.Info{
FirstDeployed: currentRelease.Info.FirstDeployed, FirstDeployed: currentRelease.Info.FirstDeployed,
LastDeployed: time.Now(), LastDeployed: time.Now(),
Status: release.StatusPendingRollback, Status: common.StatusPendingRollback,
Notes: previousRelease.Info.Notes, Notes: previousRelease.Info.Notes,
// Because we lose the reference to previous version elsewhere, we set the // Because we lose the reference to previous version elsewhere, we set the
// message here, and only override it later if we experience failure. // message here, and only override it later if we experience failure.
@ -175,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) { 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) slog.Debug("dry run", "name", targetRelease.Name)
return targetRelease, nil return targetRelease, nil
} }
@ -215,14 +233,14 @@ func (r *Rollback) performRollback(currentRelease, targetRelease *release.Releas
if err != nil { if err != nil {
msg := fmt.Sprintf("Rollback %q failed: %s", targetRelease.Name, err) msg := fmt.Sprintf("Rollback %q failed: %s", targetRelease.Name, err)
slog.Warn(msg) slog.Warn(msg)
currentRelease.Info.Status = release.StatusSuperseded currentRelease.Info.Status = common.StatusSuperseded
targetRelease.Info.Status = release.StatusFailed targetRelease.Info.Status = common.StatusFailed
targetRelease.Info.Description = msg targetRelease.Info.Description = msg
r.cfg.recordRelease(currentRelease) r.cfg.recordRelease(currentRelease)
r.cfg.recordRelease(targetRelease) r.cfg.recordRelease(targetRelease)
if r.CleanupOnFail { if r.CleanupOnFail {
slog.Debug("cleanup on fail set, cleaning up resources", "count", len(results.Created)) 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 { if errs != nil {
return targetRelease, fmt.Errorf( return targetRelease, fmt.Errorf(
"an error occurred while cleaning up resources. original rollback error: %w", "an error occurred while cleaning up resources. original rollback error: %w",
@ -239,14 +257,14 @@ func (r *Rollback) performRollback(currentRelease, targetRelease *release.Releas
} }
if r.WaitForJobs { if r.WaitForJobs {
if err := waiter.WaitWithJobs(target, r.Timeout); err != nil { 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(currentRelease)
r.cfg.recordRelease(targetRelease) r.cfg.recordRelease(targetRelease)
return targetRelease, fmt.Errorf("release %s failed: %w", targetRelease.Name, err) return targetRelease, fmt.Errorf("release %s failed: %w", targetRelease.Name, err)
} }
} else { } else {
if err := waiter.Wait(target, r.Timeout); err != nil { 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(currentRelease)
r.cfg.recordRelease(targetRelease) r.cfg.recordRelease(targetRelease)
return targetRelease, fmt.Errorf("release %s failed: %w", targetRelease.Name, err) return targetRelease, fmt.Errorf("release %s failed: %w", targetRelease.Name, err)
@ -265,13 +283,17 @@ func (r *Rollback) performRollback(currentRelease, targetRelease *release.Releas
return nil, err return nil, err
} }
// Supersede all previous deployments, see issue #2941. // 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) slog.Debug("superseding previous deployment", "version", rel.Version)
rel.Info.Status = release.StatusSuperseded rel.Info.Status = common.StatusSuperseded
r.cfg.recordRelease(rel) r.cfg.recordRelease(rel)
} }
targetRelease.Info.Status = release.StatusDeployed targetRelease.Info.Status = common.StatusDeployed
return targetRelease, nil return targetRelease, nil
} }

@ -18,10 +18,9 @@ package action
import ( import (
"bytes" "bytes"
"errors"
"helm.sh/helm/v4/pkg/kube" "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. // 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. // 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 { if err := s.cfg.KubeClient.IsReachable(); err != nil {
return nil, err return nil, err
} }
rel, err := s.cfg.releaseContent(name, s.Version) reli, err := s.cfg.releaseContent(name, s.Version)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if kubeClient, ok := s.cfg.KubeClient.(kube.InterfaceResources); ok { rel, err := releaserToV1Release(reli)
var resources kube.ResourceList if err != nil {
if s.ShowResourcesTable { return nil, err
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
}
}
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 { if err != nil {
return nil, err 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 resp, err := s.cfg.KubeClient.Get(resources, true)
if err != nil {
return rel, nil return nil, err
} }
return nil, errors.New("unable to get kubeClient with interface InterfaceResources")
rel.Info.Resources = resp
return rel, nil
} }

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

@ -27,7 +27,7 @@ import (
"helm.sh/helm/v4/pkg/kube" "helm.sh/helm/v4/pkg/kube"
kubefake "helm.sh/helm/v4/pkg/kube/fake" 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 { func uninstallAction(t *testing.T) *Uninstall {
@ -116,10 +116,12 @@ func TestUninstallRelease_Wait(t *testing.T) {
failer := unAction.cfg.KubeClient.(*kubefake.FailingKubeClient) failer := unAction.cfg.KubeClient.(*kubefake.FailingKubeClient)
failer.WaitForDeleteError = fmt.Errorf("U timed out") failer.WaitForDeleteError = fmt.Errorf("U timed out")
unAction.cfg.KubeClient = failer unAction.cfg.KubeClient = failer
res, err := unAction.Run(rel.Name) resi, err := unAction.Run(rel.Name)
is.Error(err) is.Error(err)
is.Contains(err.Error(), "U timed out") 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) { func TestUninstallRelease_Cascade(t *testing.T) {
@ -146,7 +148,7 @@ func TestUninstallRelease_Cascade(t *testing.T) {
}` }`
unAction.cfg.Releases.Create(rel) unAction.cfg.Releases.Create(rel)
failer := unAction.cfg.KubeClient.(*kubefake.FailingKubeClient) 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 failer.BuildDummy = true
unAction.cfg.KubeClient = failer unAction.cfg.KubeClient = failer
_, err := unAction.Run(rel.Name) _, err := unAction.Run(rel.Name)

@ -26,6 +26,7 @@ import (
"sync" "sync"
"time" "time"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/cli-runtime/pkg/resource" "k8s.io/cli-runtime/pkg/resource"
"helm.sh/helm/v4/pkg/chart" "helm.sh/helm/v4/pkg/chart"
@ -36,6 +37,8 @@ import (
"helm.sh/helm/v4/pkg/kube" "helm.sh/helm/v4/pkg/kube"
"helm.sh/helm/v4/pkg/postrenderer" "helm.sh/helm/v4/pkg/postrenderer"
"helm.sh/helm/v4/pkg/registry" "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" release "helm.sh/helm/v4/pkg/release/v1"
releaseutil "helm.sh/helm/v4/pkg/release/v1/util" releaseutil "helm.sh/helm/v4/pkg/release/v1/util"
"helm.sh/helm/v4/pkg/storage/driver" "helm.sh/helm/v4/pkg/storage/driver"
@ -73,10 +76,8 @@ type Upgrade struct {
WaitForJobs bool WaitForJobs bool
// DisableHooks disables hook processing if set to true. // DisableHooks disables hook processing if set to true.
DisableHooks bool DisableHooks bool
// DryRun controls whether the operation is prepared, but not executed. // DryRunStrategy can be set to prepare, but not execute the operation and whether or not to interact with the remote cluster
DryRun bool DryRunStrategy DryRunStrategy
// DryRunOption controls whether the operation is prepared, but not executed with options on whether or not to interact with the remote cluster.
DryRunOption string
// HideSecret can be set to true when DryRun is enabled in order to hide // 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. // Kubernetes Secrets in the output. It cannot be used outside of DryRun.
HideSecret bool HideSecret bool
@ -140,6 +141,7 @@ func NewUpgrade(cfg *Configuration) *Upgrade {
up := &Upgrade{ up := &Upgrade{
cfg: cfg, cfg: cfg,
ServerSideApply: "auto", ServerSideApply: "auto",
DryRunStrategy: DryRunNone,
} }
up.registryClient = cfg.RegistryClient up.registryClient = cfg.RegistryClient
@ -152,13 +154,13 @@ func (u *Upgrade) SetRegistryClient(client *registry.Client) {
} }
// Run executes the upgrade on the given release. // Run executes the upgrade on the given release.
func (u *Upgrade) Run(name string, chart chart.Charter, 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() ctx := context.Background()
return u.RunWithContext(ctx, name, chart, vals) return u.RunWithContext(ctx, name, chart, vals)
} }
// RunWithContext executes the upgrade on the given release with context. // RunWithContext executes the upgrade on the given release with context.
func (u *Upgrade) RunWithContext(ctx context.Context, name string, ch chart.Charter, 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 { if err := u.cfg.KubeClient.IsReachable(); err != nil {
return nil, err return nil, err
} }
@ -198,7 +200,7 @@ func (u *Upgrade) RunWithContext(ctx context.Context, name string, ch chart.Char
} }
// Do not update for dry runs // Do not update for dry runs
if !u.isDryRun() { if !isDryRun(u.DryRunStrategy) {
slog.Debug("updating status for upgraded release", "name", name) slog.Debug("updating status for upgraded release", "name", name)
if err := u.cfg.Releases.Update(upgradedRelease); err != nil { if err := u.cfg.Releases.Update(upgradedRelease); err != nil {
return res, err return res, err
@ -208,14 +210,6 @@ func (u *Upgrade) RunWithContext(ctx context.Context, name string, ch chart.Char
return res, nil 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. // prepareUpgrade builds an upgraded release for an upgrade operation.
func (u *Upgrade) prepareUpgrade(name string, chart *chartv2.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 { if chart == nil {
@ -223,12 +217,12 @@ func (u *Upgrade) prepareUpgrade(name string, chart *chartv2.Chart, vals map[str
} }
// HideSecret must be used with dry run. Otherwise, return an error. // 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") return nil, nil, false, errors.New("hiding Kubernetes secrets requires a dry-run mode")
} }
// finds the last non-deleted release with the given name // 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 { if err != nil {
// to keep existing behavior of returning the "%q has no deployed releases" error when an existing release does not exist // 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) { if errors.Is(err, driver.ErrReleaseNotFound) {
@ -237,26 +231,37 @@ func (u *Upgrade) prepareUpgrade(name string, chart *chartv2.Chart, vals map[str
return nil, nil, false, err 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. // 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() { if lastRelease.Info.Status.IsPending() {
return nil, nil, false, errPending return nil, nil, false, errPending
} }
var currentRelease *release.Release 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 // no need to retrieve the last deployed release from storage as the last release is deployed
currentRelease = lastRelease currentRelease = lastRelease
} else { } else {
// finds the deployed release with the given name // 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 err != nil {
if errors.Is(err, driver.ErrNoDeployedReleases) && 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 currentRelease = lastRelease
} else { } else {
return nil, nil, false, err return nil, nil, false, err
} }
} }
} }
// determine if values will be reused // determine if values will be reused
@ -289,13 +294,7 @@ func (u *Upgrade) prepareUpgrade(name string, chart *chartv2.Chart, vals map[str
return nil, nil, false, err return nil, nil, false, err
} }
// Determine whether or not to interact with remote hooks, manifestDoc, notesTxt, err := u.cfg.renderResources(chart, valuesToRender, "", "", u.SubNotes, false, false, u.PostRenderer, interactWithServer(u.DryRunStrategy), u.EnableDNS, u.HideSecret)
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)
if err != nil { if err != nil {
return nil, nil, false, err return nil, nil, false, err
} }
@ -320,7 +319,7 @@ func (u *Upgrade) prepareUpgrade(name string, chart *chartv2.Chart, vals map[str
Info: &release.Info{ Info: &release.Info{
FirstDeployed: currentRelease.Info.FirstDeployed, FirstDeployed: currentRelease.Info.FirstDeployed,
LastDeployed: Timestamper(), LastDeployed: Timestamper(),
Status: release.StatusPendingUpgrade, Status: rcommon.StatusPendingUpgrade,
Description: "Preparing upgrade", // This should be overwritten later. Description: "Preparing upgrade", // This should be overwritten later.
}, },
Version: revision, Version: revision,
@ -391,8 +390,7 @@ func (u *Upgrade) performUpgrade(ctx context.Context, originalRelease, upgradedR
return nil return nil
}) })
// Run if it is a dry run if isDryRun(u.DryRunStrategy) {
if u.isDryRun() {
slog.Debug("dry run for release", "name", upgradedRelease.Name) slog.Debug("dry run for release", "name", upgradedRelease.Name)
if len(u.Description) > 0 { if len(u.Description) > 0 {
upgradedRelease.Info.Description = u.Description upgradedRelease.Info.Description = u.Description
@ -503,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) u.cfg.recordRelease(originalRelease)
upgradedRelease.Info.Status = release.StatusDeployed upgradedRelease.Info.Status = rcommon.StatusDeployed
if len(u.Description) > 0 { if len(u.Description) > 0 {
upgradedRelease.Info.Description = u.Description upgradedRelease.Info.Description = u.Description
} else { } else {
@ -519,12 +517,12 @@ func (u *Upgrade) failRelease(rel *release.Release, created kube.ResourceList, e
msg := fmt.Sprintf("Upgrade %q failed: %s", rel.Name, err) msg := fmt.Sprintf("Upgrade %q failed: %s", rel.Name, err)
slog.Warn("upgrade failed", "name", rel.Name, slog.Any("error", 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 rel.Info.Description = msg
u.cfg.recordRelease(rel) u.cfg.recordRelease(rel)
if u.CleanupOnFail && len(created) > 0 { if u.CleanupOnFail && len(created) > 0 {
slog.Debug("cleanup on fail set", "cleaning_resources", len(created)) 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 { if errs != nil {
return rel, fmt.Errorf( return rel, fmt.Errorf(
"an error occurred while cleaning up resources. original upgrade error: %w: %w", "an error occurred while cleaning up resources. original upgrade error: %w: %w",
@ -549,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) 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 // There isn't a way to tell if a previous release was successful, but
// generally failed releases do not get superseded unless the next // generally failed releases do not get superseded unless the next
// release is successful, so this should be relatively safe // release is successful, so this should be relatively safe
filteredHistory := releaseutil.FilterFunc(func(r *release.Release) bool { filteredHistory := releaseutil.FilterFunc(func(r *release.Release) bool {
return r.Info.Status == release.StatusSuperseded || r.Info.Status == release.StatusDeployed return r.Info.Status == rcommon.StatusSuperseded || r.Info.Status == rcommon.StatusDeployed
}).Filter(fullHistory) }).Filter(fullHistoryV1)
if len(filteredHistory) == 0 { if len(filteredHistory) == 0 {
return rel, fmt.Errorf("unable to find a previously successful release when attempting to rollback. original upgrade error: %w", err) return rel, fmt.Errorf("unable to find a previously successful release when attempting to rollback. original upgrade error: %w", err)
} }

@ -33,6 +33,7 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
kubefake "helm.sh/helm/v4/pkg/kube/fake" kubefake "helm.sh/helm/v4/pkg/kube/fake"
"helm.sh/helm/v4/pkg/release/common"
release "helm.sh/helm/v4/pkg/release/v1" release "helm.sh/helm/v4/pkg/release/v1"
) )
@ -52,24 +53,28 @@ func TestUpgradeRelease_Success(t *testing.T) {
upAction := upgradeAction(t) upAction := upgradeAction(t)
rel := releaseStub() rel := releaseStub()
rel.Name = "previous-release" rel.Name = "previous-release"
rel.Info.Status = release.StatusDeployed rel.Info.Status = common.StatusDeployed
req.NoError(upAction.cfg.Releases.Create(rel)) req.NoError(upAction.cfg.Releases.Create(rel))
upAction.WaitStrategy = kube.StatusWatcherStrategy upAction.WaitStrategy = kube.StatusWatcherStrategy
vals := map[string]interface{}{} vals := map[string]interface{}{}
ctx, done := context.WithCancel(t.Context()) ctx, done := context.WithCancel(t.Context())
res, err := upAction.RunWithContext(ctx, rel.Name, buildChart(), vals) resi, err := upAction.RunWithContext(ctx, rel.Name, buildChart(), vals)
done()
req.NoError(err) 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 // Detecting previous bug where context termination after successful release
// caused release to fail. // caused release to fail.
time.Sleep(time.Millisecond * 100) time.Sleep(time.Millisecond * 100)
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) req.NoError(err)
is.Equal(lastRelease.Info.Status, release.StatusDeployed) is.Equal(lastRelease.Info.Status, common.StatusDeployed)
} }
func TestUpgradeRelease_Wait(t *testing.T) { func TestUpgradeRelease_Wait(t *testing.T) {
@ -79,7 +84,7 @@ func TestUpgradeRelease_Wait(t *testing.T) {
upAction := upgradeAction(t) upAction := upgradeAction(t)
rel := releaseStub() rel := releaseStub()
rel.Name = "come-fail-away" rel.Name = "come-fail-away"
rel.Info.Status = release.StatusDeployed rel.Info.Status = common.StatusDeployed
upAction.cfg.Releases.Create(rel) upAction.cfg.Releases.Create(rel)
failer := upAction.cfg.KubeClient.(*kubefake.FailingKubeClient) failer := upAction.cfg.KubeClient.(*kubefake.FailingKubeClient)
@ -88,10 +93,12 @@ func TestUpgradeRelease_Wait(t *testing.T) {
upAction.WaitStrategy = kube.StatusWatcherStrategy upAction.WaitStrategy = kube.StatusWatcherStrategy
vals := map[string]interface{}{} vals := map[string]interface{}{}
res, err := upAction.Run(rel.Name, buildChart(), vals) resi, err := upAction.Run(rel.Name, buildChart(), vals)
req.Error(err) req.Error(err)
res, err := releaserToV1Release(resi)
is.NoError(err)
is.Contains(res.Info.Description, "I timed out") 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) { func TestUpgradeRelease_WaitForJobs(t *testing.T) {
@ -101,7 +108,7 @@ func TestUpgradeRelease_WaitForJobs(t *testing.T) {
upAction := upgradeAction(t) upAction := upgradeAction(t)
rel := releaseStub() rel := releaseStub()
rel.Name = "come-fail-away" rel.Name = "come-fail-away"
rel.Info.Status = release.StatusDeployed rel.Info.Status = common.StatusDeployed
upAction.cfg.Releases.Create(rel) upAction.cfg.Releases.Create(rel)
failer := upAction.cfg.KubeClient.(*kubefake.FailingKubeClient) failer := upAction.cfg.KubeClient.(*kubefake.FailingKubeClient)
@ -111,10 +118,12 @@ func TestUpgradeRelease_WaitForJobs(t *testing.T) {
upAction.WaitForJobs = true upAction.WaitForJobs = true
vals := map[string]interface{}{} vals := map[string]interface{}{}
res, err := upAction.Run(rel.Name, buildChart(), vals) resi, err := upAction.Run(rel.Name, buildChart(), vals)
req.Error(err) req.Error(err)
res, err := releaserToV1Release(resi)
is.NoError(err)
is.Contains(res.Info.Description, "I timed out") 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) { func TestUpgradeRelease_CleanupOnFail(t *testing.T) {
@ -124,7 +133,7 @@ func TestUpgradeRelease_CleanupOnFail(t *testing.T) {
upAction := upgradeAction(t) upAction := upgradeAction(t)
rel := releaseStub() rel := releaseStub()
rel.Name = "come-fail-away" rel.Name = "come-fail-away"
rel.Info.Status = release.StatusDeployed rel.Info.Status = common.StatusDeployed
upAction.cfg.Releases.Create(rel) upAction.cfg.Releases.Create(rel)
failer := upAction.cfg.KubeClient.(*kubefake.FailingKubeClient) failer := upAction.cfg.KubeClient.(*kubefake.FailingKubeClient)
@ -135,11 +144,13 @@ func TestUpgradeRelease_CleanupOnFail(t *testing.T) {
upAction.CleanupOnFail = true upAction.CleanupOnFail = true
vals := map[string]interface{}{} vals := map[string]interface{}{}
res, err := upAction.Run(rel.Name, buildChart(), vals) resi, err := upAction.Run(rel.Name, buildChart(), vals)
req.Error(err) req.Error(err)
is.NotContains(err.Error(), "unable to cleanup resources") is.NotContains(err.Error(), "unable to cleanup resources")
res, err := releaserToV1Release(resi)
is.NoError(err)
is.Contains(res.Info.Description, "I timed out") 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) { func TestUpgradeRelease_RollbackOnFailure(t *testing.T) {
@ -151,7 +162,7 @@ func TestUpgradeRelease_RollbackOnFailure(t *testing.T) {
rel := releaseStub() rel := releaseStub()
rel.Name = "nuketown" rel.Name = "nuketown"
rel.Info.Status = release.StatusDeployed rel.Info.Status = common.StatusDeployed
upAction.cfg.Releases.Create(rel) upAction.cfg.Releases.Create(rel)
failer := upAction.cfg.KubeClient.(*kubefake.FailingKubeClient) failer := upAction.cfg.KubeClient.(*kubefake.FailingKubeClient)
@ -161,23 +172,27 @@ func TestUpgradeRelease_RollbackOnFailure(t *testing.T) {
upAction.RollbackOnFailure = true upAction.RollbackOnFailure = true
vals := map[string]interface{}{} vals := map[string]interface{}{}
res, err := upAction.Run(rel.Name, buildChart(), vals) resi, err := upAction.Run(rel.Name, buildChart(), vals)
req.Error(err) req.Error(err)
is.Contains(err.Error(), "arming key removed") is.Contains(err.Error(), "arming key removed")
is.Contains(err.Error(), "rollback-on-failure") is.Contains(err.Error(), "rollback-on-failure")
res, err := releaserToV1Release(resi)
is.NoError(err)
// Now make sure it is actually upgraded // 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) is.NoError(err)
// Should have rolled back to the previous // 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) { t.Run("rollback-on-failure uninstall fails", func(t *testing.T) {
upAction := upgradeAction(t) upAction := upgradeAction(t)
rel := releaseStub() rel := releaseStub()
rel.Name = "fallout" rel.Name = "fallout"
rel.Info.Status = release.StatusDeployed rel.Info.Status = common.StatusDeployed
upAction.cfg.Releases.Create(rel) upAction.cfg.Releases.Create(rel)
failer := upAction.cfg.KubeClient.(*kubefake.FailingKubeClient) failer := upAction.cfg.KubeClient.(*kubefake.FailingKubeClient)
@ -218,7 +233,7 @@ func TestUpgradeRelease_ReuseValues(t *testing.T) {
rel := releaseStub() rel := releaseStub()
rel.Name = "nuketown" rel.Name = "nuketown"
rel.Info.Status = release.StatusDeployed rel.Info.Status = common.StatusDeployed
rel.Config = existingValues rel.Config = existingValues
err := upAction.cfg.Releases.Create(rel) err := upAction.cfg.Releases.Create(rel)
@ -226,18 +241,23 @@ func TestUpgradeRelease_ReuseValues(t *testing.T) {
upAction.ReuseValues = true upAction.ReuseValues = true
// setting newValues and upgrading // 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) is.NoError(err)
// Now make sure it is actually upgraded // 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) is.NoError(err)
if updatedRes == nil { if updatedResi == nil {
is.Fail("Updated Release is nil") is.Fail("Updated Release is nil")
return 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(expectedValues, updatedRes.Config)
}) })
@ -270,7 +290,7 @@ func TestUpgradeRelease_ReuseValues(t *testing.T) {
Info: &release.Info{ Info: &release.Info{
FirstDeployed: now, FirstDeployed: now,
LastDeployed: now, LastDeployed: now,
Status: release.StatusDeployed, Status: common.StatusDeployed,
Description: "Named Release Stub", Description: "Named Release Stub",
}, },
Chart: sampleChart, Chart: sampleChart,
@ -288,18 +308,23 @@ func TestUpgradeRelease_ReuseValues(t *testing.T) {
withMetadataDependency(dependency), withMetadataDependency(dependency),
) )
// reusing values and upgrading // 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) is.NoError(err)
// Now get the upgraded release // 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) is.NoError(err)
if updatedRes == nil { if updatedResi == nil {
is.Fail("Updated Release is nil") is.Fail("Updated Release is nil")
return 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") is.Equal(0, len(updatedRes.Chart.Dependencies()), "expected 0 dependencies")
expectedValues := map[string]interface{}{ expectedValues := map[string]interface{}{
@ -339,7 +364,7 @@ func TestUpgradeRelease_ResetThenReuseValues(t *testing.T) {
rel := releaseStub() rel := releaseStub()
rel.Name = "nuketown" rel.Name = "nuketown"
rel.Info.Status = release.StatusDeployed rel.Info.Status = common.StatusDeployed
rel.Config = existingValues rel.Config = existingValues
err := upAction.cfg.Releases.Create(rel) err := upAction.cfg.Releases.Create(rel)
@ -347,18 +372,23 @@ func TestUpgradeRelease_ResetThenReuseValues(t *testing.T) {
upAction.ResetThenReuseValues = true upAction.ResetThenReuseValues = true
// setting newValues and upgrading // 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) is.NoError(err)
// Now make sure it is actually upgraded // 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) is.NoError(err)
if updatedRes == nil { if updatedResi == nil {
is.Fail("Updated Release is nil") is.Fail("Updated Release is nil")
return 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(expectedValues, updatedRes.Config)
is.Equal(newChartValues, updatedRes.Chart.Values) is.Equal(newChartValues, updatedRes.Chart.Values)
}) })
@ -370,11 +400,11 @@ func TestUpgradeRelease_Pending(t *testing.T) {
upAction := upgradeAction(t) upAction := upgradeAction(t)
rel := releaseStub() rel := releaseStub()
rel.Name = "come-fail-away" rel.Name = "come-fail-away"
rel.Info.Status = release.StatusDeployed rel.Info.Status = common.StatusDeployed
upAction.cfg.Releases.Create(rel) upAction.cfg.Releases.Create(rel)
rel2 := releaseStub() rel2 := releaseStub()
rel2.Name = "come-fail-away" rel2.Name = "come-fail-away"
rel2.Info.Status = release.StatusPendingUpgrade rel2.Info.Status = common.StatusPendingUpgrade
rel2.Version = 2 rel2.Version = 2
upAction.cfg.Releases.Create(rel2) upAction.cfg.Releases.Create(rel2)
@ -391,7 +421,7 @@ func TestUpgradeRelease_Interrupted_Wait(t *testing.T) {
upAction := upgradeAction(t) upAction := upgradeAction(t)
rel := releaseStub() rel := releaseStub()
rel.Name = "interrupted-release" rel.Name = "interrupted-release"
rel.Info.Status = release.StatusDeployed rel.Info.Status = common.StatusDeployed
upAction.cfg.Releases.Create(rel) upAction.cfg.Releases.Create(rel)
failer := upAction.cfg.KubeClient.(*kubefake.FailingKubeClient) failer := upAction.cfg.KubeClient.(*kubefake.FailingKubeClient)
@ -403,11 +433,13 @@ func TestUpgradeRelease_Interrupted_Wait(t *testing.T) {
ctx, cancel := context.WithCancel(t.Context()) ctx, cancel := context.WithCancel(t.Context())
time.AfterFunc(time.Second, cancel) 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) req.Error(err)
res, err := releaserToV1Release(resi)
is.NoError(err)
is.Contains(res.Info.Description, "Upgrade \"interrupted-release\" failed: context canceled") 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) { func TestUpgradeRelease_Interrupted_RollbackOnFailure(t *testing.T) {
@ -418,7 +450,7 @@ func TestUpgradeRelease_Interrupted_RollbackOnFailure(t *testing.T) {
upAction := upgradeAction(t) upAction := upgradeAction(t)
rel := releaseStub() rel := releaseStub()
rel.Name = "interrupted-release" rel.Name = "interrupted-release"
rel.Info.Status = release.StatusDeployed rel.Info.Status = common.StatusDeployed
upAction.cfg.Releases.Create(rel) upAction.cfg.Releases.Create(rel)
failer := upAction.cfg.KubeClient.(*kubefake.FailingKubeClient) failer := upAction.cfg.KubeClient.(*kubefake.FailingKubeClient)
@ -430,16 +462,19 @@ func TestUpgradeRelease_Interrupted_RollbackOnFailure(t *testing.T) {
ctx, cancel := context.WithCancel(t.Context()) ctx, cancel := context.WithCancel(t.Context())
time.AfterFunc(time.Second, cancel) 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) 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") 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 // 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) is.NoError(err)
// Should have rolled back to the previous // 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) { func TestMergeCustomLabels(t *testing.T) {
@ -468,7 +503,7 @@ func TestUpgradeRelease_Labels(t *testing.T) {
"key1": "val1", "key1": "val1",
"key2": "val2.1", "key2": "val2.1",
} }
rel.Info.Status = release.StatusDeployed rel.Info.Status = common.StatusDeployed
err := upAction.cfg.Releases.Create(rel) err := upAction.cfg.Releases.Create(rel)
is.NoError(err) is.NoError(err)
@ -479,29 +514,35 @@ func TestUpgradeRelease_Labels(t *testing.T) {
"key3": "val3", "key3": "val3",
} }
// setting newValues and upgrading // 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) is.NoError(err)
// Now make sure it is actually upgraded and labels were merged // 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) is.NoError(err)
if updatedRes == nil { if updatedResi == nil {
is.Fail("Updated Release is nil") is.Fail("Updated Release is nil")
return 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) is.Equal(mergeCustomLabels(rel.Labels, upAction.Labels), updatedRes.Labels)
// Now make sure it is suppressed release still contains original 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) is.NoError(err)
if initialRes == nil { if initialResi == nil {
is.Fail("Updated Release is nil") is.Fail("Updated Release is nil")
return 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) is.Equal(initialRes.Labels, rel.Labels)
} }
@ -516,7 +557,7 @@ func TestUpgradeRelease_SystemLabels(t *testing.T) {
"key1": "val1", "key1": "val1",
"key2": "val2.1", "key2": "val2.1",
} }
rel.Info.Status = release.StatusDeployed rel.Info.Status = common.StatusDeployed
err := upAction.cfg.Releases.Create(rel) err := upAction.cfg.Releases.Create(rel)
is.NoError(err) is.NoError(err)
@ -542,22 +583,26 @@ func TestUpgradeRelease_DryRun(t *testing.T) {
upAction := upgradeAction(t) upAction := upgradeAction(t)
rel := releaseStub() rel := releaseStub()
rel.Name = "previous-release" rel.Name = "previous-release"
rel.Info.Status = release.StatusDeployed rel.Info.Status = common.StatusDeployed
req.NoError(upAction.cfg.Releases.Create(rel)) req.NoError(upAction.cfg.Releases.Create(rel))
upAction.DryRun = true upAction.DryRunStrategy = DryRunClient
vals := map[string]interface{}{} vals := map[string]interface{}{}
ctx, done := context.WithCancel(t.Context()) 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() done()
req.NoError(err) 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") 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) req.NoError(err)
is.Equal(lastRelease.Info.Status, release.StatusDeployed) lastRelease, err := releaserToV1Release(lastReleasei)
req.NoError(err)
is.Equal(lastRelease.Info.Status, common.StatusDeployed)
is.Equal(1, lastRelease.Version) is.Equal(1, lastRelease.Version)
// Test the case for hiding the secret to ensure it is not displayed // Test the case for hiding the secret to ensure it is not displayed
@ -565,19 +610,23 @@ func TestUpgradeRelease_DryRun(t *testing.T) {
vals = map[string]interface{}{} vals = map[string]interface{}{}
ctx, done = context.WithCancel(t.Context()) 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() done()
req.NoError(err) 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") 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) req.NoError(err)
is.Equal(lastRelease.Info.Status, release.StatusDeployed) is.Equal(lastRelease.Info.Status, common.StatusDeployed)
is.Equal(1, lastRelease.Version) is.Equal(1, lastRelease.Version)
// Ensure in a dry run mode when using HideSecret // Ensure in a dry run mode when using HideSecret
upAction.DryRun = false upAction.DryRunStrategy = DryRunNone
vals = map[string]interface{}{} vals = map[string]interface{}{}
ctx, done = context.WithCancel(t.Context()) ctx, done = context.WithCancel(t.Context())

@ -47,6 +47,10 @@ func (r *v2DependencyAccessor) Name() string {
return r.dep.Name return r.dep.Name
} }
func (r *v2DependencyAccessor) Alias() string {
return r.dep.Alias
}
type v3DependencyAccessor struct { type v3DependencyAccessor struct {
dep *v3chart.Dependency dep *v3chart.Dependency
} }
@ -54,3 +58,7 @@ type v3DependencyAccessor struct {
func (r *v3DependencyAccessor) Name() string { func (r *v3DependencyAccessor) Name() string {
return r.dep.Name return r.dep.Name
} }
func (r *v3DependencyAccessor) Alias() string {
return r.dep.Alias
}

@ -40,4 +40,5 @@ type Accessor interface {
type DependencyAccessor interface { type DependencyAccessor interface {
Name() string Name() string
Alias() string
} }

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

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

@ -42,35 +42,66 @@ import (
) )
// Templates lints the templates in the Linter. // Templates lints the templates in the Linter.
func Templates(linter *support.Linter, values map[string]interface{}, namespace string, _ bool) { func Templates(linter *support.Linter, namespace string, values map[string]any, options ...TemplateLinterOption) {
TemplatesWithKubeVersion(linter, values, namespace, nil) templateLinter := newTemplateLinter(linter, namespace, values, options...)
templateLinter.Lint()
} }
// TemplatesWithKubeVersion lints the templates in the Linter, allowing to specify the kubernetes version. type TemplateLinterOption func(*templateLinter)
func TemplatesWithKubeVersion(linter *support.Linter, values map[string]interface{}, namespace string, kubeVersion *common.KubeVersion) {
TemplatesWithSkipSchemaValidation(linter, values, namespace, kubeVersion, false) 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 newTemplateLinter(linter *support.Linter, namespace string, values map[string]any, options ...TemplateLinterOption) templateLinter {
func TemplatesWithSkipSchemaValidation(linter *support.Linter, values map[string]interface{}, namespace string, kubeVersion *common.KubeVersion, skipSchemaValidation bool) {
fpath := "templates/" result := templateLinter{
templatesPath := filepath.Join(linter.ChartDir, fpath) linter: linter,
values: values,
namespace: namespace,
}
// Templates directory is optional for now for _, o := range options {
templatesDirExists := linter.RunLinterRule(support.WarningSev, fpath, templatesDirExists(templatesPath)) 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 { if !templatesDirExists {
return return
} }
validTemplatesDir := linter.RunLinterRule(support.ErrorSev, fpath, validateTemplatesDir(templatesPath)) validTemplatesDir := t.linter.RunLinterRule(support.ErrorSev, templatesDir, validateTemplatesDir(templatesPath))
if !validTemplatesDir { if !validTemplatesDir {
return return
} }
// Load chart and parse templates // 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 { if !chartLoaded {
return return
@ -78,35 +109,35 @@ func TemplatesWithSkipSchemaValidation(linter *support.Linter, values map[string
options := common.ReleaseOptions{ options := common.ReleaseOptions{
Name: "test-release", Name: "test-release",
Namespace: namespace, Namespace: t.namespace,
} }
caps := common.DefaultCapabilities.Copy() caps := common.DefaultCapabilities.Copy()
if kubeVersion != nil { if t.kubeVersion != nil {
caps.KubeVersion = *kubeVersion caps.KubeVersion = *t.kubeVersion
} }
// lint ignores import-values // lint ignores import-values
// See https://github.com/helm/helm/issues/9658 // 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 return
} }
cvals, err := util.CoalesceValues(chart, values) cvals, err := util.CoalesceValues(chart, t.values)
if err != nil { if err != nil {
return return
} }
valuesToRender, err := util.ToRenderValuesWithSchemaValidation(chart, cvals, options, caps, skipSchemaValidation) valuesToRender, err := util.ToRenderValuesWithSchemaValidation(chart, cvals, options, caps, t.skipSchemaValidation)
if err != nil { if err != nil {
linter.RunLinterRule(support.ErrorSev, fpath, err) t.linter.RunLinterRule(support.ErrorSev, templatesDir, err)
return return
} }
var e engine.Engine var e engine.Engine
e.LintMode = true e.LintMode = true
renderedContentMap, err := e.Render(chart, valuesToRender) renderedContentMap, err := e.Render(chart, valuesToRender)
renderOk := linter.RunLinterRule(support.ErrorSev, fpath, err) renderOk := t.linter.RunLinterRule(support.ErrorSev, templatesDir, err)
if !renderOk { if !renderOk {
return return
@ -121,9 +152,8 @@ func TemplatesWithSkipSchemaValidation(linter *support.Linter, values map[string
*/ */
for _, template := range chart.Templates { for _, template := range chart.Templates {
fileName := template.Name 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 // We only apply the following lint rules to yaml files
if filepath.Ext(fileName) != ".yaml" || filepath.Ext(fileName) == ".yml" { 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)] renderedContent := renderedContentMap[path.Join(chart.Name(), fileName)]
if strings.TrimSpace(renderedContent) != "" { 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) 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. // 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 // 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 return
} }
if yamlStruct != nil { if yamlStruct != nil {
// NOTE: set to warnings to allow users to support out-of-date kubernetes // NOTE: set to warnings to allow users to support out-of-date kubernetes
// Refs https://github.com/helm/helm/issues/8596 // Refs https://github.com/helm/helm/issues/8596
linter.RunLinterRule(support.WarningSev, fpath, validateMetadataName(yamlStruct)) t.linter.RunLinterRule(support.WarningSev, fileName, validateMetadataName(yamlStruct))
linter.RunLinterRule(support.WarningSev, fpath, validateNoDeprecations(yamlStruct, kubeVersion)) t.linter.RunLinterRule(support.WarningSev, fileName, validateNoDeprecations(yamlStruct, t.kubeVersion))
linter.RunLinterRule(support.ErrorSev, fpath, validateMatchSelector(yamlStruct, renderedContent)) t.linter.RunLinterRule(support.ErrorSev, fileName, validateMatchSelector(yamlStruct, renderedContent))
linter.RunLinterRule(support.ErrorSev, fpath, validateListAnnotations(yamlStruct, renderedContent)) t.linter.RunLinterRule(support.ErrorSev, fileName, validateListAnnotations(yamlStruct, renderedContent))
} }
} }
} }
@ -234,6 +264,7 @@ func validateYamlContent(err error) error {
if err != nil { if err != nil {
return fmt.Errorf("unable to parse YAML: %w", err) return fmt.Errorf("unable to parse YAML: %w", err)
} }
return nil return nil
} }

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

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

@ -25,6 +25,7 @@ import (
"helm.sh/helm/v4/pkg/action" "helm.sh/helm/v4/pkg/action"
chart "helm.sh/helm/v4/pkg/chart/v2" chart "helm.sh/helm/v4/pkg/chart/v2"
"helm.sh/helm/v4/pkg/release/common"
release "helm.sh/helm/v4/pkg/release/v1" release "helm.sh/helm/v4/pkg/release/v1"
) )
@ -64,35 +65,35 @@ func outputFlagCompletionTest(t *testing.T, cmdName string) {
cmd: fmt.Sprintf("__complete %s --output ''", cmdName), cmd: fmt.Sprintf("__complete %s --output ''", cmdName),
golden: "output/output-comp.txt", golden: "output/output-comp.txt",
rels: releasesMockWithStatus(&release.Info{ rels: releasesMockWithStatus(&release.Info{
Status: release.StatusDeployed, Status: common.StatusDeployed,
}), }),
}, { }, {
name: "completion for output flag long and after arg", name: "completion for output flag long and after arg",
cmd: fmt.Sprintf("__complete %s aramis --output ''", cmdName), cmd: fmt.Sprintf("__complete %s aramis --output ''", cmdName),
golden: "output/output-comp.txt", golden: "output/output-comp.txt",
rels: releasesMockWithStatus(&release.Info{ rels: releasesMockWithStatus(&release.Info{
Status: release.StatusDeployed, Status: common.StatusDeployed,
}), }),
}, { }, {
name: "completion for output flag short and before arg", name: "completion for output flag short and before arg",
cmd: fmt.Sprintf("__complete %s -o ''", cmdName), cmd: fmt.Sprintf("__complete %s -o ''", cmdName),
golden: "output/output-comp.txt", golden: "output/output-comp.txt",
rels: releasesMockWithStatus(&release.Info{ rels: releasesMockWithStatus(&release.Info{
Status: release.StatusDeployed, Status: common.StatusDeployed,
}), }),
}, { }, {
name: "completion for output flag short and after arg", name: "completion for output flag short and after arg",
cmd: fmt.Sprintf("__complete %s aramis -o ''", cmdName), cmd: fmt.Sprintf("__complete %s aramis -o ''", cmdName),
golden: "output/output-comp.txt", golden: "output/output-comp.txt",
rels: releasesMockWithStatus(&release.Info{ rels: releasesMockWithStatus(&release.Info{
Status: release.StatusDeployed, Status: common.StatusDeployed,
}), }),
}, { }, {
name: "completion for output flag, no filter", name: "completion for output flag, no filter",
cmd: fmt.Sprintf("__complete %s --output jso", cmdName), cmd: fmt.Sprintf("__complete %s --output jso", cmdName),
golden: "output/output-comp.txt", golden: "output/output-comp.txt",
rels: releasesMockWithStatus(&release.Info{ rels: releasesMockWithStatus(&release.Info{
Status: release.StatusDeployed, Status: common.StatusDeployed,
}), }),
}} }}
runTestCmd(t, tests) runTestCmd(t, tests)

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

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

@ -25,6 +25,7 @@ import (
"helm.sh/helm/v4/pkg/action" "helm.sh/helm/v4/pkg/action"
"helm.sh/helm/v4/pkg/cmd/require" "helm.sh/helm/v4/pkg/cmd/require"
"helm.sh/helm/v4/pkg/release"
) )
var getNotesHelp = ` var getNotesHelp = `
@ -50,8 +51,12 @@ func newGetNotesCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
if err != nil { if err != nil {
return err return err
} }
if len(res.Info.Notes) > 0 { rac, err := release.NewAccessor(res)
fmt.Fprintf(out, "NOTES:\n%s\n", res.Info.Notes) if err != nil {
return err
}
if len(rac.Notes()) > 0 {
fmt.Fprintf(out, "NOTES:\n%s\n", rac.Notes())
} }
return nil 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,7 +18,9 @@ package cmd
import ( import (
"bytes" "bytes"
"encoding/json"
"io" "io"
"log/slog"
"os" "os"
"strings" "strings"
"testing" "testing"
@ -26,6 +28,8 @@ import (
shellwords "github.com/mattn/go-shellwords" shellwords "github.com/mattn/go-shellwords"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"helm.sh/helm/v4/internal/test" "helm.sh/helm/v4/internal/test"
"helm.sh/helm/v4/pkg/action" "helm.sh/helm/v4/pkg/action"
@ -151,3 +155,155 @@ func resetEnv() func() {
settings = cli.New() 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())
}
})
}
}

@ -111,7 +111,11 @@ func (r releaseHistory) WriteTable(out io.Writer) error {
} }
func getHistory(client *action.History, name string) (releaseHistory, 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 { if err != nil {
return nil, err return nil, err
} }
@ -180,7 +184,11 @@ func compListRevisions(_ string, cfg *action.Configuration, releaseName string)
client := action.NewHistory(cfg) client := action.NewHistory(cfg)
var revisions []string 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 { for _, version := range hist {
appVersion := fmt.Sprintf("App: %s", version.Chart.Metadata.AppVersion) appVersion := fmt.Sprintf("App: %s", version.Chart.Metadata.AppVersion)
chartDesc := fmt.Sprintf("Chart: %s-%s", version.Chart.Metadata.Name, version.Chart.Metadata.Version) chartDesc := fmt.Sprintf("Chart: %s-%s", version.Chart.Metadata.Name, version.Chart.Metadata.Version)

@ -20,11 +20,12 @@ import (
"fmt" "fmt"
"testing" "testing"
"helm.sh/helm/v4/pkg/release/common"
release "helm.sh/helm/v4/pkg/release/v1" release "helm.sh/helm/v4/pkg/release/v1"
) )
func TestHistoryCmd(t *testing.T) { 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{ return release.Mock(&release.MockReleaseOptions{
Name: name, Name: name,
Version: vers, Version: vers,
@ -36,34 +37,34 @@ func TestHistoryCmd(t *testing.T) {
name: "get history for release", name: "get history for release",
cmd: "history angry-bird", cmd: "history angry-bird",
rels: []*release.Release{ rels: []*release.Release{
mk("angry-bird", 4, release.StatusDeployed), mk("angry-bird", 4, common.StatusDeployed),
mk("angry-bird", 3, release.StatusSuperseded), mk("angry-bird", 3, common.StatusSuperseded),
mk("angry-bird", 2, release.StatusSuperseded), mk("angry-bird", 2, common.StatusSuperseded),
mk("angry-bird", 1, release.StatusSuperseded), mk("angry-bird", 1, common.StatusSuperseded),
}, },
golden: "output/history.txt", golden: "output/history.txt",
}, { }, {
name: "get history with max limit set", name: "get history with max limit set",
cmd: "history angry-bird --max 2", cmd: "history angry-bird --max 2",
rels: []*release.Release{ rels: []*release.Release{
mk("angry-bird", 4, release.StatusDeployed), mk("angry-bird", 4, common.StatusDeployed),
mk("angry-bird", 3, release.StatusSuperseded), mk("angry-bird", 3, common.StatusSuperseded),
}, },
golden: "output/history-limit.txt", golden: "output/history-limit.txt",
}, { }, {
name: "get history with yaml output format", name: "get history with yaml output format",
cmd: "history angry-bird --output yaml", cmd: "history angry-bird --output yaml",
rels: []*release.Release{ rels: []*release.Release{
mk("angry-bird", 4, release.StatusDeployed), mk("angry-bird", 4, common.StatusDeployed),
mk("angry-bird", 3, release.StatusSuperseded), mk("angry-bird", 3, common.StatusSuperseded),
}, },
golden: "output/history.yaml", golden: "output/history.yaml",
}, { }, {
name: "get history with json output format", name: "get history with json output format",
cmd: "history angry-bird --output json", cmd: "history angry-bird --output json",
rels: []*release.Release{ rels: []*release.Release{
mk("angry-bird", 4, release.StatusDeployed), mk("angry-bird", 4, common.StatusDeployed),
mk("angry-bird", 3, release.StatusSuperseded), mk("angry-bird", 3, common.StatusSuperseded),
}, },
golden: "output/history.json", golden: "output/history.json",
}} }}
@ -76,7 +77,7 @@ func TestHistoryOutputCompletion(t *testing.T) {
func revisionFlagCompletionTest(t *testing.T, cmdName string) { func revisionFlagCompletionTest(t *testing.T, cmdName string) {
t.Helper() 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{ return release.Mock(&release.MockReleaseOptions{
Name: name, Name: name,
Version: vers, Version: vers,
@ -85,10 +86,10 @@ func revisionFlagCompletionTest(t *testing.T, cmdName string) {
} }
releases := []*release.Release{ releases := []*release.Release{
mk("musketeers", 11, release.StatusDeployed), mk("musketeers", 11, common.StatusDeployed),
mk("musketeers", 10, release.StatusSuperseded), mk("musketeers", 10, common.StatusSuperseded),
mk("musketeers", 9, release.StatusSuperseded), mk("musketeers", 9, common.StatusSuperseded),
mk("musketeers", 8, release.StatusSuperseded), mk("musketeers", 8, common.StatusSuperseded),
} }
tests := []cmdTestCase{{ tests := []cmdTestCase{{

@ -18,14 +18,12 @@ package cmd
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"io" "io"
"log" "log"
"log/slog" "log/slog"
"os" "os"
"os/signal" "os/signal"
"slices"
"syscall" "syscall"
"time" "time"
@ -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) { ValidArgsFunction: func(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return compInstall(args, toComplete, client) 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, registryClient, err := newRegistryClient(client.CertFile, client.KeyFile, client.CaFile,
client.InsecureSkipTLSverify, client.PlainHTTP, client.Username, client.Password) client.InsecureSkipTLSverify, client.PlainHTTP, client.Username, client.Password)
if err != nil { if err != nil {
@ -152,12 +150,12 @@ func newInstallCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
} }
client.SetRegistryClient(registryClient) client.SetRegistryClient(registryClient)
// This is for the case where "" is specifically passed in as a dryRunStrategy, err := cmdGetDryRunFlagStrategy(cmd, false)
// value. When there is no value passed in NoOptDefVal will be used if err != nil {
// and it is set to client. See addInstallFlags. return err
if client.DryRunOption == "" {
client.DryRunOption = "none"
} }
client.DryRunStrategy = dryRunStrategy
rel, err := runInstall(args, client, valueOpts, out) rel, err := runInstall(args, client, valueOpts, out)
if err != nil { if err != nil {
return fmt.Errorf("INSTALLATION FAILED: %w", err) 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 // hide-secret is not available in all places the install flags are used so
// it is added separately // it is added separately
f := cmd.Flags()
f.BoolVar(&client.HideSecret, "hide-secret", false, "hide Kubernetes Secrets when also using the --dry-run flag") f.BoolVar(&client.HideSecret, "hide-secret", false, "hide Kubernetes Secrets when also using the --dry-run flag")
addDryRunFlag(cmd)
bindOutputFlag(cmd, &outfmt) bindOutputFlag(cmd, &outfmt)
bindPostRenderFlag(cmd, &client.PostRenderer, settings) 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) { 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") 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-replace", false, "force resource updates by replacement")
f.BoolVar(&client.ForceReplace, "force", false, "deprecated") f.BoolVar(&client.ForceReplace, "force", false, "deprecated")
f.MarkDeprecated("force", "use --force-replace instead") f.MarkDeprecated("force", "use --force-replace instead")
@ -316,11 +308,6 @@ func runInstall(args []string, client *action.Install, valueOpts *values.Options
client.Namespace = settings.Namespace() 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 // Create context and prepare the handle of SIGTERM
ctx := context.Background() ctx := context.Background()
ctx, cancel := context.WithCancel(ctx) ctx, cancel := context.WithCancel(ctx)
@ -336,7 +323,12 @@ func runInstall(args []string, client *action.Install, valueOpts *values.Options
cancel() 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 // checkIfInstallable validates if a chart can be installed
@ -363,13 +355,3 @@ func compInstall(args []string, toComplete string, client *action.Install) ([]st
} }
return nil, cobra.ShellCompDirectiveNoFileComp 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/action" "helm.sh/helm/v4/pkg/action"
"helm.sh/helm/v4/pkg/cli/output" "helm.sh/helm/v4/pkg/cli/output"
"helm.sh/helm/v4/pkg/cmd/require" "helm.sh/helm/v4/pkg/cmd/require"
"helm.sh/helm/v4/pkg/release/common"
release "helm.sh/helm/v4/pkg/release/v1" release "helm.sh/helm/v4/pkg/release/v1"
) )
@ -79,7 +80,11 @@ func newListCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
} }
client.SetStateMask() client.SetStateMask()
results, err := client.Run() resultsi, err := client.Run()
if err != nil {
return err
}
results, err := releaseListToV1List(resultsi)
if err != nil { if err != nil {
return err return err
} }
@ -193,28 +198,28 @@ func (w *releaseListWriter) WriteTable(out io.Writer) error {
} }
for _, r := range w.releases { for _, r := range w.releases {
// Parse the status string back to a release.Status to use color // Parse the status string back to a release.Status to use color
var status release.Status var status common.Status
switch r.Status { switch r.Status {
case "deployed": case "deployed":
status = release.StatusDeployed status = common.StatusDeployed
case "failed": case "failed":
status = release.StatusFailed status = common.StatusFailed
case "pending-install": case "pending-install":
status = release.StatusPendingInstall status = common.StatusPendingInstall
case "pending-upgrade": case "pending-upgrade":
status = release.StatusPendingUpgrade status = common.StatusPendingUpgrade
case "pending-rollback": case "pending-rollback":
status = release.StatusPendingRollback status = common.StatusPendingRollback
case "uninstalling": case "uninstalling":
status = release.StatusUninstalling status = common.StatusUninstalling
case "uninstalled": case "uninstalled":
status = release.StatusUninstalled status = common.StatusUninstalled
case "superseded": case "superseded":
status = release.StatusSuperseded status = common.StatusSuperseded
case "unknown": case "unknown":
status = release.StatusUnknown status = common.StatusUnknown
default: 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) 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.Filter = fmt.Sprintf("^%s", toComplete)
client.SetStateMask() client.SetStateMask()
releases, err := client.Run() releasesi, err := client.Run()
if err != nil {
return nil, cobra.ShellCompDirectiveDefault
}
releases, err := releaseListToV1List(releasesi)
if err != nil { if err != nil {
return nil, cobra.ShellCompDirectiveDefault return nil, cobra.ShellCompDirectiveDefault
} }

@ -21,6 +21,7 @@ import (
"time" "time"
chart "helm.sh/helm/v4/pkg/chart/v2" chart "helm.sh/helm/v4/pkg/chart/v2"
"helm.sh/helm/v4/pkg/release/common"
release "helm.sh/helm/v4/pkg/release/v1" release "helm.sh/helm/v4/pkg/release/v1"
) )
@ -47,7 +48,7 @@ func TestListCmd(t *testing.T) {
Namespace: defaultNamespace, Namespace: defaultNamespace,
Info: &release.Info{ Info: &release.Info{
LastDeployed: timestamp1, LastDeployed: timestamp1,
Status: release.StatusSuperseded, Status: common.StatusSuperseded,
}, },
Chart: chartInfo, Chart: chartInfo,
}, },
@ -57,7 +58,7 @@ func TestListCmd(t *testing.T) {
Namespace: defaultNamespace, Namespace: defaultNamespace,
Info: &release.Info{ Info: &release.Info{
LastDeployed: timestamp1, LastDeployed: timestamp1,
Status: release.StatusDeployed, Status: common.StatusDeployed,
}, },
Chart: chartInfo, Chart: chartInfo,
}, },
@ -67,7 +68,7 @@ func TestListCmd(t *testing.T) {
Namespace: defaultNamespace, Namespace: defaultNamespace,
Info: &release.Info{ Info: &release.Info{
LastDeployed: timestamp1, LastDeployed: timestamp1,
Status: release.StatusUninstalled, Status: common.StatusUninstalled,
}, },
Chart: chartInfo, Chart: chartInfo,
}, },
@ -77,7 +78,7 @@ func TestListCmd(t *testing.T) {
Namespace: defaultNamespace, Namespace: defaultNamespace,
Info: &release.Info{ Info: &release.Info{
LastDeployed: timestamp1, LastDeployed: timestamp1,
Status: release.StatusSuperseded, Status: common.StatusSuperseded,
}, },
Chart: chartInfo, Chart: chartInfo,
}, },
@ -87,7 +88,7 @@ func TestListCmd(t *testing.T) {
Namespace: defaultNamespace, Namespace: defaultNamespace,
Info: &release.Info{ Info: &release.Info{
LastDeployed: timestamp2, LastDeployed: timestamp2,
Status: release.StatusFailed, Status: common.StatusFailed,
}, },
Chart: chartInfo, Chart: chartInfo,
}, },
@ -97,7 +98,7 @@ func TestListCmd(t *testing.T) {
Namespace: defaultNamespace, Namespace: defaultNamespace,
Info: &release.Info{ Info: &release.Info{
LastDeployed: timestamp1, LastDeployed: timestamp1,
Status: release.StatusUninstalling, Status: common.StatusUninstalling,
}, },
Chart: chartInfo, Chart: chartInfo,
}, },
@ -107,7 +108,7 @@ func TestListCmd(t *testing.T) {
Namespace: defaultNamespace, Namespace: defaultNamespace,
Info: &release.Info{ Info: &release.Info{
LastDeployed: timestamp1, LastDeployed: timestamp1,
Status: release.StatusPendingInstall, Status: common.StatusPendingInstall,
}, },
Chart: chartInfo, Chart: chartInfo,
}, },
@ -117,7 +118,7 @@ func TestListCmd(t *testing.T) {
Namespace: defaultNamespace, Namespace: defaultNamespace,
Info: &release.Info{ Info: &release.Info{
LastDeployed: timestamp3, LastDeployed: timestamp3,
Status: release.StatusDeployed, Status: common.StatusDeployed,
}, },
Chart: chartInfo, Chart: chartInfo,
}, },
@ -127,7 +128,7 @@ func TestListCmd(t *testing.T) {
Namespace: defaultNamespace, Namespace: defaultNamespace,
Info: &release.Info{ Info: &release.Info{
LastDeployed: timestamp4, LastDeployed: timestamp4,
Status: release.StatusDeployed, Status: common.StatusDeployed,
}, },
Chart: chartInfo, Chart: chartInfo,
}, },
@ -137,7 +138,7 @@ func TestListCmd(t *testing.T) {
Namespace: "milano", Namespace: "milano",
Info: &release.Info{ Info: &release.Info{
LastDeployed: timestamp1, LastDeployed: timestamp1,
Status: release.StatusDeployed, Status: common.StatusDeployed,
}, },
Chart: chartInfo, Chart: chartInfo,
}, },

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

@ -65,13 +65,17 @@ func newReleaseTestCmd(cfg *action.Configuration, out io.Writer) *cobra.Command
client.Filters[action.ExcludeNameFilter] = append(client.Filters[action.ExcludeNameFilter], notName.ReplaceAllLiteralString(f, "")) 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 // 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 // release, otherwise we keep going so we can print status and logs
// if requested // if requested
if runErr != nil && rel == nil { if runErr != nil && reli == nil {
return runErr return runErr
} }
rel, err := releaserToV1Release(reli)
if err != nil {
return err
}
if err := outfmt.Write(out, &statusPrinter{ if err := outfmt.Write(out, &statusPrinter{
release: rel, release: rel,

@ -26,6 +26,7 @@ import (
"helm.sh/helm/v4/pkg/chart/common" "helm.sh/helm/v4/pkg/chart/common"
chart "helm.sh/helm/v4/pkg/chart/v2" chart "helm.sh/helm/v4/pkg/chart/v2"
kubefake "helm.sh/helm/v4/pkg/kube/fake" kubefake "helm.sh/helm/v4/pkg/kube/fake"
rcommon "helm.sh/helm/v4/pkg/release/common"
release "helm.sh/helm/v4/pkg/release/v1" release "helm.sh/helm/v4/pkg/release/v1"
) )
@ -46,7 +47,7 @@ func TestReleaseTestNotesHandling(t *testing.T) {
Name: "test-release", Name: "test-release",
Namespace: "default", Namespace: "default",
Info: &release.Info{ Info: &release.Info{
Status: release.StatusDeployed, Status: rcommon.StatusDeployed,
Notes: "Some important notes that should be hidden by default", Notes: "Some important notes that should be hidden by default",
}, },
Chart: &chart.Chart{Metadata: &chart.Metadata{Name: "test", Version: "1.0.0"}}, Chart: &chart.Chart{Metadata: &chart.Metadata{Name: "test", Version: "1.0.0"}},

@ -57,7 +57,7 @@ func newRollbackCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
return noMoreArgsComp() return noMoreArgsComp()
}, },
RunE: func(_ *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
if len(args) > 1 { if len(args) > 1 {
ver, err := strconv.Atoi(args[1]) ver, err := strconv.Atoi(args[1])
if err != nil { if err != nil {
@ -66,6 +66,12 @@ func newRollbackCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
client.Version = ver client.Version = ver
} }
dryRunStrategy, err := cmdGetDryRunFlagStrategy(cmd, false)
if err != nil {
return err
}
client.DryRunStrategy = dryRunStrategy
if err := client.Run(args[0]); err != nil { if err := client.Run(args[0]); err != nil {
return err return err
} }
@ -76,7 +82,6 @@ func newRollbackCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
} }
f := cmd.Flags() 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-replace", false, "force resource updates by replacement")
f.BoolVar(&client.ForceReplace, "force", false, "deprecated") f.BoolVar(&client.ForceReplace, "force", false, "deprecated")
f.MarkDeprecated("force", "use --force-replace instead") 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.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.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") 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) AddWaitFlag(cmd, &client.WaitStrategy)
cmd.MarkFlagsMutuallyExclusive("force-replace", "force-conflicts") cmd.MarkFlagsMutuallyExclusive("force-replace", "force-conflicts")
cmd.MarkFlagsMutuallyExclusive("force", "force-conflicts") cmd.MarkFlagsMutuallyExclusive("force", "force-conflicts")

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

@ -39,6 +39,7 @@ import (
"helm.sh/helm/v4/pkg/cli" "helm.sh/helm/v4/pkg/cli"
kubefake "helm.sh/helm/v4/pkg/kube/fake" kubefake "helm.sh/helm/v4/pkg/kube/fake"
"helm.sh/helm/v4/pkg/registry" "helm.sh/helm/v4/pkg/registry"
ri "helm.sh/helm/v4/pkg/release"
release "helm.sh/helm/v4/pkg/release/v1" release "helm.sh/helm/v4/pkg/release/v1"
"helm.sh/helm/v4/pkg/repo/v1" "helm.sh/helm/v4/pkg/repo/v1"
"helm.sh/helm/v4/pkg/storage/driver" "helm.sh/helm/v4/pkg/storage/driver"
@ -465,3 +466,31 @@ type CommandError struct {
error error
ExitCode int 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/chart/common/util"
"helm.sh/helm/v4/pkg/cli/output" "helm.sh/helm/v4/pkg/cli/output"
"helm.sh/helm/v4/pkg/cmd/require" "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. // 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 { if outfmt == output.Table {
client.ShowResourcesTable = true 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 { if err != nil {
return err return err
} }
@ -110,54 +115,65 @@ func newStatusCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
} }
type statusPrinter struct { type statusPrinter struct {
release *release.Release release release.Releaser
debug bool debug bool
showMetadata bool showMetadata bool
hideNotes bool hideNotes bool
noColor 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 { 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 { 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 { func (s statusPrinter) WriteTable(out io.Writer) error {
if s.release == nil { if s.release == nil {
return nil return nil
} }
_, _ = fmt.Fprintf(out, "NAME: %s\n", s.release.Name) rel := s.getV1Release()
if !s.release.Info.LastDeployed.IsZero() { _, _ = fmt.Fprintf(out, "NAME: %s\n", rel.Name)
_, _ = fmt.Fprintf(out, "LAST DEPLOYED: %s\n", s.release.Info.LastDeployed.Format(time.ANSIC)) 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, "NAMESPACE: %s\n", coloroutput.ColorizeNamespace(rel.Namespace, s.noColor))
_, _ = fmt.Fprintf(out, "STATUS: %s\n", coloroutput.ColorizeStatus(s.release.Info.Status, s.noColor)) _, _ = fmt.Fprintf(out, "STATUS: %s\n", coloroutput.ColorizeStatus(rel.Info.Status, s.noColor))
_, _ = fmt.Fprintf(out, "REVISION: %d\n", s.release.Version) _, _ = fmt.Fprintf(out, "REVISION: %d\n", rel.Version)
if s.showMetadata { if s.showMetadata {
_, _ = fmt.Fprintf(out, "CHART: %s\n", s.release.Chart.Metadata.Name) _, _ = fmt.Fprintf(out, "CHART: %s\n", rel.Chart.Metadata.Name)
_, _ = fmt.Fprintf(out, "VERSION: %s\n", s.release.Chart.Metadata.Version) _, _ = fmt.Fprintf(out, "VERSION: %s\n", rel.Chart.Metadata.Version)
_, _ = fmt.Fprintf(out, "APP_VERSION: %s\n", s.release.Chart.Metadata.AppVersion) _, _ = 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) buf := new(bytes.Buffer)
printFlags := get.NewHumanPrintFlags() printFlags := get.NewHumanPrintFlags()
typePrinter, _ := printFlags.ToPrinter("") typePrinter, _ := printFlags.ToPrinter("")
printer := &get.TablePrinter{Delegate: typePrinter} printer := &get.TablePrinter{Delegate: typePrinter}
var keys []string var keys []string
for key := range s.release.Info.Resources { for key := range rel.Info.Resources {
keys = append(keys, key) keys = append(keys, key)
} }
for _, t := range keys { for _, t := range keys {
_, _ = fmt.Fprintf(buf, "==> %s\n", t) _, _ = fmt.Fprintf(buf, "==> %s\n", t)
vk := s.release.Info.Resources[t] vk := rel.Info.Resources[t]
for _, resource := range vk { for _, resource := range vk {
if err := printer.PrintObj(resource, buf); err != nil { if err := printer.PrintObj(resource, buf); err != nil {
_, _ = fmt.Fprintf(buf, "failed to print object type %s: %v\n", t, err) _, _ = 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()) _, _ = fmt.Fprintf(out, "RESOURCES:\n%s\n", buf.String())
} }
executions := executionsByHookEvent(s.release) executions := executionsByHookEvent(rel)
if tests, ok := executions[release.HookTest]; !ok || len(tests) == 0 { if tests, ok := executions[releasev1.HookTest]; !ok || len(tests) == 0 {
_, _ = fmt.Fprintln(out, "TEST SUITE: None") _, _ = fmt.Fprintln(out, "TEST SUITE: None")
} else { } else {
for _, h := range tests { for _, h := range tests {
@ -190,14 +206,14 @@ func (s statusPrinter) WriteTable(out io.Writer) error {
if s.debug { if s.debug {
_, _ = fmt.Fprintln(out, "USER-SUPPLIED VALUES:") _, _ = fmt.Fprintln(out, "USER-SUPPLIED VALUES:")
err := output.EncodeYAML(out, s.release.Config) err := output.EncodeYAML(out, rel.Config)
if err != nil { if err != nil {
return err return err
} }
// Print an extra newline // Print an extra newline
_, _ = fmt.Fprintln(out) _, _ = fmt.Fprintln(out)
cfg, err := util.CoalesceValues(s.release.Chart, s.release.Config) cfg, err := util.CoalesceValues(rel.Chart, rel.Config)
if err != nil { if err != nil {
return err return err
} }
@ -211,28 +227,28 @@ func (s statusPrinter) WriteTable(out io.Writer) error {
_, _ = fmt.Fprintln(out) _, _ = 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:") _, _ = 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, "---\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 // Hide notes from output - option in install and upgrades
if !s.hideNotes && len(s.release.Info.Notes) > 0 { if !s.hideNotes && len(rel.Info.Notes) > 0 {
_, _ = fmt.Fprintf(out, "NOTES:\n%s\n", strings.TrimSpace(s.release.Info.Notes)) _, _ = fmt.Fprintf(out, "NOTES:\n%s\n", strings.TrimSpace(rel.Info.Notes))
} }
return nil return nil
} }
func executionsByHookEvent(rel *release.Release) map[release.HookEvent][]*release.Hook { func executionsByHookEvent(rel *releasev1.Release) map[releasev1.HookEvent][]*releasev1.Hook {
result := make(map[release.HookEvent][]*release.Hook) result := make(map[releasev1.HookEvent][]*releasev1.Hook)
for _, h := range rel.Hooks { for _, h := range rel.Hooks {
for _, e := range h.Events { for _, e := range h.Events {
executions, ok := result[e] executions, ok := result[e]
if !ok { if !ok {
executions = []*release.Hook{} executions = []*releasev1.Hook{}
} }
result[e] = append(executions, h) result[e] = append(executions, h)
} }

@ -21,6 +21,7 @@ import (
"time" "time"
chart "helm.sh/helm/v4/pkg/chart/v2" chart "helm.sh/helm/v4/pkg/chart/v2"
"helm.sh/helm/v4/pkg/release/common"
release "helm.sh/helm/v4/pkg/release/v1" release "helm.sh/helm/v4/pkg/release/v1"
) )
@ -41,14 +42,14 @@ func TestStatusCmd(t *testing.T) {
cmd: "status flummoxed-chickadee", cmd: "status flummoxed-chickadee",
golden: "output/status.txt", golden: "output/status.txt",
rels: releasesMockWithStatus(&release.Info{ rels: releasesMockWithStatus(&release.Info{
Status: release.StatusDeployed, Status: common.StatusDeployed,
}), }),
}, { }, {
name: "get status of a deployed release, with desc", name: "get status of a deployed release, with desc",
cmd: "status flummoxed-chickadee", cmd: "status flummoxed-chickadee",
golden: "output/status-with-desc.txt", golden: "output/status-with-desc.txt",
rels: releasesMockWithStatus(&release.Info{ rels: releasesMockWithStatus(&release.Info{
Status: release.StatusDeployed, Status: common.StatusDeployed,
Description: "Mock description", Description: "Mock description",
}), }),
}, { }, {
@ -56,7 +57,7 @@ func TestStatusCmd(t *testing.T) {
cmd: "status flummoxed-chickadee", cmd: "status flummoxed-chickadee",
golden: "output/status-with-notes.txt", golden: "output/status-with-notes.txt",
rels: releasesMockWithStatus(&release.Info{ rels: releasesMockWithStatus(&release.Info{
Status: release.StatusDeployed, Status: common.StatusDeployed,
Notes: "release notes", Notes: "release notes",
}), }),
}, { }, {
@ -64,7 +65,7 @@ func TestStatusCmd(t *testing.T) {
cmd: "status flummoxed-chickadee -o json", cmd: "status flummoxed-chickadee -o json",
golden: "output/status.json", golden: "output/status.json",
rels: releasesMockWithStatus(&release.Info{ rels: releasesMockWithStatus(&release.Info{
Status: release.StatusDeployed, Status: common.StatusDeployed,
Notes: "release notes", Notes: "release notes",
}), }),
}, { }, {
@ -73,7 +74,7 @@ func TestStatusCmd(t *testing.T) {
golden: "output/status-with-resources.txt", golden: "output/status-with-resources.txt",
rels: releasesMockWithStatus( rels: releasesMockWithStatus(
&release.Info{ &release.Info{
Status: release.StatusDeployed, Status: common.StatusDeployed,
}, },
), ),
}, { }, {
@ -82,7 +83,7 @@ func TestStatusCmd(t *testing.T) {
golden: "output/status-with-resources.json", golden: "output/status-with-resources.json",
rels: releasesMockWithStatus( rels: releasesMockWithStatus(
&release.Info{ &release.Info{
Status: release.StatusDeployed, Status: common.StatusDeployed,
}, },
), ),
}, { }, {
@ -91,7 +92,7 @@ func TestStatusCmd(t *testing.T) {
golden: "output/status-with-test-suite.txt", golden: "output/status-with-test-suite.txt",
rels: releasesMockWithStatus( rels: releasesMockWithStatus(
&release.Info{ &release.Info{
Status: release.StatusDeployed, Status: common.StatusDeployed,
}, },
&release.Hook{ &release.Hook{
Name: "never-run-test", Name: "never-run-test",
@ -140,7 +141,7 @@ func TestStatusCompletion(t *testing.T) {
Name: "athos", Name: "athos",
Namespace: "default", Namespace: "default",
Info: &release.Info{ Info: &release.Info{
Status: release.StatusDeployed, Status: common.StatusDeployed,
}, },
Chart: &chart.Chart{ Chart: &chart.Chart{
Metadata: &chart.Metadata{ Metadata: &chart.Metadata{
@ -152,7 +153,7 @@ func TestStatusCompletion(t *testing.T) {
Name: "porthos", Name: "porthos",
Namespace: "default", Namespace: "default",
Info: &release.Info{ Info: &release.Info{
Status: release.StatusFailed, Status: common.StatusFailed,
}, },
Chart: &chart.Chart{ Chart: &chart.Chart{
Metadata: &chart.Metadata{ Metadata: &chart.Metadata{
@ -164,7 +165,7 @@ func TestStatusCompletion(t *testing.T) {
Name: "aramis", Name: "aramis",
Namespace: "default", Namespace: "default",
Info: &release.Info{ Info: &release.Info{
Status: release.StatusUninstalled, Status: common.StatusUninstalled,
}, },
Chart: &chart.Chart{ Chart: &chart.Chart{
Metadata: &chart.Metadata{ Metadata: &chart.Metadata{
@ -176,7 +177,7 @@ func TestStatusCompletion(t *testing.T) {
Name: "dartagnan", Name: "dartagnan",
Namespace: "gascony", Namespace: "gascony",
Info: &release.Info{ Info: &release.Info{
Status: release.StatusUnknown, Status: common.StatusUnknown,
}, },
Chart: &chart.Chart{ Chart: &chart.Chart{
Metadata: &chart.Metadata{ Metadata: &chart.Metadata{

@ -23,7 +23,6 @@ import (
"io" "io"
"io/fs" "io/fs"
"os" "os"
"path"
"path/filepath" "path/filepath"
"regexp" "regexp"
"slices" "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) { ValidArgsFunction: func(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return compInstall(args, toComplete, client) return compInstall(args, toComplete, client)
}, },
RunE: func(_ *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
if kubeVersion != "" { if kubeVersion != "" {
parsedKubeVersion, err := common.ParseKubeVersion(kubeVersion) parsedKubeVersion, err := common.ParseKubeVersion(kubeVersion)
if err != nil { if err != nil {
@ -83,16 +82,17 @@ func newTemplateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
} }
client.SetRegistryClient(registryClient) client.SetRegistryClient(registryClient)
// This is for the case where "" is specifically passed in as a dryRunStrategy, err := cmdGetDryRunFlagStrategy(cmd, true)
// value. When there is no value passed in NoOptDefVal will be used if err != nil {
// and it is set to client. See addInstallFlags. return err
if client.DryRunOption == "" { }
client.DryRunOption = "true" 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.ReleaseName = "release-name"
client.Replace = true // Skip the name check client.Replace = true // Skip the name check
client.ClientOnly = !validate
client.APIVersions = common.VersionSet(extraAPIs) client.APIVersions = common.VersionSet(extraAPIs)
client.IncludeCRDs = includeCrds client.IncludeCRDs = includeCrds
rel, err := runInstall(args, client, valueOpts, out) 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) addInstallFlags(cmd, f, client, valueOpts)
f.StringArrayVarP(&showFiles, "show-only", "s", []string{}, "only show manifests rendered from the given templates") 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.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(&includeCrds, "include-crds", false, "include CRDs in the templated output")
f.BoolVar(&skipTests, "skip-tests", false, "skip tests from 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.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.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.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.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) bindPostRenderFlag(cmd, &client.PostRenderer, settings)
cmd.MarkFlagsMutuallyExclusive("validate", "dry-run")
return cmd return cmd
} }
@ -250,7 +257,7 @@ func createOrOpenFile(filename string, appendData bool) (*os.File, error) {
} }
func ensureDirectoryForFile(file string) error { func ensureDirectoryForFile(file string) error {
baseDir := path.Dir(file) baseDir := filepath.Dir(file)
_, err := os.Stat(baseDir) _, err := os.Stat(baseDir)
if err != nil && !errors.Is(err, fs.ErrNotExist) { if err != nil && !errors.Is(err, fs.ErrNotExist) {
return err return err

@ -37,7 +37,8 @@ import (
"helm.sh/helm/v4/pkg/cmd/require" "helm.sh/helm/v4/pkg/cmd/require"
"helm.sh/helm/v4/pkg/downloader" "helm.sh/helm/v4/pkg/downloader"
"helm.sh/helm/v4/pkg/getter" "helm.sh/helm/v4/pkg/getter"
release "helm.sh/helm/v4/pkg/release/v1" ri "helm.sh/helm/v4/pkg/release"
"helm.sh/helm/v4/pkg/release/common"
"helm.sh/helm/v4/pkg/storage/driver" "helm.sh/helm/v4/pkg/storage/driver"
) )
@ -100,7 +101,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
} }
return noMoreArgsComp() return noMoreArgsComp()
}, },
RunE: func(_ *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
client.Namespace = settings.Namespace() client.Namespace = settings.Namespace()
registryClient, err := newRegistryClient(client.CertFile, client.KeyFile, client.CaFile, registryClient, err := newRegistryClient(client.CertFile, client.KeyFile, client.CaFile,
@ -110,12 +111,12 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
} }
client.SetRegistryClient(registryClient) client.SetRegistryClient(registryClient)
// This is for the case where "" is specifically passed in as a dryRunStrategy, err := cmdGetDryRunFlagStrategy(cmd, false)
// value. When there is no value passed in NoOptDefVal will be used if err != nil {
// and it is set to client. See addInstallFlags. return err
if client.DryRunOption == "" {
client.DryRunOption = "none"
} }
client.DryRunStrategy = dryRunStrategy
// Fixes #7002 - Support reading values from STDIN for `upgrade` command // Fixes #7002 - Support reading values from STDIN for `upgrade` command
// Must load values AFTER determining if we have to call install so that values loaded from stdin are not read twice // Must load values AFTER determining if we have to call install so that values loaded from stdin are not read twice
if client.Install { if client.Install {
@ -132,8 +133,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
instClient.CreateNamespace = createNamespace instClient.CreateNamespace = createNamespace
instClient.ChartPathOptions = client.ChartPathOptions instClient.ChartPathOptions = client.ChartPathOptions
instClient.ForceReplace = client.ForceReplace instClient.ForceReplace = client.ForceReplace
instClient.DryRun = client.DryRun instClient.DryRunStrategy = client.DryRunStrategy
instClient.DryRunOption = client.DryRunOption
instClient.DisableHooks = client.DisableHooks instClient.DisableHooks = client.DisableHooks
instClient.SkipCRDs = client.SkipCRDs instClient.SkipCRDs = client.SkipCRDs
instClient.Timeout = client.Timeout instClient.Timeout = client.Timeout
@ -183,10 +183,6 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
if err != nil { if err != nil {
return err return err
} }
// Validate dry-run flag value is one of the allowed values
if err := validateDryRunOptionFlag(client.DryRunOption); err != nil {
return err
}
p := getter.All(settings) p := getter.All(settings)
vals, err := valueOpts.MergeValues(p) vals, err := valueOpts.MergeValues(p)
@ -274,9 +270,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
f.BoolVar(&createNamespace, "create-namespace", false, "if --install is set, create the release namespace if not present") f.BoolVar(&createNamespace, "create-namespace", false, "if --install is set, create the release namespace if not present")
f.BoolVarP(&client.Install, "install", "i", false, "if a release by this name doesn't already exist, run an install") f.BoolVarP(&client.Install, "install", "i", false, "if a release by this name doesn't already exist, run an install")
f.BoolVar(&client.Devel, "devel", false, "use development versions, too. Equivalent to version '>0.0.0-0'. If --version is set, this is ignored") f.BoolVar(&client.Devel, "devel", false, "use development versions, too. Equivalent to version '>0.0.0-0'. If --version is set, this is ignored")
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.BoolVar(&client.HideSecret, "hide-secret", false, "hide Kubernetes Secrets when also using the --dry-run flag") f.BoolVar(&client.HideSecret, "hide-secret", false, "hide Kubernetes Secrets when also using the --dry-run flag")
f.Lookup("dry-run").NoOptDefVal = "client"
f.BoolVar(&client.ForceReplace, "force-replace", false, "force resource updates by replacement") f.BoolVar(&client.ForceReplace, "force-replace", false, "force resource updates by replacement")
f.BoolVar(&client.ForceReplace, "force", false, "deprecated") f.BoolVar(&client.ForceReplace, "force", false, "deprecated")
f.MarkDeprecated("force", "use --force-replace instead") f.MarkDeprecated("force", "use --force-replace instead")
@ -303,6 +297,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
f.BoolVar(&client.DependencyUpdate, "dependency-update", false, "update dependencies if they are missing before installing the chart") f.BoolVar(&client.DependencyUpdate, "dependency-update", false, "update dependencies if they are missing before installing the chart")
f.BoolVar(&client.EnableDNS, "enable-dns", false, "enable DNS lookups when rendering templates") f.BoolVar(&client.EnableDNS, "enable-dns", false, "enable DNS lookups when rendering templates")
f.BoolVar(&client.TakeOwnership, "take-ownership", false, "if set, upgrade will ignore the check for helm annotations and take ownership of the existing resources") f.BoolVar(&client.TakeOwnership, "take-ownership", false, "if set, upgrade will ignore the check for helm annotations and take ownership of the existing resources")
addDryRunFlag(cmd)
addChartPathOptionsFlags(f, &client.ChartPathOptions) addChartPathOptionsFlags(f, &client.ChartPathOptions)
addValueOptionsFlags(f, valueOpts) addValueOptionsFlags(f, valueOpts)
bindOutputFlag(cmd, &outfmt) bindOutputFlag(cmd, &outfmt)
@ -324,6 +319,11 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
return cmd return cmd
} }
func isReleaseUninstalled(versions []*release.Release) bool { func isReleaseUninstalled(versionsi []ri.Releaser) bool {
return len(versions) > 0 && versions[len(versions)-1].Info.Status == release.StatusUninstalled versions, err := releaseListToV1List(versionsi)
if err != nil {
slog.Error("cannot convert release list to v1 release list", "error", err)
return false
}
return len(versions) > 0 && versions[len(versions)-1].Info.Status == common.StatusUninstalled
} }

@ -28,6 +28,7 @@ import (
chart "helm.sh/helm/v4/pkg/chart/v2" chart "helm.sh/helm/v4/pkg/chart/v2"
"helm.sh/helm/v4/pkg/chart/v2/loader" "helm.sh/helm/v4/pkg/chart/v2/loader"
chartutil "helm.sh/helm/v4/pkg/chart/v2/util" chartutil "helm.sh/helm/v4/pkg/chart/v2/util"
rcommon "helm.sh/helm/v4/pkg/release/common"
release "helm.sh/helm/v4/pkg/release/v1" release "helm.sh/helm/v4/pkg/release/v1"
) )
@ -82,7 +83,7 @@ func TestUpgradeCmd(t *testing.T) {
badDepsPath := "testdata/testcharts/chart-bad-requirements" badDepsPath := "testdata/testcharts/chart-bad-requirements"
presentDepsPath := "testdata/testcharts/chart-with-subchart-update" presentDepsPath := "testdata/testcharts/chart-with-subchart-update"
relWithStatusMock := func(n string, v int, ch *chart.Chart, status release.Status) *release.Release { relWithStatusMock := func(n string, v int, ch *chart.Chart, status rcommon.Status) *release.Release {
return release.Mock(&release.MockReleaseOptions{Name: n, Version: v, Chart: ch, Status: status}) return release.Mock(&release.MockReleaseOptions{Name: n, Version: v, Chart: ch, Status: status})
} }
@ -173,20 +174,20 @@ func TestUpgradeCmd(t *testing.T) {
name: "upgrade a failed release", name: "upgrade a failed release",
cmd: fmt.Sprintf("upgrade funny-bunny '%s'", chartPath), cmd: fmt.Sprintf("upgrade funny-bunny '%s'", chartPath),
golden: "output/upgrade.txt", golden: "output/upgrade.txt",
rels: []*release.Release{relWithStatusMock("funny-bunny", 2, ch, release.StatusFailed)}, rels: []*release.Release{relWithStatusMock("funny-bunny", 2, ch, rcommon.StatusFailed)},
}, },
{ {
name: "upgrade a pending install release", name: "upgrade a pending install release",
cmd: fmt.Sprintf("upgrade funny-bunny '%s'", chartPath), cmd: fmt.Sprintf("upgrade funny-bunny '%s'", chartPath),
golden: "output/upgrade-with-pending-install.txt", golden: "output/upgrade-with-pending-install.txt",
wantError: true, wantError: true,
rels: []*release.Release{relWithStatusMock("funny-bunny", 2, ch, release.StatusPendingInstall)}, rels: []*release.Release{relWithStatusMock("funny-bunny", 2, ch, rcommon.StatusPendingInstall)},
}, },
{ {
name: "install a previously uninstalled release with '--keep-history' using 'upgrade --install'", name: "install a previously uninstalled release with '--keep-history' using 'upgrade --install'",
cmd: fmt.Sprintf("upgrade funny-bunny -i '%s'", chartPath), cmd: fmt.Sprintf("upgrade funny-bunny -i '%s'", chartPath),
golden: "output/upgrade-uninstalled-with-keep-history.txt", golden: "output/upgrade-uninstalled-with-keep-history.txt",
rels: []*release.Release{relWithStatusMock("funny-bunny", 2, ch, release.StatusUninstalled)}, rels: []*release.Release{relWithStatusMock("funny-bunny", 2, ch, rcommon.StatusUninstalled)},
}, },
} }
runTestCmd(t, tests) runTestCmd(t, tests)
@ -208,7 +209,11 @@ func TestUpgradeWithValue(t *testing.T) {
t.Errorf("unexpected error, got '%v'", err) t.Errorf("unexpected error, got '%v'", err)
} }
updatedRel, err := store.Get(releaseName, 4) updatedReli, err := store.Get(releaseName, 4)
if err != nil {
t.Errorf("unexpected error, got '%v'", err)
}
updatedRel, err := releaserToV1Release(updatedReli)
if err != nil { if err != nil {
t.Errorf("unexpected error, got '%v'", err) t.Errorf("unexpected error, got '%v'", err)
} }
@ -235,7 +240,11 @@ func TestUpgradeWithStringValue(t *testing.T) {
t.Errorf("unexpected error, got '%v'", err) t.Errorf("unexpected error, got '%v'", err)
} }
updatedRel, err := store.Get(releaseName, 4) updatedReli, err := store.Get(releaseName, 4)
if err != nil {
t.Errorf("unexpected error, got '%v'", err)
}
updatedRel, err := releaserToV1Release(updatedReli)
if err != nil { if err != nil {
t.Errorf("unexpected error, got '%v'", err) t.Errorf("unexpected error, got '%v'", err)
} }
@ -263,7 +272,11 @@ func TestUpgradeInstallWithSubchartNotes(t *testing.T) {
t.Errorf("unexpected error, got '%v'", err) t.Errorf("unexpected error, got '%v'", err)
} }
upgradedRel, err := store.Get(releaseName, 2) upgradedReli, err := store.Get(releaseName, 2)
if err != nil {
t.Errorf("unexpected error, got '%v'", err)
}
upgradedRel, err := releaserToV1Release(upgradedReli)
if err != nil { if err != nil {
t.Errorf("unexpected error, got '%v'", err) t.Errorf("unexpected error, got '%v'", err)
} }
@ -295,7 +308,11 @@ func TestUpgradeWithValuesFile(t *testing.T) {
t.Errorf("unexpected error, got '%v'", err) t.Errorf("unexpected error, got '%v'", err)
} }
updatedRel, err := store.Get(releaseName, 4) updatedReli, err := store.Get(releaseName, 4)
if err != nil {
t.Errorf("unexpected error, got '%v'", err)
}
updatedRel, err := releaserToV1Release(updatedReli)
if err != nil { if err != nil {
t.Errorf("unexpected error, got '%v'", err) t.Errorf("unexpected error, got '%v'", err)
} }
@ -328,7 +345,11 @@ func TestUpgradeWithValuesFromStdin(t *testing.T) {
t.Errorf("unexpected error, got '%v'", err) t.Errorf("unexpected error, got '%v'", err)
} }
updatedRel, err := store.Get(releaseName, 4) updatedReli, err := store.Get(releaseName, 4)
if err != nil {
t.Errorf("unexpected error, got '%v'", err)
}
updatedRel, err := releaserToV1Release(updatedReli)
if err != nil { if err != nil {
t.Errorf("unexpected error, got '%v'", err) t.Errorf("unexpected error, got '%v'", err)
} }
@ -358,7 +379,11 @@ func TestUpgradeInstallWithValuesFromStdin(t *testing.T) {
t.Errorf("unexpected error, got '%v'", err) t.Errorf("unexpected error, got '%v'", err)
} }
updatedRel, err := store.Get(releaseName, 1) updatedReli, err := store.Get(releaseName, 1)
if err != nil {
t.Errorf("unexpected error, got '%v'", err)
}
updatedRel, err := releaserToV1Release(updatedReli)
if err != nil { if err != nil {
t.Errorf("unexpected error, got '%v'", err) t.Errorf("unexpected error, got '%v'", err)
} }
@ -463,7 +488,11 @@ func TestUpgradeInstallWithLabels(t *testing.T) {
t.Errorf("unexpected error, got '%v'", err) t.Errorf("unexpected error, got '%v'", err)
} }
updatedRel, err := store.Get(releaseName, 1) updatedReli, err := store.Get(releaseName, 1)
if err != nil {
t.Errorf("unexpected error, got '%v'", err)
}
updatedRel, err := releaserToV1Release(updatedReli)
if err != nil { if err != nil {
t.Errorf("unexpected error, got '%v'", err) t.Errorf("unexpected error, got '%v'", err)
} }

@ -86,6 +86,8 @@ type Client struct {
kubeClient kubernetes.Interface kubeClient kubernetes.Interface
} }
var _ Interface = (*Client)(nil)
type WaitStrategy string type WaitStrategy string
const ( const (
@ -271,10 +273,6 @@ func (c *Client) Create(resources ResourceList, options ...ClientCreateOption) (
return nil, fmt.Errorf("invalid client create option(s): %w", err) return nil, fmt.Errorf("invalid client create option(s): %w", err)
} }
if createOptions.forceConflicts && !createOptions.serverSideApply {
return nil, fmt.Errorf("invalid operation: force conflicts can only be used with server-side apply")
}
makeCreateApplyFunc := func() func(target *resource.Info) error { makeCreateApplyFunc := func() func(target *resource.Info) error {
if createOptions.serverSideApply { if createOptions.serverSideApply {
slog.Debug("using server-side apply for resource creation", slog.Bool("forceConflicts", createOptions.forceConflicts), slog.Bool("dryRun", createOptions.dryRun), slog.String("fieldValidationDirective", string(createOptions.fieldValidationDirective))) slog.Debug("using server-side apply for resource creation", slog.Bool("forceConflicts", createOptions.forceConflicts), slog.Bool("dryRun", createOptions.dryRun), slog.String("fieldValidationDirective", string(createOptions.fieldValidationDirective)))
@ -769,29 +767,17 @@ func (c *Client) Update(originals, targets ResourceList, options ...ClientUpdate
return c.update(originals, targets, makeUpdateApplyFunc()) return c.update(originals, targets, makeUpdateApplyFunc())
} }
// Delete deletes Kubernetes resources specified in the resources list with
// background cascade deletion. It will attempt to delete all resources even
// if one or more fail and collect any errors. All successfully deleted items
// will be returned in the `Deleted` ResourceList that is part of the result.
func (c *Client) Delete(resources ResourceList) (*Result, []error) {
return deleteResources(resources, metav1.DeletePropagationBackground)
}
// Delete deletes Kubernetes resources specified in the resources list with // Delete deletes Kubernetes resources specified in the resources list with
// given deletion propagation policy. It will attempt to delete all resources even // given deletion propagation policy. It will attempt to delete all resources even
// if one or more fail and collect any errors. All successfully deleted items // if one or more fail and collect any errors. All successfully deleted items
// will be returned in the `Deleted` ResourceList that is part of the result. // will be returned in the `Deleted` ResourceList that is part of the result.
func (c *Client) DeleteWithPropagationPolicy(resources ResourceList, policy metav1.DeletionPropagation) (*Result, []error) { func (c *Client) Delete(resources ResourceList, policy metav1.DeletionPropagation) (*Result, []error) {
return deleteResources(resources, policy)
}
func deleteResources(resources ResourceList, propagation metav1.DeletionPropagation) (*Result, []error) {
var errs []error var errs []error
res := &Result{} res := &Result{}
mtx := sync.Mutex{} mtx := sync.Mutex{}
err := perform(resources, func(target *resource.Info) error { err := perform(resources, func(target *resource.Info) error {
slog.Debug("starting delete resource", "namespace", target.Namespace, "name", target.Name, "kind", target.Mapping.GroupVersionKind.Kind) slog.Debug("starting delete resource", "namespace", target.Namespace, "name", target.Name, "kind", target.Mapping.GroupVersionKind.Kind)
err := deleteResource(target, propagation) err := deleteResource(target, policy)
if err == nil || apierrors.IsNotFound(err) { if err == nil || apierrors.IsNotFound(err) {
if err != nil { if err != nil {
slog.Debug("ignoring delete failure", "namespace", target.Namespace, "name", target.Name, "kind", target.Mapping.GroupVersionKind.Kind, slog.Any("error", err)) slog.Debug("ignoring delete failure", "namespace", target.Namespace, "name", target.Name, "kind", target.Mapping.GroupVersionKind.Kind, slog.Any("error", err))
@ -1031,7 +1017,7 @@ func patchResourceClientSide(original runtime.Object, target *resource.Info, thr
} }
// upgradeClientSideFieldManager is simply a wrapper around csaupgrade.UpgradeManagedFields // upgradeClientSideFieldManager is simply a wrapper around csaupgrade.UpgradeManagedFields
// that ugrade CSA managed fields to SSA apply // that upgrade CSA managed fields to SSA apply
// see: https://github.com/kubernetes/kubernetes/pull/112905 // see: https://github.com/kubernetes/kubernetes/pull/112905
func upgradeClientSideFieldManager(info *resource.Info, dryRun bool, fieldValidationDirective FieldValidationDirective) (bool, error) { func upgradeClientSideFieldManager(info *resource.Info, dryRun bool, fieldValidationDirective FieldValidationDirective) (bool, error) {

@ -142,7 +142,7 @@ func NewRequestResponseLogClient(t *testing.T, cb RoundTripperTestFunc) RequestR
} }
// RequestResponseLogClient is a test client that logs requests and responses // RequestResponseLogClient is a test client that logs requests and responses
// Satifying http.RoundTripper interface, it can be used to mock HTTP requests in tests. // Satisfying http.RoundTripper interface, it can be used to mock HTTP requests in tests.
// Forwarding requests to a callback function (cb) that can be used to simulate server responses. // Forwarding requests to a callback function (cb) that can be used to simulate server responses.
type RequestResponseLogClient struct { type RequestResponseLogClient struct {
t *testing.T t *testing.T
@ -816,7 +816,7 @@ func TestWaitDelete(t *testing.T) {
if len(result.Created) != 1 { if len(result.Created) != 1 {
t.Errorf("expected 1 resource created, got %d", len(result.Created)) t.Errorf("expected 1 resource created, got %d", len(result.Created))
} }
if _, err := c.Delete(resources); err != nil { if _, err := c.Delete(resources, metav1.DeletePropagationBackground); err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -855,7 +855,7 @@ func TestReal(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
if _, errs := c.Delete(resources); errs != nil { if _, errs := c.Delete(resources, metav1.DeletePropagationBackground); errs != nil {
t.Fatal(errs) t.Fatal(errs)
} }
@ -864,7 +864,7 @@ func TestReal(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
// ensures that delete does not fail if a resource is not found // ensures that delete does not fail if a resource is not found
if _, errs := c.Delete(resources); errs != nil { if _, errs := c.Delete(resources, metav1.DeletePropagationBackground); errs != nil {
t.Fatal(errs) t.Fatal(errs)
} }
} }

@ -33,22 +33,23 @@ import (
// delegates all its calls to `PrintingKubeClient` // delegates all its calls to `PrintingKubeClient`
type FailingKubeClient struct { type FailingKubeClient struct {
PrintingKubeClient PrintingKubeClient
CreateError error CreateError error
GetError error GetError error
DeleteError error DeleteError error
DeleteWithPropagationError error UpdateError error
UpdateError error BuildError error
BuildError error BuildTableError error
BuildTableError error ConnectionError error
ConnectionError error BuildDummy bool
BuildDummy bool DummyResources kube.ResourceList
DummyResources kube.ResourceList BuildUnstructuredError error
BuildUnstructuredError error WaitError error
WaitError error WaitForDeleteError error
WaitForDeleteError error WatchUntilReadyError error
WatchUntilReadyError error WaitDuration time.Duration
WaitDuration time.Duration }
}
var _ kube.Interface = &FailingKubeClient{}
// FailingKubeWaiter implements kube.Waiter for testing purposes. // FailingKubeWaiter implements kube.Waiter for testing purposes.
// It also has additional errors you can set to fail different functions, otherwise it delegates all its calls to `PrintingKubeWaiter` // It also has additional errors you can set to fail different functions, otherwise it delegates all its calls to `PrintingKubeWaiter`
@ -102,11 +103,12 @@ func (f *FailingKubeWaiter) WaitForDelete(resources kube.ResourceList, d time.Du
} }
// Delete returns the configured error if set or prints // Delete returns the configured error if set or prints
func (f *FailingKubeClient) Delete(resources kube.ResourceList) (*kube.Result, []error) { func (f *FailingKubeClient) Delete(resources kube.ResourceList, deletionPropagation metav1.DeletionPropagation) (*kube.Result, []error) {
if f.DeleteError != nil { if f.DeleteError != nil {
return nil, []error{f.DeleteError} return nil, []error{f.DeleteError}
} }
return f.PrintingKubeClient.Delete(resources)
return f.PrintingKubeClient.Delete(resources, deletionPropagation)
} }
// WatchUntilReady returns the configured error if set or prints // WatchUntilReady returns the configured error if set or prints
@ -147,14 +149,6 @@ func (f *FailingKubeClient) BuildTable(r io.Reader, _ bool) (kube.ResourceList,
return f.PrintingKubeClient.BuildTable(r, false) return f.PrintingKubeClient.BuildTable(r, false)
} }
// DeleteWithPropagationPolicy returns the configured error if set or prints
func (f *FailingKubeClient) DeleteWithPropagationPolicy(resources kube.ResourceList, policy metav1.DeletionPropagation) (*kube.Result, []error) {
if f.DeleteWithPropagationError != nil {
return nil, []error{f.DeleteWithPropagationError}
}
return f.PrintingKubeClient.DeleteWithPropagationPolicy(resources, policy)
}
func (f *FailingKubeClient) GetWaiter(ws kube.WaitStrategy) (kube.Waiter, error) { func (f *FailingKubeClient) GetWaiter(ws kube.WaitStrategy) (kube.Waiter, error) {
waiter, _ := f.PrintingKubeClient.GetWaiter(ws) waiter, _ := f.PrintingKubeClient.GetWaiter(ws)
printingKubeWaiter, _ := waiter.(*PrintingKubeWaiter) printingKubeWaiter, _ := waiter.(*PrintingKubeWaiter)

@ -43,6 +43,8 @@ type PrintingKubeWaiter struct {
LogOutput io.Writer LogOutput io.Writer
} }
var _ kube.Interface = &PrintingKubeClient{}
// IsReachable checks if the cluster is reachable // IsReachable checks if the cluster is reachable
func (p *PrintingKubeClient) IsReachable() error { func (p *PrintingKubeClient) IsReachable() error {
return nil return nil
@ -89,7 +91,7 @@ func (p *PrintingKubeWaiter) WatchUntilReady(resources kube.ResourceList, _ time
// Delete implements KubeClient delete. // Delete implements KubeClient delete.
// //
// It only prints out the content to be deleted. // It only prints out the content to be deleted.
func (p *PrintingKubeClient) Delete(resources kube.ResourceList) (*kube.Result, []error) { func (p *PrintingKubeClient) Delete(resources kube.ResourceList, _ metav1.DeletionPropagation) (*kube.Result, []error) {
_, err := io.Copy(p.Out, bufferize(resources)) _, err := io.Copy(p.Out, bufferize(resources))
if err != nil { if err != nil {
return nil, []error{err} return nil, []error{err}

@ -29,11 +29,18 @@ import (
// //
// A KubernetesClient must be concurrency safe. // A KubernetesClient must be concurrency safe.
type Interface interface { type Interface interface {
// Get details of deployed resources.
// The first argument is a list of resources to get. The second argument
// specifies if related pods should be fetched. For example, the pods being
// managed by a deployment.
Get(resources ResourceList, related bool) (map[string][]runtime.Object, error)
// Create creates one or more resources. // Create creates one or more resources.
Create(resources ResourceList, options ...ClientCreateOption) (*Result, error) Create(resources ResourceList, options ...ClientCreateOption) (*Result, error)
// Delete destroys one or more resources. // Delete destroys one or more resources using the specified deletion propagation policy.
Delete(resources ResourceList) (*Result, []error) // The 'policy' parameter determines how child resources are handled during deletion.
Delete(resources ResourceList, policy metav1.DeletionPropagation) (*Result, []error)
// Update updates one or more resources or creates the resource // Update updates one or more resources or creates the resource
// if it doesn't exist. // if it doesn't exist.
@ -51,6 +58,23 @@ type Interface interface {
// Get Waiter gets the Kube.Waiter // Get Waiter gets the Kube.Waiter
GetWaiter(ws WaitStrategy) (Waiter, error) GetWaiter(ws WaitStrategy) (Waiter, error)
// GetPodList lists all pods that match the specified listOptions
GetPodList(namespace string, listOptions metav1.ListOptions) (*v1.PodList, error)
// OutputContainerLogsForPodList outputs the logs for a pod list
OutputContainerLogsForPodList(podList *v1.PodList, namespace string, writerFunc func(namespace, pod, container string) io.Writer) error
// BuildTable creates a resource list from a Reader. This differs from
// Interface.Build() in that a table kind is returned. A table is useful
// if you want to use a printer to display the information.
//
// Reader must contain a YAML stream (one or more YAML documents separated
// by "\n---\n")
//
// Validates against OpenAPI schema if validate is true.
// TODO Helm 4: Integrate into Build with an argument
BuildTable(reader io.Reader, validate bool) (ResourceList, error)
} }
// Waiter defines methods related to waiting for resource states. // Waiter defines methods related to waiting for resource states.
@ -75,49 +99,3 @@ type Waiter interface {
// error. // error.
WatchUntilReady(resources ResourceList, timeout time.Duration) error WatchUntilReady(resources ResourceList, timeout time.Duration) error
} }
// InterfaceLogs was introduced to avoid breaking backwards compatibility for Interface implementers.
//
// TODO Helm 4: Remove InterfaceLogs and integrate its method(s) into the Interface.
type InterfaceLogs interface {
// GetPodList list all pods that match the specified listOptions
GetPodList(namespace string, listOptions metav1.ListOptions) (*v1.PodList, error)
// OutputContainerLogsForPodList output the logs for a pod list
OutputContainerLogsForPodList(podList *v1.PodList, namespace string, writerFunc func(namespace, pod, container string) io.Writer) error
}
// InterfaceDeletionPropagation is introduced to avoid breaking backwards compatibility for Interface implementers.
//
// TODO Helm 4: Remove InterfaceDeletionPropagation and integrate its method(s) into the Interface.
type InterfaceDeletionPropagation interface {
// DeleteWithPropagationPolicy destroys one or more resources. The deletion propagation is handled as per the given deletion propagation value.
DeleteWithPropagationPolicy(resources ResourceList, policy metav1.DeletionPropagation) (*Result, []error)
}
// InterfaceResources is introduced to avoid breaking backwards compatibility for Interface implementers.
//
// TODO Helm 4: Remove InterfaceResources and integrate its method(s) into the Interface.
type InterfaceResources interface {
// Get details of deployed resources.
// The first argument is a list of resources to get. The second argument
// specifies if related pods should be fetched. For example, the pods being
// managed by a deployment.
Get(resources ResourceList, related bool) (map[string][]runtime.Object, error)
// BuildTable creates a resource list from a Reader. This differs from
// Interface.Build() in that a table kind is returned. A table is useful
// if you want to use a printer to display the information.
//
// Reader must contain a YAML stream (one or more YAML documents separated
// by "\n---\n")
//
// Validates against OpenAPI schema if validate is true.
// TODO Helm 4: Integrate into Build with an argument
BuildTable(reader io.Reader, validate bool) (ResourceList, error)
}
var _ Interface = (*Client)(nil)
var _ InterfaceLogs = (*Client)(nil)
var _ InterfaceDeletionPropagation = (*Client)(nil)
var _ InterfaceResources = (*Client)(nil)

@ -25,9 +25,9 @@ import (
"os" "os"
"strings" "strings"
"golang.org/x/crypto/openpgp" //nolint "github.com/ProtonMail/go-crypto/openpgp" //nolint
"golang.org/x/crypto/openpgp/clearsign" //nolint "github.com/ProtonMail/go-crypto/openpgp/clearsign" //nolint
"golang.org/x/crypto/openpgp/packet" //nolint "github.com/ProtonMail/go-crypto/openpgp/packet" //nolint
"sigs.k8s.io/yaml" "sigs.k8s.io/yaml"
) )
@ -281,8 +281,9 @@ func (s *Signatory) Verify(archiveData, provData []byte, filename string) (*Veri
func (s *Signatory) verifySignature(block *clearsign.Block) (*openpgp.Entity, error) { func (s *Signatory) verifySignature(block *clearsign.Block) (*openpgp.Entity, error) {
return openpgp.CheckDetachedSignature( return openpgp.CheckDetachedSignature(
s.KeyRing, s.KeyRing,
bytes.NewBuffer(block.Bytes), bytes.NewReader(block.Bytes),
block.ArmoredSignature.Body, block.ArmoredSignature.Body,
&defaultPGPConfig,
) )
} }

@ -24,7 +24,10 @@ import (
"strings" "strings"
"testing" "testing"
pgperrors "golang.org/x/crypto/openpgp/errors" //nolint pgperrors "github.com/ProtonMail/go-crypto/openpgp/errors" //nolint
"github.com/ProtonMail/go-crypto/openpgp/packet" //nolint
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"sigs.k8s.io/yaml" "sigs.k8s.io/yaml"
"helm.sh/helm/v4/pkg/chart/v2/loader" "helm.sh/helm/v4/pkg/chart/v2/loader"
@ -59,6 +62,9 @@ const (
// testTamperedSigBlock is a tampered copy of msgblock.yaml.asc // testTamperedSigBlock is a tampered copy of msgblock.yaml.asc
testTamperedSigBlock = "testdata/msgblock.yaml.tampered" testTamperedSigBlock = "testdata/msgblock.yaml.tampered"
// testMixedKeyring points to a keyring containing RSA and ed25519 keys.
testMixedKeyring = "testdata/helm-mixed-keyring.pub"
// testSumfile points to a SHA256 sum generated by an external tool. // testSumfile points to a SHA256 sum generated by an external tool.
// We always want to validate against an external tool's representation to // We always want to validate against an external tool's representation to
// verify that we haven't done something stupid. This file was generated // verify that we haven't done something stupid. This file was generated
@ -266,6 +272,56 @@ func TestClearSign(t *testing.T) {
} }
} }
func TestMixedKeyringRSASigningAndVerification(t *testing.T) {
signer, err := NewFromFiles(testKeyfile, testMixedKeyring)
require.NoError(t, err)
require.NotEmpty(t, signer.KeyRing, "expected signer keyring to be loaded")
hasEdDSA := false
for _, entity := range signer.KeyRing {
if entity.PrimaryKey != nil && entity.PrimaryKey.PubKeyAlgo == packet.PubKeyAlgoEdDSA {
hasEdDSA = true
break
}
for _, subkey := range entity.Subkeys {
if subkey.PublicKey != nil && subkey.PublicKey.PubKeyAlgo == packet.PubKeyAlgoEdDSA {
hasEdDSA = true
break
}
}
if hasEdDSA {
break
}
}
assert.True(t, hasEdDSA, "expected %s to include an Ed25519 public key", testMixedKeyring)
require.NotNil(t, signer.Entity, "expected signer entity to be loaded")
require.NotNil(t, signer.Entity.PrivateKey, "expected signer private key to be loaded")
assert.Equal(t, packet.PubKeyAlgoRSA, signer.Entity.PrivateKey.PubKeyAlgo, "expected RSA key")
metadataBytes := loadChartMetadataForSigning(t, testChartfile)
archiveData, err := os.ReadFile(testChartfile)
require.NoError(t, err)
sig, err := signer.ClearSign(archiveData, filepath.Base(testChartfile), metadataBytes)
require.NoError(t, err, "failed to sign chart")
verification, err := signer.Verify(archiveData, []byte(sig), filepath.Base(testChartfile))
require.NoError(t, err, "failed to verify chart signature")
require.NotNil(t, verification.SignedBy, "expected verification to include signer")
require.NotNil(t, verification.SignedBy.PrimaryKey, "expected verification to include signer primary key")
assert.Equal(t, packet.PubKeyAlgoRSA, verification.SignedBy.PrimaryKey.PubKeyAlgo, "expected verification to report RSA key")
_, ok := verification.SignedBy.Identities[testKeyName]
assert.True(t, ok, "expected verification to be signed by %q", testKeyName)
}
// failSigner always fails to sign and returns an error // failSigner always fails to sign and returns an error
type failSigner struct{} type failSigner struct{}

Binary file not shown.

@ -0,0 +1,116 @@
/*
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 release
import (
"errors"
"fmt"
"time"
"helm.sh/helm/v4/pkg/chart"
v1release "helm.sh/helm/v4/pkg/release/v1"
)
var NewAccessor func(rel Releaser) (Accessor, error) = newDefaultAccessor //nolint:revive
var NewHookAccessor func(rel Hook) (HookAccessor, error) = newDefaultHookAccessor //nolint:revive
func newDefaultAccessor(rel Releaser) (Accessor, error) {
switch v := rel.(type) {
case v1release.Release:
return &v1Accessor{&v}, nil
case *v1release.Release:
return &v1Accessor{v}, nil
default:
return nil, fmt.Errorf("unsupported release type: %T", rel)
}
}
func newDefaultHookAccessor(hook Hook) (HookAccessor, error) {
switch h := hook.(type) {
case v1release.Hook:
return &v1HookAccessor{&h}, nil
case *v1release.Hook:
return &v1HookAccessor{h}, nil
default:
return nil, errors.New("unsupported release hook type")
}
}
type v1Accessor struct {
rel *v1release.Release
}
func (a *v1Accessor) Name() string {
return a.rel.Name
}
func (a *v1Accessor) Namespace() string {
return a.rel.Namespace
}
func (a *v1Accessor) Version() int {
return a.rel.Version
}
func (a *v1Accessor) Hooks() []Hook {
var hooks = make([]Hook, len(a.rel.Hooks))
for i, h := range a.rel.Hooks {
hooks[i] = h
}
return hooks
}
func (a *v1Accessor) Manifest() string {
return a.rel.Manifest
}
func (a *v1Accessor) Notes() string {
return a.rel.Info.Notes
}
func (a *v1Accessor) Labels() map[string]string {
return a.rel.Labels
}
func (a *v1Accessor) Chart() chart.Charter {
return a.rel.Chart
}
func (a *v1Accessor) Status() string {
return a.rel.Info.Status.String()
}
func (a *v1Accessor) ApplyMethod() string {
return a.rel.ApplyMethod
}
func (a *v1Accessor) DeployedAt() time.Time {
return a.rel.Info.LastDeployed
}
type v1HookAccessor struct {
hook *v1release.Hook
}
func (a *v1HookAccessor) Path() string {
return a.hook.Path
}
func (a *v1HookAccessor) Manifest() string {
return a.hook.Manifest
}

@ -13,7 +13,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package v1 package common
// Status is the status of a release // Status is the status of a release
type Status string type Status string

@ -0,0 +1,65 @@
/*
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 release
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"helm.sh/helm/v4/pkg/release/common"
rspb "helm.sh/helm/v4/pkg/release/v1"
)
func TestNewDefaultAccessor(t *testing.T) {
// Testing the default implementation rather than NewAccessor which can be
// overridden by developers.
is := assert.New(t)
// Create release
info := &rspb.Info{Status: common.StatusDeployed, LastDeployed: time.Now().Add(1000)}
labels := make(map[string]string)
labels["foo"] = "bar"
rel := &rspb.Release{
Name: "happy-cats",
Version: 2,
Info: info,
Labels: labels,
Namespace: "default",
ApplyMethod: "csa",
}
// newDefaultAccessor should not be called directly Instead, NewAccessor should be
// called and it will call NewDefaultAccessor. NewAccessor can be changed to a
// non-default accessor by a user so the test calls the default implementation.
// The accessor provides a means to access data on resources that are different types
// but have the same interface. Instead of properties, methods are used to access
// information. Structs with properties are useful in Go when it comes to marshalling
// and unmarshalling data (e.g. coming and going from JSON or YAML). But, structs
// can't be used with interfaces. The accessors enable access to the underlying data
// in a manner that works with Go interfaces.
accessor, err := newDefaultAccessor(rel)
is.NoError(err)
// Verify information
is.Equal(rel.Name, accessor.Name())
is.Equal(rel.Namespace, accessor.Namespace())
is.Equal(rel.Version, accessor.Version())
is.Equal(rel.ApplyMethod, accessor.ApplyMethod())
is.Equal(rel.Labels, accessor.Labels())
}

@ -0,0 +1,46 @@
/*
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 release
import (
"time"
"helm.sh/helm/v4/pkg/chart"
)
type Releaser interface{}
type Hook interface{}
type Accessor interface {
Name() string
Namespace() string
Version() int
Hooks() []Hook
Manifest() string
Notes() string
Labels() map[string]string
Chart() chart.Charter
Status() string
ApplyMethod() string
DeployedAt() time.Time
}
type HookAccessor interface {
Path() string
Manifest() string
}

@ -13,12 +13,12 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package v1 package release
// UninstallReleaseResponse represents a successful response to an uninstall request. // UninstallReleaseResponse represents a successful response to an uninstall request.
type UninstallReleaseResponse struct { type UninstallReleaseResponse struct {
// Release is the release that was marked deleted. // Release is the release that was marked deleted.
Release *Release `json:"release,omitempty"` Release Releaser `json:"release,omitempty"`
// Info is an uninstall message // Info is an uninstall message
Info string `json:"info,omitempty"` Info string `json:"info,omitempty"`
} }

@ -18,6 +18,8 @@ package v1
import ( import (
"time" "time"
"helm.sh/helm/v4/pkg/release/common"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
) )
@ -32,7 +34,7 @@ type Info struct {
// Description is human-friendly "log entry" about this release. // Description is human-friendly "log entry" about this release.
Description string `json:"description,omitempty"` Description string `json:"description,omitempty"`
// Status is the current state of the release // Status is the current state of the release
Status Status `json:"status,omitempty"` Status common.Status `json:"status,omitempty"`
// Contains the rendered templates/NOTES.txt if available // Contains the rendered templates/NOTES.txt if available
Notes string `json:"notes,omitempty"` Notes string `json:"notes,omitempty"`
// Contains the deployed resources information // Contains the deployed resources information

@ -23,6 +23,7 @@ import (
"helm.sh/helm/v4/pkg/chart/common" "helm.sh/helm/v4/pkg/chart/common"
chart "helm.sh/helm/v4/pkg/chart/v2" chart "helm.sh/helm/v4/pkg/chart/v2"
rcommon "helm.sh/helm/v4/pkg/release/common"
) )
// MockHookTemplate is the hook template used for all mock release objects. // MockHookTemplate is the hook template used for all mock release objects.
@ -45,7 +46,7 @@ type MockReleaseOptions struct {
Name string Name string
Version int Version int
Chart *chart.Chart Chart *chart.Chart
Status Status Status rcommon.Status
Namespace string Namespace string
Labels map[string]string Labels map[string]string
} }
@ -105,7 +106,7 @@ func Mock(opts *MockReleaseOptions) *Release {
} }
} }
scode := StatusDeployed scode := rcommon.StatusDeployed
if len(opts.Status) > 0 { if len(opts.Status) > 0 {
scode = opts.Status scode = opts.Status
} }

@ -17,6 +17,7 @@ package v1
import ( import (
chart "helm.sh/helm/v4/pkg/chart/v2" chart "helm.sh/helm/v4/pkg/chart/v2"
"helm.sh/helm/v4/pkg/release/common"
) )
type ApplyMethod string type ApplyMethod string
@ -53,7 +54,7 @@ type Release struct {
} }
// SetStatus is a helper for setting the status on a release. // SetStatus is a helper for setting the status on a release.
func (r *Release) SetStatus(status Status, msg string) { func (r *Release) SetStatus(status common.Status, msg string) {
r.Info.Status = status r.Info.Status = status
r.Info.Description = msg r.Info.Description = msg
} }

@ -16,7 +16,10 @@ limitations under the License.
package util // import "helm.sh/helm/v4/pkg/release/v1/util" package util // import "helm.sh/helm/v4/pkg/release/v1/util"
import rspb "helm.sh/helm/v4/pkg/release/v1" import (
"helm.sh/helm/v4/pkg/release/common"
rspb "helm.sh/helm/v4/pkg/release/v1"
)
// FilterFunc returns true if the release object satisfies // FilterFunc returns true if the release object satisfies
// the predicate of the underlying filter func. // the predicate of the underlying filter func.
@ -68,7 +71,7 @@ func All(filters ...FilterFunc) FilterFunc {
} }
// StatusFilter filters a set of releases by status code. // StatusFilter filters a set of releases by status code.
func StatusFilter(status rspb.Status) FilterFunc { func StatusFilter(status common.Status) FilterFunc {
return FilterFunc(func(rls *rspb.Release) bool { return FilterFunc(func(rls *rspb.Release) bool {
if rls == nil { if rls == nil {
return true return true

@ -19,20 +19,21 @@ package util // import "helm.sh/helm/v4/pkg/release/v1/util"
import ( import (
"testing" "testing"
"helm.sh/helm/v4/pkg/release/common"
rspb "helm.sh/helm/v4/pkg/release/v1" rspb "helm.sh/helm/v4/pkg/release/v1"
) )
func TestFilterAny(t *testing.T) { func TestFilterAny(t *testing.T) {
ls := Any(StatusFilter(rspb.StatusUninstalled)).Filter(releases) ls := Any(StatusFilter(common.StatusUninstalled)).Filter(releases)
if len(ls) != 2 { if len(ls) != 2 {
t.Fatalf("expected 2 results, got '%d'", len(ls)) t.Fatalf("expected 2 results, got '%d'", len(ls))
} }
r0, r1 := ls[0], ls[1] r0, r1 := ls[0], ls[1]
switch { switch {
case r0.Info.Status != rspb.StatusUninstalled: case r0.Info.Status != common.StatusUninstalled:
t.Fatalf("expected UNINSTALLED result, got '%s'", r1.Info.Status.String()) t.Fatalf("expected UNINSTALLED result, got '%s'", r1.Info.Status.String())
case r1.Info.Status != rspb.StatusUninstalled: case r1.Info.Status != common.StatusUninstalled:
t.Fatalf("expected UNINSTALLED result, got '%s'", r1.Info.Status.String()) t.Fatalf("expected UNINSTALLED result, got '%s'", r1.Info.Status.String())
} }
} }
@ -40,7 +41,7 @@ func TestFilterAny(t *testing.T) {
func TestFilterAll(t *testing.T) { func TestFilterAll(t *testing.T) {
fn := FilterFunc(func(rls *rspb.Release) bool { fn := FilterFunc(func(rls *rspb.Release) bool {
// true if not uninstalled and version < 4 // true if not uninstalled and version < 4
v0 := !StatusFilter(rspb.StatusUninstalled).Check(rls) v0 := !StatusFilter(common.StatusUninstalled).Check(rls)
v1 := rls.Version < 4 v1 := rls.Version < 4
return v0 && v1 return v0 && v1
}) })
@ -53,7 +54,7 @@ func TestFilterAll(t *testing.T) {
switch r0 := ls[0]; { switch r0 := ls[0]; {
case r0.Version == 4: case r0.Version == 4:
t.Fatal("got release with status revision 4") t.Fatal("got release with status revision 4")
case r0.Info.Status == rspb.StatusUninstalled: case r0.Info.Status == common.StatusUninstalled:
t.Fatal("got release with status UNINSTALLED") t.Fatal("got release with status UNINSTALLED")
} }
} }

@ -20,19 +20,20 @@ import (
"testing" "testing"
"time" "time"
"helm.sh/helm/v4/pkg/release/common"
rspb "helm.sh/helm/v4/pkg/release/v1" rspb "helm.sh/helm/v4/pkg/release/v1"
) )
// note: this test data is shared with filter_test.go. // note: this test data is shared with filter_test.go.
var releases = []*rspb.Release{ var releases = []*rspb.Release{
tsRelease("quiet-bear", 2, 2000, rspb.StatusSuperseded), tsRelease("quiet-bear", 2, 2000, common.StatusSuperseded),
tsRelease("angry-bird", 4, 3000, rspb.StatusDeployed), tsRelease("angry-bird", 4, 3000, common.StatusDeployed),
tsRelease("happy-cats", 1, 4000, rspb.StatusUninstalled), tsRelease("happy-cats", 1, 4000, common.StatusUninstalled),
tsRelease("vocal-dogs", 3, 6000, rspb.StatusUninstalled), tsRelease("vocal-dogs", 3, 6000, common.StatusUninstalled),
} }
func tsRelease(name string, vers int, dur time.Duration, status rspb.Status) *rspb.Release { func tsRelease(name string, vers int, dur time.Duration, status common.Status) *rspb.Release {
info := &rspb.Info{Status: status, LastDeployed: time.Now().Add(dur)} info := &rspb.Info{Status: status, LastDeployed: time.Now().Add(dur)}
return &rspb.Release{ return &rspb.Release{
Name: name, Name: name,

@ -31,6 +31,7 @@ import (
"k8s.io/apimachinery/pkg/util/validation" "k8s.io/apimachinery/pkg/util/validation"
corev1 "k8s.io/client-go/kubernetes/typed/core/v1" corev1 "k8s.io/client-go/kubernetes/typed/core/v1"
"helm.sh/helm/v4/pkg/release"
rspb "helm.sh/helm/v4/pkg/release/v1" rspb "helm.sh/helm/v4/pkg/release/v1"
) )
@ -60,7 +61,7 @@ func (cfgmaps *ConfigMaps) Name() string {
// Get fetches the release named by key. The corresponding release is returned // Get fetches the release named by key. The corresponding release is returned
// or error if not found. // or error if not found.
func (cfgmaps *ConfigMaps) Get(key string) (*rspb.Release, error) { func (cfgmaps *ConfigMaps) Get(key string) (release.Releaser, error) {
// fetch the configmap holding the release named by key // fetch the configmap holding the release named by key
obj, err := cfgmaps.impl.Get(context.Background(), key, metav1.GetOptions{}) obj, err := cfgmaps.impl.Get(context.Background(), key, metav1.GetOptions{})
if err != nil { if err != nil {
@ -85,7 +86,7 @@ func (cfgmaps *ConfigMaps) Get(key string) (*rspb.Release, error) {
// List fetches all releases and returns the list releases such // List fetches all releases and returns the list releases such
// that filter(release) == true. An error is returned if the // that filter(release) == true. An error is returned if the
// configmap fails to retrieve the releases. // configmap fails to retrieve the releases.
func (cfgmaps *ConfigMaps) List(filter func(*rspb.Release) bool) ([]*rspb.Release, error) { func (cfgmaps *ConfigMaps) List(filter func(release.Releaser) bool) ([]release.Releaser, error) {
lsel := kblabels.Set{"owner": "helm"}.AsSelector() lsel := kblabels.Set{"owner": "helm"}.AsSelector()
opts := metav1.ListOptions{LabelSelector: lsel.String()} opts := metav1.ListOptions{LabelSelector: lsel.String()}
@ -95,7 +96,7 @@ func (cfgmaps *ConfigMaps) List(filter func(*rspb.Release) bool) ([]*rspb.Releas
return nil, err return nil, err
} }
var results []*rspb.Release var results []release.Releaser
// iterate over the configmaps object list // iterate over the configmaps object list
// and decode each release // and decode each release
@ -117,7 +118,7 @@ func (cfgmaps *ConfigMaps) List(filter func(*rspb.Release) bool) ([]*rspb.Releas
// Query fetches all releases that match the provided map of labels. // Query fetches all releases that match the provided map of labels.
// An error is returned if the configmap fails to retrieve the releases. // An error is returned if the configmap fails to retrieve the releases.
func (cfgmaps *ConfigMaps) Query(labels map[string]string) ([]*rspb.Release, error) { func (cfgmaps *ConfigMaps) Query(labels map[string]string) ([]release.Releaser, error) {
ls := kblabels.Set{} ls := kblabels.Set{}
for k, v := range labels { for k, v := range labels {
if errs := validation.IsValidLabelValue(v); len(errs) != 0 { if errs := validation.IsValidLabelValue(v); len(errs) != 0 {
@ -138,7 +139,7 @@ func (cfgmaps *ConfigMaps) Query(labels map[string]string) ([]*rspb.Release, err
return nil, ErrReleaseNotFound return nil, ErrReleaseNotFound
} }
var results []*rspb.Release var results []release.Releaser
for _, item := range list.Items { for _, item := range list.Items {
rls, err := decodeRelease(item.Data["release"]) rls, err := decodeRelease(item.Data["release"])
if err != nil { if err != nil {
@ -153,18 +154,28 @@ func (cfgmaps *ConfigMaps) Query(labels map[string]string) ([]*rspb.Release, err
// Create creates a new ConfigMap holding the release. If the // Create creates a new ConfigMap holding the release. If the
// ConfigMap already exists, ErrReleaseExists is returned. // ConfigMap already exists, ErrReleaseExists is returned.
func (cfgmaps *ConfigMaps) Create(key string, rls *rspb.Release) error { func (cfgmaps *ConfigMaps) Create(key string, rls release.Releaser) error {
// set labels for configmaps object meta data // set labels for configmaps object meta data
var lbs labels var lbs labels
rac, err := release.NewAccessor(rls)
if err != nil {
return err
}
lbs.init() lbs.init()
lbs.fromMap(rls.Labels) lbs.fromMap(rac.Labels())
lbs.set("createdAt", fmt.Sprintf("%v", time.Now().Unix())) lbs.set("createdAt", fmt.Sprintf("%v", time.Now().Unix()))
rel, err := releaserToV1Release(rls)
if err != nil {
return err
}
// create a new configmap to hold the release // create a new configmap to hold the release
obj, err := newConfigMapsObject(key, rls, lbs) obj, err := newConfigMapsObject(key, rel, lbs)
if err != nil { if err != nil {
slog.Debug("failed to encode release", "name", rls.Name, slog.Any("error", err)) slog.Debug("failed to encode release", "name", rac.Name(), slog.Any("error", err))
return err return err
} }
// push the configmap object out into the kubiverse // push the configmap object out into the kubiverse
@ -181,10 +192,15 @@ func (cfgmaps *ConfigMaps) Create(key string, rls *rspb.Release) error {
// Update updates the ConfigMap holding the release. If not found // Update updates the ConfigMap holding the release. If not found
// the ConfigMap is created to hold the release. // the ConfigMap is created to hold the release.
func (cfgmaps *ConfigMaps) Update(key string, rls *rspb.Release) error { func (cfgmaps *ConfigMaps) Update(key string, rel release.Releaser) error {
// set labels for configmaps object meta data // set labels for configmaps object meta data
var lbs labels var lbs labels
rls, err := releaserToV1Release(rel)
if err != nil {
return err
}
lbs.init() lbs.init()
lbs.fromMap(rls.Labels) lbs.fromMap(rls.Labels)
lbs.set("modifiedAt", fmt.Sprintf("%v", time.Now().Unix())) lbs.set("modifiedAt", fmt.Sprintf("%v", time.Now().Unix()))
@ -205,7 +221,7 @@ func (cfgmaps *ConfigMaps) Update(key string, rls *rspb.Release) error {
} }
// Delete deletes the ConfigMap holding the release named by key. // Delete deletes the ConfigMap holding the release named by key.
func (cfgmaps *ConfigMaps) Delete(key string) (rls *rspb.Release, err error) { func (cfgmaps *ConfigMaps) Delete(key string) (rls release.Releaser, err error) {
// fetch the release to check existence // fetch the release to check existence
if rls, err = cfgmaps.Get(key); err != nil { if rls, err = cfgmaps.Get(key); err != nil {
return nil, err return nil, err

@ -22,6 +22,8 @@ import (
v1 "k8s.io/api/core/v1" v1 "k8s.io/api/core/v1"
"helm.sh/helm/v4/pkg/release"
"helm.sh/helm/v4/pkg/release/common"
rspb "helm.sh/helm/v4/pkg/release/v1" rspb "helm.sh/helm/v4/pkg/release/v1"
) )
@ -37,7 +39,7 @@ func TestConfigMapGet(t *testing.T) {
name := "smug-pigeon" name := "smug-pigeon"
namespace := "default" namespace := "default"
key := testKey(name, vers) key := testKey(name, vers)
rel := releaseStub(name, vers, namespace, rspb.StatusDeployed) rel := releaseStub(name, vers, namespace, common.StatusDeployed)
cfgmaps := newTestFixtureCfgMaps(t, []*rspb.Release{rel}...) cfgmaps := newTestFixtureCfgMaps(t, []*rspb.Release{rel}...)
@ -57,7 +59,7 @@ func TestUncompressedConfigMapGet(t *testing.T) {
name := "smug-pigeon" name := "smug-pigeon"
namespace := "default" namespace := "default"
key := testKey(name, vers) key := testKey(name, vers)
rel := releaseStub(name, vers, namespace, rspb.StatusDeployed) rel := releaseStub(name, vers, namespace, common.StatusDeployed)
// Create a test fixture which contains an uncompressed release // Create a test fixture which contains an uncompressed release
cfgmap, err := newConfigMapsObject(key, rel, nil) cfgmap, err := newConfigMapsObject(key, rel, nil)
@ -84,19 +86,35 @@ func TestUncompressedConfigMapGet(t *testing.T) {
} }
} }
func convertReleaserToV1(t *testing.T, rel release.Releaser) *rspb.Release {
t.Helper()
switch r := rel.(type) {
case rspb.Release:
return &r
case *rspb.Release:
return r
case nil:
return nil
}
t.Fatalf("Unsupported release type: %T", rel)
return nil
}
func TestConfigMapList(t *testing.T) { func TestConfigMapList(t *testing.T) {
cfgmaps := newTestFixtureCfgMaps(t, []*rspb.Release{ cfgmaps := newTestFixtureCfgMaps(t, []*rspb.Release{
releaseStub("key-1", 1, "default", rspb.StatusUninstalled), releaseStub("key-1", 1, "default", common.StatusUninstalled),
releaseStub("key-2", 1, "default", rspb.StatusUninstalled), releaseStub("key-2", 1, "default", common.StatusUninstalled),
releaseStub("key-3", 1, "default", rspb.StatusDeployed), releaseStub("key-3", 1, "default", common.StatusDeployed),
releaseStub("key-4", 1, "default", rspb.StatusDeployed), releaseStub("key-4", 1, "default", common.StatusDeployed),
releaseStub("key-5", 1, "default", rspb.StatusSuperseded), releaseStub("key-5", 1, "default", common.StatusSuperseded),
releaseStub("key-6", 1, "default", rspb.StatusSuperseded), releaseStub("key-6", 1, "default", common.StatusSuperseded),
}...) }...)
// list all deleted releases // list all deleted releases
del, err := cfgmaps.List(func(rel *rspb.Release) bool { del, err := cfgmaps.List(func(rel release.Releaser) bool {
return rel.Info.Status == rspb.StatusUninstalled rls := convertReleaserToV1(t, rel)
return rls.Info.Status == common.StatusUninstalled
}) })
// check // check
if err != nil { if err != nil {
@ -107,8 +125,9 @@ func TestConfigMapList(t *testing.T) {
} }
// list all deployed releases // list all deployed releases
dpl, err := cfgmaps.List(func(rel *rspb.Release) bool { dpl, err := cfgmaps.List(func(rel release.Releaser) bool {
return rel.Info.Status == rspb.StatusDeployed rls := convertReleaserToV1(t, rel)
return rls.Info.Status == common.StatusDeployed
}) })
// check // check
if err != nil { if err != nil {
@ -119,8 +138,9 @@ func TestConfigMapList(t *testing.T) {
} }
// list all superseded releases // list all superseded releases
ssd, err := cfgmaps.List(func(rel *rspb.Release) bool { ssd, err := cfgmaps.List(func(rel release.Releaser) bool {
return rel.Info.Status == rspb.StatusSuperseded rls := convertReleaserToV1(t, rel)
return rls.Info.Status == common.StatusSuperseded
}) })
// check // check
if err != nil { if err != nil {
@ -130,7 +150,7 @@ func TestConfigMapList(t *testing.T) {
t.Errorf("Expected 2 superseded, got %d", len(ssd)) t.Errorf("Expected 2 superseded, got %d", len(ssd))
} }
// Check if release having both system and custom labels, this is needed to ensure that selector filtering would work. // Check if release having both system and custom labels, this is needed to ensure that selector filtering would work.
rls := ssd[0] rls := convertReleaserToV1(t, ssd[0])
_, ok := rls.Labels["name"] _, ok := rls.Labels["name"]
if !ok { if !ok {
t.Fatalf("Expected 'name' label in results, actual %v", rls.Labels) t.Fatalf("Expected 'name' label in results, actual %v", rls.Labels)
@ -143,12 +163,12 @@ func TestConfigMapList(t *testing.T) {
func TestConfigMapQuery(t *testing.T) { func TestConfigMapQuery(t *testing.T) {
cfgmaps := newTestFixtureCfgMaps(t, []*rspb.Release{ cfgmaps := newTestFixtureCfgMaps(t, []*rspb.Release{
releaseStub("key-1", 1, "default", rspb.StatusUninstalled), releaseStub("key-1", 1, "default", common.StatusUninstalled),
releaseStub("key-2", 1, "default", rspb.StatusUninstalled), releaseStub("key-2", 1, "default", common.StatusUninstalled),
releaseStub("key-3", 1, "default", rspb.StatusDeployed), releaseStub("key-3", 1, "default", common.StatusDeployed),
releaseStub("key-4", 1, "default", rspb.StatusDeployed), releaseStub("key-4", 1, "default", common.StatusDeployed),
releaseStub("key-5", 1, "default", rspb.StatusSuperseded), releaseStub("key-5", 1, "default", common.StatusSuperseded),
releaseStub("key-6", 1, "default", rspb.StatusSuperseded), releaseStub("key-6", 1, "default", common.StatusSuperseded),
}...) }...)
rls, err := cfgmaps.Query(map[string]string{"status": "deployed"}) rls, err := cfgmaps.Query(map[string]string{"status": "deployed"})
@ -172,7 +192,7 @@ func TestConfigMapCreate(t *testing.T) {
name := "smug-pigeon" name := "smug-pigeon"
namespace := "default" namespace := "default"
key := testKey(name, vers) key := testKey(name, vers)
rel := releaseStub(name, vers, namespace, rspb.StatusDeployed) rel := releaseStub(name, vers, namespace, common.StatusDeployed)
// store the release in a configmap // store the release in a configmap
if err := cfgmaps.Create(key, rel); err != nil { if err := cfgmaps.Create(key, rel); err != nil {
@ -196,12 +216,12 @@ func TestConfigMapUpdate(t *testing.T) {
name := "smug-pigeon" name := "smug-pigeon"
namespace := "default" namespace := "default"
key := testKey(name, vers) key := testKey(name, vers)
rel := releaseStub(name, vers, namespace, rspb.StatusDeployed) rel := releaseStub(name, vers, namespace, common.StatusDeployed)
cfgmaps := newTestFixtureCfgMaps(t, []*rspb.Release{rel}...) cfgmaps := newTestFixtureCfgMaps(t, []*rspb.Release{rel}...)
// modify release status code // modify release status code
rel.Info.Status = rspb.StatusSuperseded rel.Info.Status = common.StatusSuperseded
// perform the update // perform the update
if err := cfgmaps.Update(key, rel); err != nil { if err := cfgmaps.Update(key, rel); err != nil {
@ -209,10 +229,11 @@ func TestConfigMapUpdate(t *testing.T) {
} }
// fetch the updated release // fetch the updated release
got, err := cfgmaps.Get(key) goti, err := cfgmaps.Get(key)
if err != nil { if err != nil {
t.Fatalf("Failed to get release with key %q: %s", key, err) t.Fatalf("Failed to get release with key %q: %s", key, err)
} }
got := convertReleaserToV1(t, goti)
// check release has actually been updated by comparing modified fields // check release has actually been updated by comparing modified fields
if rel.Info.Status != got.Info.Status { if rel.Info.Status != got.Info.Status {
@ -225,7 +246,7 @@ func TestConfigMapDelete(t *testing.T) {
name := "smug-pigeon" name := "smug-pigeon"
namespace := "default" namespace := "default"
key := testKey(name, vers) key := testKey(name, vers)
rel := releaseStub(name, vers, namespace, rspb.StatusDeployed) rel := releaseStub(name, vers, namespace, common.StatusDeployed)
cfgmaps := newTestFixtureCfgMaps(t, []*rspb.Release{rel}...) cfgmaps := newTestFixtureCfgMaps(t, []*rspb.Release{rel}...)

@ -20,6 +20,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"helm.sh/helm/v4/pkg/release"
rspb "helm.sh/helm/v4/pkg/release/v1" rspb "helm.sh/helm/v4/pkg/release/v1"
) )
@ -58,7 +59,7 @@ func NewErrNoDeployedReleases(releaseName string) error {
// Create stores the release or returns ErrReleaseExists // Create stores the release or returns ErrReleaseExists
// if an identical release already exists. // if an identical release already exists.
type Creator interface { type Creator interface {
Create(key string, rls *rspb.Release) error Create(key string, rls release.Releaser) error
} }
// Updator is the interface that wraps the Update method. // Updator is the interface that wraps the Update method.
@ -66,7 +67,7 @@ type Creator interface {
// Update updates an existing release or returns // Update updates an existing release or returns
// ErrReleaseNotFound if the release does not exist. // ErrReleaseNotFound if the release does not exist.
type Updator interface { type Updator interface {
Update(key string, rls *rspb.Release) error Update(key string, rls release.Releaser) error
} }
// Deletor is the interface that wraps the Delete method. // Deletor is the interface that wraps the Delete method.
@ -74,7 +75,7 @@ type Updator interface {
// Delete deletes the release named by key or returns // Delete deletes the release named by key or returns
// ErrReleaseNotFound if the release does not exist. // ErrReleaseNotFound if the release does not exist.
type Deletor interface { type Deletor interface {
Delete(key string) (*rspb.Release, error) Delete(key string) (release.Releaser, error)
} }
// Queryor is the interface that wraps the Get and List methods. // Queryor is the interface that wraps the Get and List methods.
@ -86,9 +87,9 @@ type Deletor interface {
// //
// Query returns the set of all releases that match the provided label set. // Query returns the set of all releases that match the provided label set.
type Queryor interface { type Queryor interface {
Get(key string) (*rspb.Release, error) Get(key string) (release.Releaser, error)
List(filter func(*rspb.Release) bool) ([]*rspb.Release, error) List(filter func(release.Releaser) bool) ([]release.Releaser, error)
Query(labels map[string]string) ([]*rspb.Release, error) Query(labels map[string]string) ([]release.Releaser, error)
} }
// Driver is the interface composed of Creator, Updator, Deletor, and Queryor // Driver is the interface composed of Creator, Updator, Deletor, and Queryor
@ -102,3 +103,18 @@ type Driver interface {
Queryor Queryor
Name() string Name() string
} }
// 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)
}
}

@ -21,7 +21,7 @@ import (
"strings" "strings"
"sync" "sync"
rspb "helm.sh/helm/v4/pkg/release/v1" "helm.sh/helm/v4/pkg/release"
) )
var _ Driver = (*Memory)(nil) var _ Driver = (*Memory)(nil)
@ -61,7 +61,7 @@ func (mem *Memory) Name() string {
} }
// Get returns the release named by key or returns ErrReleaseNotFound. // Get returns the release named by key or returns ErrReleaseNotFound.
func (mem *Memory) Get(key string) (*rspb.Release, error) { func (mem *Memory) Get(key string) (release.Releaser, error) {
defer unlock(mem.rlock()) defer unlock(mem.rlock())
keyWithoutPrefix := strings.TrimPrefix(key, "sh.helm.release.v1.") keyWithoutPrefix := strings.TrimPrefix(key, "sh.helm.release.v1.")
@ -83,10 +83,10 @@ func (mem *Memory) Get(key string) (*rspb.Release, error) {
} }
// List returns the list of all releases such that filter(release) == true // List returns the list of all releases such that filter(release) == true
func (mem *Memory) List(filter func(*rspb.Release) bool) ([]*rspb.Release, error) { func (mem *Memory) List(filter func(release.Releaser) bool) ([]release.Releaser, error) {
defer unlock(mem.rlock()) defer unlock(mem.rlock())
var ls []*rspb.Release var ls []release.Releaser
for namespace := range mem.cache { for namespace := range mem.cache {
if mem.namespace != "" { if mem.namespace != "" {
// Should only list releases of this namespace // Should only list releases of this namespace
@ -109,7 +109,7 @@ func (mem *Memory) List(filter func(*rspb.Release) bool) ([]*rspb.Release, error
} }
// Query returns the set of releases that match the provided set of labels // Query returns the set of releases that match the provided set of labels
func (mem *Memory) Query(keyvals map[string]string) ([]*rspb.Release, error) { func (mem *Memory) Query(keyvals map[string]string) ([]release.Releaser, error) {
defer unlock(mem.rlock()) defer unlock(mem.rlock())
var lbs labels var lbs labels
@ -117,7 +117,7 @@ func (mem *Memory) Query(keyvals map[string]string) ([]*rspb.Release, error) {
lbs.init() lbs.init()
lbs.fromMap(keyvals) lbs.fromMap(keyvals)
var ls []*rspb.Release var ls []release.Releaser
for namespace := range mem.cache { for namespace := range mem.cache {
if mem.namespace != "" { if mem.namespace != "" {
// Should only query releases of this namespace // Should only query releases of this namespace
@ -150,9 +150,13 @@ func (mem *Memory) Query(keyvals map[string]string) ([]*rspb.Release, error) {
} }
// Create creates a new release or returns ErrReleaseExists. // Create creates a new release or returns ErrReleaseExists.
func (mem *Memory) Create(key string, rls *rspb.Release) error { func (mem *Memory) Create(key string, rel release.Releaser) error {
defer unlock(mem.wlock()) defer unlock(mem.wlock())
rls, err := releaserToV1Release(rel)
if err != nil {
return err
}
// For backwards compatibility, we protect against an unset namespace // For backwards compatibility, we protect against an unset namespace
namespace := rls.Namespace namespace := rls.Namespace
if namespace == "" { if namespace == "" {
@ -176,9 +180,14 @@ func (mem *Memory) Create(key string, rls *rspb.Release) error {
} }
// Update updates a release or returns ErrReleaseNotFound. // Update updates a release or returns ErrReleaseNotFound.
func (mem *Memory) Update(key string, rls *rspb.Release) error { func (mem *Memory) Update(key string, rel release.Releaser) error {
defer unlock(mem.wlock()) defer unlock(mem.wlock())
rls, err := releaserToV1Release(rel)
if err != nil {
return err
}
// For backwards compatibility, we protect against an unset namespace // For backwards compatibility, we protect against an unset namespace
namespace := rls.Namespace namespace := rls.Namespace
if namespace == "" { if namespace == "" {
@ -196,7 +205,7 @@ func (mem *Memory) Update(key string, rls *rspb.Release) error {
} }
// Delete deletes a release or returns ErrReleaseNotFound. // Delete deletes a release or returns ErrReleaseNotFound.
func (mem *Memory) Delete(key string) (*rspb.Release, error) { func (mem *Memory) Delete(key string) (release.Releaser, error) {
defer unlock(mem.wlock()) defer unlock(mem.wlock())
keyWithoutPrefix := strings.TrimPrefix(key, "sh.helm.release.v1.") keyWithoutPrefix := strings.TrimPrefix(key, "sh.helm.release.v1.")

@ -21,6 +21,10 @@ import (
"reflect" "reflect"
"testing" "testing"
"github.com/stretchr/testify/assert"
"helm.sh/helm/v4/pkg/release"
"helm.sh/helm/v4/pkg/release/common"
rspb "helm.sh/helm/v4/pkg/release/v1" rspb "helm.sh/helm/v4/pkg/release/v1"
) )
@ -38,22 +42,22 @@ func TestMemoryCreate(t *testing.T) {
}{ }{
{ {
"create should succeed", "create should succeed",
releaseStub("rls-c", 1, "default", rspb.StatusDeployed), releaseStub("rls-c", 1, "default", common.StatusDeployed),
false, false,
}, },
{ {
"create should fail (release already exists)", "create should fail (release already exists)",
releaseStub("rls-a", 1, "default", rspb.StatusDeployed), releaseStub("rls-a", 1, "default", common.StatusDeployed),
true, true,
}, },
{ {
"create in namespace should succeed", "create in namespace should succeed",
releaseStub("rls-a", 1, "mynamespace", rspb.StatusDeployed), releaseStub("rls-a", 1, "mynamespace", common.StatusDeployed),
false, false,
}, },
{ {
"create in other namespace should fail (release already exists)", "create in other namespace should fail (release already exists)",
releaseStub("rls-c", 1, "mynamespace", rspb.StatusDeployed), releaseStub("rls-c", 1, "mynamespace", common.StatusDeployed),
true, true,
}, },
} }
@ -104,8 +108,9 @@ func TestMemoryList(t *testing.T) {
ts.SetNamespace("default") ts.SetNamespace("default")
// list all deployed releases // list all deployed releases
dpl, err := ts.List(func(rel *rspb.Release) bool { dpl, err := ts.List(func(rel release.Releaser) bool {
return rel.Info.Status == rspb.StatusDeployed rls := convertReleaserToV1(t, rel)
return rls.Info.Status == common.StatusDeployed
}) })
// check // check
if err != nil { if err != nil {
@ -116,8 +121,9 @@ func TestMemoryList(t *testing.T) {
} }
// list all superseded releases // list all superseded releases
ssd, err := ts.List(func(rel *rspb.Release) bool { ssd, err := ts.List(func(rel release.Releaser) bool {
return rel.Info.Status == rspb.StatusSuperseded rls := convertReleaserToV1(t, rel)
return rls.Info.Status == common.StatusSuperseded
}) })
// check // check
if err != nil { if err != nil {
@ -128,8 +134,9 @@ func TestMemoryList(t *testing.T) {
} }
// list all deleted releases // list all deleted releases
del, err := ts.List(func(rel *rspb.Release) bool { del, err := ts.List(func(rel release.Releaser) bool {
return rel.Info.Status == rspb.StatusUninstalled rls := convertReleaserToV1(t, rel)
return rls.Info.Status == common.StatusUninstalled
}) })
// check // check
if err != nil { if err != nil {
@ -185,25 +192,25 @@ func TestMemoryUpdate(t *testing.T) {
{ {
"update release status", "update release status",
"rls-a.v4", "rls-a.v4",
releaseStub("rls-a", 4, "default", rspb.StatusSuperseded), releaseStub("rls-a", 4, "default", common.StatusSuperseded),
false, false,
}, },
{ {
"update release does not exist", "update release does not exist",
"rls-c.v1", "rls-c.v1",
releaseStub("rls-c", 1, "default", rspb.StatusUninstalled), releaseStub("rls-c", 1, "default", common.StatusUninstalled),
true, true,
}, },
{ {
"update release status in namespace", "update release status in namespace",
"rls-c.v4", "rls-c.v4",
releaseStub("rls-c", 4, "mynamespace", rspb.StatusSuperseded), releaseStub("rls-c", 4, "mynamespace", common.StatusSuperseded),
false, false,
}, },
{ {
"update release in namespace does not exist", "update release in namespace does not exist",
"rls-a.v1", "rls-a.v1",
releaseStub("rls-a", 1, "mynamespace", rspb.StatusUninstalled), releaseStub("rls-a", 1, "mynamespace", common.StatusUninstalled),
true, true,
}, },
} }
@ -255,17 +262,23 @@ func TestMemoryDelete(t *testing.T) {
startLen := len(start) startLen := len(start)
for _, tt := range tests { for _, tt := range tests {
ts.SetNamespace(tt.namespace) ts.SetNamespace(tt.namespace)
if rel, err := ts.Delete(tt.key); err != nil {
rel, err := ts.Delete(tt.key)
var rls *rspb.Release
if err == nil {
rls = convertReleaserToV1(t, rel)
}
if err != nil {
if !tt.err { if !tt.err {
t.Fatalf("Failed %q to get '%s': %q\n", tt.desc, tt.key, err) t.Fatalf("Failed %q to get '%s': %q\n", tt.desc, tt.key, err)
} }
continue continue
} else if tt.err { } else if tt.err {
t.Fatalf("Did not get expected error for %q '%s'\n", tt.desc, tt.key) t.Fatalf("Did not get expected error for %q '%s'\n", tt.desc, tt.key)
} else if fmt.Sprintf("%s.v%d", rel.Name, rel.Version) != tt.key { } else if fmt.Sprintf("%s.v%d", rls.Name, rls.Version) != tt.key {
t.Fatalf("Asked for delete on %s, but deleted %d", tt.key, rel.Version) t.Fatalf("Asked for delete on %s, but deleted %d", tt.key, rls.Version)
} }
_, err := ts.Get(tt.key) _, err = ts.Get(tt.key)
if err == nil { if err == nil {
t.Errorf("Expected an error when asking for a deleted key") t.Errorf("Expected an error when asking for a deleted key")
} }
@ -282,7 +295,9 @@ func TestMemoryDelete(t *testing.T) {
if startLen-2 != endLen { if startLen-2 != endLen {
t.Errorf("expected end to be %d instead of %d", startLen-2, endLen) t.Errorf("expected end to be %d instead of %d", startLen-2, endLen)
for _, ee := range end { for _, ee := range end {
t.Logf("Name: %s, Version: %d", ee.Name, ee.Version) rac, err := release.NewAccessor(ee)
assert.NoError(t, err, "unable to get release accessor")
t.Logf("Name: %s, Version: %d", rac.Name(), rac.Version())
} }
} }

@ -31,10 +31,11 @@ import (
kblabels "k8s.io/apimachinery/pkg/labels" kblabels "k8s.io/apimachinery/pkg/labels"
corev1 "k8s.io/client-go/kubernetes/typed/core/v1" corev1 "k8s.io/client-go/kubernetes/typed/core/v1"
"helm.sh/helm/v4/pkg/release/common"
rspb "helm.sh/helm/v4/pkg/release/v1" rspb "helm.sh/helm/v4/pkg/release/v1"
) )
func releaseStub(name string, vers int, namespace string, status rspb.Status) *rspb.Release { func releaseStub(name string, vers int, namespace string, status common.Status) *rspb.Release {
return &rspb.Release{ return &rspb.Release{
Name: name, Name: name,
Version: vers, Version: vers,
@ -55,20 +56,20 @@ func tsFixtureMemory(t *testing.T) *Memory {
t.Helper() t.Helper()
hs := []*rspb.Release{ hs := []*rspb.Release{
// rls-a // rls-a
releaseStub("rls-a", 4, "default", rspb.StatusDeployed), releaseStub("rls-a", 4, "default", common.StatusDeployed),
releaseStub("rls-a", 1, "default", rspb.StatusSuperseded), releaseStub("rls-a", 1, "default", common.StatusSuperseded),
releaseStub("rls-a", 3, "default", rspb.StatusSuperseded), releaseStub("rls-a", 3, "default", common.StatusSuperseded),
releaseStub("rls-a", 2, "default", rspb.StatusSuperseded), releaseStub("rls-a", 2, "default", common.StatusSuperseded),
// rls-b // rls-b
releaseStub("rls-b", 4, "default", rspb.StatusDeployed), releaseStub("rls-b", 4, "default", common.StatusDeployed),
releaseStub("rls-b", 1, "default", rspb.StatusSuperseded), releaseStub("rls-b", 1, "default", common.StatusSuperseded),
releaseStub("rls-b", 3, "default", rspb.StatusSuperseded), releaseStub("rls-b", 3, "default", common.StatusSuperseded),
releaseStub("rls-b", 2, "default", rspb.StatusSuperseded), releaseStub("rls-b", 2, "default", common.StatusSuperseded),
// rls-c in other namespace // rls-c in other namespace
releaseStub("rls-c", 4, "mynamespace", rspb.StatusDeployed), releaseStub("rls-c", 4, "mynamespace", common.StatusDeployed),
releaseStub("rls-c", 1, "mynamespace", rspb.StatusSuperseded), releaseStub("rls-c", 1, "mynamespace", common.StatusSuperseded),
releaseStub("rls-c", 3, "mynamespace", rspb.StatusSuperseded), releaseStub("rls-c", 3, "mynamespace", common.StatusSuperseded),
releaseStub("rls-c", 2, "mynamespace", rspb.StatusSuperseded), releaseStub("rls-c", 2, "mynamespace", common.StatusSuperseded),
} }
mem := NewMemory() mem := NewMemory()

@ -20,13 +20,13 @@ import (
"reflect" "reflect"
"testing" "testing"
rspb "helm.sh/helm/v4/pkg/release/v1" "helm.sh/helm/v4/pkg/release/common"
) )
func TestRecordsAdd(t *testing.T) { func TestRecordsAdd(t *testing.T) {
rs := records([]*record{ rs := records([]*record{
newRecord("rls-a.v1", releaseStub("rls-a", 1, "default", rspb.StatusSuperseded)), newRecord("rls-a.v1", releaseStub("rls-a", 1, "default", common.StatusSuperseded)),
newRecord("rls-a.v2", releaseStub("rls-a", 2, "default", rspb.StatusDeployed)), newRecord("rls-a.v2", releaseStub("rls-a", 2, "default", common.StatusDeployed)),
}) })
var tests = []struct { var tests = []struct {
@ -39,13 +39,13 @@ func TestRecordsAdd(t *testing.T) {
"add valid key", "add valid key",
"rls-a.v3", "rls-a.v3",
false, false,
newRecord("rls-a.v3", releaseStub("rls-a", 3, "default", rspb.StatusSuperseded)), newRecord("rls-a.v3", releaseStub("rls-a", 3, "default", common.StatusSuperseded)),
}, },
{ {
"add already existing key", "add already existing key",
"rls-a.v1", "rls-a.v1",
true, true,
newRecord("rls-a.v1", releaseStub("rls-a", 1, "default", rspb.StatusDeployed)), newRecord("rls-a.v1", releaseStub("rls-a", 1, "default", common.StatusDeployed)),
}, },
} }
@ -70,8 +70,8 @@ func TestRecordsRemove(t *testing.T) {
} }
rs := records([]*record{ rs := records([]*record{
newRecord("rls-a.v1", releaseStub("rls-a", 1, "default", rspb.StatusSuperseded)), newRecord("rls-a.v1", releaseStub("rls-a", 1, "default", common.StatusSuperseded)),
newRecord("rls-a.v2", releaseStub("rls-a", 2, "default", rspb.StatusDeployed)), newRecord("rls-a.v2", releaseStub("rls-a", 2, "default", common.StatusDeployed)),
}) })
startLen := rs.Len() startLen := rs.Len()
@ -98,8 +98,8 @@ func TestRecordsRemove(t *testing.T) {
func TestRecordsRemoveAt(t *testing.T) { func TestRecordsRemoveAt(t *testing.T) {
rs := records([]*record{ rs := records([]*record{
newRecord("rls-a.v1", releaseStub("rls-a", 1, "default", rspb.StatusSuperseded)), newRecord("rls-a.v1", releaseStub("rls-a", 1, "default", common.StatusSuperseded)),
newRecord("rls-a.v2", releaseStub("rls-a", 2, "default", rspb.StatusDeployed)), newRecord("rls-a.v2", releaseStub("rls-a", 2, "default", common.StatusDeployed)),
}) })
if len(rs) != 2 { if len(rs) != 2 {
@ -114,8 +114,8 @@ func TestRecordsRemoveAt(t *testing.T) {
func TestRecordsGet(t *testing.T) { func TestRecordsGet(t *testing.T) {
rs := records([]*record{ rs := records([]*record{
newRecord("rls-a.v1", releaseStub("rls-a", 1, "default", rspb.StatusSuperseded)), newRecord("rls-a.v1", releaseStub("rls-a", 1, "default", common.StatusSuperseded)),
newRecord("rls-a.v2", releaseStub("rls-a", 2, "default", rspb.StatusDeployed)), newRecord("rls-a.v2", releaseStub("rls-a", 2, "default", common.StatusDeployed)),
}) })
var tests = []struct { var tests = []struct {
@ -126,7 +126,7 @@ func TestRecordsGet(t *testing.T) {
{ {
"get valid key", "get valid key",
"rls-a.v1", "rls-a.v1",
newRecord("rls-a.v1", releaseStub("rls-a", 1, "default", rspb.StatusSuperseded)), newRecord("rls-a.v1", releaseStub("rls-a", 1, "default", common.StatusSuperseded)),
}, },
{ {
"get invalid key", "get invalid key",
@ -145,8 +145,8 @@ func TestRecordsGet(t *testing.T) {
func TestRecordsIndex(t *testing.T) { func TestRecordsIndex(t *testing.T) {
rs := records([]*record{ rs := records([]*record{
newRecord("rls-a.v1", releaseStub("rls-a", 1, "default", rspb.StatusSuperseded)), newRecord("rls-a.v1", releaseStub("rls-a", 1, "default", common.StatusSuperseded)),
newRecord("rls-a.v2", releaseStub("rls-a", 2, "default", rspb.StatusDeployed)), newRecord("rls-a.v2", releaseStub("rls-a", 2, "default", common.StatusDeployed)),
}) })
var tests = []struct { var tests = []struct {
@ -176,8 +176,8 @@ func TestRecordsIndex(t *testing.T) {
func TestRecordsExists(t *testing.T) { func TestRecordsExists(t *testing.T) {
rs := records([]*record{ rs := records([]*record{
newRecord("rls-a.v1", releaseStub("rls-a", 1, "default", rspb.StatusSuperseded)), newRecord("rls-a.v1", releaseStub("rls-a", 1, "default", common.StatusSuperseded)),
newRecord("rls-a.v2", releaseStub("rls-a", 2, "default", rspb.StatusDeployed)), newRecord("rls-a.v2", releaseStub("rls-a", 2, "default", common.StatusDeployed)),
}) })
var tests = []struct { var tests = []struct {
@ -207,8 +207,8 @@ func TestRecordsExists(t *testing.T) {
func TestRecordsReplace(t *testing.T) { func TestRecordsReplace(t *testing.T) {
rs := records([]*record{ rs := records([]*record{
newRecord("rls-a.v1", releaseStub("rls-a", 1, "default", rspb.StatusSuperseded)), newRecord("rls-a.v1", releaseStub("rls-a", 1, "default", common.StatusSuperseded)),
newRecord("rls-a.v2", releaseStub("rls-a", 2, "default", rspb.StatusDeployed)), newRecord("rls-a.v2", releaseStub("rls-a", 2, "default", common.StatusDeployed)),
}) })
var tests = []struct { var tests = []struct {
@ -220,13 +220,13 @@ func TestRecordsReplace(t *testing.T) {
{ {
"replace with existing key", "replace with existing key",
"rls-a.v2", "rls-a.v2",
newRecord("rls-a.v3", releaseStub("rls-a", 3, "default", rspb.StatusSuperseded)), newRecord("rls-a.v3", releaseStub("rls-a", 3, "default", common.StatusSuperseded)),
newRecord("rls-a.v2", releaseStub("rls-a", 2, "default", rspb.StatusDeployed)), newRecord("rls-a.v2", releaseStub("rls-a", 2, "default", common.StatusDeployed)),
}, },
{ {
"replace with non existing key", "replace with non existing key",
"rls-a.v4", "rls-a.v4",
newRecord("rls-a.v4", releaseStub("rls-a", 4, "default", rspb.StatusDeployed)), newRecord("rls-a.v4", releaseStub("rls-a", 4, "default", common.StatusDeployed)),
nil, nil,
}, },
} }

@ -31,6 +31,7 @@ import (
"k8s.io/apimachinery/pkg/util/validation" "k8s.io/apimachinery/pkg/util/validation"
corev1 "k8s.io/client-go/kubernetes/typed/core/v1" corev1 "k8s.io/client-go/kubernetes/typed/core/v1"
"helm.sh/helm/v4/pkg/release"
rspb "helm.sh/helm/v4/pkg/release/v1" rspb "helm.sh/helm/v4/pkg/release/v1"
) )
@ -60,7 +61,7 @@ func (secrets *Secrets) Name() string {
// Get fetches the release named by key. The corresponding release is returned // Get fetches the release named by key. The corresponding release is returned
// or error if not found. // or error if not found.
func (secrets *Secrets) Get(key string) (*rspb.Release, error) { func (secrets *Secrets) Get(key string) (release.Releaser, error) {
// fetch the secret holding the release named by key // fetch the secret holding the release named by key
obj, err := secrets.impl.Get(context.Background(), key, metav1.GetOptions{}) obj, err := secrets.impl.Get(context.Background(), key, metav1.GetOptions{})
if err != nil { if err != nil {
@ -81,7 +82,7 @@ func (secrets *Secrets) Get(key string) (*rspb.Release, error) {
// List fetches all releases and returns the list releases such // List fetches all releases and returns the list releases such
// that filter(release) == true. An error is returned if the // that filter(release) == true. An error is returned if the
// secret fails to retrieve the releases. // secret fails to retrieve the releases.
func (secrets *Secrets) List(filter func(*rspb.Release) bool) ([]*rspb.Release, error) { func (secrets *Secrets) List(filter func(release.Releaser) bool) ([]release.Releaser, error) {
lsel := kblabels.Set{"owner": "helm"}.AsSelector() lsel := kblabels.Set{"owner": "helm"}.AsSelector()
opts := metav1.ListOptions{LabelSelector: lsel.String()} opts := metav1.ListOptions{LabelSelector: lsel.String()}
@ -90,7 +91,7 @@ func (secrets *Secrets) List(filter func(*rspb.Release) bool) ([]*rspb.Release,
return nil, fmt.Errorf("list: failed to list: %w", err) return nil, fmt.Errorf("list: failed to list: %w", err)
} }
var results []*rspb.Release var results []release.Releaser
// iterate over the secrets object list // iterate over the secrets object list
// and decode each release // and decode each release
@ -112,7 +113,7 @@ func (secrets *Secrets) List(filter func(*rspb.Release) bool) ([]*rspb.Release,
// Query fetches all releases that match the provided map of labels. // Query fetches all releases that match the provided map of labels.
// An error is returned if the secret fails to retrieve the releases. // An error is returned if the secret fails to retrieve the releases.
func (secrets *Secrets) Query(labels map[string]string) ([]*rspb.Release, error) { func (secrets *Secrets) Query(labels map[string]string) ([]release.Releaser, error) {
ls := kblabels.Set{} ls := kblabels.Set{}
for k, v := range labels { for k, v := range labels {
if errs := validation.IsValidLabelValue(v); len(errs) != 0 { if errs := validation.IsValidLabelValue(v); len(errs) != 0 {
@ -132,7 +133,7 @@ func (secrets *Secrets) Query(labels map[string]string) ([]*rspb.Release, error)
return nil, ErrReleaseNotFound return nil, ErrReleaseNotFound
} }
var results []*rspb.Release var results []release.Releaser
for _, item := range list.Items { for _, item := range list.Items {
rls, err := decodeRelease(string(item.Data["release"])) rls, err := decodeRelease(string(item.Data["release"]))
if err != nil { if err != nil {
@ -147,10 +148,15 @@ func (secrets *Secrets) Query(labels map[string]string) ([]*rspb.Release, error)
// Create creates a new Secret holding the release. If the // Create creates a new Secret holding the release. If the
// Secret already exists, ErrReleaseExists is returned. // Secret already exists, ErrReleaseExists is returned.
func (secrets *Secrets) Create(key string, rls *rspb.Release) error { func (secrets *Secrets) Create(key string, rel release.Releaser) error {
// set labels for secrets object meta data // set labels for secrets object meta data
var lbs labels var lbs labels
rls, err := releaserToV1Release(rel)
if err != nil {
return err
}
lbs.init() lbs.init()
lbs.fromMap(rls.Labels) lbs.fromMap(rls.Labels)
lbs.set("createdAt", fmt.Sprintf("%v", time.Now().Unix())) lbs.set("createdAt", fmt.Sprintf("%v", time.Now().Unix()))
@ -173,10 +179,15 @@ func (secrets *Secrets) Create(key string, rls *rspb.Release) error {
// Update updates the Secret holding the release. If not found // Update updates the Secret holding the release. If not found
// the Secret is created to hold the release. // the Secret is created to hold the release.
func (secrets *Secrets) Update(key string, rls *rspb.Release) error { func (secrets *Secrets) Update(key string, rel release.Releaser) error {
// set labels for secrets object meta data // set labels for secrets object meta data
var lbs labels var lbs labels
rls, err := releaserToV1Release(rel)
if err != nil {
return err
}
lbs.init() lbs.init()
lbs.fromMap(rls.Labels) lbs.fromMap(rls.Labels)
lbs.set("modifiedAt", fmt.Sprintf("%v", time.Now().Unix())) lbs.set("modifiedAt", fmt.Sprintf("%v", time.Now().Unix()))
@ -195,7 +206,7 @@ func (secrets *Secrets) Update(key string, rls *rspb.Release) error {
} }
// Delete deletes the Secret holding the release named by key. // Delete deletes the Secret holding the release named by key.
func (secrets *Secrets) Delete(key string) (rls *rspb.Release, err error) { func (secrets *Secrets) Delete(key string) (rls release.Releaser, err error) {
// fetch the release to check existence // fetch the release to check existence
if rls, err = secrets.Get(key); err != nil { if rls, err = secrets.Get(key); err != nil {
return nil, err return nil, err

@ -22,6 +22,8 @@ import (
v1 "k8s.io/api/core/v1" v1 "k8s.io/api/core/v1"
"helm.sh/helm/v4/pkg/release"
"helm.sh/helm/v4/pkg/release/common"
rspb "helm.sh/helm/v4/pkg/release/v1" rspb "helm.sh/helm/v4/pkg/release/v1"
) )
@ -37,7 +39,7 @@ func TestSecretGet(t *testing.T) {
name := "smug-pigeon" name := "smug-pigeon"
namespace := "default" namespace := "default"
key := testKey(name, vers) key := testKey(name, vers)
rel := releaseStub(name, vers, namespace, rspb.StatusDeployed) rel := releaseStub(name, vers, namespace, common.StatusDeployed)
secrets := newTestFixtureSecrets(t, []*rspb.Release{rel}...) secrets := newTestFixtureSecrets(t, []*rspb.Release{rel}...)
@ -57,7 +59,7 @@ func TestUNcompressedSecretGet(t *testing.T) {
name := "smug-pigeon" name := "smug-pigeon"
namespace := "default" namespace := "default"
key := testKey(name, vers) key := testKey(name, vers)
rel := releaseStub(name, vers, namespace, rspb.StatusDeployed) rel := releaseStub(name, vers, namespace, common.StatusDeployed)
// Create a test fixture which contains an uncompressed release // Create a test fixture which contains an uncompressed release
secret, err := newSecretsObject(key, rel, nil) secret, err := newSecretsObject(key, rel, nil)
@ -86,17 +88,18 @@ func TestUNcompressedSecretGet(t *testing.T) {
func TestSecretList(t *testing.T) { func TestSecretList(t *testing.T) {
secrets := newTestFixtureSecrets(t, []*rspb.Release{ secrets := newTestFixtureSecrets(t, []*rspb.Release{
releaseStub("key-1", 1, "default", rspb.StatusUninstalled), releaseStub("key-1", 1, "default", common.StatusUninstalled),
releaseStub("key-2", 1, "default", rspb.StatusUninstalled), releaseStub("key-2", 1, "default", common.StatusUninstalled),
releaseStub("key-3", 1, "default", rspb.StatusDeployed), releaseStub("key-3", 1, "default", common.StatusDeployed),
releaseStub("key-4", 1, "default", rspb.StatusDeployed), releaseStub("key-4", 1, "default", common.StatusDeployed),
releaseStub("key-5", 1, "default", rspb.StatusSuperseded), releaseStub("key-5", 1, "default", common.StatusSuperseded),
releaseStub("key-6", 1, "default", rspb.StatusSuperseded), releaseStub("key-6", 1, "default", common.StatusSuperseded),
}...) }...)
// list all deleted releases // list all deleted releases
del, err := secrets.List(func(rel *rspb.Release) bool { del, err := secrets.List(func(rel release.Releaser) bool {
return rel.Info.Status == rspb.StatusUninstalled rls := convertReleaserToV1(t, rel)
return rls.Info.Status == common.StatusUninstalled
}) })
// check // check
if err != nil { if err != nil {
@ -107,8 +110,9 @@ func TestSecretList(t *testing.T) {
} }
// list all deployed releases // list all deployed releases
dpl, err := secrets.List(func(rel *rspb.Release) bool { dpl, err := secrets.List(func(rel release.Releaser) bool {
return rel.Info.Status == rspb.StatusDeployed rls := convertReleaserToV1(t, rel)
return rls.Info.Status == common.StatusDeployed
}) })
// check // check
if err != nil { if err != nil {
@ -119,8 +123,9 @@ func TestSecretList(t *testing.T) {
} }
// list all superseded releases // list all superseded releases
ssd, err := secrets.List(func(rel *rspb.Release) bool { ssd, err := secrets.List(func(rel release.Releaser) bool {
return rel.Info.Status == rspb.StatusSuperseded rls := convertReleaserToV1(t, rel)
return rls.Info.Status == common.StatusSuperseded
}) })
// check // check
if err != nil { if err != nil {
@ -130,7 +135,7 @@ func TestSecretList(t *testing.T) {
t.Errorf("Expected 2 superseded, got %d", len(ssd)) t.Errorf("Expected 2 superseded, got %d", len(ssd))
} }
// Check if release having both system and custom labels, this is needed to ensure that selector filtering would work. // Check if release having both system and custom labels, this is needed to ensure that selector filtering would work.
rls := ssd[0] rls := convertReleaserToV1(t, ssd[0])
_, ok := rls.Labels["name"] _, ok := rls.Labels["name"]
if !ok { if !ok {
t.Fatalf("Expected 'name' label in results, actual %v", rls.Labels) t.Fatalf("Expected 'name' label in results, actual %v", rls.Labels)
@ -143,12 +148,12 @@ func TestSecretList(t *testing.T) {
func TestSecretQuery(t *testing.T) { func TestSecretQuery(t *testing.T) {
secrets := newTestFixtureSecrets(t, []*rspb.Release{ secrets := newTestFixtureSecrets(t, []*rspb.Release{
releaseStub("key-1", 1, "default", rspb.StatusUninstalled), releaseStub("key-1", 1, "default", common.StatusUninstalled),
releaseStub("key-2", 1, "default", rspb.StatusUninstalled), releaseStub("key-2", 1, "default", common.StatusUninstalled),
releaseStub("key-3", 1, "default", rspb.StatusDeployed), releaseStub("key-3", 1, "default", common.StatusDeployed),
releaseStub("key-4", 1, "default", rspb.StatusDeployed), releaseStub("key-4", 1, "default", common.StatusDeployed),
releaseStub("key-5", 1, "default", rspb.StatusSuperseded), releaseStub("key-5", 1, "default", common.StatusSuperseded),
releaseStub("key-6", 1, "default", rspb.StatusSuperseded), releaseStub("key-6", 1, "default", common.StatusSuperseded),
}...) }...)
rls, err := secrets.Query(map[string]string{"status": "deployed"}) rls, err := secrets.Query(map[string]string{"status": "deployed"})
@ -172,7 +177,7 @@ func TestSecretCreate(t *testing.T) {
name := "smug-pigeon" name := "smug-pigeon"
namespace := "default" namespace := "default"
key := testKey(name, vers) key := testKey(name, vers)
rel := releaseStub(name, vers, namespace, rspb.StatusDeployed) rel := releaseStub(name, vers, namespace, common.StatusDeployed)
// store the release in a secret // store the release in a secret
if err := secrets.Create(key, rel); err != nil { if err := secrets.Create(key, rel); err != nil {
@ -196,12 +201,12 @@ func TestSecretUpdate(t *testing.T) {
name := "smug-pigeon" name := "smug-pigeon"
namespace := "default" namespace := "default"
key := testKey(name, vers) key := testKey(name, vers)
rel := releaseStub(name, vers, namespace, rspb.StatusDeployed) rel := releaseStub(name, vers, namespace, common.StatusDeployed)
secrets := newTestFixtureSecrets(t, []*rspb.Release{rel}...) secrets := newTestFixtureSecrets(t, []*rspb.Release{rel}...)
// modify release status code // modify release status code
rel.Info.Status = rspb.StatusSuperseded rel.Info.Status = common.StatusSuperseded
// perform the update // perform the update
if err := secrets.Update(key, rel); err != nil { if err := secrets.Update(key, rel); err != nil {
@ -209,10 +214,11 @@ func TestSecretUpdate(t *testing.T) {
} }
// fetch the updated release // fetch the updated release
got, err := secrets.Get(key) goti, err := secrets.Get(key)
if err != nil { if err != nil {
t.Fatalf("Failed to get release with key %q: %s", key, err) t.Fatalf("Failed to get release with key %q: %s", key, err)
} }
got := convertReleaserToV1(t, goti)
// check release has actually been updated by comparing modified fields // check release has actually been updated by comparing modified fields
if rel.Info.Status != got.Info.Status { if rel.Info.Status != got.Info.Status {
@ -225,7 +231,7 @@ func TestSecretDelete(t *testing.T) {
name := "smug-pigeon" name := "smug-pigeon"
namespace := "default" namespace := "default"
key := testKey(name, vers) key := testKey(name, vers)
rel := releaseStub(name, vers, namespace, rspb.StatusDeployed) rel := releaseStub(name, vers, namespace, common.StatusDeployed)
secrets := newTestFixtureSecrets(t, []*rspb.Release{rel}...) secrets := newTestFixtureSecrets(t, []*rspb.Release{rel}...)

@ -32,6 +32,7 @@ import (
// Import pq for postgres dialect // Import pq for postgres dialect
_ "github.com/lib/pq" _ "github.com/lib/pq"
"helm.sh/helm/v4/pkg/release"
rspb "helm.sh/helm/v4/pkg/release/v1" rspb "helm.sh/helm/v4/pkg/release/v1"
) )
@ -297,7 +298,7 @@ func NewSQL(connectionString string, namespace string) (*SQL, error) {
} }
// Get returns the release named by key. // Get returns the release named by key.
func (s *SQL) Get(key string) (*rspb.Release, error) { func (s *SQL) Get(key string) (release.Releaser, error) {
var record SQLReleaseWrapper var record SQLReleaseWrapper
qb := s.statementBuilder. qb := s.statementBuilder.
@ -333,7 +334,7 @@ func (s *SQL) Get(key string) (*rspb.Release, error) {
} }
// List returns the list of all releases such that filter(release) == true // List returns the list of all releases such that filter(release) == true
func (s *SQL) List(filter func(*rspb.Release) bool) ([]*rspb.Release, error) { func (s *SQL) List(filter func(release.Releaser) bool) ([]release.Releaser, error) {
sb := s.statementBuilder. sb := s.statementBuilder.
Select(sqlReleaseTableKeyColumn, sqlReleaseTableNamespaceColumn, sqlReleaseTableBodyColumn). Select(sqlReleaseTableKeyColumn, sqlReleaseTableNamespaceColumn, sqlReleaseTableBodyColumn).
From(sqlReleaseTableName). From(sqlReleaseTableName).
@ -356,7 +357,7 @@ func (s *SQL) List(filter func(*rspb.Release) bool) ([]*rspb.Release, error) {
return nil, err return nil, err
} }
var releases []*rspb.Release var releases []release.Releaser
for _, record := range records { for _, record := range records {
release, err := decodeRelease(record.Body) release, err := decodeRelease(record.Body)
if err != nil { if err != nil {
@ -379,7 +380,7 @@ func (s *SQL) List(filter func(*rspb.Release) bool) ([]*rspb.Release, error) {
} }
// Query returns the set of releases that match the provided set of labels. // Query returns the set of releases that match the provided set of labels.
func (s *SQL) Query(labels map[string]string) ([]*rspb.Release, error) { func (s *SQL) Query(labels map[string]string) ([]release.Releaser, error) {
sb := s.statementBuilder. sb := s.statementBuilder.
Select(sqlReleaseTableKeyColumn, sqlReleaseTableNamespaceColumn, sqlReleaseTableBodyColumn). Select(sqlReleaseTableKeyColumn, sqlReleaseTableNamespaceColumn, sqlReleaseTableBodyColumn).
From(sqlReleaseTableName) From(sqlReleaseTableName)
@ -420,7 +421,7 @@ func (s *SQL) Query(labels map[string]string) ([]*rspb.Release, error) {
return nil, ErrReleaseNotFound return nil, ErrReleaseNotFound
} }
var releases []*rspb.Release var releases []release.Releaser
for _, record := range records { for _, record := range records {
release, err := decodeRelease(record.Body) release, err := decodeRelease(record.Body)
if err != nil { if err != nil {
@ -444,7 +445,12 @@ func (s *SQL) Query(labels map[string]string) ([]*rspb.Release, error) {
} }
// Create creates a new release. // Create creates a new release.
func (s *SQL) Create(key string, rls *rspb.Release) error { func (s *SQL) Create(key string, rel release.Releaser) error {
rls, err := releaserToV1Release(rel)
if err != nil {
return err
}
namespace := rls.Namespace namespace := rls.Namespace
if namespace == "" { if namespace == "" {
namespace = defaultNamespace namespace = defaultNamespace
@ -551,7 +557,11 @@ func (s *SQL) Create(key string, rls *rspb.Release) error {
} }
// Update updates a release. // Update updates a release.
func (s *SQL) Update(key string, rls *rspb.Release) error { func (s *SQL) Update(key string, rel release.Releaser) error {
rls, err := releaserToV1Release(rel)
if err != nil {
return err
}
namespace := rls.Namespace namespace := rls.Namespace
if namespace == "" { if namespace == "" {
namespace = defaultNamespace namespace = defaultNamespace
@ -590,7 +600,7 @@ func (s *SQL) Update(key string, rls *rspb.Release) error {
} }
// Delete deletes a release or returns ErrReleaseNotFound. // Delete deletes a release or returns ErrReleaseNotFound.
func (s *SQL) Delete(key string) (*rspb.Release, error) { func (s *SQL) Delete(key string) (release.Releaser, error) {
transaction, err := s.db.Beginx() transaction, err := s.db.Beginx()
if err != nil { if err != nil {
slog.Debug("failed to start SQL transaction", slog.Any("error", err)) slog.Debug("failed to start SQL transaction", slog.Any("error", err))

@ -14,6 +14,7 @@ limitations under the License.
package driver package driver
import ( import (
"database/sql/driver"
"fmt" "fmt"
"reflect" "reflect"
"regexp" "regexp"
@ -23,9 +24,38 @@ import (
sqlmock "github.com/DATA-DOG/go-sqlmock" sqlmock "github.com/DATA-DOG/go-sqlmock"
migrate "github.com/rubenv/sql-migrate" migrate "github.com/rubenv/sql-migrate"
"helm.sh/helm/v4/pkg/release"
"helm.sh/helm/v4/pkg/release/common"
rspb "helm.sh/helm/v4/pkg/release/v1" rspb "helm.sh/helm/v4/pkg/release/v1"
) )
const recentTimestampTolerance = time.Second
func recentUnixTimestamp() sqlmock.Argument {
return recentUnixTimestampArgument{}
}
type recentUnixTimestampArgument struct{}
func (recentUnixTimestampArgument) Match(value driver.Value) bool {
var ts int64
switch v := value.(type) {
case int:
ts = int64(v)
case int64:
ts = v
default:
return false
}
diff := time.Since(time.Unix(ts, 0))
if diff < 0 {
diff = -diff
}
return diff <= recentTimestampTolerance
}
func TestSQLName(t *testing.T) { func TestSQLName(t *testing.T) {
sqlDriver, _ := newTestFixtureSQL(t) sqlDriver, _ := newTestFixtureSQL(t)
if sqlDriver.Name() != SQLDriverName { if sqlDriver.Name() != SQLDriverName {
@ -38,7 +68,7 @@ func TestSQLGet(t *testing.T) {
name := "smug-pigeon" name := "smug-pigeon"
namespace := "default" namespace := "default"
key := testKey(name, vers) key := testKey(name, vers)
rel := releaseStub(name, vers, namespace, rspb.StatusDeployed) rel := releaseStub(name, vers, namespace, common.StatusDeployed)
body, _ := encodeRelease(rel) body, _ := encodeRelease(rel)
@ -81,12 +111,12 @@ func TestSQLGet(t *testing.T) {
func TestSQLList(t *testing.T) { func TestSQLList(t *testing.T) {
releases := []*rspb.Release{} releases := []*rspb.Release{}
releases = append(releases, releaseStub("key-1", 1, "default", rspb.StatusUninstalled)) releases = append(releases, releaseStub("key-1", 1, "default", common.StatusUninstalled))
releases = append(releases, releaseStub("key-2", 1, "default", rspb.StatusUninstalled)) releases = append(releases, releaseStub("key-2", 1, "default", common.StatusUninstalled))
releases = append(releases, releaseStub("key-3", 1, "default", rspb.StatusDeployed)) releases = append(releases, releaseStub("key-3", 1, "default", common.StatusDeployed))
releases = append(releases, releaseStub("key-4", 1, "default", rspb.StatusDeployed)) releases = append(releases, releaseStub("key-4", 1, "default", common.StatusDeployed))
releases = append(releases, releaseStub("key-5", 1, "default", rspb.StatusSuperseded)) releases = append(releases, releaseStub("key-5", 1, "default", common.StatusSuperseded))
releases = append(releases, releaseStub("key-6", 1, "default", rspb.StatusSuperseded)) releases = append(releases, releaseStub("key-6", 1, "default", common.StatusSuperseded))
sqlDriver, mock := newTestFixtureSQL(t) sqlDriver, mock := newTestFixtureSQL(t)
@ -119,8 +149,9 @@ func TestSQLList(t *testing.T) {
} }
// list all deleted releases // list all deleted releases
del, err := sqlDriver.List(func(rel *rspb.Release) bool { del, err := sqlDriver.List(func(rel release.Releaser) bool {
return rel.Info.Status == rspb.StatusUninstalled rls := convertReleaserToV1(t, rel)
return rls.Info.Status == common.StatusUninstalled
}) })
// check // check
if err != nil { if err != nil {
@ -131,8 +162,9 @@ func TestSQLList(t *testing.T) {
} }
// list all deployed releases // list all deployed releases
dpl, err := sqlDriver.List(func(rel *rspb.Release) bool { dpl, err := sqlDriver.List(func(rel release.Releaser) bool {
return rel.Info.Status == rspb.StatusDeployed rls := convertReleaserToV1(t, rel)
return rls.Info.Status == common.StatusDeployed
}) })
// check // check
if err != nil { if err != nil {
@ -143,8 +175,9 @@ func TestSQLList(t *testing.T) {
} }
// list all superseded releases // list all superseded releases
ssd, err := sqlDriver.List(func(rel *rspb.Release) bool { ssd, err := sqlDriver.List(func(rel release.Releaser) bool {
return rel.Info.Status == rspb.StatusSuperseded rls := convertReleaserToV1(t, rel)
return rls.Info.Status == common.StatusSuperseded
}) })
// check // check
if err != nil { if err != nil {
@ -159,7 +192,7 @@ func TestSQLList(t *testing.T) {
} }
// Check if release having both system and custom labels, this is needed to ensure that selector filtering would work. // Check if release having both system and custom labels, this is needed to ensure that selector filtering would work.
rls := ssd[0] rls := convertReleaserToV1(t, ssd[0])
_, ok := rls.Labels["name"] _, ok := rls.Labels["name"]
if !ok { if !ok {
t.Fatalf("Expected 'name' label in results, actual %v", rls.Labels) t.Fatalf("Expected 'name' label in results, actual %v", rls.Labels)
@ -175,7 +208,7 @@ func TestSqlCreate(t *testing.T) {
name := "smug-pigeon" name := "smug-pigeon"
namespace := "default" namespace := "default"
key := testKey(name, vers) key := testKey(name, vers)
rel := releaseStub(name, vers, namespace, rspb.StatusDeployed) rel := releaseStub(name, vers, namespace, common.StatusDeployed)
sqlDriver, mock := newTestFixtureSQL(t) sqlDriver, mock := newTestFixtureSQL(t)
body, _ := encodeRelease(rel) body, _ := encodeRelease(rel)
@ -197,7 +230,7 @@ func TestSqlCreate(t *testing.T) {
mock.ExpectBegin() mock.ExpectBegin()
mock. mock.
ExpectExec(regexp.QuoteMeta(query)). ExpectExec(regexp.QuoteMeta(query)).
WithArgs(key, sqlReleaseDefaultType, body, rel.Name, rel.Namespace, int(rel.Version), rel.Info.Status.String(), sqlReleaseDefaultOwner, int(time.Now().Unix())). WithArgs(key, sqlReleaseDefaultType, body, rel.Name, rel.Namespace, int(rel.Version), rel.Info.Status.String(), sqlReleaseDefaultOwner, recentUnixTimestamp()).
WillReturnResult(sqlmock.NewResult(1, 1)) WillReturnResult(sqlmock.NewResult(1, 1))
labelsQuery := fmt.Sprintf( labelsQuery := fmt.Sprintf(
@ -232,7 +265,7 @@ func TestSqlCreateAlreadyExists(t *testing.T) {
name := "smug-pigeon" name := "smug-pigeon"
namespace := "default" namespace := "default"
key := testKey(name, vers) key := testKey(name, vers)
rel := releaseStub(name, vers, namespace, rspb.StatusDeployed) rel := releaseStub(name, vers, namespace, common.StatusDeployed)
sqlDriver, mock := newTestFixtureSQL(t) sqlDriver, mock := newTestFixtureSQL(t)
body, _ := encodeRelease(rel) body, _ := encodeRelease(rel)
@ -255,7 +288,7 @@ func TestSqlCreateAlreadyExists(t *testing.T) {
mock.ExpectBegin() mock.ExpectBegin()
mock. mock.
ExpectExec(regexp.QuoteMeta(insertQuery)). ExpectExec(regexp.QuoteMeta(insertQuery)).
WithArgs(key, sqlReleaseDefaultType, body, rel.Name, rel.Namespace, int(rel.Version), rel.Info.Status.String(), sqlReleaseDefaultOwner, int(time.Now().Unix())). WithArgs(key, sqlReleaseDefaultType, body, rel.Name, rel.Namespace, int(rel.Version), rel.Info.Status.String(), sqlReleaseDefaultOwner, recentUnixTimestamp()).
WillReturnError(fmt.Errorf("dialect dependent SQL error")) WillReturnError(fmt.Errorf("dialect dependent SQL error"))
selectQuery := fmt.Sprintf( selectQuery := fmt.Sprintf(
@ -293,7 +326,7 @@ func TestSqlUpdate(t *testing.T) {
name := "smug-pigeon" name := "smug-pigeon"
namespace := "default" namespace := "default"
key := testKey(name, vers) key := testKey(name, vers)
rel := releaseStub(name, vers, namespace, rspb.StatusDeployed) rel := releaseStub(name, vers, namespace, common.StatusDeployed)
sqlDriver, mock := newTestFixtureSQL(t) sqlDriver, mock := newTestFixtureSQL(t)
body, _ := encodeRelease(rel) body, _ := encodeRelease(rel)
@ -313,7 +346,7 @@ func TestSqlUpdate(t *testing.T) {
mock. mock.
ExpectExec(regexp.QuoteMeta(query)). ExpectExec(regexp.QuoteMeta(query)).
WithArgs(body, rel.Name, int(rel.Version), rel.Info.Status.String(), sqlReleaseDefaultOwner, int(time.Now().Unix()), key, namespace). WithArgs(body, rel.Name, int(rel.Version), rel.Info.Status.String(), sqlReleaseDefaultOwner, recentUnixTimestamp(), key, namespace).
WillReturnResult(sqlmock.NewResult(0, 1)) WillReturnResult(sqlmock.NewResult(0, 1))
if err := sqlDriver.Update(key, rel); err != nil { if err := sqlDriver.Update(key, rel); err != nil {
@ -342,9 +375,9 @@ func TestSqlQuery(t *testing.T) {
"owner": sqlReleaseDefaultOwner, "owner": sqlReleaseDefaultOwner,
} }
supersededRelease := releaseStub("smug-pigeon", 1, "default", rspb.StatusSuperseded) supersededRelease := releaseStub("smug-pigeon", 1, "default", common.StatusSuperseded)
supersededReleaseBody, _ := encodeRelease(supersededRelease) supersededReleaseBody, _ := encodeRelease(supersededRelease)
deployedRelease := releaseStub("smug-pigeon", 2, "default", rspb.StatusDeployed) deployedRelease := releaseStub("smug-pigeon", 2, "default", common.StatusDeployed)
deployedReleaseBody, _ := encodeRelease(deployedRelease) deployedReleaseBody, _ := encodeRelease(deployedRelease)
// Let's actually start our test // Let's actually start our test
@ -454,7 +487,7 @@ func TestSqlDelete(t *testing.T) {
name := "smug-pigeon" name := "smug-pigeon"
namespace := "default" namespace := "default"
key := testKey(name, vers) key := testKey(name, vers)
rel := releaseStub(name, vers, namespace, rspb.StatusDeployed) rel := releaseStub(name, vers, namespace, common.StatusDeployed)
body, _ := encodeRelease(rel) body, _ := encodeRelease(rel)

@ -22,6 +22,8 @@ import (
"log/slog" "log/slog"
"strings" "strings"
"helm.sh/helm/v4/pkg/release"
"helm.sh/helm/v4/pkg/release/common"
rspb "helm.sh/helm/v4/pkg/release/v1" rspb "helm.sh/helm/v4/pkg/release/v1"
relutil "helm.sh/helm/v4/pkg/release/v1/util" relutil "helm.sh/helm/v4/pkg/release/v1/util"
"helm.sh/helm/v4/pkg/storage/driver" "helm.sh/helm/v4/pkg/storage/driver"
@ -47,7 +49,7 @@ type Storage struct {
// Get retrieves the release from storage. An error is returned // Get retrieves the release from storage. An error is returned
// if the storage driver failed to fetch the release, or the // if the storage driver failed to fetch the release, or the
// release identified by the key, version pair does not exist. // release identified by the key, version pair does not exist.
func (s *Storage) Get(name string, version int) (*rspb.Release, error) { func (s *Storage) Get(name string, version int) (release.Releaser, error) {
slog.Debug("getting release", "key", makeKey(name, version)) slog.Debug("getting release", "key", makeKey(name, version))
return s.Driver.Get(makeKey(name, version)) return s.Driver.Get(makeKey(name, version))
} }
@ -55,62 +57,99 @@ func (s *Storage) Get(name string, version int) (*rspb.Release, error) {
// Create creates a new storage entry holding the release. An // Create creates a new storage entry holding the release. An
// error is returned if the storage driver fails to store the // error is returned if the storage driver fails to store the
// release, or a release with an identical key already exists. // release, or a release with an identical key already exists.
func (s *Storage) Create(rls *rspb.Release) error { func (s *Storage) Create(rls release.Releaser) error {
slog.Debug("creating release", "key", makeKey(rls.Name, rls.Version)) rac, err := release.NewAccessor(rls)
if err != nil {
return err
}
slog.Debug("creating release", "key", makeKey(rac.Name(), rac.Version()))
if s.MaxHistory > 0 { if s.MaxHistory > 0 {
// Want to make space for one more release. // Want to make space for one more release.
if err := s.removeLeastRecent(rls.Name, s.MaxHistory-1); err != nil && if err := s.removeLeastRecent(rac.Name(), s.MaxHistory-1); err != nil &&
!errors.Is(err, driver.ErrReleaseNotFound) { !errors.Is(err, driver.ErrReleaseNotFound) {
return err return err
} }
} }
return s.Driver.Create(makeKey(rls.Name, rls.Version), rls) return s.Driver.Create(makeKey(rac.Name(), rac.Version()), rls)
} }
// Update updates the release in storage. An error is returned if the // Update updates the release in storage. An error is returned if the
// storage backend fails to update the release or if the release // storage backend fails to update the release or if the release
// does not exist. // does not exist.
func (s *Storage) Update(rls *rspb.Release) error { func (s *Storage) Update(rls release.Releaser) error {
slog.Debug("updating release", "key", makeKey(rls.Name, rls.Version)) rac, err := release.NewAccessor(rls)
return s.Driver.Update(makeKey(rls.Name, rls.Version), rls) if err != nil {
return err
}
slog.Debug("updating release", "key", makeKey(rac.Name(), rac.Version()))
return s.Driver.Update(makeKey(rac.Name(), rac.Version()), rls)
} }
// Delete deletes the release from storage. An error is returned if // Delete deletes the release from storage. An error is returned if
// the storage backend fails to delete the release or if the release // the storage backend fails to delete the release or if the release
// does not exist. // does not exist.
func (s *Storage) Delete(name string, version int) (*rspb.Release, error) { func (s *Storage) Delete(name string, version int) (release.Releaser, error) {
slog.Debug("deleting release", "key", makeKey(name, version)) slog.Debug("deleting release", "key", makeKey(name, version))
return s.Driver.Delete(makeKey(name, version)) return s.Driver.Delete(makeKey(name, version))
} }
// ListReleases returns all releases from storage. An error is returned if the // ListReleases returns all releases from storage. An error is returned if the
// storage backend fails to retrieve the releases. // storage backend fails to retrieve the releases.
func (s *Storage) ListReleases() ([]*rspb.Release, error) { func (s *Storage) ListReleases() ([]release.Releaser, error) {
slog.Debug("listing all releases in storage") slog.Debug("listing all releases in storage")
return s.List(func(_ *rspb.Release) bool { return true }) return s.List(func(_ release.Releaser) bool { return true })
}
// 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)
}
} }
// ListUninstalled returns all releases with Status == UNINSTALLED. An error is returned // ListUninstalled returns all releases with Status == UNINSTALLED. An error is returned
// if the storage backend fails to retrieve the releases. // if the storage backend fails to retrieve the releases.
func (s *Storage) ListUninstalled() ([]*rspb.Release, error) { func (s *Storage) ListUninstalled() ([]release.Releaser, error) {
slog.Debug("listing uninstalled releases in storage") slog.Debug("listing uninstalled releases in storage")
return s.List(func(rls *rspb.Release) bool { return s.List(func(rls release.Releaser) bool {
return relutil.StatusFilter(rspb.StatusUninstalled).Check(rls) rel, err := releaserToV1Release(rls)
if err != nil {
// This will only happen if calling code does not pass the proper types. This is
// a problem with the application and not user data.
slog.Error("unable to convert release to typed release", slog.Any("error", err))
panic(fmt.Sprintf("unable to convert release to typed release: %s", err))
}
return relutil.StatusFilter(common.StatusUninstalled).Check(rel)
}) })
} }
// ListDeployed returns all releases with Status == DEPLOYED. An error is returned // ListDeployed returns all releases with Status == DEPLOYED. An error is returned
// if the storage backend fails to retrieve the releases. // if the storage backend fails to retrieve the releases.
func (s *Storage) ListDeployed() ([]*rspb.Release, error) { func (s *Storage) ListDeployed() ([]release.Releaser, error) {
slog.Debug("listing all deployed releases in storage") slog.Debug("listing all deployed releases in storage")
return s.List(func(rls *rspb.Release) bool { return s.List(func(rls release.Releaser) bool {
return relutil.StatusFilter(rspb.StatusDeployed).Check(rls) rel, err := releaserToV1Release(rls)
if err != nil {
// This will only happen if calling code does not pass the proper types. This is
// a problem with the application and not user data.
slog.Error("unable to convert release to typed release", slog.Any("error", err))
panic(fmt.Sprintf("unable to convert release to typed release: %s", err))
}
return relutil.StatusFilter(common.StatusDeployed).Check(rel)
}) })
} }
// Deployed returns the last deployed release with the provided release name, or // Deployed returns the last deployed release with the provided release name, or
// returns driver.NewErrNoDeployedReleases if not found. // returns driver.NewErrNoDeployedReleases if not found.
func (s *Storage) Deployed(name string) (*rspb.Release, error) { func (s *Storage) Deployed(name string) (release.Releaser, error) {
ls, err := s.DeployedAll(name) ls, err := s.DeployedAll(name)
if err != nil { if err != nil {
return nil, err return nil, err
@ -120,16 +159,34 @@ func (s *Storage) Deployed(name string) (*rspb.Release, error) {
return nil, driver.NewErrNoDeployedReleases(name) return nil, driver.NewErrNoDeployedReleases(name)
} }
rls, err := releaseListToV1List(ls)
if err != nil {
return nil, err
}
// If executed concurrently, Helm's database gets corrupted // If executed concurrently, Helm's database gets corrupted
// and multiple releases are DEPLOYED. Take the latest. // and multiple releases are DEPLOYED. Take the latest.
relutil.Reverse(ls, relutil.SortByRevision) relutil.Reverse(rls, relutil.SortByRevision)
return ls[0], nil return rls[0], nil
}
func releaseListToV1List(ls []release.Releaser) ([]*rspb.Release, error) {
rls := make([]*rspb.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
} }
// DeployedAll returns all deployed releases with the provided name, or // DeployedAll returns all deployed releases with the provided name, or
// returns driver.NewErrNoDeployedReleases if not found. // returns driver.NewErrNoDeployedReleases if not found.
func (s *Storage) DeployedAll(name string) ([]*rspb.Release, error) { func (s *Storage) DeployedAll(name string) ([]release.Releaser, error) {
slog.Debug("getting deployed releases", "name", name) slog.Debug("getting deployed releases", "name", name)
ls, err := s.Query(map[string]string{ ls, err := s.Query(map[string]string{
@ -148,7 +205,7 @@ func (s *Storage) DeployedAll(name string) ([]*rspb.Release, error) {
// History returns the revision history for the release with the provided name, or // History returns the revision history for the release with the provided name, or
// returns driver.ErrReleaseNotFound if no such release name exists. // returns driver.ErrReleaseNotFound if no such release name exists.
func (s *Storage) History(name string) ([]*rspb.Release, error) { func (s *Storage) History(name string) ([]release.Releaser, error) {
slog.Debug("getting release history", "name", name) slog.Debug("getting release history", "name", name)
return s.Query(map[string]string{"name": name, "owner": "helm"}) return s.Query(map[string]string{"name": name, "owner": "helm"})
@ -170,23 +227,31 @@ func (s *Storage) removeLeastRecent(name string, maximum int) error {
if len(h) <= maximum { if len(h) <= maximum {
return nil return nil
} }
rls, err := releaseListToV1List(h)
if err != nil {
return err
}
// We want oldest to newest // We want oldest to newest
relutil.SortByRevision(h) relutil.SortByRevision(rls)
lastDeployed, err := s.Deployed(name) lastDeployed, err := s.Deployed(name)
if err != nil && !errors.Is(err, driver.ErrNoDeployedReleases) { if err != nil && !errors.Is(err, driver.ErrNoDeployedReleases) {
return err return err
} }
var toDelete []*rspb.Release var toDelete []release.Releaser
for _, rel := range h { for _, rel := range rls {
// once we have enough releases to delete to reach the maximum, stop // once we have enough releases to delete to reach the maximum, stop
if len(h)-len(toDelete) == maximum { if len(rls)-len(toDelete) == maximum {
break break
} }
if lastDeployed != nil { if lastDeployed != nil {
if rel.Version != lastDeployed.Version { ldac, err := release.NewAccessor(lastDeployed)
if err != nil {
return err
}
if rel.Version != ldac.Version() {
toDelete = append(toDelete, rel) toDelete = append(toDelete, rel)
} }
} else { } else {
@ -198,7 +263,12 @@ func (s *Storage) removeLeastRecent(name string, maximum int) error {
// multiple invocations of this function will eventually delete them all. // multiple invocations of this function will eventually delete them all.
errs := []error{} errs := []error{}
for _, rel := range toDelete { for _, rel := range toDelete {
err = s.deleteReleaseVersion(name, rel.Version) rac, err := release.NewAccessor(rel)
if err != nil {
errs = append(errs, err)
continue
}
err = s.deleteReleaseVersion(name, rac.Version())
if err != nil { if err != nil {
errs = append(errs, err) errs = append(errs, err)
} }
@ -226,7 +296,7 @@ func (s *Storage) deleteReleaseVersion(name string, version int) error {
} }
// Last fetches the last revision of the named release. // Last fetches the last revision of the named release.
func (s *Storage) Last(name string) (*rspb.Release, error) { func (s *Storage) Last(name string) (release.Releaser, error) {
slog.Debug("getting last revision", "name", name) slog.Debug("getting last revision", "name", name)
h, err := s.History(name) h, err := s.History(name)
if err != nil { if err != nil {
@ -235,9 +305,13 @@ func (s *Storage) Last(name string) (*rspb.Release, error) {
if len(h) == 0 { if len(h) == 0 {
return nil, fmt.Errorf("no revision for release %q", name) return nil, fmt.Errorf("no revision for release %q", name)
} }
rls, err := releaseListToV1List(h)
if err != nil {
return nil, err
}
relutil.Reverse(h, relutil.SortByRevision) relutil.Reverse(rls, relutil.SortByRevision)
return h[0], nil return rls[0], nil
} }
// makeKey concatenates the Kubernetes storage object type, a release name and version // makeKey concatenates the Kubernetes storage object type, a release name and version

@ -22,6 +22,10 @@ import (
"reflect" "reflect"
"testing" "testing"
"github.com/stretchr/testify/assert"
"helm.sh/helm/v4/pkg/release"
"helm.sh/helm/v4/pkg/release/common"
rspb "helm.sh/helm/v4/pkg/release/v1" rspb "helm.sh/helm/v4/pkg/release/v1"
"helm.sh/helm/v4/pkg/storage/driver" "helm.sh/helm/v4/pkg/storage/driver"
) )
@ -56,13 +60,13 @@ func TestStorageUpdate(t *testing.T) {
rls := ReleaseTestData{ rls := ReleaseTestData{
Name: "angry-beaver", Name: "angry-beaver",
Version: 1, Version: 1,
Status: rspb.StatusDeployed, Status: common.StatusDeployed,
}.ToRelease() }.ToRelease()
assertErrNil(t.Fatal, storage.Create(rls), "StoreRelease") assertErrNil(t.Fatal, storage.Create(rls), "StoreRelease")
// modify the release // modify the release
rls.Info.Status = rspb.StatusUninstalled rls.Info.Status = common.StatusUninstalled
assertErrNil(t.Fatal, storage.Update(rls), "UpdateRelease") assertErrNil(t.Fatal, storage.Update(rls), "UpdateRelease")
// retrieve the updated release // retrieve the updated release
@ -106,13 +110,16 @@ func TestStorageDelete(t *testing.T) {
t.Errorf("unexpected error: %s", err) t.Errorf("unexpected error: %s", err)
} }
rhist, err := releaseListToV1List(hist)
assert.NoError(t, err)
// We have now deleted one of the two records. // We have now deleted one of the two records.
if len(hist) != 1 { if len(rhist) != 1 {
t.Errorf("expected 1 record for deleted release version, got %d", len(hist)) t.Errorf("expected 1 record for deleted release version, got %d", len(hist))
} }
if hist[0].Version != 2 { if rhist[0].Version != 2 {
t.Errorf("Expected version to be 2, got %d", hist[0].Version) t.Errorf("Expected version to be 2, got %d", rhist[0].Version)
} }
} }
@ -123,13 +130,13 @@ func TestStorageList(t *testing.T) {
// setup storage with test releases // setup storage with test releases
setup := func() { setup := func() {
// release records // release records
rls0 := ReleaseTestData{Name: "happy-catdog", Status: rspb.StatusSuperseded}.ToRelease() rls0 := ReleaseTestData{Name: "happy-catdog", Status: common.StatusSuperseded}.ToRelease()
rls1 := ReleaseTestData{Name: "livid-human", Status: rspb.StatusSuperseded}.ToRelease() rls1 := ReleaseTestData{Name: "livid-human", Status: common.StatusSuperseded}.ToRelease()
rls2 := ReleaseTestData{Name: "relaxed-cat", Status: rspb.StatusSuperseded}.ToRelease() rls2 := ReleaseTestData{Name: "relaxed-cat", Status: common.StatusSuperseded}.ToRelease()
rls3 := ReleaseTestData{Name: "hungry-hippo", Status: rspb.StatusDeployed}.ToRelease() rls3 := ReleaseTestData{Name: "hungry-hippo", Status: common.StatusDeployed}.ToRelease()
rls4 := ReleaseTestData{Name: "angry-beaver", Status: rspb.StatusDeployed}.ToRelease() rls4 := ReleaseTestData{Name: "angry-beaver", Status: common.StatusDeployed}.ToRelease()
rls5 := ReleaseTestData{Name: "opulent-frog", Status: rspb.StatusUninstalled}.ToRelease() rls5 := ReleaseTestData{Name: "opulent-frog", Status: common.StatusUninstalled}.ToRelease()
rls6 := ReleaseTestData{Name: "happy-liger", Status: rspb.StatusUninstalled}.ToRelease() rls6 := ReleaseTestData{Name: "happy-liger", Status: common.StatusUninstalled}.ToRelease()
// create the release records in the storage // create the release records in the storage
assertErrNil(t.Fatal, storage.Create(rls0), "Storing release 'rls0'") assertErrNil(t.Fatal, storage.Create(rls0), "Storing release 'rls0'")
@ -144,7 +151,7 @@ func TestStorageList(t *testing.T) {
var listTests = []struct { var listTests = []struct {
Description string Description string
NumExpected int NumExpected int
ListFunc func() ([]*rspb.Release, error) ListFunc func() ([]release.Releaser, error)
}{ }{
{"ListDeployed", 2, storage.ListDeployed}, {"ListDeployed", 2, storage.ListDeployed},
{"ListReleases", 7, storage.ListReleases}, {"ListReleases", 7, storage.ListReleases},
@ -175,10 +182,10 @@ func TestStorageDeployed(t *testing.T) {
// setup storage with test releases // setup storage with test releases
setup := func() { setup := func() {
// release records // release records
rls0 := ReleaseTestData{Name: name, Version: 1, Status: rspb.StatusSuperseded}.ToRelease() rls0 := ReleaseTestData{Name: name, Version: 1, Status: common.StatusSuperseded}.ToRelease()
rls1 := ReleaseTestData{Name: name, Version: 2, Status: rspb.StatusSuperseded}.ToRelease() rls1 := ReleaseTestData{Name: name, Version: 2, Status: common.StatusSuperseded}.ToRelease()
rls2 := ReleaseTestData{Name: name, Version: 3, Status: rspb.StatusSuperseded}.ToRelease() rls2 := ReleaseTestData{Name: name, Version: 3, Status: common.StatusSuperseded}.ToRelease()
rls3 := ReleaseTestData{Name: name, Version: 4, Status: rspb.StatusDeployed}.ToRelease() rls3 := ReleaseTestData{Name: name, Version: 4, Status: common.StatusDeployed}.ToRelease()
// create the release records in the storage // create the release records in the storage
assertErrNil(t.Fatal, storage.Create(rls0), "Storing release 'angry-bird' (v1)") assertErrNil(t.Fatal, storage.Create(rls0), "Storing release 'angry-bird' (v1)")
@ -194,15 +201,18 @@ func TestStorageDeployed(t *testing.T) {
t.Fatalf("Failed to query for deployed release: %s\n", err) t.Fatalf("Failed to query for deployed release: %s\n", err)
} }
rel, err := releaserToV1Release(rls)
assert.NoError(t, err)
switch { switch {
case rls == nil: case rls == nil:
t.Fatalf("Release is nil") t.Fatalf("Release is nil")
case rls.Name != name: case rel.Name != name:
t.Fatalf("Expected release name %q, actual %q\n", name, rls.Name) t.Fatalf("Expected release name %q, actual %q\n", name, rel.Name)
case rls.Version != vers: case rel.Version != vers:
t.Fatalf("Expected release version %d, actual %d\n", vers, rls.Version) t.Fatalf("Expected release version %d, actual %d\n", vers, rel.Version)
case rls.Info.Status != rspb.StatusDeployed: case rel.Info.Status != common.StatusDeployed:
t.Fatalf("Expected release status 'DEPLOYED', actual %s\n", rls.Info.Status.String()) t.Fatalf("Expected release status 'DEPLOYED', actual %s\n", rel.Info.Status.String())
} }
} }
@ -215,10 +225,10 @@ func TestStorageDeployedWithCorruption(t *testing.T) {
// setup storage with test releases // setup storage with test releases
setup := func() { setup := func() {
// release records (notice odd order and corruption) // release records (notice odd order and corruption)
rls0 := ReleaseTestData{Name: name, Version: 1, Status: rspb.StatusSuperseded}.ToRelease() rls0 := ReleaseTestData{Name: name, Version: 1, Status: common.StatusSuperseded}.ToRelease()
rls1 := ReleaseTestData{Name: name, Version: 4, Status: rspb.StatusDeployed}.ToRelease() rls1 := ReleaseTestData{Name: name, Version: 4, Status: common.StatusDeployed}.ToRelease()
rls2 := ReleaseTestData{Name: name, Version: 3, Status: rspb.StatusSuperseded}.ToRelease() rls2 := ReleaseTestData{Name: name, Version: 3, Status: common.StatusSuperseded}.ToRelease()
rls3 := ReleaseTestData{Name: name, Version: 2, Status: rspb.StatusDeployed}.ToRelease() rls3 := ReleaseTestData{Name: name, Version: 2, Status: common.StatusDeployed}.ToRelease()
// create the release records in the storage // create the release records in the storage
assertErrNil(t.Fatal, storage.Create(rls0), "Storing release 'angry-bird' (v1)") assertErrNil(t.Fatal, storage.Create(rls0), "Storing release 'angry-bird' (v1)")
@ -234,15 +244,18 @@ func TestStorageDeployedWithCorruption(t *testing.T) {
t.Fatalf("Failed to query for deployed release: %s\n", err) t.Fatalf("Failed to query for deployed release: %s\n", err)
} }
rel, err := releaserToV1Release(rls)
assert.NoError(t, err)
switch { switch {
case rls == nil: case rls == nil:
t.Fatalf("Release is nil") t.Fatalf("Release is nil")
case rls.Name != name: case rel.Name != name:
t.Fatalf("Expected release name %q, actual %q\n", name, rls.Name) t.Fatalf("Expected release name %q, actual %q\n", name, rel.Name)
case rls.Version != vers: case rel.Version != vers:
t.Fatalf("Expected release version %d, actual %d\n", vers, rls.Version) t.Fatalf("Expected release version %d, actual %d\n", vers, rel.Version)
case rls.Info.Status != rspb.StatusDeployed: case rel.Info.Status != common.StatusDeployed:
t.Fatalf("Expected release status 'DEPLOYED', actual %s\n", rls.Info.Status.String()) t.Fatalf("Expected release status 'DEPLOYED', actual %s\n", rel.Info.Status.String())
} }
} }
@ -254,10 +267,10 @@ func TestStorageHistory(t *testing.T) {
// setup storage with test releases // setup storage with test releases
setup := func() { setup := func() {
// release records // release records
rls0 := ReleaseTestData{Name: name, Version: 1, Status: rspb.StatusSuperseded}.ToRelease() rls0 := ReleaseTestData{Name: name, Version: 1, Status: common.StatusSuperseded}.ToRelease()
rls1 := ReleaseTestData{Name: name, Version: 2, Status: rspb.StatusSuperseded}.ToRelease() rls1 := ReleaseTestData{Name: name, Version: 2, Status: common.StatusSuperseded}.ToRelease()
rls2 := ReleaseTestData{Name: name, Version: 3, Status: rspb.StatusSuperseded}.ToRelease() rls2 := ReleaseTestData{Name: name, Version: 3, Status: common.StatusSuperseded}.ToRelease()
rls3 := ReleaseTestData{Name: name, Version: 4, Status: rspb.StatusDeployed}.ToRelease() rls3 := ReleaseTestData{Name: name, Version: 4, Status: common.StatusDeployed}.ToRelease()
// create the release records in the storage // create the release records in the storage
assertErrNil(t.Fatal, storage.Create(rls0), "Storing release 'angry-bird' (v1)") assertErrNil(t.Fatal, storage.Create(rls0), "Storing release 'angry-bird' (v1)")
@ -286,22 +299,22 @@ type MaxHistoryMockDriver struct {
func NewMaxHistoryMockDriver(d driver.Driver) *MaxHistoryMockDriver { func NewMaxHistoryMockDriver(d driver.Driver) *MaxHistoryMockDriver {
return &MaxHistoryMockDriver{Driver: d} return &MaxHistoryMockDriver{Driver: d}
} }
func (d *MaxHistoryMockDriver) Create(key string, rls *rspb.Release) error { func (d *MaxHistoryMockDriver) Create(key string, rls release.Releaser) error {
return d.Driver.Create(key, rls) return d.Driver.Create(key, rls)
} }
func (d *MaxHistoryMockDriver) Update(key string, rls *rspb.Release) error { func (d *MaxHistoryMockDriver) Update(key string, rls release.Releaser) error {
return d.Driver.Update(key, rls) return d.Driver.Update(key, rls)
} }
func (d *MaxHistoryMockDriver) Delete(_ string) (*rspb.Release, error) { func (d *MaxHistoryMockDriver) Delete(_ string) (release.Releaser, error) {
return nil, errMaxHistoryMockDriverSomethingHappened return nil, errMaxHistoryMockDriverSomethingHappened
} }
func (d *MaxHistoryMockDriver) Get(key string) (*rspb.Release, error) { func (d *MaxHistoryMockDriver) Get(key string) (release.Releaser, error) {
return d.Driver.Get(key) return d.Driver.Get(key)
} }
func (d *MaxHistoryMockDriver) List(filter func(*rspb.Release) bool) ([]*rspb.Release, error) { func (d *MaxHistoryMockDriver) List(filter func(release.Releaser) bool) ([]release.Releaser, error) {
return d.Driver.List(filter) return d.Driver.List(filter)
} }
func (d *MaxHistoryMockDriver) Query(labels map[string]string) ([]*rspb.Release, error) { func (d *MaxHistoryMockDriver) Query(labels map[string]string) ([]release.Releaser, error) {
return d.Driver.Query(labels) return d.Driver.Query(labels)
} }
func (d *MaxHistoryMockDriver) Name() string { func (d *MaxHistoryMockDriver) Name() string {
@ -319,14 +332,14 @@ func TestMaxHistoryErrorHandling(t *testing.T) {
// setup storage with test releases // setup storage with test releases
setup := func() { setup := func() {
// release records // release records
rls1 := ReleaseTestData{Name: name, Version: 1, Status: rspb.StatusSuperseded}.ToRelease() rls1 := ReleaseTestData{Name: name, Version: 1, Status: common.StatusSuperseded}.ToRelease()
// create the release records in the storage // create the release records in the storage
assertErrNil(t.Fatal, storage.Driver.Create(makeKey(rls1.Name, rls1.Version), rls1), "Storing release 'angry-bird' (v1)") assertErrNil(t.Fatal, storage.Driver.Create(makeKey(rls1.Name, rls1.Version), rls1), "Storing release 'angry-bird' (v1)")
} }
setup() setup()
rls2 := ReleaseTestData{Name: name, Version: 2, Status: rspb.StatusSuperseded}.ToRelease() rls2 := ReleaseTestData{Name: name, Version: 2, Status: common.StatusSuperseded}.ToRelease()
wantErr := errMaxHistoryMockDriverSomethingHappened wantErr := errMaxHistoryMockDriverSomethingHappened
gotErr := storage.Create(rls2) gotErr := storage.Create(rls2)
if !errors.Is(gotErr, wantErr) { if !errors.Is(gotErr, wantErr) {
@ -345,10 +358,10 @@ func TestStorageRemoveLeastRecent(t *testing.T) {
// setup storage with test releases // setup storage with test releases
setup := func() { setup := func() {
// release records // release records
rls0 := ReleaseTestData{Name: name, Version: 1, Status: rspb.StatusSuperseded}.ToRelease() rls0 := ReleaseTestData{Name: name, Version: 1, Status: common.StatusSuperseded}.ToRelease()
rls1 := ReleaseTestData{Name: name, Version: 2, Status: rspb.StatusSuperseded}.ToRelease() rls1 := ReleaseTestData{Name: name, Version: 2, Status: common.StatusSuperseded}.ToRelease()
rls2 := ReleaseTestData{Name: name, Version: 3, Status: rspb.StatusSuperseded}.ToRelease() rls2 := ReleaseTestData{Name: name, Version: 3, Status: common.StatusSuperseded}.ToRelease()
rls3 := ReleaseTestData{Name: name, Version: 4, Status: rspb.StatusDeployed}.ToRelease() rls3 := ReleaseTestData{Name: name, Version: 4, Status: common.StatusDeployed}.ToRelease()
// create the release records in the storage // create the release records in the storage
assertErrNil(t.Fatal, storage.Create(rls0), "Storing release 'angry-bird' (v1)") assertErrNil(t.Fatal, storage.Create(rls0), "Storing release 'angry-bird' (v1)")
@ -367,22 +380,25 @@ func TestStorageRemoveLeastRecent(t *testing.T) {
} }
storage.MaxHistory = 3 storage.MaxHistory = 3
rls5 := ReleaseTestData{Name: name, Version: 5, Status: rspb.StatusDeployed}.ToRelease() rls5 := ReleaseTestData{Name: name, Version: 5, Status: common.StatusDeployed}.ToRelease()
assertErrNil(t.Fatal, storage.Create(rls5), "Storing release 'angry-bird' (v5)") assertErrNil(t.Fatal, storage.Create(rls5), "Storing release 'angry-bird' (v5)")
// On inserting the 5th record, we expect two records to be pruned from history. // On inserting the 5th record, we expect two records to be pruned from history.
hist, err := storage.History(name) hist, err := storage.History(name)
assert.NoError(t, err)
rhist, err := releaseListToV1List(hist)
assert.NoError(t, err)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} else if len(hist) != storage.MaxHistory { } else if len(rhist) != storage.MaxHistory {
for _, item := range hist { for _, item := range rhist {
t.Logf("%s %v", item.Name, item.Version) t.Logf("%s %v", item.Name, item.Version)
} }
t.Fatalf("expected %d items in history, got %d", storage.MaxHistory, len(hist)) t.Fatalf("expected %d items in history, got %d", storage.MaxHistory, len(rhist))
} }
// We expect the existing records to be 3, 4, and 5. // We expect the existing records to be 3, 4, and 5.
for i, item := range hist { for i, item := range rhist {
v := item.Version v := item.Version
if expect := i + 3; v != expect { if expect := i + 3; v != expect {
t.Errorf("Expected release %d, got %d", expect, v) t.Errorf("Expected release %d, got %d", expect, v)
@ -399,10 +415,10 @@ func TestStorageDoNotDeleteDeployed(t *testing.T) {
// setup storage with test releases // setup storage with test releases
setup := func() { setup := func() {
// release records // release records
rls0 := ReleaseTestData{Name: name, Version: 1, Status: rspb.StatusSuperseded}.ToRelease() rls0 := ReleaseTestData{Name: name, Version: 1, Status: common.StatusSuperseded}.ToRelease()
rls1 := ReleaseTestData{Name: name, Version: 2, Status: rspb.StatusDeployed}.ToRelease() rls1 := ReleaseTestData{Name: name, Version: 2, Status: common.StatusDeployed}.ToRelease()
rls2 := ReleaseTestData{Name: name, Version: 3, Status: rspb.StatusFailed}.ToRelease() rls2 := ReleaseTestData{Name: name, Version: 3, Status: common.StatusFailed}.ToRelease()
rls3 := ReleaseTestData{Name: name, Version: 4, Status: rspb.StatusFailed}.ToRelease() rls3 := ReleaseTestData{Name: name, Version: 4, Status: common.StatusFailed}.ToRelease()
// create the release records in the storage // create the release records in the storage
assertErrNil(t.Fatal, storage.Create(rls0), "Storing release 'angry-bird' (v1)") assertErrNil(t.Fatal, storage.Create(rls0), "Storing release 'angry-bird' (v1)")
@ -412,7 +428,7 @@ func TestStorageDoNotDeleteDeployed(t *testing.T) {
} }
setup() setup()
rls5 := ReleaseTestData{Name: name, Version: 5, Status: rspb.StatusFailed}.ToRelease() rls5 := ReleaseTestData{Name: name, Version: 5, Status: common.StatusFailed}.ToRelease()
assertErrNil(t.Fatal, storage.Create(rls5), "Storing release 'angry-bird' (v5)") assertErrNil(t.Fatal, storage.Create(rls5), "Storing release 'angry-bird' (v5)")
// On inserting the 5th record, we expect a total of 3 releases, but we expect version 2 // On inserting the 5th record, we expect a total of 3 releases, but we expect version 2
@ -421,10 +437,12 @@ func TestStorageDoNotDeleteDeployed(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} else if len(hist) != storage.MaxHistory { } else if len(hist) != storage.MaxHistory {
for _, item := range hist { rhist, err := releaseListToV1List(hist)
assert.NoError(t, err)
for _, item := range rhist {
t.Logf("%s %v", item.Name, item.Version) t.Logf("%s %v", item.Name, item.Version)
} }
t.Fatalf("expected %d items in history, got %d", storage.MaxHistory, len(hist)) t.Fatalf("expected %d items in history, got %d", storage.MaxHistory, len(rhist))
} }
expectedVersions := map[int]bool{ expectedVersions := map[int]bool{
@ -433,7 +451,9 @@ func TestStorageDoNotDeleteDeployed(t *testing.T) {
5: true, 5: true,
} }
for _, item := range hist { rhist, err := releaseListToV1List(hist)
assert.NoError(t, err)
for _, item := range rhist {
if !expectedVersions[item.Version] { if !expectedVersions[item.Version] {
t.Errorf("Release version %d, found when not expected", item.Version) t.Errorf("Release version %d, found when not expected", item.Version)
} }
@ -448,10 +468,10 @@ func TestStorageLast(t *testing.T) {
// Set up storage with test releases. // Set up storage with test releases.
setup := func() { setup := func() {
// release records // release records
rls0 := ReleaseTestData{Name: name, Version: 1, Status: rspb.StatusSuperseded}.ToRelease() rls0 := ReleaseTestData{Name: name, Version: 1, Status: common.StatusSuperseded}.ToRelease()
rls1 := ReleaseTestData{Name: name, Version: 2, Status: rspb.StatusSuperseded}.ToRelease() rls1 := ReleaseTestData{Name: name, Version: 2, Status: common.StatusSuperseded}.ToRelease()
rls2 := ReleaseTestData{Name: name, Version: 3, Status: rspb.StatusSuperseded}.ToRelease() rls2 := ReleaseTestData{Name: name, Version: 3, Status: common.StatusSuperseded}.ToRelease()
rls3 := ReleaseTestData{Name: name, Version: 4, Status: rspb.StatusFailed}.ToRelease() rls3 := ReleaseTestData{Name: name, Version: 4, Status: common.StatusFailed}.ToRelease()
// create the release records in the storage // create the release records in the storage
assertErrNil(t.Fatal, storage.Create(rls0), "Storing release 'angry-bird' (v1)") assertErrNil(t.Fatal, storage.Create(rls0), "Storing release 'angry-bird' (v1)")
@ -467,8 +487,11 @@ func TestStorageLast(t *testing.T) {
t.Fatalf("Failed to query for release history (%q): %s\n", name, err) t.Fatalf("Failed to query for release history (%q): %s\n", name, err)
} }
if h.Version != 4 { rel, err := releaserToV1Release(h)
t.Errorf("Expected revision 4, got %d", h.Version) assert.NoError(t, err)
if rel.Version != 4 {
t.Errorf("Expected revision 4, got %d", rel.Version)
} }
} }
@ -483,10 +506,10 @@ func TestUpgradeInitiallyFailedReleaseWithHistoryLimit(t *testing.T) {
// setup storage with test releases // setup storage with test releases
setup := func() { setup := func() {
// release records // release records
rls0 := ReleaseTestData{Name: name, Version: 1, Status: rspb.StatusFailed}.ToRelease() rls0 := ReleaseTestData{Name: name, Version: 1, Status: common.StatusFailed}.ToRelease()
rls1 := ReleaseTestData{Name: name, Version: 2, Status: rspb.StatusFailed}.ToRelease() rls1 := ReleaseTestData{Name: name, Version: 2, Status: common.StatusFailed}.ToRelease()
rls2 := ReleaseTestData{Name: name, Version: 3, Status: rspb.StatusFailed}.ToRelease() rls2 := ReleaseTestData{Name: name, Version: 3, Status: common.StatusFailed}.ToRelease()
rls3 := ReleaseTestData{Name: name, Version: 4, Status: rspb.StatusFailed}.ToRelease() rls3 := ReleaseTestData{Name: name, Version: 4, Status: common.StatusFailed}.ToRelease()
// create the release records in the storage // create the release records in the storage
assertErrNil(t.Fatal, storage.Create(rls0), "Storing release 'angry-bird' (v1)") assertErrNil(t.Fatal, storage.Create(rls0), "Storing release 'angry-bird' (v1)")
@ -507,7 +530,7 @@ func TestUpgradeInitiallyFailedReleaseWithHistoryLimit(t *testing.T) {
setup() setup()
rls5 := ReleaseTestData{Name: name, Version: 5, Status: rspb.StatusFailed}.ToRelease() rls5 := ReleaseTestData{Name: name, Version: 5, Status: common.StatusFailed}.ToRelease()
err := storage.Create(rls5) err := storage.Create(rls5)
if err != nil { if err != nil {
t.Fatalf("Failed to create a new release version: %s", err) t.Fatalf("Failed to create a new release version: %s", err)
@ -518,13 +541,15 @@ func TestUpgradeInitiallyFailedReleaseWithHistoryLimit(t *testing.T) {
t.Fatalf("unexpected error: %s", err) t.Fatalf("unexpected error: %s", err)
} }
for i, rel := range hist { rhist, err := releaseListToV1List(hist)
assert.NoError(t, err)
for i, rel := range rhist {
wantVersion := i + 2 wantVersion := i + 2
if rel.Version != wantVersion { if rel.Version != wantVersion {
t.Fatalf("Expected history release %d version to equal %d, got %d", i+1, wantVersion, rel.Version) t.Fatalf("Expected history release %d version to equal %d, got %d", i+1, wantVersion, rel.Version)
} }
wantStatus := rspb.StatusFailed wantStatus := common.StatusFailed
if rel.Info.Status != wantStatus { if rel.Info.Status != wantStatus {
t.Fatalf("Expected history release %d status to equal %q, got %q", i+1, wantStatus, rel.Info.Status) t.Fatalf("Expected history release %d status to equal %q, got %q", i+1, wantStatus, rel.Info.Status)
} }
@ -536,7 +561,7 @@ type ReleaseTestData struct {
Version int Version int
Manifest string Manifest string
Namespace string Namespace string
Status rspb.Status Status common.Status
} }
func (test ReleaseTestData) ToRelease() *rspb.Release { func (test ReleaseTestData) ToRelease() *rspb.Release {

@ -19,7 +19,18 @@ set -euo pipefail
covermode=${COVERMODE:-atomic} covermode=${COVERMODE:-atomic}
coverdir=$(mktemp -d /tmp/coverage.XXXXXXXXXX) coverdir=$(mktemp -d /tmp/coverage.XXXXXXXXXX)
profile="${coverdir}/cover.out" profile="${coverdir}/cover.out"
target="${1:-./...}" # by default the whole repository is tested html=false
target="./..." # by default the whole repository is tested
for arg in "$@"; do
case "${arg}" in
--html)
html=true
;;
*)
target="${arg}"
;;
esac
done
generate_cover_data() { generate_cover_data() {
for d in $(go list "$target"); do for d in $(go list "$target"); do
@ -36,9 +47,7 @@ generate_cover_data() {
generate_cover_data generate_cover_data
go tool cover -func "${profile}" go tool cover -func "${profile}"
case "${1-}" in if [ "${html}" == "true" ] ; then
--html)
go tool cover -html "${profile}" go tool cover -html "${profile}"
;; fi
esac

Loading…
Cancel
Save