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
steps:
- 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
run: cat ".github/env" >> "$GITHUB_ENV"
- name: Setup Go

@ -43,7 +43,7 @@ jobs:
steps:
- 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.
- name: Initialize CodeQL

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

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

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

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

@ -14,7 +14,7 @@ require (
github.com/cyphar/filepath-securejoin v0.4.1
github.com/distribution/distribution/v3 v3.0.0
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/foxcpp/go-mockdns v1.1.0
github.com/gobwas/glob v0.2.3
@ -32,9 +32,9 @@ require (
github.com/spf13/pflag v1.0.7
github.com/stretchr/testify v1.10.0
go.yaml.in/yaml/v3 v3.0.4
golang.org/x/crypto v0.40.0
golang.org/x/term v0.33.0
golang.org/x/text v0.27.0
golang.org/x/crypto v0.41.0
golang.org/x/term v0.34.0
golang.org/x/text v0.28.0
k8s.io/api v0.33.3
k8s.io/apiextensions-apiserver 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/mailru/easyjson v0.9.0 // 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/miekg/dns v1.1.57 // 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/proto/otlp v1.4.0 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect
golang.org/x/mod v0.25.0 // indirect
golang.org/x/net v0.41.0 // indirect
golang.org/x/mod v0.26.0 // indirect
golang.org/x/net v0.42.0 // indirect
golang.org/x/oauth2 v0.30.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/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/rpc v0.0.0-20241209162323-e6fa225c2576 // 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/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/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
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/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
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/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
github.com/mattn/go-colorable v0.1.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/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.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
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/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
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.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
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.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg=
golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@ -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.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
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.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
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/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
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-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-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-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-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-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-20220811171246-fbc7d0a398ab/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.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
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-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
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.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
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.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0=
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
@ -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.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.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
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/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
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.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.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0=
golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

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

@ -17,13 +17,606 @@ limitations under the License.
package action
import (
"errors"
"io"
"testing"
"time"
"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"
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) {
rel := releaseStub()
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
// 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.Phase = release.HookPhaseFailed
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
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 apierrors.IsAlreadyExists(err) {
crdName := res[0].Name
@ -399,7 +401,9 @@ func (i *Install) RunWithContext(ctx context.Context, chrt *chart.Chart, vals ma
if err != nil {
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
}
}
@ -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
// to true, since that is basically an upgrade operation.
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 {
if i.TakeOwnership {
_, err = i.cfg.KubeClient.(kube.InterfaceThreeWayMerge).UpdateThreeWayMerge(toBeAdopted, resources, i.ForceReplace)
} else {
_, err = i.cfg.KubeClient.Update(toBeAdopted, resources, i.ForceReplace)
}
updateThreeWayMergeForUnstructured := i.TakeOwnership
_, err = i.cfg.KubeClient.Update(
toBeAdopted,
resources,
kube.ClientUpdateOptionServerSideApply(false, false),
kube.ClientUpdateOptionThreeWayMergeForUnstructured(updateThreeWayMergeForUnstructured),
kube.ClientUpdateOptionForceReplace(i.ForceReplace))
}
if err != nil {
return rel, err

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

@ -114,6 +114,7 @@ func (p *Pull) Run(chartRef string) (string, error) {
defer os.RemoveAll(dest)
}
downloadSourceRef := chartRef
if p.RepoURL != "" {
chartURL, err := repo.FindChartInRepoURL(
p.RepoURL,
@ -128,10 +129,10 @@ func (p *Pull) Run(chartRef string) (string, error) {
if err != nil {
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 {
return out.String(), err
}

@ -190,7 +190,11 @@ func (r *Rollback) performRollback(currentRelease, targetRelease *release.Releas
if err != nil {
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 {
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 {
crds := s.chart.CRDObjects()
if len(crds) > 0 {
if s.OutputFormat == ShowAll && !bytes.HasPrefix(crds[0].File.Data, []byte("---")) {
fmt.Fprintln(&out, "---")
}
for _, crd := range crds {
if !bytes.HasPrefix(crd.File.Data, []byte("---")) {
fmt.Fprintln(&out, "---")
}
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/foo.yaml", Data: []byte("---\nfoo\n")},
{Name: "crds/bar.json", Data: []byte("---\nbar\n")},
{Name: "crds/baz.yaml", Data: []byte("baz\n")},
},
Raw: []*chart.File{
{Name: "values.yaml", Data: []byte("VALUES\n")},
@ -58,6 +59,9 @@ foo
---
bar
---
baz
`
if output != expect {
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/foo.yaml", Data: []byte("---\nfoo\n")},
{Name: "crds/bar.json", Data: []byte("---\nbar\n")},
{Name: "crds/baz.yaml", Data: []byte("baz\n")},
},
}
@ -119,6 +124,9 @@ foo
---
bar
---
baz
`
if output != expect {
t.Errorf("Expected\n%q\nGot\n%q\n", expect, output)

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

@ -34,6 +34,17 @@ func uninstallAction(t *testing.T) *Uninstall {
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) {
unAction := uninstallAction(t)
unAction.DryRun = false
@ -44,7 +55,6 @@ func TestUninstallRelease_ignoreNotFound(t *testing.T) {
is.Nil(res)
is.NoError(err)
}
func TestUninstallRelease_deleteRelease(t *testing.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)
}
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 {
u.cfg.recordRelease(originalRelease)
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/
serviceAccount:
# Specifies whether a service account should be created
# Specifies whether a service account should be created.
create: true
# Automatically mount a ServiceAccount's API credentials?
automount: true
# Annotations to add to the service account
# Annotations to add to the service account.
annotations: {}
# 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: ""
# This is for setting Kubernetes Annotations to a Pod.
@ -174,9 +174,9 @@ ingress:
- path: /
pathType: ImplementationSpecific
tls: []
# - secretName: chart-example-tls
# hosts:
# - chart-example.local
# - secretName: chart-example-tls
# hosts:
# - chart-example.local
# -- Expose the service via gateway-api HTTPRoute
# Requires Gateway API resources and suitable controller installed within the cluster
@ -248,16 +248,16 @@ autoscaling:
# Additional volumes on the output Deployment definition.
volumes: []
# - name: foo
# secret:
# secretName: mysecret
# optional: false
# - name: foo
# secret:
# secretName: mysecret
# optional: false
# Additional volumeMounts on the output Deployment definition.
volumeMounts: []
# - name: foo
# mountPath: "/etc/foo"
# readOnly: true
# - name: foo
# mountPath: "/etc/foo"
# readOnly: true
nodeSelector: {}

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

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

@ -147,6 +147,18 @@ func TestPullCmd(t *testing.T) {
failExpect: "Failed to fetch chart version",
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",
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()
notName := regexp.MustCompile(`^!\s?name=`)
for _, f := range filter {
if strings.HasPrefix(f, "name=") {
client.Filters[action.IncludeNameFilter] = append(client.Filters[action.IncludeNameFilter], strings.TrimPrefix(f, "name="))
if after, ok := strings.CutPrefix(f, "name="); ok {
client.Filters[action.IncludeNameFilter] = append(client.Filters[action.IncludeNameFilter], after)
} else if notName.MatchString(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{
DisableCompression: true,
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 (
"bytes"
"crypto/tls"
"fmt"
"net"
"net/http"
@ -124,6 +125,9 @@ func (g *OCIGetter) newRegistryClient() (*registry.Client, error) {
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
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"
"io"
"log/slog"
"net/http"
"os"
"path/filepath"
"reflect"
@ -91,6 +92,14 @@ const (
HookOnlyStrategy WaitStrategy = "hookOnly"
)
type FieldValidationDirective string
const (
FieldValidationDirectiveIgnore FieldValidationDirective = "Ignore"
FieldValidationDirectiveWarn FieldValidationDirective = "Warn"
FieldValidationDirectiveStrict FieldValidationDirective = "Strict"
)
func init() {
// Add CRDs to the scheme. They are missing by default.
if err := apiextv1.AddToScheme(scheme.Scheme); err != nil {
@ -194,10 +203,102 @@ func (c *Client) IsReachable() error {
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.
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))
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 &Result{Created: resources}, nil
@ -348,96 +449,98 @@ func (c *Client) namespace() string {
return v1.NamespaceDefault
}
// newBuilder returns a new resource builder for structured api objects.
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
func determineFieldValidationDirective(validate bool) FieldValidationDirective {
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 {
return nil, err
}
result, err := c.newBuilder().
builder := f.NewBuilder().
ContinueOnError().
NamespaceParam(namespace).
DefaultNamespace().
Flatten().
Unstructured().
Schema(schema).
Stream(reader, "").
Do().Infos()
Stream(reader, "")
if transformRequest != nil {
builder.TransformRequests(transformRequest)
}
result, err := builder.Do().Infos()
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.
// The returned kind is a Table.
func (c *Client) BuildTable(reader io.Reader, validate bool) (ResourceList, error) {
validationDirective := metav1.FieldValidationIgnore
if validate {
validationDirective = metav1.FieldValidationStrict
}
schema, err := c.Factory.Validator(validationDirective)
if err != nil {
return nil, err
}
result, err := c.newBuilder().
Unstructured().
Schema(schema).
Stream(reader, "").
TransformRequests(transformRequests).
Do().Infos()
return result, scrubValidationError(err)
return buildResourceList(
c.Factory,
c.namespace(),
determineFieldValidationDirective(validate),
reader,
transformRequests)
}
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{}
res := &Result{}
slog.Debug("checking resources for changes", "resources", len(target))
err := target.Visit(func(info *resource.Info, err error) error {
slog.Debug("checking resources for changes", "resources", len(targets))
err := targets.Visit(func(target *resource.Info, err error) error {
if err != nil {
return err
}
helper := resource.NewHelper(info.Client, info.Mapping).WithFieldManager(getManagedFieldsManager())
if _, err := helper.Get(info.Namespace, info.Name); err != nil {
helper := resource.NewHelper(target.Client, target.Mapping).WithFieldManager(getManagedFieldsManager())
if _, err := helper.Get(target.Namespace, target.Name); err != nil {
if !apierrors.IsNotFound(err) {
return fmt.Errorf("could not get information about the resource: %w", err)
}
// 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.
if err := createResource(info); err != nil {
if err := createResource(target); err != nil {
return fmt.Errorf("failed to create resource: %w", err)
}
kind := info.Mapping.GroupVersionKind.Kind
slog.Debug("created a new resource", "namespace", info.Namespace, "name", info.Name, "kind", kind)
kind := target.Mapping.GroupVersionKind.Kind
slog.Debug("created a new resource", "namespace", target.Namespace, "name", target.Name, "kind", kind)
return nil
}
originalInfo := original.Get(info)
if originalInfo == nil {
kind := info.Mapping.GroupVersionKind.Kind
return fmt.Errorf("no %s with the name %q found", kind, info.Name)
original := originals.Get(target)
if original == nil {
kind := target.Mapping.GroupVersionKind.Kind
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 {
slog.Debug("error updating the resource", "namespace", info.Namespace, "name", info.Name, "kind", info.Mapping.GroupVersionKind.Kind, slog.Any("error", err))
if err := updateApplyFunc(original, target); err != nil {
updateErrors = append(updateErrors, err)
}
// Because we check for errors later, append the info regardless
res.Updated = append(res.Updated, info)
res.Updated = append(res.Updated, target)
return nil
})
@ -449,7 +552,7 @@ func (c *Client) update(original, target ResourceList, force, threeWayMerge bool
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)
if err := info.Get(); err != nil {
@ -473,20 +576,80 @@ func (c *Client) update(original, target ResourceList, force, threeWayMerge bool
return res, nil
}
// 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
// modified in the target configuration, and deletes resources from the current
// configuration that are not present in the target configuration. If an error
// occurs, a Result will still be returned with the error, containing all
// resource updates, creations, and deletions that were attempted. These can be
// used for cleanup or other logging purposes.
type clientUpdateOptions struct {
threeWayMergeForUnstructured bool
serverSideApply bool
forceReplace bool
forceConflicts bool
dryRun bool
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
// for unstructured objects.
func (c *Client) UpdateThreeWayMerge(original, target ResourceList, force bool) (*Result, error) {
return c.update(original, target, force, true)
// `forceConflicts` forces conflicts to be resolved (may be enabled when serverSideApply enabled only)
// see: https://kubernetes.io/docs/reference/using-api/server-side-apply/#conflicts
func ClientUpdateOptionServerSideApply(serverSideApply, forceConflicts bool) ClientUpdateOption {
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
// creates resources that don't already exist, updates resources that have been
// 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
// resource updates, creations, and deletions that were attempted. These can be
// 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
@ -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
// will be returned in the `Deleted` ResourceList that is part of the result.
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
@ -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
// will be returned in the `Deleted` ResourceList that is part of the result.
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
res := &Result{}
mtx := sync.Mutex{}
err := perform(resources, func(info *resource.Info) error {
slog.Debug("starting delete resource", "namespace", info.Namespace, "name", info.Name, "kind", info.Mapping.GroupVersionKind.Kind)
err := deleteResource(info, propagation)
err := perform(resources, func(target *resource.Info) error {
slog.Debug("starting delete resource", "namespace", target.Namespace, "name", target.Name, "kind", target.Mapping.GroupVersionKind.Kind)
err := deleteResource(target, propagation)
if err == nil || apierrors.IsNotFound(err) {
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()
defer mtx.Unlock()
res.Deleted = append(res.Deleted, info)
res.Deleted = append(res.Deleted, target)
return nil
}
mtx.Lock()
@ -548,6 +785,17 @@ func rdelete(_ *Client, resources ResourceList, propagation metav1.DeletionPropa
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.
// Otherwise, one is calculated based on the name of the binary.
func getManagedFieldsManager() string {
@ -568,18 +816,41 @@ func getManagedFieldsManager() string {
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) {
var kind string
var wg sync.WaitGroup
defer wg.Wait()
for _, info := range infos {
currentKind := info.Object.GetObjectKind().GroupVersionKind().Kind
if kind != currentKind {
wg.Wait()
kind = currentKind
}
wg.Add(1)
go func(i *resource.Info) {
errs <- fn(i)
go func(info *resource.Info) {
errs <- fn(info)
wg.Done()
}(info)
}
@ -597,6 +868,7 @@ func createResource(info *resource.Info) error {
if err != nil {
return err
}
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) {
oldData, err := json.Marshal(current)
func createPatch(original runtime.Object, target *resource.Info, threeWayMergeForUnstructured bool) ([]byte, types.PatchType, error) {
oldData, err := json.Marshal(original)
if err != nil {
return nil, types.StrategicMergePatchType, fmt.Errorf("serializing current configuration: %w", err)
}
@ -674,48 +946,95 @@ func createPatch(target *resource.Info, current runtime.Object, threeWayMergeFor
return patch, types.StrategicMergePatchType, err
}
func updateResource(_ *Client, target *resource.Info, currentObj runtime.Object, force, threeWayMergeForUnstructured bool) error {
var (
obj runtime.Object
helper = resource.NewHelper(target.Client, target.Mapping).WithFieldManager(getManagedFieldsManager())
kind = target.Mapping.GroupVersionKind.Kind
)
func replaceResource(target *resource.Info, fieldValidationDirective FieldValidationDirective) error {
// if --force is applied, attempt to replace the existing resource with the new object.
if force {
var err error
obj, err = helper.Replace(target.Namespace, target.Name, true, target.Object)
if err != nil {
return fmt.Errorf("failed to replace object: %w", err)
}
slog.Debug("replace succeeded", "name", target.Name, "initialKind", currentObj.GetObjectKind().GroupVersionKind().Kind, "kind", kind)
} else {
patch, patchType, err := createPatch(target, currentObj, threeWayMergeForUnstructured)
if err != nil {
return fmt.Errorf("failed to create patch: %w", err)
}
helper := resource.NewHelper(target.Client, target.Mapping).
WithFieldValidation(string(fieldValidationDirective)).
WithFieldManager(getManagedFieldsManager())
if patch == nil || string(patch) == "{}" {
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
// Otherwise there will be no labels and other functions that use labels will panic
if err := target.Get(); err != nil {
return fmt.Errorf("failed to refresh resource information: %w", err)
}
return nil
}
// send patch to server
slog.Debug("patching resource", "kind", kind, "name", target.Name, "namespace", target.Namespace)
obj, err = helper.Patch(target.Namespace, target.Name, patchType, patch, nil)
if err != nil {
return fmt.Errorf("cannot patch %q with kind %s: %w", target.Name, kind, err)
obj, err := helper.Replace(target.Namespace, target.Name, true, target.Object)
if err != nil {
return fmt.Errorf("failed to replace object: %w", err)
}
if err := target.Refresh(obj, true); err != nil {
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 {
return fmt.Errorf("failed to create patch: %w", err)
}
kind := target.Mapping.GroupVersionKind.Kind
if patch == nil || string(patch) == "{}" {
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
// Otherwise there will be no labels and other functions that use labels will panic
if err := target.Get(); err != nil {
return fmt.Errorf("failed to refresh resource information: %w", err)
}
return nil
}
// send patch to server
slog.Debug("patching resource", "kind", kind, "name", target.Name, "namespace", target.Namespace)
helper := resource.NewHelper(target.Client, target.Mapping).WithFieldManager(getManagedFieldsManager())
obj, err := helper.Patch(target.Namespace, target.Name, patchType, patch, nil)
if err != nil {
return fmt.Errorf("cannot patch %q with kind %s: %w", target.Name, kind, err)
}
target.Refresh(obj, true)
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
func (c *Client) GetPodList(namespace string, listOptions metav1.ListOptions) (*v1.PodList, error) {
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
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 {
return nil, f.CreateError
}
return f.PrintingKubeClient.Create(resources)
return f.PrintingKubeClient.Create(resources, options...)
}
// 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
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 {
return &kube.Result{}, f.UpdateError
}
return f.PrintingKubeClient.Update(r, modified, ignoreMe)
}
// 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)
return f.PrintingKubeClient.Update(r, modified, options...)
}
// 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.
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))
if err != nil {
return nil, err
@ -98,7 +98,7 @@ func (p *PrintingKubeClient) Delete(resources kube.ResourceList) (*kube.Result,
}
// 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))
if err != nil {
return nil, err

@ -30,14 +30,14 @@ import (
// A KubernetesClient must be concurrency safe.
type Interface interface {
// 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(resources ResourceList) (*Result, []error)
// Update updates one or more resources or creates the resource
// 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.
//
@ -53,13 +53,6 @@ type Interface interface {
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.
type Waiter interface {
// 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 _ InterfaceThreeWayMerge = (*Client)(nil)
var _ InterfaceLogs = (*Client)(nil)
var _ InterfaceDeletionPropagation = (*Client)(nil)
var _ InterfaceResources = (*Client)(nil)

@ -18,7 +18,6 @@ package kube // import "helm.sh/helm/v4/pkg/kube"
import (
"context"
"errors"
"fmt"
"log/slog"
"net/http"
@ -223,26 +222,6 @@ func (hw *legacyWaiter) WatchUntilReady(resources ResourceList, timeout time.Dur
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 {
kind := info.Mapping.GroupVersionKind.Kind
switch kind {

@ -160,6 +160,9 @@ func validateChartVersion(cf *chart.Metadata) error {
func validateChartMaintainer(cf *chart.Metadata) error {
for _, maintainer := range cf.Maintainers {
if maintainer == nil {
return errors.New("a maintainer entry is empty")
}
if maintainer.Name == "" {
return errors.New("each maintainer requires a name")
} 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())
}
}
// 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) {

@ -17,6 +17,7 @@ package plugin // import "helm.sh/helm/v4/pkg/plugin"
import (
"fmt"
"log/slog"
"os"
"path/filepath"
"regexp"
@ -83,7 +84,7 @@ type Metadata struct {
PlatformCommand []PlatformCommand `json:"platformCommand"`
// 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
// 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
// pointing the command to a shell script.
//
// DEPRECATED: Use PlatformCommand instead. Remove in Helm 4.
// DEPRECATED: Use PlatformCommand instead
Command string `json:"command"`
// IgnoreFlags ignores any flags passed in from Helm
@ -119,14 +120,14 @@ type Metadata struct {
PlatformHooks PlatformHooks `json:"platformHooks"`
// 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
// be present in this command.
//
// Note that the command is executed in the sh shell.
//
// DEPRECATED: Use PlatformHooks instead. Remove in Helm 4.
// DEPRECATED: Use PlatformHooks instead
Hooks Hooks
// 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)
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 {
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.

@ -496,8 +496,8 @@ func TestValidatePluginData(t *testing.T) {
{false, mockMissingMeta}, // Test if the metadata section missing
{true, mockNoCommand}, // Test no command metadata works
{true, mockLegacyCommand}, // Test legacy command metadata works
{false, mockWithCommand}, // Test platformCommand and command both set fails
{false, mockWithHooks}, // Test platformHooks and hooks both set fails
{true, mockWithCommand}, // Test platformCommand and command both set works
{true, mockWithHooks}, // Test platformHooks and hooks both set works
} {
err := validatePluginData(item.plug, fmt.Sprintf("test-%d", i))
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
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)
}
if transport.TLSClientConfig == nil {
switch {
case setConfig != nil:
transport.TLSClientConfig = setConfig
case transport.TLSClientConfig == nil:
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
func LoginOptInsecure(insecure bool) LoginOption {
return func(o *loginOperation) {
tlsConfig, err := ensureTLSConfig(o.client.authorizer)
tlsConfig, err := ensureTLSConfig(o.client.authorizer, nil)
if err != nil {
panic(err)
@ -324,7 +327,7 @@ func LoginOptTLSClientConfig(certFile, keyFile, caFile string) LoginOption {
if (certFile == "" || keyFile == "") && caFile == "" {
return
}
tlsConfig, err := ensureTLSConfig(o.client.authorizer)
tlsConfig, err := ensureTLSConfig(o.client.authorizer, nil)
if err != nil {
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 (
// LogoutOption allows specifying various settings on logout
LogoutOption func(*logoutOperation)

@ -17,6 +17,8 @@ limitations under the License.
package registry
import (
"crypto/tls"
"crypto/x509"
"os"
"testing"
@ -52,6 +54,30 @@ func (suite *TLSRegistryClientTestSuite) Test_0_Login() {
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() {
testPush(&suite.TestSuite)
}

@ -22,35 +22,6 @@ import (
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.
func Reverse(list []*rspb.Release, sortFn func([]*rspb.Release)) {
sortFn(list)
@ -62,17 +33,25 @@ func Reverse(list []*rspb.Release, sortFn func([]*rspb.Release)) {
// SortByName returns the list of releases sorted
// in lexicographical order.
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
// release's last deployed time (in seconds).
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
// release's revision number (release.Version).
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 idx := len(cvs) - 1; idx >= 0; idx-- {
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
}
// 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
}
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:]...)
}
}

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

Loading…
Cancel
Save