Merge branch 'main' into em/fix-username-password

Signed-off-by: Evans Mungai <mbuevans@gmail.com>
pull/31115/head
Evans Mungai 1 month ago committed by GitHub
commit 0943d032a7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -18,7 +18,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout source code - name: Checkout source code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # pin@v4.2.2 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # pin@v5.0.0
- name: Add variables to environment file - name: Add variables to environment file
run: cat ".github/env" >> "$GITHUB_ENV" run: cat ".github/env" >> "$GITHUB_ENV"
- name: Setup Go - name: Setup Go

@ -43,7 +43,7 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # pin@v4.2.2 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # pin@v5.0.0
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL

@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # pin@v4.2.2 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # pin@v5.0.0
- name: Add variables to environment file - name: Add variables to environment file
run: cat ".github/env" >> "$GITHUB_ENV" run: cat ".github/env" >> "$GITHUB_ENV"
- name: Setup Go - name: Setup Go

@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # pin@v4.2.2 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # pin@v5.0.0
- name: Add variables to environment file - name: Add variables to environment file
run: cat ".github/env" >> "$GITHUB_ENV" run: cat ".github/env" >> "$GITHUB_ENV"
- name: Setup Go - name: Setup Go

@ -20,7 +20,7 @@ jobs:
runs-on: ubuntu-latest-16-cores runs-on: ubuntu-latest-16-cores
steps: steps:
- name: Checkout source code - name: Checkout source code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # pin@v4.2.2 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # pin@v5.0.0
with: with:
fetch-depth: 0 fetch-depth: 0
@ -79,7 +79,7 @@ jobs:
if: github.ref == 'refs/heads/main' if: github.ref == 'refs/heads/main'
steps: steps:
- name: Checkout source code - name: Checkout source code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # pin@v4.2.2 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # pin@v5.0.0
- name: Add variables to environment file - name: Add variables to environment file
run: cat ".github/env" >> "$GITHUB_ENV" run: cat ".github/env" >> "$GITHUB_ENV"

@ -28,7 +28,7 @@ jobs:
steps: steps:
- name: "Checkout code" - name: "Checkout code"
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with: with:
persist-credentials: false persist-credentials: false

@ -14,7 +14,7 @@ require (
github.com/cyphar/filepath-securejoin v0.4.1 github.com/cyphar/filepath-securejoin v0.4.1
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/fatih/color v1.13.0 github.com/fatih/color v1.18.0
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
@ -32,9 +32,9 @@ require (
github.com/spf13/pflag v1.0.7 github.com/spf13/pflag v1.0.7
github.com/stretchr/testify v1.10.0 github.com/stretchr/testify v1.10.0
go.yaml.in/yaml/v3 v3.0.4 go.yaml.in/yaml/v3 v3.0.4
golang.org/x/crypto v0.40.0 golang.org/x/crypto v0.41.0
golang.org/x/term v0.33.0 golang.org/x/term v0.34.0
golang.org/x/text v0.27.0 golang.org/x/text v0.28.0
k8s.io/api v0.33.3 k8s.io/api v0.33.3
k8s.io/apiextensions-apiserver v0.33.3 k8s.io/apiextensions-apiserver v0.33.3
k8s.io/apimachinery v0.33.3 k8s.io/apimachinery v0.33.3
@ -102,7 +102,7 @@ require (
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect
github.com/mailru/easyjson v0.9.0 // indirect github.com/mailru/easyjson v0.9.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.9 // indirect github.com/mattn/go-runewidth v0.0.9 // indirect
github.com/miekg/dns v1.1.57 // indirect github.com/miekg/dns v1.1.57 // indirect
github.com/mitchellh/go-wordwrap v1.0.1 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect
@ -155,13 +155,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.4.0 // indirect go.opentelemetry.io/proto/otlp v1.4.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.25.0 // indirect golang.org/x/mod v0.26.0 // indirect
golang.org/x/net v0.41.0 // indirect golang.org/x/net v0.42.0 // indirect
golang.org/x/oauth2 v0.30.0 // indirect golang.org/x/oauth2 v0.30.0 // indirect
golang.org/x/sync v0.16.0 // indirect golang.org/x/sync v0.16.0 // indirect
golang.org/x/sys v0.34.0 // indirect golang.org/x/sys v0.35.0 // indirect
golang.org/x/time v0.12.0 // indirect golang.org/x/time v0.12.0 // indirect
golang.org/x/tools v0.34.0 // indirect golang.org/x/tools v0.35.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 // indirect
google.golang.org/grpc v1.68.1 // indirect google.golang.org/grpc v1.68.1 // indirect

@ -83,8 +83,8 @@ github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjT
github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM=
github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f h1:Wl78ApPPB2Wvf/TIe2xdyJxTlb6obmF18d8QdkxNDu4= github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f h1:Wl78ApPPB2Wvf/TIe2xdyJxTlb6obmF18d8QdkxNDu4=
github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f/go.mod h1:OSYXu++VVOHnXeitef/D8n/6y4QV8uLHSFXX4NeXMGc= github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f/go.mod h1:OSYXu++VVOHnXeitef/D8n/6y4QV8uLHSFXX4NeXMGc=
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fluxcd/cli-utils v0.36.0-flux.14 h1:I//AMVUXTc+M04UtIXArMXQZCazGMwfemodV1j/yG8c= github.com/fluxcd/cli-utils v0.36.0-flux.14 h1:I//AMVUXTc+M04UtIXArMXQZCazGMwfemodV1j/yG8c=
@ -198,14 +198,11 @@ github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhn
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE=
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk= github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk=
@ -387,16 +384,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.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
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.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg=
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ=
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=
@ -410,8 +407,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.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
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=
@ -431,24 +428,22 @@ golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
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=
@ -456,8 +451,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.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg= golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0= golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
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=
@ -465,8 +460,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.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
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=
@ -477,8 +472,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.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0=
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw=
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=

@ -16,6 +16,7 @@ limitations under the License.
package util package util
import ( import (
"fmt"
"log/slog" "log/slog"
"strings" "strings"
@ -265,8 +266,8 @@ func processImportValues(c *chart.Chart, merge bool) error {
for _, riv := range r.ImportValues { for _, riv := range r.ImportValues {
switch iv := riv.(type) { switch iv := riv.(type) {
case map[string]interface{}: case map[string]interface{}:
child := iv["child"].(string) child := fmt.Sprintf("%v", iv["child"])
parent := iv["parent"].(string) parent := fmt.Sprintf("%v", iv["parent"])
outiv = append(outiv, map[string]string{ outiv = append(outiv, map[string]string{
"child": child, "child": child,

@ -17,13 +17,606 @@ limitations under the License.
package action package action
import ( import (
"errors"
"io"
"testing" "testing"
"time"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
chart "helm.sh/helm/v4/pkg/chart/v2"
kubefake "helm.sh/helm/v4/pkg/kube/fake"
release "helm.sh/helm/v4/pkg/release/v1" release "helm.sh/helm/v4/pkg/release/v1"
helmtime "helm.sh/helm/v4/pkg/time"
) )
// unreachableKubeClient is a test client that always returns an error for IsReachable
type unreachableKubeClient struct {
kubefake.PrintingKubeClient
}
func (u *unreachableKubeClient) IsReachable() error {
return errors.New("connection refused")
}
func TestNewGetMetadata(t *testing.T) {
cfg := actionConfigFixture(t)
client := NewGetMetadata(cfg)
assert.NotNil(t, client)
assert.Equal(t, cfg, client.cfg)
assert.Equal(t, 0, client.Version)
}
func TestGetMetadata_Run_BasicMetadata(t *testing.T) {
cfg := actionConfigFixture(t)
client := NewGetMetadata(cfg)
releaseName := "test-release"
deployedTime := helmtime.Now()
rel := &release.Release{
Name: releaseName,
Info: &release.Info{
Status: release.StatusDeployed,
LastDeployed: deployedTime,
},
Chart: &chart.Chart{
Metadata: &chart.Metadata{
Name: "test-chart",
Version: "1.0.0",
AppVersion: "v1.2.3",
},
},
Version: 1,
Namespace: "default",
}
cfg.Releases.Create(rel)
result, err := client.Run(releaseName)
require.NoError(t, err)
assert.Equal(t, releaseName, result.Name)
assert.Equal(t, "test-chart", result.Chart)
assert.Equal(t, "1.0.0", result.Version)
assert.Equal(t, "v1.2.3", result.AppVersion)
assert.Equal(t, "default", result.Namespace)
assert.Equal(t, 1, result.Revision)
assert.Equal(t, "deployed", result.Status)
assert.Equal(t, deployedTime.Format(time.RFC3339), result.DeployedAt)
assert.Empty(t, result.Dependencies)
assert.Empty(t, result.Annotations)
}
func TestGetMetadata_Run_WithDependencies(t *testing.T) {
cfg := actionConfigFixture(t)
client := NewGetMetadata(cfg)
releaseName := "test-release"
deployedTime := helmtime.Now()
dependencies := []*chart.Dependency{
{
Name: "mysql",
Version: "8.0.25",
Repository: "https://charts.bitnami.com/bitnami",
},
{
Name: "redis",
Version: "6.2.4",
Repository: "https://charts.bitnami.com/bitnami",
},
}
rel := &release.Release{
Name: releaseName,
Info: &release.Info{
Status: release.StatusDeployed,
LastDeployed: deployedTime,
},
Chart: &chart.Chart{
Metadata: &chart.Metadata{
Name: "test-chart",
Version: "1.0.0",
AppVersion: "v1.2.3",
Dependencies: dependencies,
},
},
Version: 1,
Namespace: "default",
}
cfg.Releases.Create(rel)
result, err := client.Run(releaseName)
require.NoError(t, err)
assert.Equal(t, releaseName, result.Name)
assert.Equal(t, "test-chart", result.Chart)
assert.Equal(t, "1.0.0", result.Version)
assert.Equal(t, dependencies, result.Dependencies)
assert.Len(t, result.Dependencies, 2)
assert.Equal(t, "mysql", result.Dependencies[0].Name)
assert.Equal(t, "redis", result.Dependencies[1].Name)
}
func TestGetMetadata_Run_WithDependenciesAliases(t *testing.T) {
cfg := actionConfigFixture(t)
client := NewGetMetadata(cfg)
releaseName := "test-release"
deployedTime := helmtime.Now()
dependencies := []*chart.Dependency{
{
Name: "mysql",
Version: "8.0.25",
Repository: "https://charts.bitnami.com/bitnami",
Alias: "database",
},
{
Name: "redis",
Version: "6.2.4",
Repository: "https://charts.bitnami.com/bitnami",
Alias: "cache",
},
}
rel := &release.Release{
Name: releaseName,
Info: &release.Info{
Status: release.StatusDeployed,
LastDeployed: deployedTime,
},
Chart: &chart.Chart{
Metadata: &chart.Metadata{
Name: "test-chart",
Version: "1.0.0",
AppVersion: "v1.2.3",
Dependencies: dependencies,
},
},
Version: 1,
Namespace: "default",
}
cfg.Releases.Create(rel)
result, err := client.Run(releaseName)
require.NoError(t, err)
assert.Equal(t, releaseName, result.Name)
assert.Equal(t, "test-chart", result.Chart)
assert.Equal(t, "1.0.0", result.Version)
assert.Equal(t, dependencies, result.Dependencies)
assert.Len(t, result.Dependencies, 2)
assert.Equal(t, "mysql", result.Dependencies[0].Name)
assert.Equal(t, "database", result.Dependencies[0].Alias)
assert.Equal(t, "redis", result.Dependencies[1].Name)
assert.Equal(t, "cache", result.Dependencies[1].Alias)
}
func TestGetMetadata_Run_WithMixedDependencies(t *testing.T) {
cfg := actionConfigFixture(t)
client := NewGetMetadata(cfg)
releaseName := "test-release"
deployedTime := helmtime.Now()
dependencies := []*chart.Dependency{
{
Name: "mysql",
Version: "8.0.25",
Repository: "https://charts.bitnami.com/bitnami",
Alias: "database",
},
{
Name: "nginx",
Version: "1.20.0",
Repository: "https://charts.bitnami.com/bitnami",
},
{
Name: "redis",
Version: "6.2.4",
Repository: "https://charts.bitnami.com/bitnami",
Alias: "cache",
},
{
Name: "postgresql",
Version: "11.0.0",
Repository: "https://charts.bitnami.com/bitnami",
},
}
rel := &release.Release{
Name: releaseName,
Info: &release.Info{
Status: release.StatusDeployed,
LastDeployed: deployedTime,
},
Chart: &chart.Chart{
Metadata: &chart.Metadata{
Name: "test-chart",
Version: "1.0.0",
AppVersion: "v1.2.3",
Dependencies: dependencies,
},
},
Version: 1,
Namespace: "default",
}
cfg.Releases.Create(rel)
result, err := client.Run(releaseName)
require.NoError(t, err)
assert.Equal(t, releaseName, result.Name)
assert.Equal(t, "test-chart", result.Chart)
assert.Equal(t, "1.0.0", result.Version)
assert.Equal(t, dependencies, result.Dependencies)
assert.Len(t, result.Dependencies, 4)
// Verify dependencies with aliases
assert.Equal(t, "mysql", result.Dependencies[0].Name)
assert.Equal(t, "database", result.Dependencies[0].Alias)
assert.Equal(t, "redis", result.Dependencies[2].Name)
assert.Equal(t, "cache", result.Dependencies[2].Alias)
// Verify dependencies without aliases
assert.Equal(t, "nginx", result.Dependencies[1].Name)
assert.Equal(t, "", result.Dependencies[1].Alias)
assert.Equal(t, "postgresql", result.Dependencies[3].Name)
assert.Equal(t, "", result.Dependencies[3].Alias)
}
func TestGetMetadata_Run_WithAnnotations(t *testing.T) {
cfg := actionConfigFixture(t)
client := NewGetMetadata(cfg)
releaseName := "test-release"
deployedTime := helmtime.Now()
annotations := map[string]string{
"helm.sh/hook": "pre-install",
"helm.sh/hook-weight": "5",
"custom.annotation": "test-value",
}
rel := &release.Release{
Name: releaseName,
Info: &release.Info{
Status: release.StatusDeployed,
LastDeployed: deployedTime,
},
Chart: &chart.Chart{
Metadata: &chart.Metadata{
Name: "test-chart",
Version: "1.0.0",
AppVersion: "v1.2.3",
Annotations: annotations,
},
},
Version: 1,
Namespace: "default",
}
cfg.Releases.Create(rel)
result, err := client.Run(releaseName)
require.NoError(t, err)
assert.Equal(t, releaseName, result.Name)
assert.Equal(t, "test-chart", result.Chart)
assert.Equal(t, annotations, result.Annotations)
assert.Equal(t, "pre-install", result.Annotations["helm.sh/hook"])
assert.Equal(t, "5", result.Annotations["helm.sh/hook-weight"])
assert.Equal(t, "test-value", result.Annotations["custom.annotation"])
}
func TestGetMetadata_Run_SpecificVersion(t *testing.T) {
cfg := actionConfigFixture(t)
client := NewGetMetadata(cfg)
client.Version = 2
releaseName := "test-release"
deployedTime := helmtime.Now()
rel1 := &release.Release{
Name: releaseName,
Info: &release.Info{
Status: release.StatusSuperseded,
LastDeployed: helmtime.Time{Time: deployedTime.Time.Add(-time.Hour)},
},
Chart: &chart.Chart{
Metadata: &chart.Metadata{
Name: "test-chart",
Version: "1.0.0",
AppVersion: "v1.0.0",
},
},
Version: 1,
Namespace: "default",
}
rel2 := &release.Release{
Name: releaseName,
Info: &release.Info{
Status: release.StatusDeployed,
LastDeployed: deployedTime,
},
Chart: &chart.Chart{
Metadata: &chart.Metadata{
Name: "test-chart",
Version: "1.1.0",
AppVersion: "v1.1.0",
},
},
Version: 2,
Namespace: "default",
}
cfg.Releases.Create(rel1)
cfg.Releases.Create(rel2)
result, err := client.Run(releaseName)
require.NoError(t, err)
assert.Equal(t, releaseName, result.Name)
assert.Equal(t, "test-chart", result.Chart)
assert.Equal(t, "1.1.0", result.Version)
assert.Equal(t, "v1.1.0", result.AppVersion)
assert.Equal(t, 2, result.Revision)
assert.Equal(t, "deployed", result.Status)
}
func TestGetMetadata_Run_DifferentStatuses(t *testing.T) {
cfg := actionConfigFixture(t)
client := NewGetMetadata(cfg)
testCases := []struct {
name string
status release.Status
expected string
}{
{"deployed", release.StatusDeployed, "deployed"},
{"failed", release.StatusFailed, "failed"},
{"uninstalled", release.StatusUninstalled, "uninstalled"},
{"pending-install", release.StatusPendingInstall, "pending-install"},
{"pending-upgrade", release.StatusPendingUpgrade, "pending-upgrade"},
{"pending-rollback", release.StatusPendingRollback, "pending-rollback"},
{"superseded", release.StatusSuperseded, "superseded"},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
releaseName := "test-release-" + tc.name
deployedTime := helmtime.Now()
rel := &release.Release{
Name: releaseName,
Info: &release.Info{
Status: tc.status,
LastDeployed: deployedTime,
},
Chart: &chart.Chart{
Metadata: &chart.Metadata{
Name: "test-chart",
Version: "1.0.0",
AppVersion: "v1.0.0",
},
},
Version: 1,
Namespace: "default",
}
cfg.Releases.Create(rel)
result, err := client.Run(releaseName)
require.NoError(t, err)
assert.Equal(t, tc.expected, result.Status)
})
}
}
func TestGetMetadata_Run_UnreachableKubeClient(t *testing.T) {
cfg := actionConfigFixture(t)
cfg.KubeClient = &unreachableKubeClient{
PrintingKubeClient: kubefake.PrintingKubeClient{Out: io.Discard},
}
client := NewGetMetadata(cfg)
_, err := client.Run("test-release")
assert.Error(t, err)
assert.Contains(t, err.Error(), "connection refused")
}
func TestGetMetadata_Run_ReleaseNotFound(t *testing.T) {
cfg := actionConfigFixture(t)
client := NewGetMetadata(cfg)
_, err := client.Run("non-existent-release")
assert.Error(t, err)
assert.Contains(t, err.Error(), "not found")
}
func TestGetMetadata_Run_EmptyAppVersion(t *testing.T) {
cfg := actionConfigFixture(t)
client := NewGetMetadata(cfg)
releaseName := "test-release"
deployedTime := helmtime.Now()
rel := &release.Release{
Name: releaseName,
Info: &release.Info{
Status: release.StatusDeployed,
LastDeployed: deployedTime,
},
Chart: &chart.Chart{
Metadata: &chart.Metadata{
Name: "test-chart",
Version: "1.0.0",
AppVersion: "", // Empty app version
},
},
Version: 1,
Namespace: "default",
}
cfg.Releases.Create(rel)
result, err := client.Run(releaseName)
require.NoError(t, err)
assert.Equal(t, "", result.AppVersion)
}
func TestMetadata_FormattedDepNames(t *testing.T) {
testCases := []struct {
name string
dependencies []*chart.Dependency
expected string
}{
{
name: "no dependencies",
dependencies: []*chart.Dependency{},
expected: "",
},
{
name: "single dependency",
dependencies: []*chart.Dependency{
{Name: "mysql"},
},
expected: "mysql",
},
{
name: "multiple dependencies sorted",
dependencies: []*chart.Dependency{
{Name: "redis"},
{Name: "mysql"},
{Name: "nginx"},
},
expected: "mysql,nginx,redis",
},
{
name: "already sorted dependencies",
dependencies: []*chart.Dependency{
{Name: "apache"},
{Name: "mysql"},
{Name: "zookeeper"},
},
expected: "apache,mysql,zookeeper",
},
{
name: "duplicate names",
dependencies: []*chart.Dependency{
{Name: "mysql"},
{Name: "redis"},
{Name: "mysql"},
},
expected: "mysql,mysql,redis",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
metadata := &Metadata{
Dependencies: tc.dependencies,
}
result := metadata.FormattedDepNames()
assert.Equal(t, tc.expected, result)
})
}
}
func TestMetadata_FormattedDepNames_WithComplexDependencies(t *testing.T) {
dependencies := []*chart.Dependency{
{
Name: "zookeeper",
Version: "10.0.0",
Repository: "https://charts.bitnami.com/bitnami",
Condition: "zookeeper.enabled",
},
{
Name: "apache",
Version: "9.0.0",
Repository: "https://charts.bitnami.com/bitnami",
},
{
Name: "mysql",
Version: "8.0.25",
Repository: "https://charts.bitnami.com/bitnami",
Condition: "mysql.enabled",
},
}
metadata := &Metadata{
Dependencies: dependencies,
}
result := metadata.FormattedDepNames()
assert.Equal(t, "apache,mysql,zookeeper", result)
}
func TestMetadata_FormattedDepNames_WithAliases(t *testing.T) {
testCases := []struct {
name string
dependencies []*chart.Dependency
expected string
}{
{
name: "dependencies with aliases",
dependencies: []*chart.Dependency{
{Name: "mysql", Alias: "database"},
{Name: "redis", Alias: "cache"},
},
expected: "mysql,redis",
},
{
name: "mixed dependencies with and without aliases",
dependencies: []*chart.Dependency{
{Name: "mysql", Alias: "database"},
{Name: "nginx"},
{Name: "redis", Alias: "cache"},
},
expected: "mysql,nginx,redis",
},
{
name: "empty alias should use name",
dependencies: []*chart.Dependency{
{Name: "mysql", Alias: ""},
{Name: "redis", Alias: "cache"},
},
expected: "mysql,redis",
},
{
name: "sorted by name not alias",
dependencies: []*chart.Dependency{
{Name: "zookeeper", Alias: "a-service"},
{Name: "apache", Alias: "z-service"},
},
expected: "apache,zookeeper",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
metadata := &Metadata{
Dependencies: tc.dependencies,
}
result := metadata.FormattedDepNames()
assert.Equal(t, tc.expected, result)
})
}
}
func TestGetMetadata_Labels(t *testing.T) { func TestGetMetadata_Labels(t *testing.T) {
rel := releaseStub() rel := releaseStub()
rel.Info.Status = release.StatusDeployed rel.Info.Status = release.StatusDeployed

@ -0,0 +1,218 @@
/*
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 action
import (
"io"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
chart "helm.sh/helm/v4/pkg/chart/v2"
kubefake "helm.sh/helm/v4/pkg/kube/fake"
release "helm.sh/helm/v4/pkg/release/v1"
)
func TestNewGetValues(t *testing.T) {
cfg := actionConfigFixture(t)
client := NewGetValues(cfg)
assert.NotNil(t, client)
assert.Equal(t, cfg, client.cfg)
assert.Equal(t, 0, client.Version)
assert.Equal(t, false, client.AllValues)
}
func TestGetValues_Run_UserConfigOnly(t *testing.T) {
cfg := actionConfigFixture(t)
client := NewGetValues(cfg)
releaseName := "test-release"
userConfig := map[string]interface{}{
"database": map[string]interface{}{
"host": "localhost",
"port": 5432,
},
"app": map[string]interface{}{
"name": "my-app",
"replicas": 3,
},
}
rel := &release.Release{
Name: releaseName,
Info: &release.Info{
Status: release.StatusDeployed,
},
Chart: &chart.Chart{
Metadata: &chart.Metadata{
Name: "test-chart",
Version: "1.0.0",
},
Values: map[string]interface{}{
"defaultKey": "defaultValue",
"app": map[string]interface{}{
"name": "default-app",
"timeout": 30,
},
},
},
Config: userConfig,
Version: 1,
Namespace: "default",
}
cfg.Releases.Create(rel)
result, err := client.Run(releaseName)
require.NoError(t, err)
assert.Equal(t, userConfig, result)
}
func TestGetValues_Run_AllValues(t *testing.T) {
cfg := actionConfigFixture(t)
client := NewGetValues(cfg)
client.AllValues = true
releaseName := "test-release"
userConfig := map[string]interface{}{
"database": map[string]interface{}{
"host": "localhost",
"port": 5432,
},
"app": map[string]interface{}{
"name": "my-app",
},
}
chartDefaultValues := map[string]interface{}{
"defaultKey": "defaultValue",
"app": map[string]interface{}{
"name": "default-app",
"timeout": 30,
},
}
rel := &release.Release{
Name: releaseName,
Info: &release.Info{
Status: release.StatusDeployed,
},
Chart: &chart.Chart{
Metadata: &chart.Metadata{
Name: "test-chart",
Version: "1.0.0",
},
Values: chartDefaultValues,
},
Config: userConfig,
Version: 1,
Namespace: "default",
}
cfg.Releases.Create(rel)
result, err := client.Run(releaseName)
require.NoError(t, err)
assert.Equal(t, "my-app", result["app"].(map[string]interface{})["name"])
assert.Equal(t, 30, result["app"].(map[string]interface{})["timeout"])
assert.Equal(t, "defaultValue", result["defaultKey"])
assert.Equal(t, "localhost", result["database"].(map[string]interface{})["host"])
assert.Equal(t, 5432, result["database"].(map[string]interface{})["port"])
}
func TestGetValues_Run_EmptyValues(t *testing.T) {
cfg := actionConfigFixture(t)
client := NewGetValues(cfg)
releaseName := "test-release"
rel := &release.Release{
Name: releaseName,
Info: &release.Info{
Status: release.StatusDeployed,
},
Chart: &chart.Chart{
Metadata: &chart.Metadata{
Name: "test-chart",
Version: "1.0.0",
},
},
Config: map[string]interface{}{},
Version: 1,
Namespace: "default",
}
cfg.Releases.Create(rel)
result, err := client.Run(releaseName)
require.NoError(t, err)
assert.Equal(t, map[string]interface{}{}, result)
}
func TestGetValues_Run_UnreachableKubeClient(t *testing.T) {
cfg := actionConfigFixture(t)
cfg.KubeClient = &unreachableKubeClient{
PrintingKubeClient: kubefake.PrintingKubeClient{Out: io.Discard},
}
client := NewGetValues(cfg)
_, err := client.Run("test-release")
assert.Error(t, err)
assert.Contains(t, err.Error(), "connection refused")
}
func TestGetValues_Run_ReleaseNotFound(t *testing.T) {
cfg := actionConfigFixture(t)
client := NewGetValues(cfg)
_, err := client.Run("non-existent-release")
assert.Error(t, err)
assert.Contains(t, err.Error(), "not found")
}
func TestGetValues_Run_NilConfig(t *testing.T) {
cfg := actionConfigFixture(t)
client := NewGetValues(cfg)
releaseName := "test-release"
rel := &release.Release{
Name: releaseName,
Info: &release.Info{
Status: release.StatusDeployed,
},
Chart: &chart.Chart{
Metadata: &chart.Metadata{
Name: "test-chart",
Version: "1.0.0",
},
},
Config: nil,
Version: 1,
Namespace: "default",
}
cfg.Releases.Create(rel)
result, err := client.Run(releaseName)
require.NoError(t, err)
assert.Nil(t, result)
}

@ -73,7 +73,9 @@ func (cfg *Configuration) execHook(rl *release.Release, hook release.HookEvent,
h.LastRun.Phase = release.HookPhaseUnknown h.LastRun.Phase = release.HookPhaseUnknown
// Create hook resources // Create hook resources
if _, err := cfg.KubeClient.Create(resources); err != nil { if _, err := cfg.KubeClient.Create(
resources,
kube.ClientCreateOptionServerSideApply(false, false)); err != nil {
h.LastRun.CompletedAt = helmtime.Now() h.LastRun.CompletedAt = helmtime.Now()
h.LastRun.Phase = release.HookPhaseFailed h.LastRun.Phase = release.HookPhaseFailed
return fmt.Errorf("warning: Hook %s %s failed: %w", hook, h.Path, err) return fmt.Errorf("warning: Hook %s %s failed: %w", hook, h.Path, err)

@ -173,7 +173,9 @@ func (i *Install) installCRDs(crds []chart.CRD) error {
} }
// Send them to Kube // Send them to Kube
if _, err := i.cfg.KubeClient.Create(res); err != nil { if _, err := i.cfg.KubeClient.Create(
res,
kube.ClientCreateOptionServerSideApply(false, false)); err != nil {
// If the error is CRD already exists, continue. // If the error is CRD already exists, continue.
if apierrors.IsAlreadyExists(err) { if apierrors.IsAlreadyExists(err) {
crdName := res[0].Name crdName := res[0].Name
@ -399,7 +401,9 @@ func (i *Install) RunWithContext(ctx context.Context, chrt *chart.Chart, vals ma
if err != nil { if err != nil {
return nil, err return nil, err
} }
if _, err := i.cfg.KubeClient.Create(resourceList); err != nil && !apierrors.IsAlreadyExists(err) { if _, err := i.cfg.KubeClient.Create(
resourceList,
kube.ClientCreateOptionServerSideApply(false, false)); err != nil && !apierrors.IsAlreadyExists(err) {
return nil, err return nil, err
} }
} }
@ -468,13 +472,17 @@ func (i *Install) performInstall(rel *release.Release, toBeAdopted kube.Resource
// do an update, but it's not clear whether we WANT to do an update if the reuse is set // do an update, but it's not clear whether we WANT to do an update if the reuse is set
// to true, since that is basically an upgrade operation. // to true, since that is basically an upgrade operation.
if len(toBeAdopted) == 0 && len(resources) > 0 { if len(toBeAdopted) == 0 && len(resources) > 0 {
_, err = i.cfg.KubeClient.Create(resources) _, err = i.cfg.KubeClient.Create(
resources,
kube.ClientCreateOptionServerSideApply(false, false))
} else if len(resources) > 0 { } else if len(resources) > 0 {
if i.TakeOwnership { updateThreeWayMergeForUnstructured := i.TakeOwnership
_, err = i.cfg.KubeClient.(kube.InterfaceThreeWayMerge).UpdateThreeWayMerge(toBeAdopted, resources, i.ForceReplace) _, err = i.cfg.KubeClient.Update(
} else { toBeAdopted,
_, err = i.cfg.KubeClient.Update(toBeAdopted, resources, i.ForceReplace) resources,
} kube.ClientUpdateOptionServerSideApply(false, false),
kube.ClientUpdateOptionThreeWayMergeForUnstructured(updateThreeWayMergeForUnstructured),
kube.ClientUpdateOptionForceReplace(i.ForceReplace))
} }
if err != nil { if err != nil {
return rel, err return rel, err

@ -890,7 +890,6 @@ func TestNameAndChartGenerateName(t *testing.T) {
} }
for _, tc := range tests { for _, tc := range tests {
tc := tc
t.Run(tc.Name, func(t *testing.T) { t.Run(tc.Name, func(t *testing.T) {
t.Parallel() t.Parallel()

@ -114,6 +114,7 @@ func (p *Pull) Run(chartRef string) (string, error) {
defer os.RemoveAll(dest) defer os.RemoveAll(dest)
} }
downloadSourceRef := chartRef
if p.RepoURL != "" { if p.RepoURL != "" {
chartURL, err := repo.FindChartInRepoURL( chartURL, err := repo.FindChartInRepoURL(
p.RepoURL, p.RepoURL,
@ -128,10 +129,10 @@ func (p *Pull) Run(chartRef string) (string, error) {
if err != nil { if err != nil {
return out.String(), err return out.String(), err
} }
chartRef = chartURL downloadSourceRef = chartURL
} }
saved, v, err := c.DownloadTo(chartRef, p.Version, dest) saved, v, err := c.DownloadTo(downloadSourceRef, p.Version, dest)
if err != nil { if err != nil {
return out.String(), err return out.String(), err
} }

@ -190,7 +190,11 @@ func (r *Rollback) performRollback(currentRelease, targetRelease *release.Releas
if err != nil { if err != nil {
return targetRelease, fmt.Errorf("unable to set metadata visitor from target release: %w", err) return targetRelease, fmt.Errorf("unable to set metadata visitor from target release: %w", err)
} }
results, err := r.cfg.KubeClient.Update(current, target, r.ForceReplace) results, err := r.cfg.KubeClient.Update(
current,
target,
kube.ClientUpdateOptionServerSideApply(false, false),
kube.ClientUpdateOptionForceReplace(r.ForceReplace))
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)

@ -129,10 +129,10 @@ func (s *Show) Run(chartpath string) (string, error) {
if s.OutputFormat == ShowCRDs || s.OutputFormat == ShowAll { if s.OutputFormat == ShowCRDs || s.OutputFormat == ShowAll {
crds := s.chart.CRDObjects() crds := s.chart.CRDObjects()
if len(crds) > 0 { if len(crds) > 0 {
if s.OutputFormat == ShowAll && !bytes.HasPrefix(crds[0].File.Data, []byte("---")) { for _, crd := range crds {
if !bytes.HasPrefix(crd.File.Data, []byte("---")) {
fmt.Fprintln(&out, "---") fmt.Fprintln(&out, "---")
} }
for _, crd := range crds {
fmt.Fprintf(&out, "%s\n", string(crd.File.Data)) fmt.Fprintf(&out, "%s\n", string(crd.File.Data))
} }
} }

@ -32,6 +32,7 @@ func TestShow(t *testing.T) {
{Name: "crds/ignoreme.txt", Data: []byte("error")}, {Name: "crds/ignoreme.txt", Data: []byte("error")},
{Name: "crds/foo.yaml", Data: []byte("---\nfoo\n")}, {Name: "crds/foo.yaml", Data: []byte("---\nfoo\n")},
{Name: "crds/bar.json", Data: []byte("---\nbar\n")}, {Name: "crds/bar.json", Data: []byte("---\nbar\n")},
{Name: "crds/baz.yaml", Data: []byte("baz\n")},
}, },
Raw: []*chart.File{ Raw: []*chart.File{
{Name: "values.yaml", Data: []byte("VALUES\n")}, {Name: "values.yaml", Data: []byte("VALUES\n")},
@ -58,6 +59,9 @@ foo
--- ---
bar bar
---
baz
` `
if output != expect { if output != expect {
t.Errorf("Expected\n%q\nGot\n%q\n", expect, output) t.Errorf("Expected\n%q\nGot\n%q\n", expect, output)
@ -105,6 +109,7 @@ func TestShowCRDs(t *testing.T) {
{Name: "crds/ignoreme.txt", Data: []byte("error")}, {Name: "crds/ignoreme.txt", Data: []byte("error")},
{Name: "crds/foo.yaml", Data: []byte("---\nfoo\n")}, {Name: "crds/foo.yaml", Data: []byte("---\nfoo\n")},
{Name: "crds/bar.json", Data: []byte("---\nbar\n")}, {Name: "crds/bar.json", Data: []byte("---\nbar\n")},
{Name: "crds/baz.yaml", Data: []byte("baz\n")},
}, },
} }
@ -119,6 +124,9 @@ foo
--- ---
bar bar
---
baz
` `
if output != expect { if output != expect {
t.Errorf("Expected\n%q\nGot\n%q\n", expect, output) t.Errorf("Expected\n%q\nGot\n%q\n", expect, output)

@ -17,6 +17,7 @@ limitations under the License.
package action package action
import ( import (
"errors"
"fmt" "fmt"
"log/slog" "log/slog"
"strings" "strings"
@ -28,6 +29,7 @@ import (
"helm.sh/helm/v4/pkg/kube" "helm.sh/helm/v4/pkg/kube"
releaseutil "helm.sh/helm/v4/pkg/release/util" releaseutil "helm.sh/helm/v4/pkg/release/util"
release "helm.sh/helm/v4/pkg/release/v1" release "helm.sh/helm/v4/pkg/release/v1"
"helm.sh/helm/v4/pkg/storage/driver"
helmtime "helm.sh/helm/v4/pkg/time" helmtime "helm.sh/helm/v4/pkg/time"
) )
@ -66,9 +68,11 @@ func (u *Uninstall) Run(name string) (*release.UninstallReleaseResponse, error)
} }
if u.DryRun { if u.DryRun {
// In the dry run case, just see if the release exists
r, err := u.cfg.releaseContent(name, 0) r, err := u.cfg.releaseContent(name, 0)
if err != nil { if err != nil {
if u.IgnoreNotFound && errors.Is(err, driver.ErrReleaseNotFound) {
return nil, nil
}
return &release.UninstallReleaseResponse{}, err return &release.UninstallReleaseResponse{}, err
} }
return &release.UninstallReleaseResponse{Release: r}, nil return &release.UninstallReleaseResponse{Release: r}, nil

@ -34,6 +34,17 @@ func uninstallAction(t *testing.T) *Uninstall {
return unAction return unAction
} }
func TestUninstallRelease_dryRun_ignoreNotFound(t *testing.T) {
unAction := uninstallAction(t)
unAction.DryRun = true
unAction.IgnoreNotFound = true
is := assert.New(t)
res, err := unAction.Run("release-non-exist")
is.Nil(res)
is.NoError(err)
}
func TestUninstallRelease_ignoreNotFound(t *testing.T) { func TestUninstallRelease_ignoreNotFound(t *testing.T) {
unAction := uninstallAction(t) unAction := uninstallAction(t)
unAction.DryRun = false unAction.DryRun = false
@ -44,7 +55,6 @@ func TestUninstallRelease_ignoreNotFound(t *testing.T) {
is.Nil(res) is.Nil(res)
is.NoError(err) is.NoError(err)
} }
func TestUninstallRelease_deleteRelease(t *testing.T) { func TestUninstallRelease_deleteRelease(t *testing.T) {
is := assert.New(t) is := assert.New(t)

@ -426,7 +426,11 @@ func (u *Upgrade) releasingUpgrade(c chan<- resultMessage, upgradedRelease *rele
slog.Debug("upgrade hooks disabled", "name", upgradedRelease.Name) slog.Debug("upgrade hooks disabled", "name", upgradedRelease.Name)
} }
results, err := u.cfg.KubeClient.Update(current, target, u.ForceReplace) results, err := u.cfg.KubeClient.Update(
current,
target,
kube.ClientUpdateOptionServerSideApply(false, false),
kube.ClientUpdateOptionForceReplace(u.ForceReplace))
if err != nil { if err != nil {
u.cfg.recordRelease(originalRelease) u.cfg.recordRelease(originalRelease)
u.reportToPerformUpgrade(c, upgradedRelease, results.Created, err) u.reportToPerformUpgrade(c, upgradedRelease, results.Created, err)

@ -126,14 +126,14 @@ fullnameOverride: ""
# This section builds out the service account more information can be found here: https://kubernetes.io/docs/concepts/security/service-accounts/ # This section builds out the service account more information can be found here: https://kubernetes.io/docs/concepts/security/service-accounts/
serviceAccount: serviceAccount:
# Specifies whether a service account should be created # Specifies whether a service account should be created.
create: true create: true
# Automatically mount a ServiceAccount's API credentials? # Automatically mount a ServiceAccount's API credentials?
automount: true automount: true
# Annotations to add to the service account # Annotations to add to the service account.
annotations: {} annotations: {}
# The name of the service account to use. # The name of the service account to use.
# If not set and create is true, a name is generated using the fullname template # If not set and create is true, a name is generated using the fullname template.
name: "" name: ""
# This is for setting Kubernetes Annotations to a Pod. # This is for setting Kubernetes Annotations to a Pod.
@ -248,16 +248,16 @@ autoscaling:
# Additional volumes on the output Deployment definition. # Additional volumes on the output Deployment definition.
volumes: [] volumes: []
# - name: foo # - name: foo
# secret: # secret:
# secretName: mysecret # secretName: mysecret
# optional: false # optional: false
# Additional volumeMounts on the output Deployment definition. # Additional volumeMounts on the output Deployment definition.
volumeMounts: [] volumeMounts: []
# - name: foo # - name: foo
# mountPath: "/etc/foo" # mountPath: "/etc/foo"
# readOnly: true # readOnly: true
nodeSelector: {} nodeSelector: {}

@ -16,6 +16,7 @@ limitations under the License.
package util package util
import ( import (
"fmt"
"log/slog" "log/slog"
"strings" "strings"
@ -265,8 +266,8 @@ func processImportValues(c *chart.Chart, merge bool) error {
for _, riv := range r.ImportValues { for _, riv := range r.ImportValues {
switch iv := riv.(type) { switch iv := riv.(type) {
case map[string]interface{}: case map[string]interface{}:
child := iv["child"].(string) child := fmt.Sprintf("%v", iv["child"])
parent := iv["parent"].(string) parent := fmt.Sprintf("%v", iv["parent"])
outiv = append(outiv, map[string]string{ outiv = append(outiv, map[string]string{
"child": child, "child": child,

@ -63,7 +63,6 @@ func loadPlugins(baseCmd *cobra.Command, out io.Writer) {
// Now we create commands for all of these. // Now we create commands for all of these.
for _, plug := range found { for _, plug := range found {
plug := plug
md := plug.Metadata md := plug.Metadata
if md.Usage == "" { if md.Usage == "" {
md.Usage = fmt.Sprintf("the %q plugin", md.Name) md.Usage = fmt.Sprintf("the %q plugin", md.Name)

@ -147,6 +147,18 @@ func TestPullCmd(t *testing.T) {
failExpect: "Failed to fetch chart version", failExpect: "Failed to fetch chart version",
wantError: true, wantError: true,
}, },
{
name: "Chart fetch using repo URL with untardir",
args: "signtest --version=0.1.0 --untar --untardir repo-url-test --repo " + srv.URL(),
expectFile: "./signtest",
expectDir: true,
},
{
name: "Chart fetch using repo URL with untardir and previous pull",
args: "signtest --version=0.1.0 --untar --untardir repo-url-test --repo " + srv.URL(),
failExpect: "failed to untar",
wantError: true,
},
{ {
name: "Fetch OCI Chart", name: "Fetch OCI Chart",
args: fmt.Sprintf("oci://%s/u/ocitestuser/oci-dependent-chart --version 0.1.0", ociSrv.RegistryURL), args: fmt.Sprintf("oci://%s/u/ocitestuser/oci-dependent-chart --version 0.1.0", ociSrv.RegistryURL),

@ -59,8 +59,8 @@ func newReleaseTestCmd(cfg *action.Configuration, out io.Writer) *cobra.Command
client.Namespace = settings.Namespace() client.Namespace = settings.Namespace()
notName := regexp.MustCompile(`^!\s?name=`) notName := regexp.MustCompile(`^!\s?name=`)
for _, f := range filter { for _, f := range filter {
if strings.HasPrefix(f, "name=") { if after, ok := strings.CutPrefix(f, "name="); ok {
client.Filters[action.IncludeNameFilter] = append(client.Filters[action.IncludeNameFilter], strings.TrimPrefix(f, "name=")) client.Filters[action.IncludeNameFilter] = append(client.Filters[action.IncludeNameFilter], after)
} else if notName.MatchString(f) { } else if notName.MatchString(f) {
client.Filters[action.ExcludeNameFilter] = append(client.Filters[action.ExcludeNameFilter], notName.ReplaceAllLiteralString(f, "")) client.Filters[action.ExcludeNameFilter] = append(client.Filters[action.ExcludeNameFilter], notName.ReplaceAllLiteralString(f, ""))
} }

@ -122,6 +122,9 @@ func (g *HTTPGetter) httpClient() (*http.Client, error) {
g.transport = &http.Transport{ g.transport = &http.Transport{
DisableCompression: true, DisableCompression: true,
Proxy: http.ProxyFromEnvironment, Proxy: http.ProxyFromEnvironment,
// Being nil would cause the tls.Config default to be used
// "NewTLSConfig" modifies an empty TLS config, not the default one
TLSClientConfig: &tls.Config{},
} }
}) })

@ -17,6 +17,7 @@ package getter
import ( import (
"bytes" "bytes"
"crypto/tls"
"fmt" "fmt"
"net" "net"
"net/http" "net/http"
@ -124,6 +125,9 @@ func (g *OCIGetter) newRegistryClient() (*registry.Client, error) {
TLSHandshakeTimeout: 10 * time.Second, TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second, ExpectContinueTimeout: 1 * time.Second,
Proxy: http.ProxyFromEnvironment, Proxy: http.ProxyFromEnvironment,
// Being nil would cause the tls.Config default to be used
// "NewTLSConfig" modifies an empty TLS config, not the default one
TLSClientConfig: &tls.Config{},
} }
}) })

@ -24,6 +24,7 @@ import (
"fmt" "fmt"
"io" "io"
"log/slog" "log/slog"
"net/http"
"os" "os"
"path/filepath" "path/filepath"
"reflect" "reflect"
@ -91,6 +92,14 @@ const (
HookOnlyStrategy WaitStrategy = "hookOnly" HookOnlyStrategy WaitStrategy = "hookOnly"
) )
type FieldValidationDirective string
const (
FieldValidationDirectiveIgnore FieldValidationDirective = "Ignore"
FieldValidationDirectiveWarn FieldValidationDirective = "Warn"
FieldValidationDirectiveStrict FieldValidationDirective = "Strict"
)
func init() { func init() {
// Add CRDs to the scheme. They are missing by default. // Add CRDs to the scheme. They are missing by default.
if err := apiextv1.AddToScheme(scheme.Scheme); err != nil { if err := apiextv1.AddToScheme(scheme.Scheme); err != nil {
@ -194,10 +203,102 @@ func (c *Client) IsReachable() error {
return nil return nil
} }
type clientCreateOptions struct {
serverSideApply bool
forceConflicts bool
dryRun bool
fieldValidationDirective FieldValidationDirective
}
type ClientCreateOption func(*clientCreateOptions) error
// ClientUpdateOptionServerSideApply enables performing object apply server-side
// see: https://kubernetes.io/docs/reference/using-api/server-side-apply/
//
// `forceConflicts` forces conflicts to be resolved (may be when serverSideApply enabled only)
// see: https://kubernetes.io/docs/reference/using-api/server-side-apply/#conflicts
func ClientCreateOptionServerSideApply(serverSideApply, forceConflicts bool) ClientCreateOption {
return func(o *clientCreateOptions) error {
if !serverSideApply && forceConflicts {
return fmt.Errorf("forceConflicts enabled when serverSideApply disabled")
}
o.serverSideApply = serverSideApply
o.forceConflicts = forceConflicts
return nil
}
}
// ClientCreateOptionDryRun requests the server to perform non-mutating operations only
func ClientCreateOptionDryRun(dryRun bool) ClientCreateOption {
return func(o *clientCreateOptions) error {
o.dryRun = dryRun
return nil
}
}
// ClientCreateOptionFieldValidationDirective specifies show API operations validate object's schema
// - For client-side apply: this is ignored
// - For server-side apply: the directive is sent to the server to perform the validation
//
// Defaults to `FieldValidationDirectiveStrict`
func ClientCreateOptionFieldValidationDirective(fieldValidationDirective FieldValidationDirective) ClientCreateOption {
return func(o *clientCreateOptions) error {
o.fieldValidationDirective = fieldValidationDirective
return nil
}
}
// Create creates Kubernetes resources specified in the resource list. // Create creates Kubernetes resources specified in the resource list.
func (c *Client) Create(resources ResourceList) (*Result, error) { func (c *Client) Create(resources ResourceList, options ...ClientCreateOption) (*Result, error) {
slog.Debug("creating resource(s)", "resources", len(resources)) slog.Debug("creating resource(s)", "resources", len(resources))
if err := perform(resources, createResource); err != nil {
createOptions := clientCreateOptions{
serverSideApply: true, // Default to server-side apply
fieldValidationDirective: FieldValidationDirectiveStrict,
}
errs := make([]error, 0, len(options))
for _, o := range options {
errs = append(errs, o(&createOptions))
}
if err := errors.Join(errs...); err != nil {
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 {
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)))
return func(target *resource.Info) error {
err := patchResourceServerSide(target, createOptions.dryRun, createOptions.forceConflicts, createOptions.fieldValidationDirective)
logger := slog.With(
slog.String("namespace", target.Namespace),
slog.String("name", target.Name),
slog.String("gvk", target.Mapping.GroupVersionKind.String()))
if err != nil {
logger.Debug("Error patching resource", slog.Any("error", err))
return err
}
logger.Debug("Patched resource")
return nil
}
}
slog.Debug("using client-side apply for resource creation")
return createResource
}
if err := perform(resources, makeCreateApplyFunc()); err != nil {
return nil, err return nil, err
} }
return &Result{Created: resources}, nil return &Result{Created: resources}, nil
@ -348,96 +449,98 @@ func (c *Client) namespace() string {
return v1.NamespaceDefault return v1.NamespaceDefault
} }
// newBuilder returns a new resource builder for structured api objects. func determineFieldValidationDirective(validate bool) FieldValidationDirective {
func (c *Client) newBuilder() *resource.Builder {
return c.Factory.NewBuilder().
ContinueOnError().
NamespaceParam(c.namespace()).
DefaultNamespace().
Flatten()
}
// Build validates for Kubernetes objects and returns unstructured infos.
func (c *Client) Build(reader io.Reader, validate bool) (ResourceList, error) {
validationDirective := metav1.FieldValidationIgnore
if validate { if validate {
validationDirective = metav1.FieldValidationStrict return FieldValidationDirectiveStrict
} }
schema, err := c.Factory.Validator(validationDirective) return FieldValidationDirectiveIgnore
}
func buildResourceList(f Factory, namespace string, validationDirective FieldValidationDirective, reader io.Reader, transformRequest resource.RequestTransform) (ResourceList, error) {
schema, err := f.Validator(string(validationDirective))
if err != nil { if err != nil {
return nil, err return nil, err
} }
result, err := c.newBuilder().
builder := f.NewBuilder().
ContinueOnError().
NamespaceParam(namespace).
DefaultNamespace().
Flatten().
Unstructured(). Unstructured().
Schema(schema). Schema(schema).
Stream(reader, ""). Stream(reader, "")
Do().Infos() if transformRequest != nil {
builder.TransformRequests(transformRequest)
}
result, err := builder.Do().Infos()
return result, scrubValidationError(err) return result, scrubValidationError(err)
} }
// Build validates for Kubernetes objects and returns unstructured infos.
func (c *Client) Build(reader io.Reader, validate bool) (ResourceList, error) {
return buildResourceList(
c.Factory,
c.namespace(),
determineFieldValidationDirective(validate),
reader,
nil)
}
// BuildTable validates for Kubernetes objects and returns unstructured infos. // BuildTable validates for Kubernetes objects and returns unstructured infos.
// The returned kind is a Table. // The returned kind is a Table.
func (c *Client) BuildTable(reader io.Reader, validate bool) (ResourceList, error) { func (c *Client) BuildTable(reader io.Reader, validate bool) (ResourceList, error) {
validationDirective := metav1.FieldValidationIgnore return buildResourceList(
if validate { c.Factory,
validationDirective = metav1.FieldValidationStrict c.namespace(),
} determineFieldValidationDirective(validate),
reader,
schema, err := c.Factory.Validator(validationDirective) transformRequests)
if err != nil {
return nil, err
}
result, err := c.newBuilder().
Unstructured().
Schema(schema).
Stream(reader, "").
TransformRequests(transformRequests).
Do().Infos()
return result, scrubValidationError(err)
} }
func (c *Client) update(original, target ResourceList, force, threeWayMerge bool) (*Result, error) { func (c *Client) update(originals, targets ResourceList, updateApplyFunc UpdateApplyFunc) (*Result, error) {
updateErrors := []error{} updateErrors := []error{}
res := &Result{} res := &Result{}
slog.Debug("checking resources for changes", "resources", len(target)) slog.Debug("checking resources for changes", "resources", len(targets))
err := target.Visit(func(info *resource.Info, err error) error { err := targets.Visit(func(target *resource.Info, err error) error {
if err != nil { if err != nil {
return err return err
} }
helper := resource.NewHelper(info.Client, info.Mapping).WithFieldManager(getManagedFieldsManager()) helper := resource.NewHelper(target.Client, target.Mapping).WithFieldManager(getManagedFieldsManager())
if _, err := helper.Get(info.Namespace, info.Name); err != nil { if _, err := helper.Get(target.Namespace, target.Name); err != nil {
if !apierrors.IsNotFound(err) { if !apierrors.IsNotFound(err) {
return fmt.Errorf("could not get information about the resource: %w", err) return fmt.Errorf("could not get information about the resource: %w", err)
} }
// Append the created resource to the results, even if something fails // Append the created resource to the results, even if something fails
res.Created = append(res.Created, info) res.Created = append(res.Created, target)
// Since the resource does not exist, create it. // Since the resource does not exist, create it.
if err := createResource(info); err != nil { if err := createResource(target); err != nil {
return fmt.Errorf("failed to create resource: %w", err) return fmt.Errorf("failed to create resource: %w", err)
} }
kind := info.Mapping.GroupVersionKind.Kind kind := target.Mapping.GroupVersionKind.Kind
slog.Debug("created a new resource", "namespace", info.Namespace, "name", info.Name, "kind", kind) slog.Debug("created a new resource", "namespace", target.Namespace, "name", target.Name, "kind", kind)
return nil return nil
} }
originalInfo := original.Get(info) original := originals.Get(target)
if originalInfo == nil { if original == nil {
kind := info.Mapping.GroupVersionKind.Kind kind := target.Mapping.GroupVersionKind.Kind
return fmt.Errorf("no %s with the name %q found", kind, info.Name) return fmt.Errorf("original object %s with the name %q not found", kind, target.Name)
} }
if err := updateResource(c, info, originalInfo.Object, force, threeWayMerge); err != nil { if err := updateApplyFunc(original, target); err != nil {
slog.Debug("error updating the resource", "namespace", info.Namespace, "name", info.Name, "kind", info.Mapping.GroupVersionKind.Kind, slog.Any("error", err))
updateErrors = append(updateErrors, err) updateErrors = append(updateErrors, err)
} }
// Because we check for errors later, append the info regardless // Because we check for errors later, append the info regardless
res.Updated = append(res.Updated, info) res.Updated = append(res.Updated, target)
return nil return nil
}) })
@ -449,7 +552,7 @@ func (c *Client) update(original, target ResourceList, force, threeWayMerge bool
return res, joinErrors(updateErrors, " && ") return res, joinErrors(updateErrors, " && ")
} }
for _, info := range original.Difference(target) { for _, info := range originals.Difference(targets) {
slog.Debug("deleting resource", "namespace", info.Namespace, "name", info.Name, "kind", info.Mapping.GroupVersionKind.Kind) slog.Debug("deleting resource", "namespace", info.Namespace, "name", info.Name, "kind", info.Mapping.GroupVersionKind.Kind)
if err := info.Get(); err != nil { if err := info.Get(); err != nil {
@ -473,20 +576,80 @@ func (c *Client) update(original, target ResourceList, force, threeWayMerge bool
return res, nil return res, nil
} }
// Update takes the current list of objects and target list of objects and type clientUpdateOptions struct {
// creates resources that don't already exist, updates resources that have been threeWayMergeForUnstructured bool
// modified in the target configuration, and deletes resources from the current serverSideApply bool
// configuration that are not present in the target configuration. If an error forceReplace bool
// occurs, a Result will still be returned with the error, containing all forceConflicts bool
// resource updates, creations, and deletions that were attempted. These can be dryRun bool
// used for cleanup or other logging purposes. fieldValidationDirective FieldValidationDirective
}
type ClientUpdateOption func(*clientUpdateOptions) error
// ClientUpdateOptionThreeWayMergeForUnstructured enables performing three-way merge for unstructured objects
// Must not be enabled when ClientUpdateOptionServerSideApply is enabled
func ClientUpdateOptionThreeWayMergeForUnstructured(threeWayMergeForUnstructured bool) ClientUpdateOption {
return func(o *clientUpdateOptions) error {
o.threeWayMergeForUnstructured = threeWayMergeForUnstructured
return nil
}
}
// ClientUpdateOptionServerSideApply enables performing object apply server-side (default)
// see: https://kubernetes.io/docs/reference/using-api/server-side-apply/
// Must not be enabled when ClientUpdateOptionThreeWayMerge is enabled
// //
// The difference to Update is that UpdateThreeWayMerge does a three-way-merge // `forceConflicts` forces conflicts to be resolved (may be enabled when serverSideApply enabled only)
// for unstructured objects. // see: https://kubernetes.io/docs/reference/using-api/server-side-apply/#conflicts
func (c *Client) UpdateThreeWayMerge(original, target ResourceList, force bool) (*Result, error) { func ClientUpdateOptionServerSideApply(serverSideApply, forceConflicts bool) ClientUpdateOption {
return c.update(original, target, force, true) return func(o *clientUpdateOptions) error {
if !serverSideApply && forceConflicts {
return fmt.Errorf("forceConflicts enabled when serverSideApply disabled")
}
o.serverSideApply = serverSideApply
o.forceConflicts = forceConflicts
return nil
}
}
// ClientUpdateOptionForceReplace forces objects to be replaced rather than updated via patch
// Must not be enabled when ClientUpdateOptionForceConflicts is enabled
func ClientUpdateOptionForceReplace(forceReplace bool) ClientUpdateOption {
return func(o *clientUpdateOptions) error {
o.forceReplace = forceReplace
return nil
}
}
// ClientUpdateOptionDryRun requests the server to perform non-mutating operations only
func ClientUpdateOptionDryRun(dryRun bool) ClientUpdateOption {
return func(o *clientUpdateOptions) error {
o.dryRun = dryRun
return nil
}
}
// ClientUpdateOptionFieldValidationDirective specifies show API operations validate object's schema
// - For client-side apply: this is ignored
// - For server-side apply: the directive is sent to the server to perform the validation
//
// Defaults to `FieldValidationDirectiveStrict`
func ClientUpdateOptionFieldValidationDirective(fieldValidationDirective FieldValidationDirective) ClientCreateOption {
return func(o *clientCreateOptions) error {
o.fieldValidationDirective = fieldValidationDirective
return nil
}
} }
type UpdateApplyFunc func(original, target *resource.Info) error
// Update takes the current list of objects and target list of objects and // Update takes the current list of objects and target list of objects and
// creates resources that don't already exist, updates resources that have been // creates resources that don't already exist, updates resources that have been
// modified in the target configuration, and deletes resources from the current // modified in the target configuration, and deletes resources from the current
@ -494,8 +657,82 @@ func (c *Client) UpdateThreeWayMerge(original, target ResourceList, force bool)
// occurs, a Result will still be returned with the error, containing all // occurs, a Result will still be returned with the error, containing all
// resource updates, creations, and deletions that were attempted. These can be // resource updates, creations, and deletions that were attempted. These can be
// used for cleanup or other logging purposes. // used for cleanup or other logging purposes.
func (c *Client) Update(original, target ResourceList, force bool) (*Result, error) { //
return c.update(original, target, force, false) // The default is to use server-side apply, equivalent to: `ClientUpdateOptionServerSideApply(true)`
func (c *Client) Update(originals, targets ResourceList, options ...ClientUpdateOption) (*Result, error) {
updateOptions := clientUpdateOptions{
serverSideApply: true, // Default to server-side apply
fieldValidationDirective: FieldValidationDirectiveStrict,
}
errs := make([]error, 0, len(options))
for _, o := range options {
errs = append(errs, o(&updateOptions))
}
if err := errors.Join(errs...); err != nil {
return nil, fmt.Errorf("invalid client update option(s): %w", err)
}
if updateOptions.threeWayMergeForUnstructured && updateOptions.serverSideApply {
return nil, fmt.Errorf("invalid operation: cannot use three-way merge for unstructured and server-side apply together")
}
if updateOptions.forceConflicts && updateOptions.forceReplace {
return nil, fmt.Errorf("invalid operation: cannot use force conflicts and force replace together")
}
if updateOptions.serverSideApply && updateOptions.forceReplace {
return nil, fmt.Errorf("invalid operation: cannot use server-side apply and force replace together")
}
makeUpdateApplyFunc := func() UpdateApplyFunc {
if updateOptions.forceReplace {
slog.Debug(
"using resource replace update strategy",
slog.String("fieldValidationDirective", string(updateOptions.fieldValidationDirective)))
return func(original, target *resource.Info) error {
if err := replaceResource(target, updateOptions.fieldValidationDirective); err != nil {
slog.Debug("error replacing the resource", "namespace", target.Namespace, "name", target.Name, "kind", target.Mapping.GroupVersionKind.Kind, slog.Any("error", err))
return err
}
originalObject := original.Object
kind := target.Mapping.GroupVersionKind.Kind
slog.Debug("replace succeeded", "name", original.Name, "initialKind", originalObject.GetObjectKind().GroupVersionKind().Kind, "kind", kind)
return nil
}
} else if updateOptions.serverSideApply {
slog.Debug(
"using server-side apply for resource update",
slog.Bool("forceConflicts", updateOptions.forceConflicts),
slog.Bool("dryRun", updateOptions.dryRun),
slog.String("fieldValidationDirective", string(updateOptions.fieldValidationDirective)))
return func(_, target *resource.Info) error {
err := patchResourceServerSide(target, updateOptions.dryRun, updateOptions.forceConflicts, updateOptions.fieldValidationDirective)
logger := slog.With(
slog.String("namespace", target.Namespace),
slog.String("name", target.Name),
slog.String("gvk", target.Mapping.GroupVersionKind.String()))
if err != nil {
logger.Debug("Error patching resource", slog.Any("error", err))
return err
}
logger.Debug("Patched resource")
return nil
}
}
slog.Debug("using client-side apply for resource update", slog.Bool("threeWayMergeForUnstructured", updateOptions.threeWayMergeForUnstructured))
return func(original, target *resource.Info) error {
return patchResourceClientSide(original.Object, target, updateOptions.threeWayMergeForUnstructured)
}
}
return c.update(originals, targets, makeUpdateApplyFunc())
} }
// Delete deletes Kubernetes resources specified in the resources list with // Delete deletes Kubernetes resources specified in the resources list with
@ -503,7 +740,7 @@ func (c *Client) Update(original, target ResourceList, force bool) (*Result, err
// 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) Delete(resources ResourceList) (*Result, []error) { func (c *Client) Delete(resources ResourceList) (*Result, []error) {
return rdelete(c, resources, metav1.DeletePropagationBackground) 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
@ -511,23 +748,23 @@ func (c *Client) Delete(resources ResourceList) (*Result, []error) {
// 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) DeleteWithPropagationPolicy(resources ResourceList, policy metav1.DeletionPropagation) (*Result, []error) {
return rdelete(c, resources, policy) return deleteResources(resources, policy)
} }
func rdelete(_ *Client, resources ResourceList, propagation metav1.DeletionPropagation) (*Result, []error) { 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(info *resource.Info) error { err := perform(resources, func(target *resource.Info) error {
slog.Debug("starting delete resource", "namespace", info.Namespace, "name", info.Name, "kind", info.Mapping.GroupVersionKind.Kind) slog.Debug("starting delete resource", "namespace", target.Namespace, "name", target.Name, "kind", target.Mapping.GroupVersionKind.Kind)
err := deleteResource(info, propagation) err := deleteResource(target, propagation)
if err == nil || apierrors.IsNotFound(err) { if err == nil || apierrors.IsNotFound(err) {
if err != nil { if err != nil {
slog.Debug("ignoring delete failure", "namespace", info.Namespace, "name", info.Name, "kind", info.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))
} }
mtx.Lock() mtx.Lock()
defer mtx.Unlock() defer mtx.Unlock()
res.Deleted = append(res.Deleted, info) res.Deleted = append(res.Deleted, target)
return nil return nil
} }
mtx.Lock() mtx.Lock()
@ -548,6 +785,17 @@ func rdelete(_ *Client, resources ResourceList, propagation metav1.DeletionPropa
return res, nil return res, nil
} }
// https://github.com/kubernetes/kubectl/blob/197123726db24c61aa0f78d1f0ba6e91a2ec2f35/pkg/cmd/apply/apply.go#L439
func isIncompatibleServerError(err error) bool {
// 415: Unsupported media type means we're talking to a server which doesn't
// support server-side apply.
if _, ok := err.(*apierrors.StatusError); !ok {
// Non-StatusError means the error isn't because the server is incompatible.
return false
}
return err.(*apierrors.StatusError).Status().Code == http.StatusUnsupportedMediaType
}
// getManagedFieldsManager returns the manager string. If one was set it will be returned. // getManagedFieldsManager returns the manager string. If one was set it will be returned.
// Otherwise, one is calculated based on the name of the binary. // Otherwise, one is calculated based on the name of the binary.
func getManagedFieldsManager() string { func getManagedFieldsManager() string {
@ -568,18 +816,41 @@ func getManagedFieldsManager() string {
return filepath.Base(os.Args[0]) return filepath.Base(os.Args[0])
} }
func perform(infos ResourceList, fn func(*resource.Info) error) error {
var result error
if len(infos) == 0 {
return ErrNoObjectsVisited
}
errs := make(chan error)
go batchPerform(infos, fn, errs)
for range infos {
err := <-errs
if err != nil {
result = errors.Join(result, err)
}
}
return result
}
func batchPerform(infos ResourceList, fn func(*resource.Info) error, errs chan<- error) { func batchPerform(infos ResourceList, fn func(*resource.Info) error, errs chan<- error) {
var kind string var kind string
var wg sync.WaitGroup var wg sync.WaitGroup
defer wg.Wait()
for _, info := range infos { for _, info := range infos {
currentKind := info.Object.GetObjectKind().GroupVersionKind().Kind currentKind := info.Object.GetObjectKind().GroupVersionKind().Kind
if kind != currentKind { if kind != currentKind {
wg.Wait() wg.Wait()
kind = currentKind kind = currentKind
} }
wg.Add(1) wg.Add(1)
go func(i *resource.Info) { go func(info *resource.Info) {
errs <- fn(i) errs <- fn(info)
wg.Done() wg.Done()
}(info) }(info)
} }
@ -597,6 +868,7 @@ func createResource(info *resource.Info) error {
if err != nil { if err != nil {
return err return err
} }
return info.Refresh(obj, true) return info.Refresh(obj, true)
}) })
} }
@ -611,8 +883,8 @@ func deleteResource(info *resource.Info, policy metav1.DeletionPropagation) erro
}) })
} }
func createPatch(target *resource.Info, current runtime.Object, threeWayMergeForUnstructured bool) ([]byte, types.PatchType, error) { func createPatch(original runtime.Object, target *resource.Info, threeWayMergeForUnstructured bool) ([]byte, types.PatchType, error) {
oldData, err := json.Marshal(current) oldData, err := json.Marshal(original)
if err != nil { if err != nil {
return nil, types.StrategicMergePatchType, fmt.Errorf("serializing current configuration: %w", err) return nil, types.StrategicMergePatchType, fmt.Errorf("serializing current configuration: %w", err)
} }
@ -674,27 +946,33 @@ func createPatch(target *resource.Info, current runtime.Object, threeWayMergeFor
return patch, types.StrategicMergePatchType, err return patch, types.StrategicMergePatchType, err
} }
func updateResource(_ *Client, target *resource.Info, currentObj runtime.Object, force, threeWayMergeForUnstructured bool) error { func replaceResource(target *resource.Info, fieldValidationDirective FieldValidationDirective) error {
var (
obj runtime.Object
helper = resource.NewHelper(target.Client, target.Mapping).WithFieldManager(getManagedFieldsManager())
kind = target.Mapping.GroupVersionKind.Kind
)
// if --force is applied, attempt to replace the existing resource with the new object. helper := resource.NewHelper(target.Client, target.Mapping).
if force { WithFieldValidation(string(fieldValidationDirective)).
var err error WithFieldManager(getManagedFieldsManager())
obj, err = helper.Replace(target.Namespace, target.Name, true, target.Object)
obj, err := helper.Replace(target.Namespace, target.Name, true, target.Object)
if err != nil { if err != nil {
return fmt.Errorf("failed to replace object: %w", err) return fmt.Errorf("failed to replace object: %w", err)
} }
slog.Debug("replace succeeded", "name", target.Name, "initialKind", currentObj.GetObjectKind().GroupVersionKind().Kind, "kind", kind)
} else { if err := target.Refresh(obj, true); err != nil {
patch, patchType, err := createPatch(target, currentObj, threeWayMergeForUnstructured) return fmt.Errorf("failed to refresh object after replace: %w", err)
}
return nil
}
func patchResourceClientSide(original runtime.Object, target *resource.Info, threeWayMergeForUnstructured bool) error {
patch, patchType, err := createPatch(original, target, threeWayMergeForUnstructured)
if err != nil { if err != nil {
return fmt.Errorf("failed to create patch: %w", err) return fmt.Errorf("failed to create patch: %w", err)
} }
kind := target.Mapping.GroupVersionKind.Kind
if patch == nil || string(patch) == "{}" { if patch == nil || string(patch) == "{}" {
slog.Debug("no changes detected", "kind", kind, "name", target.Name) slog.Debug("no changes detected", "kind", kind, "name", target.Name)
// This needs to happen to make sure that Helm has the latest info from the API // This needs to happen to make sure that Helm has the latest info from the API
@ -704,18 +982,59 @@ func updateResource(_ *Client, target *resource.Info, currentObj runtime.Object,
} }
return nil return nil
} }
// send patch to server // send patch to server
slog.Debug("patching resource", "kind", kind, "name", target.Name, "namespace", target.Namespace) slog.Debug("patching resource", "kind", kind, "name", target.Name, "namespace", target.Namespace)
obj, err = helper.Patch(target.Namespace, target.Name, patchType, patch, nil) helper := resource.NewHelper(target.Client, target.Mapping).WithFieldManager(getManagedFieldsManager())
obj, err := helper.Patch(target.Namespace, target.Name, patchType, patch, nil)
if err != nil { if err != nil {
return fmt.Errorf("cannot patch %q with kind %s: %w", target.Name, kind, err) return fmt.Errorf("cannot patch %q with kind %s: %w", target.Name, kind, err)
} }
}
target.Refresh(obj, true) target.Refresh(obj, true)
return nil return nil
} }
// Patch reource using server-side apply
func patchResourceServerSide(target *resource.Info, dryRun bool, forceConflicts bool, fieldValidationDirective FieldValidationDirective) error {
helper := resource.NewHelper(
target.Client,
target.Mapping).
DryRun(dryRun).
WithFieldManager(ManagedFieldsManager).
WithFieldValidation(string(fieldValidationDirective))
// Send the full object to be applied on the server side.
data, err := runtime.Encode(unstructured.UnstructuredJSONScheme, target.Object)
if err != nil {
return fmt.Errorf("failed to encode object %s/%s with kind %s: %w", target.Namespace, target.Name, target.Mapping.GroupVersionKind.Kind, err)
}
options := metav1.PatchOptions{
Force: &forceConflicts,
}
obj, err := helper.Patch(
target.Namespace,
target.Name,
types.ApplyPatchType,
data,
&options,
)
if err != nil {
if isIncompatibleServerError(err) {
return fmt.Errorf("server-side apply not available on the server: %v", err)
}
if apierrors.IsConflict(err) {
return fmt.Errorf("conflict occurred while applying %s/%s with kind %s: %w", target.Namespace, target.Name, target.Mapping.GroupVersionKind.Kind, err)
}
return err
}
return target.Refresh(obj, true)
}
// GetPodList uses the kubernetes interface to get the list of pods filtered by listOptions // GetPodList uses the kubernetes interface to get the list of pods filtered by listOptions
func (c *Client) GetPodList(namespace string, listOptions metav1.ListOptions) (*v1.PodList, error) { func (c *Client) GetPodList(namespace string, listOptions metav1.ListOptions) (*v1.PodList, error) {
podList, err := c.kubeClient.CoreV1().Pods(namespace).List(context.Background(), listOptions) podList, err := c.kubeClient.CoreV1().Pods(namespace).List(context.Background(), listOptions)

File diff suppressed because it is too large Load Diff

@ -60,11 +60,11 @@ type FailingKubeWaiter struct {
} }
// Create returns the configured error if set or prints // Create returns the configured error if set or prints
func (f *FailingKubeClient) Create(resources kube.ResourceList) (*kube.Result, error) { func (f *FailingKubeClient) Create(resources kube.ResourceList, options ...kube.ClientCreateOption) (*kube.Result, error) {
if f.CreateError != nil { if f.CreateError != nil {
return nil, f.CreateError return nil, f.CreateError
} }
return f.PrintingKubeClient.Create(resources) return f.PrintingKubeClient.Create(resources, options...)
} }
// Get returns the configured error if set or prints // Get returns the configured error if set or prints
@ -117,19 +117,11 @@ func (f *FailingKubeWaiter) WatchUntilReady(resources kube.ResourceList, d time.
} }
// Update returns the configured error if set or prints // Update returns the configured error if set or prints
func (f *FailingKubeClient) Update(r, modified kube.ResourceList, ignoreMe bool) (*kube.Result, error) { func (f *FailingKubeClient) Update(r, modified kube.ResourceList, options ...kube.ClientUpdateOption) (*kube.Result, error) {
if f.UpdateError != nil { if f.UpdateError != nil {
return &kube.Result{}, f.UpdateError return &kube.Result{}, f.UpdateError
} }
return f.PrintingKubeClient.Update(r, modified, ignoreMe) return f.PrintingKubeClient.Update(r, modified, options...)
}
// Update returns the configured error if set or prints
func (f *FailingKubeClient) UpdateThreeWayMerge(r, modified kube.ResourceList, ignoreMe bool) (*kube.Result, error) {
if f.UpdateError != nil {
return &kube.Result{}, f.UpdateError
}
return f.PrintingKubeClient.Update(r, modified, ignoreMe)
} }
// Build returns the configured error if set or prints // Build returns the configured error if set or prints

@ -49,7 +49,7 @@ func (p *PrintingKubeClient) IsReachable() error {
} }
// Create prints the values of what would be created with a real KubeClient. // Create prints the values of what would be created with a real KubeClient.
func (p *PrintingKubeClient) Create(resources kube.ResourceList) (*kube.Result, error) { func (p *PrintingKubeClient) Create(resources kube.ResourceList, _ ...kube.ClientCreateOption) (*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, err return nil, err
@ -98,7 +98,7 @@ func (p *PrintingKubeClient) Delete(resources kube.ResourceList) (*kube.Result,
} }
// Update implements KubeClient Update. // Update implements KubeClient Update.
func (p *PrintingKubeClient) Update(_, modified kube.ResourceList, _ bool) (*kube.Result, error) { func (p *PrintingKubeClient) Update(_, modified kube.ResourceList, _ ...kube.ClientUpdateOption) (*kube.Result, error) {
_, err := io.Copy(p.Out, bufferize(modified)) _, err := io.Copy(p.Out, bufferize(modified))
if err != nil { if err != nil {
return nil, err return nil, err

@ -30,14 +30,14 @@ import (
// A KubernetesClient must be concurrency safe. // A KubernetesClient must be concurrency safe.
type Interface interface { type Interface interface {
// Create creates one or more resources. // Create creates one or more resources.
Create(resources ResourceList) (*Result, error) Create(resources ResourceList, options ...ClientCreateOption) (*Result, error)
// Delete destroys one or more resources. // Delete destroys one or more resources.
Delete(resources ResourceList) (*Result, []error) Delete(resources ResourceList) (*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.
Update(original, target ResourceList, force bool) (*Result, error) Update(original, target ResourceList, options ...ClientUpdateOption) (*Result, error)
// Build creates a resource list from a Reader. // Build creates a resource list from a Reader.
// //
@ -53,13 +53,6 @@ type Interface interface {
GetWaiter(ws WaitStrategy) (Waiter, error) GetWaiter(ws WaitStrategy) (Waiter, error)
} }
// InterfaceThreeWayMerge was introduced to avoid breaking backwards compatibility for Interface implementers.
//
// TODO Helm 4: Remove InterfaceThreeWayMerge and integrate its method(s) into the Interface.
type InterfaceThreeWayMerge interface {
UpdateThreeWayMerge(original, target ResourceList, force bool) (*Result, error)
}
// Waiter defines methods related to waiting for resource states. // Waiter defines methods related to waiting for resource states.
type Waiter interface { type Waiter interface {
// Wait waits up to the given timeout for the specified resources to be ready. // Wait waits up to the given timeout for the specified resources to be ready.
@ -125,7 +118,6 @@ type InterfaceResources interface {
} }
var _ Interface = (*Client)(nil) var _ Interface = (*Client)(nil)
var _ InterfaceThreeWayMerge = (*Client)(nil)
var _ InterfaceLogs = (*Client)(nil) var _ InterfaceLogs = (*Client)(nil)
var _ InterfaceDeletionPropagation = (*Client)(nil) var _ InterfaceDeletionPropagation = (*Client)(nil)
var _ InterfaceResources = (*Client)(nil) var _ InterfaceResources = (*Client)(nil)

@ -18,7 +18,6 @@ package kube // import "helm.sh/helm/v4/pkg/kube"
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"log/slog" "log/slog"
"net/http" "net/http"
@ -223,26 +222,6 @@ func (hw *legacyWaiter) WatchUntilReady(resources ResourceList, timeout time.Dur
return perform(resources, hw.watchTimeout(timeout)) return perform(resources, hw.watchTimeout(timeout))
} }
func perform(infos ResourceList, fn func(*resource.Info) error) error {
var result error
if len(infos) == 0 {
return ErrNoObjectsVisited
}
errs := make(chan error)
go batchPerform(infos, fn, errs)
for range infos {
err := <-errs
if err != nil {
result = errors.Join(result, err)
}
}
return result
}
func (hw *legacyWaiter) watchUntilReady(timeout time.Duration, info *resource.Info) error { func (hw *legacyWaiter) watchUntilReady(timeout time.Duration, info *resource.Info) error {
kind := info.Mapping.GroupVersionKind.Kind kind := info.Mapping.GroupVersionKind.Kind
switch kind { switch kind {

@ -160,6 +160,9 @@ func validateChartVersion(cf *chart.Metadata) error {
func validateChartMaintainer(cf *chart.Metadata) error { func validateChartMaintainer(cf *chart.Metadata) error {
for _, maintainer := range cf.Maintainers { for _, maintainer := range cf.Maintainers {
if maintainer == nil {
return errors.New("a maintainer entry is empty")
}
if maintainer.Name == "" { if maintainer.Name == "" {
return errors.New("each maintainer requires a name") return errors.New("each maintainer requires a name")
} else if maintainer.Email != "" && !govalidator.IsEmail(maintainer.Email) { } else if maintainer.Email != "" && !govalidator.IsEmail(maintainer.Email) {

@ -142,6 +142,16 @@ func TestValidateChartMaintainer(t *testing.T) {
t.Errorf("validateChartMaintainer(%s, %s) to return no error, got %s", test.Name, test.Email, err.Error()) t.Errorf("validateChartMaintainer(%s, %s) to return no error, got %s", test.Name, test.Email, err.Error())
} }
} }
// Testing for an empty maintainer
badChart.Maintainers = []*chart.Maintainer{nil}
err := validateChartMaintainer(badChart)
if err == nil {
t.Errorf("validateChartMaintainer did not return error for nil maintainer as expected")
}
if err.Error() != "a maintainer entry is empty" {
t.Errorf("validateChartMaintainer returned unexpected error for nil maintainer: %s", err.Error())
}
} }
func TestValidateChartSources(t *testing.T) { func TestValidateChartSources(t *testing.T) {

@ -17,6 +17,7 @@ package plugin // import "helm.sh/helm/v4/pkg/plugin"
import ( import (
"fmt" "fmt"
"log/slog"
"os" "os"
"path/filepath" "path/filepath"
"regexp" "regexp"
@ -83,7 +84,7 @@ type Metadata struct {
PlatformCommand []PlatformCommand `json:"platformCommand"` PlatformCommand []PlatformCommand `json:"platformCommand"`
// Command is the plugin command, as a single string. // Command is the plugin command, as a single string.
// Providing a command will result in an error if PlatformCommand is also set. // Providing Command and PlatformCommand will result in a warning being emitted (PlatformCommand takes precedence).
// //
// The command will be passed through environment expansion, so env vars can // The command will be passed through environment expansion, so env vars can
// be present in this command. Unless IgnoreFlags is set, this will // be present in this command. Unless IgnoreFlags is set, this will
@ -92,7 +93,7 @@ type Metadata struct {
// Note that command is not executed in a shell. To do so, we suggest // Note that command is not executed in a shell. To do so, we suggest
// pointing the command to a shell script. // pointing the command to a shell script.
// //
// DEPRECATED: Use PlatformCommand instead. Remove in Helm 4. // DEPRECATED: Use PlatformCommand instead
Command string `json:"command"` Command string `json:"command"`
// IgnoreFlags ignores any flags passed in from Helm // IgnoreFlags ignores any flags passed in from Helm
@ -119,14 +120,14 @@ type Metadata struct {
PlatformHooks PlatformHooks `json:"platformHooks"` PlatformHooks PlatformHooks `json:"platformHooks"`
// Hooks are commands that will run on plugin events, as a single string. // Hooks are commands that will run on plugin events, as a single string.
// Providing a hooks will result in an error if PlatformHooks is also set. // Providing Hook and PlatformHooks will result in a warning being emitted (PlatformHooks takes precedence).
// //
// The command will be passed through environment expansion, so env vars can // The command will be passed through environment expansion, so env vars can
// be present in this command. // be present in this command.
// //
// Note that the command is executed in the sh shell. // Note that the command is executed in the sh shell.
// //
// DEPRECATED: Use PlatformHooks instead. Remove in Helm 4. // DEPRECATED: Use PlatformHooks instead
Hooks Hooks Hooks Hooks
// Downloaders field is used if the plugin supply downloader mechanism // Downloaders field is used if the plugin supply downloader mechanism
@ -259,11 +260,11 @@ func validatePluginData(plug *Plugin, filepath string) error {
plug.Metadata.Usage = sanitizeString(plug.Metadata.Usage) plug.Metadata.Usage = sanitizeString(plug.Metadata.Usage)
if len(plug.Metadata.PlatformCommand) > 0 && len(plug.Metadata.Command) > 0 { if len(plug.Metadata.PlatformCommand) > 0 && len(plug.Metadata.Command) > 0 {
return fmt.Errorf("both platformCommand and command are set in %q", filepath) slog.Warn("both 'platformCommand' and 'command' are set (this will become an error in a future Helm version)", slog.String("filepath", filepath))
} }
if len(plug.Metadata.PlatformHooks) > 0 && len(plug.Metadata.Hooks) > 0 { if len(plug.Metadata.PlatformHooks) > 0 && len(plug.Metadata.Hooks) > 0 {
return fmt.Errorf("both platformHooks and hooks are set in %q", filepath) slog.Warn("both 'platformHooks' and 'hooks' are set (this will become an error in a future Helm version)", slog.String("filepath", filepath))
} }
// We could also validate SemVer, executable, and other fields should we so choose. // We could also validate SemVer, executable, and other fields should we so choose.

@ -496,8 +496,8 @@ func TestValidatePluginData(t *testing.T) {
{false, mockMissingMeta}, // Test if the metadata section missing {false, mockMissingMeta}, // Test if the metadata section missing
{true, mockNoCommand}, // Test no command metadata works {true, mockNoCommand}, // Test no command metadata works
{true, mockLegacyCommand}, // Test legacy command metadata works {true, mockLegacyCommand}, // Test legacy command metadata works
{false, mockWithCommand}, // Test platformCommand and command both set fails {true, mockWithCommand}, // Test platformCommand and command both set works
{false, mockWithHooks}, // Test platformHooks and hooks both set fails {true, mockWithHooks}, // Test platformHooks and hooks both set works
} { } {
err := validatePluginData(item.plug, fmt.Sprintf("test-%d", i)) err := validatePluginData(item.plug, fmt.Sprintf("test-%d", i))
if item.pass && err != nil { if item.pass && err != nil {

@ -274,7 +274,7 @@ func LoginOptPlainText(isPlainText bool) LoginOption {
} }
} }
func ensureTLSConfig(client *auth.Client) (*tls.Config, error) { func ensureTLSConfig(client *auth.Client, setConfig *tls.Config) (*tls.Config, error) {
var transport *http.Transport var transport *http.Transport
switch t := client.Client.Transport.(type) { switch t := client.Client.Transport.(type) {
@ -298,7 +298,10 @@ func ensureTLSConfig(client *auth.Client) (*tls.Config, error) {
return nil, fmt.Errorf("unable to access TLS client configuration, the provided HTTP Transport is not supported, given: %T", client.Client.Transport) return nil, fmt.Errorf("unable to access TLS client configuration, the provided HTTP Transport is not supported, given: %T", client.Client.Transport)
} }
if transport.TLSClientConfig == nil { switch {
case setConfig != nil:
transport.TLSClientConfig = setConfig
case transport.TLSClientConfig == nil:
transport.TLSClientConfig = &tls.Config{} transport.TLSClientConfig = &tls.Config{}
} }
@ -308,7 +311,7 @@ func ensureTLSConfig(client *auth.Client) (*tls.Config, error) {
// LoginOptInsecure returns a function that sets the insecure setting on login // LoginOptInsecure returns a function that sets the insecure setting on login
func LoginOptInsecure(insecure bool) LoginOption { func LoginOptInsecure(insecure bool) LoginOption {
return func(o *loginOperation) { return func(o *loginOperation) {
tlsConfig, err := ensureTLSConfig(o.client.authorizer) tlsConfig, err := ensureTLSConfig(o.client.authorizer, nil)
if err != nil { if err != nil {
panic(err) panic(err)
@ -324,7 +327,7 @@ func LoginOptTLSClientConfig(certFile, keyFile, caFile string) LoginOption {
if (certFile == "" || keyFile == "") && caFile == "" { if (certFile == "" || keyFile == "") && caFile == "" {
return return
} }
tlsConfig, err := ensureTLSConfig(o.client.authorizer) tlsConfig, err := ensureTLSConfig(o.client.authorizer, nil)
if err != nil { if err != nil {
panic(err) panic(err)
} }
@ -351,6 +354,17 @@ func LoginOptTLSClientConfig(certFile, keyFile, caFile string) LoginOption {
} }
} }
// LoginOptTLSClientConfigFromConfig returns a function that sets the TLS settings on login
// receiving the configuration in memory rather than from files.
func LoginOptTLSClientConfigFromConfig(conf *tls.Config) LoginOption {
return func(o *loginOperation) {
_, err := ensureTLSConfig(o.client.authorizer, conf)
if err != nil {
panic(err)
}
}
}
type ( type (
// LogoutOption allows specifying various settings on logout // LogoutOption allows specifying various settings on logout
LogoutOption func(*logoutOperation) LogoutOption func(*logoutOperation)

@ -17,6 +17,8 @@ limitations under the License.
package registry package registry
import ( import (
"crypto/tls"
"crypto/x509"
"os" "os"
"testing" "testing"
@ -52,6 +54,30 @@ func (suite *TLSRegistryClientTestSuite) Test_0_Login() {
suite.Nil(err, "no error logging into registry with good credentials") suite.Nil(err, "no error logging into registry with good credentials")
} }
func (suite *TLSRegistryClientTestSuite) Test_1_Login() {
err := suite.RegistryClient.Login(suite.DockerRegistryHost,
LoginOptBasicAuth("badverybad", "ohsobad"),
LoginOptTLSClientConfigFromConfig(&tls.Config{}))
suite.NotNil(err, "error logging into registry with bad credentials")
// Create a *tls.Config from tlsCert, tlsKey, and tlsCA.
cert, err := tls.LoadX509KeyPair(tlsCert, tlsKey)
suite.Nil(err, "error loading x509 key pair")
rootCAs := x509.NewCertPool()
caCert, err := os.ReadFile(tlsCA)
suite.Nil(err, "error reading CA certificate")
rootCAs.AppendCertsFromPEM(caCert)
conf := &tls.Config{
Certificates: []tls.Certificate{cert},
RootCAs: rootCAs,
}
err = suite.RegistryClient.Login(suite.DockerRegistryHost,
LoginOptBasicAuth(testUsername, testPassword),
LoginOptTLSClientConfigFromConfig(conf))
suite.Nil(err, "no error logging into registry with good credentials")
}
func (suite *TLSRegistryClientTestSuite) Test_1_Push() { func (suite *TLSRegistryClientTestSuite) Test_1_Push() {
testPush(&suite.TestSuite) testPush(&suite.TestSuite)
} }

@ -22,35 +22,6 @@ import (
rspb "helm.sh/helm/v4/pkg/release/v1" rspb "helm.sh/helm/v4/pkg/release/v1"
) )
type list []*rspb.Release
func (s list) Len() int { return len(s) }
func (s list) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
// ByName sorts releases by name
type ByName struct{ list }
// Less compares to releases
func (s ByName) Less(i, j int) bool { return s.list[i].Name < s.list[j].Name }
// ByDate sorts releases by date
type ByDate struct{ list }
// Less compares to releases
func (s ByDate) Less(i, j int) bool {
ti := s.list[i].Info.LastDeployed.Unix()
tj := s.list[j].Info.LastDeployed.Unix()
return ti < tj
}
// ByRevision sorts releases by revision number
type ByRevision struct{ list }
// Less compares to releases
func (s ByRevision) Less(i, j int) bool {
return s.list[i].Version < s.list[j].Version
}
// Reverse reverses the list of releases sorted by the sort func. // Reverse reverses the list of releases sorted by the sort func.
func Reverse(list []*rspb.Release, sortFn func([]*rspb.Release)) { func Reverse(list []*rspb.Release, sortFn func([]*rspb.Release)) {
sortFn(list) sortFn(list)
@ -62,17 +33,25 @@ func Reverse(list []*rspb.Release, sortFn func([]*rspb.Release)) {
// SortByName returns the list of releases sorted // SortByName returns the list of releases sorted
// in lexicographical order. // in lexicographical order.
func SortByName(list []*rspb.Release) { func SortByName(list []*rspb.Release) {
sort.Sort(ByName{list}) sort.Slice(list, func(i, j int) bool {
return list[i].Name < list[j].Name
})
} }
// SortByDate returns the list of releases sorted by a // SortByDate returns the list of releases sorted by a
// release's last deployed time (in seconds). // release's last deployed time (in seconds).
func SortByDate(list []*rspb.Release) { func SortByDate(list []*rspb.Release) {
sort.Sort(ByDate{list}) sort.Slice(list, func(i, j int) bool {
ti := list[i].Info.LastDeployed.Unix()
tj := list[j].Info.LastDeployed.Unix()
return ti < tj
})
} }
// SortByRevision returns the list of releases sorted by a // SortByRevision returns the list of releases sorted by a
// release's revision number (release.Version). // release's revision number (release.Version).
func SortByRevision(list []*rspb.Release) { func SortByRevision(list []*rspb.Release) {
sort.Sort(ByRevision{list}) sort.Slice(list, func(i, j int) bool {
return list[i].Version < list[j].Version
})
} }

@ -355,7 +355,8 @@ func loadIndex(data []byte, source string) (*IndexFile, error) {
for name, cvs := range i.Entries { for name, cvs := range i.Entries {
for idx := len(cvs) - 1; idx >= 0; idx-- { for idx := len(cvs) - 1; idx >= 0; idx-- {
if cvs[idx] == nil { if cvs[idx] == nil {
slog.Warn("skipping loading invalid entry for chart %q from %s: empty entry", name, source) slog.Warn(fmt.Sprintf("skipping loading invalid entry for chart %q from %s: empty entry", name, source))
cvs = append(cvs[:idx], cvs[idx+1:]...)
continue continue
} }
// When metadata section missing, initialize with no data // When metadata section missing, initialize with no data
@ -366,7 +367,7 @@ func loadIndex(data []byte, source string) (*IndexFile, error) {
cvs[idx].APIVersion = chart.APIVersionV1 cvs[idx].APIVersion = chart.APIVersionV1
} }
if err := cvs[idx].Validate(); ignoreSkippableChartValidationError(err) != nil { if err := cvs[idx].Validate(); ignoreSkippableChartValidationError(err) != nil {
slog.Warn("skipping loading invalid entry for chart %q %q from %s: %s", name, cvs[idx].Version, source, err) slog.Warn(fmt.Sprintf("skipping loading invalid entry for chart %q %q from %s: %s", name, cvs[idx].Version, source, err))
cvs = append(cvs[:idx], cvs[idx+1:]...) cvs = append(cvs[:idx], cvs[idx+1:]...)
} }
} }

@ -68,6 +68,7 @@ entries:
grafana: grafana:
- apiVersion: v2 - apiVersion: v2
name: grafana name: grafana
- null
foo: foo:
- -
bar: bar:
@ -159,7 +160,6 @@ func TestLoadIndex(t *testing.T) {
} }
for _, tc := range tests { for _, tc := range tests {
tc := tc
t.Run(tc.Name, func(t *testing.T) { t.Run(tc.Name, func(t *testing.T) {
t.Parallel() t.Parallel()
i, err := LoadIndexFile(tc.Filename) i, err := LoadIndexFile(tc.Filename)

Loading…
Cancel
Save