Merge branch 'main' into rebase-recursive-dependencies

Signed-off-by: Alik Khilazhev <7482065+alikhil@users.noreply.github.com>
pull/30855/head
Alik Khilazhev 3 months ago committed by GitHub
commit f2d5516930
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

2
.github/env vendored

@ -1,2 +1,2 @@
GOLANG_VERSION=1.24
GOLANGCI_LINT_VERSION=v2.0.2
GOLANGCI_LINT_VERSION=v2.1.0

@ -22,7 +22,7 @@ jobs:
- name: Add variables to environment file
run: cat ".github/env" >> "$GITHUB_ENV"
- name: Setup Go
uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # pin@5.4.0
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # pin@5.5.0
with:
go-version: '${{ env.GOLANG_VERSION }}'
check-latest: true

@ -17,11 +17,11 @@ jobs:
- name: Add variables to environment file
run: cat ".github/env" >> "$GITHUB_ENV"
- name: Setup Go
uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # pin@5.4.0
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # pin@5.5.0
with:
go-version: '${{ env.GOLANG_VERSION }}'
check-latest: true
- name: golangci-lint
uses: golangci/golangci-lint-action@1481404843c368bc19ca9406f87d6e0fc97bdcfd #pin@7.0.0
uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 #pin@8.0.0
with:
version: ${{ env.GOLANGCI_LINT_VERSION }}

@ -18,7 +18,7 @@ jobs:
- name: Add variables to environment file
run: cat ".github/env" >> "$GITHUB_ENV"
- name: Setup Go
uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # pin@5.4.0
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # pin@5.5.0
with:
go-version: '${{ env.GOLANG_VERSION }}'
check-latest: true

@ -28,7 +28,7 @@ jobs:
run: cat ".github/env" >> "$GITHUB_ENV"
- name: Setup Go
uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # pin@5.4.0
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # pin@5.5.0
with:
go-version: '${{ env.GOLANG_VERSION }}'
- name: Run unit tests
@ -85,7 +85,7 @@ jobs:
run: cat ".github/env" >> "$GITHUB_ENV"
- name: Setup Go
uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # pin@5.4.0
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # pin@5.5.0
with:
go-version: '${{ env.GOLANG_VERSION }}'
check-latest: true

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

@ -20,14 +20,17 @@ linters:
enable:
- depguard
- dupl
- gomodguard
- govet
- ineffassign
- misspell
- nakedret
- revive
- staticcheck
- thelper
- unused
- usestdlibvars
- usetesting
exclusions:
generated: lax
@ -55,6 +58,13 @@ linters:
dupl:
threshold: 400
gomodguard:
blocked:
modules:
- github.com/evanphx/json-patch:
recommendations:
- github.com/evanphx/json-patch/v5
run:
timeout: 10m

@ -5,6 +5,7 @@
[![GoDoc](https://img.shields.io/static/v1?label=godoc&message=reference&color=blue)](https://pkg.go.dev/helm.sh/helm/v4)
[![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/3131/badge)](https://bestpractices.coreinfrastructure.org/projects/3131)
[![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/helm/helm/badge)](https://scorecard.dev/viewer/?uri=github.com/helm/helm)
[![LFX Health Score](https://img.shields.io/static/v1?label=Health%20Score&message=Healthy&color=A7F3D0&logo=linuxfoundation&logoColor=white&style=flat)](https://insights.linuxfoundation.org/project/helm)
Helm is a tool for managing Charts. Charts are packages of pre-configured Kubernetes resources.

@ -41,7 +41,6 @@ func main() {
}
if err := cmd.Execute(); err != nil {
slog.Debug("error", slog.Any("error", err))
switch e := err.(type) {
case helmcmd.PluginError:
os.Exit(e.Code)

@ -13,7 +13,7 @@ require (
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2
github.com/cyphar/filepath-securejoin v0.4.1
github.com/distribution/distribution/v3 v3.0.0
github.com/evanphx/json-patch v5.9.11+incompatible
github.com/evanphx/json-patch/v5 v5.9.11
github.com/fluxcd/cli-utils v0.36.0-flux.13
github.com/foxcpp/go-mockdns v1.1.0
github.com/gobwas/glob v0.2.3
@ -27,24 +27,24 @@ require (
github.com/opencontainers/image-spec v1.1.1
github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5
github.com/rubenv/sql-migrate v1.8.0
github.com/santhosh-tekuri/jsonschema/v6 v6.0.1
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2
github.com/spf13/cobra v1.9.1
github.com/spf13/pflag v1.0.6
github.com/stretchr/testify v1.10.0
golang.org/x/crypto v0.38.0
golang.org/x/crypto v0.39.0
golang.org/x/term v0.32.0
golang.org/x/text v0.25.0
golang.org/x/text v0.26.0
gopkg.in/yaml.v3 v3.0.1
k8s.io/api v0.33.0
k8s.io/apiextensions-apiserver v0.33.0
k8s.io/apimachinery v0.33.0
k8s.io/apiserver v0.33.0
k8s.io/cli-runtime v0.33.0
k8s.io/client-go v0.33.0
k8s.io/api v0.33.1
k8s.io/apiextensions-apiserver v0.33.1
k8s.io/apimachinery v0.33.1
k8s.io/apiserver v0.33.1
k8s.io/cli-runtime v0.33.1
k8s.io/client-go v0.33.1
k8s.io/klog/v2 v2.130.1
k8s.io/kubectl v0.33.0
k8s.io/kubectl v0.33.1
oras.land/oras-go/v2 v2.6.0
sigs.k8s.io/controller-runtime v0.20.4
sigs.k8s.io/controller-runtime v0.21.0
sigs.k8s.io/yaml v1.4.0
)
@ -68,7 +68,6 @@ require (
github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c // indirect
github.com/docker/go-metrics v0.0.1 // indirect
github.com/emicklei/go-restful/v3 v3.12.1 // indirect
github.com/evanphx/json-patch/v5 v5.9.11 // indirect
github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect
github.com/fatih/color v1.13.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
@ -155,13 +154,13 @@ require (
go.opentelemetry.io/otel/sdk/metric v1.32.0 // indirect
go.opentelemetry.io/otel/trace v1.34.0 // indirect
go.opentelemetry.io/proto/otlp v1.4.0 // indirect
golang.org/x/mod v0.24.0 // indirect
golang.org/x/net v0.39.0 // indirect
golang.org/x/mod v0.25.0 // indirect
golang.org/x/net v0.40.0 // indirect
golang.org/x/oauth2 v0.29.0 // indirect
golang.org/x/sync v0.14.0 // indirect
golang.org/x/sync v0.15.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/time v0.11.0 // indirect
golang.org/x/tools v0.32.0 // indirect
golang.org/x/tools v0.33.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
@ -169,7 +168,7 @@ require (
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
k8s.io/component-base v0.33.0 // indirect
k8s.io/component-base v0.33.1 // indirect
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect
k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e // indirect
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect

@ -77,8 +77,6 @@ github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQ
github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw=
github.com/emicklei/go-restful/v3 v3.12.1 h1:PJMDIM/ak7btuL8Ex0iYET9hxM3CI2sjZtzpL63nKAU=
github.com/emicklei/go-restful/v3 v3.12.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/evanphx/json-patch v5.9.11+incompatible h1:ixHHqfcGvxhWkniF1tWxBHA0yb4Z+d1UQi45df52xW8=
github.com/evanphx/json-patch v5.9.11+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU=
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=
@ -292,8 +290,8 @@ github.com/rubenv/sql-migrate v1.8.0 h1:dXnYiJk9k3wetp7GfQbKJcPHjVJL6YK19tKj8t2N
github.com/rubenv/sql-migrate v1.8.0/go.mod h1:F2bGFBwCU+pnmbtNYDeKvSuvL6lBVtXDXUUv5t+u1qw=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 h1:PKK9DyHxif4LZo+uQSgXNqs0jj5+xZwwfKHgph2lxBw=
github.com/santhosh-tekuri/jsonschema/v6 v6.0.1/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ=
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ=
github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
@ -386,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.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
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.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
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/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=
@ -409,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.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98=
golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -423,8 +421,8 @@ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -464,8 +462,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.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@ -476,8 +474,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.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU=
golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s=
golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
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=
@ -504,32 +502,32 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
k8s.io/api v0.33.0 h1:yTgZVn1XEe6opVpP1FylmNrIFWuDqe2H0V8CT5gxfIU=
k8s.io/api v0.33.0/go.mod h1:CTO61ECK/KU7haa3qq8sarQ0biLq2ju405IZAd9zsiM=
k8s.io/apiextensions-apiserver v0.33.0 h1:d2qpYL7Mngbsc1taA4IjJPRJ9ilnsXIrndH+r9IimOs=
k8s.io/apiextensions-apiserver v0.33.0/go.mod h1:VeJ8u9dEEN+tbETo+lFkwaaZPg6uFKLGj5vyNEwwSzc=
k8s.io/apimachinery v0.33.0 h1:1a6kHrJxb2hs4t8EE5wuR/WxKDwGN1FKH3JvDtA0CIQ=
k8s.io/apimachinery v0.33.0/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM=
k8s.io/apiserver v0.33.0 h1:QqcM6c+qEEjkOODHppFXRiw/cE2zP85704YrQ9YaBbc=
k8s.io/apiserver v0.33.0/go.mod h1:EixYOit0YTxt8zrO2kBU7ixAtxFce9gKGq367nFmqI8=
k8s.io/cli-runtime v0.33.0 h1:Lbl/pq/1o8BaIuyn+aVLdEPHVN665tBAXUePs8wjX7c=
k8s.io/cli-runtime v0.33.0/go.mod h1:QcA+r43HeUM9jXFJx7A+yiTPfCooau/iCcP1wQh4NFw=
k8s.io/client-go v0.33.0 h1:UASR0sAYVUzs2kYuKn/ZakZlcs2bEHaizrrHUZg0G98=
k8s.io/client-go v0.33.0/go.mod h1:kGkd+l/gNGg8GYWAPr0xF1rRKvVWvzh9vmZAMXtaKOg=
k8s.io/component-base v0.33.0 h1:Ot4PyJI+0JAD9covDhwLp9UNkUja209OzsJ4FzScBNk=
k8s.io/component-base v0.33.0/go.mod h1:aXYZLbw3kihdkOPMDhWbjGCO6sg+luw554KP51t8qCU=
k8s.io/api v0.33.1 h1:tA6Cf3bHnLIrUK4IqEgb2v++/GYUtqiu9sRVk3iBXyw=
k8s.io/api v0.33.1/go.mod h1:87esjTn9DRSRTD4fWMXamiXxJhpOIREjWOSjsW1kEHw=
k8s.io/apiextensions-apiserver v0.33.1 h1:N7ccbSlRN6I2QBcXevB73PixX2dQNIW0ZRuguEE91zI=
k8s.io/apiextensions-apiserver v0.33.1/go.mod h1:uNQ52z1A1Gu75QSa+pFK5bcXc4hq7lpOXbweZgi4dqA=
k8s.io/apimachinery v0.33.1 h1:mzqXWV8tW9Rw4VeW9rEkqvnxj59k1ezDUl20tFK/oM4=
k8s.io/apimachinery v0.33.1/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM=
k8s.io/apiserver v0.33.1 h1:yLgLUPDVC6tHbNcw5uE9mo1T6ELhJj7B0geifra3Qdo=
k8s.io/apiserver v0.33.1/go.mod h1:VMbE4ArWYLO01omz+k8hFjAdYfc3GVAYPrhP2tTKccs=
k8s.io/cli-runtime v0.33.1 h1:TvpjEtF71ViFmPeYMj1baZMJR4iWUEplklsUQ7D3quA=
k8s.io/cli-runtime v0.33.1/go.mod h1:9dz5Q4Uh8io4OWCLiEf/217DXwqNgiTS/IOuza99VZE=
k8s.io/client-go v0.33.1 h1:ZZV/Ks2g92cyxWkRRnfUDsnhNn28eFpt26aGc8KbXF4=
k8s.io/client-go v0.33.1/go.mod h1:JAsUrl1ArO7uRVFWfcj6kOomSlCv+JpvIsp6usAGefA=
k8s.io/component-base v0.33.1 h1:EoJ0xA+wr77T+G8p6T3l4efT2oNwbqBVKR71E0tBIaI=
k8s.io/component-base v0.33.1/go.mod h1:guT/w/6piyPfTgq7gfvgetyXMIh10zuXA6cRRm3rDuY=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4=
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8=
k8s.io/kubectl v0.33.0 h1:HiRb1yqibBSCqic4pRZP+viiOBAnIdwYDpzUFejs07g=
k8s.io/kubectl v0.33.0/go.mod h1:gAlGBuS1Jq1fYZ9AjGWbI/5Vk3M/VW2DK4g10Fpyn/0=
k8s.io/kubectl v0.33.1 h1:OJUXa6FV5bap6iRy345ezEjU9dTLxqv1zFTVqmeHb6A=
k8s.io/kubectl v0.33.1/go.mod h1:Z07pGqXoP4NgITlPRrnmiM3qnoo1QrK1zjw85Aiz8J0=
k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e h1:KqK5c/ghOm8xkHYhlodbp6i6+r+ChV2vuAuVRdFbLro=
k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
oras.land/oras-go/v2 v2.6.0 h1:X4ELRsiGkrbeox69+9tzTu492FMUu7zJQW6eJU+I2oc=
oras.land/oras-go/v2 v2.6.0/go.mod h1:magiQDfG6H1O9APp+rOsvCPcW1GD2MM7vgnKY0Y+u1o=
sigs.k8s.io/controller-runtime v0.20.4 h1:X3c+Odnxz+iPTRobG4tp092+CvBU9UK0t/bRf+n0DGU=
sigs.k8s.io/controller-runtime v0.20.4/go.mod h1:xg2XB0K5ShQzAgsoujxuKN4LNXR2LfwwHsPj7Iaw+XY=
sigs.k8s.io/controller-runtime v0.21.0 h1:CYfjpEuicjUecRk+KAeyYh+ouUBn4llGyDYytIGcJS8=
sigs.k8s.io/controller-runtime v0.21.0/go.mod h1:OSg14+F65eWqIu4DceX7k/+QRAbTTvxeQSNSOQpukWM=
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE=
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
sigs.k8s.io/kustomize/api v0.19.0 h1:F+2HB2mU1MSiR9Hp1NEgoU2q9ItNOaBJl0I4Dlus5SQ=

@ -76,6 +76,7 @@ func walkTree(n *Node, path string, f func(path string, n *Node)) {
}
func makeTree(t *testing.T) {
t.Helper()
walkTree(tree, tree.name, func(path string, n *Node) {
if n.entries == nil {
if n.symLinkedTo != "" {
@ -99,6 +100,7 @@ func makeTree(t *testing.T) {
}
func checkMarks(t *testing.T, report bool) {
t.Helper()
walkTree(tree, tree.name, func(path string, n *Node) {
if n.marks != n.expectedMarks && report {
t.Errorf("node %s mark = %d; expected %d", path, n.marks, n.expectedMarks)

@ -29,12 +29,12 @@ import (
func HelmHome(t *testing.T) {
t.Helper()
base := t.TempDir()
os.Setenv(xdg.CacheHomeEnvVar, base)
os.Setenv(xdg.ConfigHomeEnvVar, base)
os.Setenv(xdg.DataHomeEnvVar, base)
os.Setenv(helmpath.CacheHomeEnvVar, "")
os.Setenv(helmpath.ConfigHomeEnvVar, "")
os.Setenv(helmpath.DataHomeEnvVar, "")
t.Setenv(xdg.CacheHomeEnvVar, base)
t.Setenv(xdg.ConfigHomeEnvVar, base)
t.Setenv(xdg.DataHomeEnvVar, base)
t.Setenv(helmpath.CacheHomeEnvVar, "")
t.Setenv(helmpath.ConfigHomeEnvVar, "")
t.Setenv(helmpath.DataHomeEnvVar, "")
}
// TempFile ensures a temp file for unit testing purposes.
@ -46,9 +46,10 @@ func HelmHome(t *testing.T) {
// tempdir := TempFile(t, "foo", []byte("bar"))
// filename := filepath.Join(tempdir, "foo")
func TempFile(t *testing.T, name string, data []byte) string {
t.Helper()
path := t.TempDir()
filename := filepath.Join(path, name)
if err := os.WriteFile(filename, data, 0755); err != nil {
if err := os.WriteFile(filename, data, 0o755); err != nil {
t.Fatal(err)
}
return path

@ -457,6 +457,7 @@ func TestCopyFileFail(t *testing.T) {
// files this function creates. It is the caller's responsibility to call
// this function before the test is done running, whether there's an error or not.
func setupInaccessibleDir(t *testing.T, op func(dir string) error) func() {
t.Helper()
dir := t.TempDir()
subdir := filepath.Join(dir, "dir")

@ -30,8 +30,9 @@ const (
)
func testfile(t *testing.T, file string) (path string) {
var err error
if path, err = filepath.Abs(filepath.Join(tlsTestDir, file)); err != nil {
t.Helper()
path, err := filepath.Abs(filepath.Join(tlsTestDir, file))
if err != nil {
t.Fatalf("error getting absolute path to test file %q: %v", file, err)
}
return path

@ -26,6 +26,7 @@ import (
"path"
"path/filepath"
"strings"
"sync"
"text/template"
"k8s.io/apimachinery/pkg/api/meta"
@ -86,6 +87,8 @@ type Configuration struct {
// HookOutputFunc called with container name and returns and expects writer that will receive the log output.
HookOutputFunc func(namespace, pod, container string) io.Writer
mutex sync.Mutex
}
// renderResources renders the templates in a chart

@ -40,6 +40,7 @@ import (
var verbose = flag.Bool("test.log", false, "enable test logging (debug by default)")
func actionConfigFixture(t *testing.T) *Configuration {
t.Helper()
return actionConfigFixtureWithDummyResources(t, nil)
}

@ -49,13 +49,7 @@ func (cfg *Configuration) execHook(rl *release.Release, hook release.HookEvent,
for i, h := range executingHooks {
// Set default delete policy to before-hook-creation
if len(h.DeletePolicies) == 0 {
// TODO(jlegrone): Only apply before-hook-creation delete policy to run to completion
// resources. For all other resource types update in place if a
// resource with the same name already exists and is owned by the
// current release.
h.DeletePolicies = []release.HookDeletePolicy{release.HookBeforeHookCreation}
}
cfg.hookSetDeletePolicy(h)
if err := cfg.deleteHookByPolicy(h, release.HookBeforeHookCreation, waitStrategy, timeout); err != nil {
return err
@ -154,7 +148,7 @@ func (cfg *Configuration) deleteHookByPolicy(h *release.Hook, policy release.Hoo
if h.Kind == "CustomResourceDefinition" {
return nil
}
if hookHasDeletePolicy(h, policy) {
if cfg.hookHasDeletePolicy(h, policy) {
resources, err := cfg.KubeClient.Build(bytes.NewBufferString(h.Manifest), false)
if err != nil {
return fmt.Errorf("unable to build kubernetes object for deleting hook %s: %w", h.Path, err)
@ -188,10 +182,26 @@ func (cfg *Configuration) deleteHooksByPolicy(hooks []*release.Hook, policy rele
// hookHasDeletePolicy determines whether the defined hook deletion policy matches the hook deletion polices
// supported by helm. If so, mark the hook as one should be deleted.
func hookHasDeletePolicy(h *release.Hook, policy release.HookDeletePolicy) bool {
func (cfg *Configuration) hookHasDeletePolicy(h *release.Hook, policy release.HookDeletePolicy) bool {
cfg.mutex.Lock()
defer cfg.mutex.Unlock()
return slices.Contains(h.DeletePolicies, policy)
}
// hookSetDeletePolicy determines whether the defined hook deletion policy matches the hook deletion polices
// supported by helm. If so, mark the hook as one should be deleted.
func (cfg *Configuration) hookSetDeletePolicy(h *release.Hook) {
cfg.mutex.Lock()
defer cfg.mutex.Unlock()
if len(h.DeletePolicies) == 0 {
// TODO(jlegrone): Only apply before-hook-creation delete policy to run to completion
// resources. For all other resource types update in place if a
// resource with the same name already exists and is owned by the
// current release.
h.DeletePolicies = []release.HookDeletePolicy{release.HookBeforeHookCreation}
}
}
// outputLogsByPolicy outputs a pods logs if the hook policy instructs it to
func (cfg *Configuration) outputLogsByPolicy(h *release.Hook, releaseNamespace string, policy release.HookOutputLogPolicy) error {
if !hookHasOutputLogPolicy(h, policy) {

@ -167,6 +167,7 @@ func TestInstallRelease_HooksOutputLogsOnSuccessAndFailure(t *testing.T) {
}
func runInstallForHooksWithSuccess(t *testing.T, manifest, expectedNamespace string, shouldOutput bool) {
t.Helper()
var expectedOutput string
if shouldOutput {
expectedOutput = fmt.Sprintf("attempted to output logs for namespace: %s", expectedNamespace)
@ -190,6 +191,7 @@ func runInstallForHooksWithSuccess(t *testing.T, manifest, expectedNamespace str
}
func runInstallForHooksWithFailure(t *testing.T, manifest, expectedNamespace string, shouldOutput bool) {
t.Helper()
var expectedOutput string
if shouldOutput {
expectedOutput = fmt.Sprintf("attempted to output logs for namespace: %s", expectedNamespace)

@ -116,6 +116,7 @@ func installActionWithConfig(config *Configuration) *Install {
}
func installAction(t *testing.T) *Install {
t.Helper()
config := actionConfigFixture(t)
instAction := NewInstall(config)
instAction.Namespace = "spaced"
@ -130,7 +131,7 @@ func TestInstallRelease(t *testing.T) {
instAction := installAction(t)
vals := map[string]interface{}{}
ctx, done := context.WithCancel(context.Background())
ctx, done := context.WithCancel(t.Context())
res, err := instAction.RunWithContext(ctx, buildChart(), vals)
if err != nil {
t.Fatalf("Failed install: %s", err)
@ -446,7 +447,9 @@ func TestInstallReleaseIncorrectTemplate_DryRun(t *testing.T) {
instAction.DryRun = true
vals := map[string]interface{}{}
_, err := instAction.Run(buildChart(withSampleIncludingIncorrectTemplates()), vals)
expectedErr := "\"hello/templates/incorrect\" at <.Values.bad.doh>: nil pointer evaluating interface {}.doh"
expectedErr := `hello/templates/incorrect:1:10
executing "hello/templates/incorrect" at <.Values.bad.doh>:
nil pointer evaluating interface {}.doh`
if err == nil {
t.Fatalf("Install should fail containing error: %s", expectedErr)
}
@ -556,7 +559,7 @@ func TestInstallRelease_Wait_Interrupted(t *testing.T) {
instAction.WaitStrategy = kube.StatusWatcherStrategy
vals := map[string]interface{}{}
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(t.Context())
time.AfterFunc(time.Second, cancel)
goroutines := runtime.NumGoroutine()
@ -640,7 +643,7 @@ func TestInstallRelease_Atomic_Interrupted(t *testing.T) {
instAction.Atomic = true
vals := map[string]interface{}{}
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(t.Context())
time.AfterFunc(time.Second, cancel)
goroutines := runtime.NumGoroutine()

@ -64,13 +64,14 @@ func TestList_Empty(t *testing.T) {
}
func newListFixture(t *testing.T) *List {
t.Helper()
return NewList(actionConfigFixture(t))
}
func TestList_OneNamespace(t *testing.T) {
is := assert.New(t)
lister := newListFixture(t)
makeMeSomeReleases(lister.cfg.Releases, t)
makeMeSomeReleases(t, lister.cfg.Releases)
list, err := lister.Run()
is.NoError(err)
is.Len(list, 3)
@ -79,7 +80,7 @@ func TestList_OneNamespace(t *testing.T) {
func TestList_AllNamespaces(t *testing.T) {
is := assert.New(t)
lister := newListFixture(t)
makeMeSomeReleases(lister.cfg.Releases, t)
makeMeSomeReleases(t, lister.cfg.Releases)
lister.AllNamespaces = true
lister.SetStateMask()
list, err := lister.Run()
@ -91,7 +92,7 @@ func TestList_Sort(t *testing.T) {
is := assert.New(t)
lister := newListFixture(t)
lister.Sort = ByNameDesc // Other sorts are tested elsewhere
makeMeSomeReleases(lister.cfg.Releases, t)
makeMeSomeReleases(t, lister.cfg.Releases)
list, err := lister.Run()
is.NoError(err)
is.Len(list, 3)
@ -104,7 +105,7 @@ func TestList_Limit(t *testing.T) {
is := assert.New(t)
lister := newListFixture(t)
lister.Limit = 2
makeMeSomeReleases(lister.cfg.Releases, t)
makeMeSomeReleases(t, lister.cfg.Releases)
list, err := lister.Run()
is.NoError(err)
is.Len(list, 2)
@ -117,7 +118,7 @@ func TestList_BigLimit(t *testing.T) {
is := assert.New(t)
lister := newListFixture(t)
lister.Limit = 20
makeMeSomeReleases(lister.cfg.Releases, t)
makeMeSomeReleases(t, lister.cfg.Releases)
list, err := lister.Run()
is.NoError(err)
is.Len(list, 3)
@ -133,7 +134,7 @@ func TestList_LimitOffset(t *testing.T) {
lister := newListFixture(t)
lister.Limit = 2
lister.Offset = 1
makeMeSomeReleases(lister.cfg.Releases, t)
makeMeSomeReleases(t, lister.cfg.Releases)
list, err := lister.Run()
is.NoError(err)
is.Len(list, 2)
@ -148,7 +149,7 @@ func TestList_LimitOffsetOutOfBounds(t *testing.T) {
lister := newListFixture(t)
lister.Limit = 2
lister.Offset = 3 // Last item is index 2
makeMeSomeReleases(lister.cfg.Releases, t)
makeMeSomeReleases(t, lister.cfg.Releases)
list, err := lister.Run()
is.NoError(err)
is.Len(list, 0)
@ -163,7 +164,7 @@ func TestList_LimitOffsetOutOfBounds(t *testing.T) {
func TestList_StateMask(t *testing.T) {
is := assert.New(t)
lister := newListFixture(t)
makeMeSomeReleases(lister.cfg.Releases, t)
makeMeSomeReleases(t, lister.cfg.Releases)
one, err := lister.cfg.Releases.Get("one", 1)
is.NoError(err)
one.SetStatus(release.StatusUninstalled, "uninstalled")
@ -193,7 +194,7 @@ func TestList_StateMaskWithStaleRevisions(t *testing.T) {
lister := newListFixture(t)
lister.StateMask = ListFailed
makeMeSomeReleasesWithStaleFailure(lister.cfg.Releases, t)
makeMeSomeReleasesWithStaleFailure(t, lister.cfg.Releases)
res, err := lister.Run()
@ -205,7 +206,7 @@ func TestList_StateMaskWithStaleRevisions(t *testing.T) {
is.Equal("failed", res[0].Name)
}
func makeMeSomeReleasesWithStaleFailure(store *storage.Storage, t *testing.T) {
func makeMeSomeReleasesWithStaleFailure(t *testing.T, store *storage.Storage) {
t.Helper()
one := namedReleaseStub("clean", release.StatusDeployed)
one.Namespace = "default"
@ -242,7 +243,7 @@ func TestList_Filter(t *testing.T) {
is := assert.New(t)
lister := newListFixture(t)
lister.Filter = "th."
makeMeSomeReleases(lister.cfg.Releases, t)
makeMeSomeReleases(t, lister.cfg.Releases)
res, err := lister.Run()
is.NoError(err)
@ -254,13 +255,13 @@ func TestList_FilterFailsCompile(t *testing.T) {
is := assert.New(t)
lister := newListFixture(t)
lister.Filter = "t[h.{{{"
makeMeSomeReleases(lister.cfg.Releases, t)
makeMeSomeReleases(t, lister.cfg.Releases)
_, err := lister.Run()
is.Error(err)
}
func makeMeSomeReleases(store *storage.Storage, t *testing.T) {
func makeMeSomeReleases(t *testing.T, store *storage.Storage) {
t.Helper()
one := releaseStub()
one.Name = "one"

@ -28,6 +28,7 @@ import (
)
func uninstallAction(t *testing.T) *Uninstall {
t.Helper()
config := actionConfigFixture(t)
unAction := NewUninstall(config)
return unAction

@ -36,6 +36,7 @@ import (
)
func upgradeAction(t *testing.T) *Upgrade {
t.Helper()
config := actionConfigFixture(t)
upAction := NewUpgrade(config)
upAction.Namespace = "spaced"
@ -56,7 +57,7 @@ func TestUpgradeRelease_Success(t *testing.T) {
upAction.WaitStrategy = kube.StatusWatcherStrategy
vals := map[string]interface{}{}
ctx, done := context.WithCancel(context.Background())
ctx, done := context.WithCancel(t.Context())
res, err := upAction.RunWithContext(ctx, rel.Name, buildChart(), vals)
done()
req.NoError(err)
@ -383,7 +384,6 @@ func TestUpgradeRelease_Pending(t *testing.T) {
}
func TestUpgradeRelease_Interrupted_Wait(t *testing.T) {
is := assert.New(t)
req := require.New(t)
@ -399,8 +399,7 @@ func TestUpgradeRelease_Interrupted_Wait(t *testing.T) {
upAction.WaitStrategy = kube.StatusWatcherStrategy
vals := map[string]interface{}{}
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
ctx, cancel := context.WithCancel(t.Context())
time.AfterFunc(time.Second, cancel)
res, err := upAction.RunWithContext(ctx, rel.Name, buildChart(), vals)
@ -408,11 +407,9 @@ func TestUpgradeRelease_Interrupted_Wait(t *testing.T) {
req.Error(err)
is.Contains(res.Info.Description, "Upgrade \"interrupted-release\" failed: context canceled")
is.Equal(res.Info.Status, release.StatusFailed)
}
func TestUpgradeRelease_Interrupted_Atomic(t *testing.T) {
is := assert.New(t)
req := require.New(t)
@ -428,8 +425,7 @@ func TestUpgradeRelease_Interrupted_Atomic(t *testing.T) {
upAction.Atomic = true
vals := map[string]interface{}{}
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
ctx, cancel := context.WithCancel(t.Context())
time.AfterFunc(time.Second, cancel)
res, err := upAction.RunWithContext(ctx, rel.Name, buildChart(), vals)
@ -445,7 +441,7 @@ func TestUpgradeRelease_Interrupted_Atomic(t *testing.T) {
}
func TestMergeCustomLabels(t *testing.T) {
var tests = [][3]map[string]string{
tests := [][3]map[string]string{
{nil, nil, map[string]string{}},
{map[string]string{}, map[string]string{}, map[string]string{}},
{map[string]string{"k1": "v1", "k2": "v2"}, nil, map[string]string{"k1": "v1", "k2": "v2"}},
@ -550,7 +546,7 @@ func TestUpgradeRelease_DryRun(t *testing.T) {
upAction.DryRun = true
vals := map[string]interface{}{}
ctx, done := context.WithCancel(context.Background())
ctx, done := context.WithCancel(t.Context())
res, err := upAction.RunWithContext(ctx, rel.Name, buildChart(withSampleSecret()), vals)
done()
req.NoError(err)
@ -566,7 +562,7 @@ func TestUpgradeRelease_DryRun(t *testing.T) {
upAction.HideSecret = true
vals = map[string]interface{}{}
ctx, done = context.WithCancel(context.Background())
ctx, done = context.WithCancel(t.Context())
res, err = upAction.RunWithContext(ctx, rel.Name, buildChart(withSampleSecret()), vals)
done()
req.NoError(err)
@ -582,7 +578,7 @@ func TestUpgradeRelease_DryRun(t *testing.T) {
upAction.DryRun = false
vals = map[string]interface{}{}
ctx, done = context.WithCancel(context.Background())
ctx, done = context.WithCancel(t.Context())
_, err = upAction.RunWithContext(ctx, rel.Name, buildChart(withSampleSecret()), vals)
done()
req.Error(err)

@ -33,6 +33,7 @@ func TestLoadArchiveFiles(t *testing.T) {
name: "empty input should return no files",
generate: func(_ *tar.Writer) {},
check: func(t *testing.T, _ []*BufferedFile, err error) {
t.Helper()
if err.Error() != "no files in chart archive" {
t.Fatalf(`expected "no files in chart archive", got [%#v]`, err)
}
@ -61,6 +62,7 @@ func TestLoadArchiveFiles(t *testing.T) {
}
},
check: func(t *testing.T, files []*BufferedFile, err error) {
t.Helper()
if err != nil {
t.Fatalf(`got unwanted error [%#v] for tar file with pax_global_header content`, err)
}

@ -19,7 +19,6 @@ package loader
import (
"bufio"
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
@ -224,10 +223,7 @@ func LoadValues(data io.Reader) (map[string]interface{}, error) {
}
return nil, fmt.Errorf("error reading yaml document: %w", err)
}
if err := yaml.Unmarshal(raw, &currentMap, func(d *json.Decoder) *json.Decoder {
d.UseNumber()
return d
}); err != nil {
if err := yaml.Unmarshal(raw, &currentMap); err != nil {
return nil, fmt.Errorf("cannot unmarshal yaml document: %w", err)
}
values = MergeMaps(values, currentMap)

@ -648,6 +648,7 @@ func verifyChart(t *testing.T, c *chart.Chart) {
}
func verifyDependencies(t *testing.T, c *chart.Chart) {
t.Helper()
if len(c.Metadata.Dependencies) != 2 {
t.Errorf("Expected 2 dependencies, got %d", len(c.Metadata.Dependencies))
}
@ -670,6 +671,7 @@ func verifyDependencies(t *testing.T, c *chart.Chart) {
}
func verifyDependenciesLock(t *testing.T, c *chart.Chart) {
t.Helper()
if len(c.Metadata.Dependencies) != 2 {
t.Errorf("Expected 2 dependencies, got %d", len(c.Metadata.Dependencies))
}
@ -692,10 +694,12 @@ func verifyDependenciesLock(t *testing.T, c *chart.Chart) {
}
func verifyFrobnitz(t *testing.T, c *chart.Chart) {
t.Helper()
verifyChartFileAndTemplate(t, c, "frobnitz")
}
func verifyChartFileAndTemplate(t *testing.T, c *chart.Chart, name string) {
t.Helper()
if c.Metadata == nil {
t.Fatal("Metadata is nil")
}
@ -750,6 +754,7 @@ func verifyChartFileAndTemplate(t *testing.T, c *chart.Chart, name string) {
}
func verifyBomStripped(t *testing.T, files []*chart.File) {
t.Helper()
for _, file := range files {
if bytes.HasPrefix(file.Data, utf8bom) {
t.Errorf("Byte Order Mark still present in processed file %s", file.Name)

@ -39,7 +39,7 @@ func LoadChartfile(filename string) (*chart.Metadata, error) {
return y, err
}
// StrictLoadChartFile loads a Chart.yaml into a *chart.Metadata using a strict unmarshaling
// StrictLoadChartfile loads a Chart.yaml into a *chart.Metadata using a strict unmarshaling
func StrictLoadChartfile(filename string) (*chart.Metadata, error) {
b, err := os.ReadFile(filename)
if err != nil {

@ -34,7 +34,7 @@ func TestLoadChartfile(t *testing.T) {
}
func verifyChartfile(t *testing.T, f *chart.Metadata, name string) {
t.Helper()
if f == nil { //nolint:staticcheck
t.Fatal("Failed verifyChartfile because f is nil")
}

@ -15,7 +15,6 @@ limitations under the License.
package util
import (
"encoding/json"
"os"
"path/filepath"
"sort"
@ -238,20 +237,6 @@ func TestProcessDependencyImportValues(t *testing.T) {
if b := strconv.FormatBool(pv); b != vv {
t.Errorf("failed to match imported bool value %v with expected %v for key %q", b, vv, kk)
}
case json.Number:
if fv, err := pv.Float64(); err == nil {
if sfv := strconv.FormatFloat(fv, 'f', -1, 64); sfv != vv {
t.Errorf("failed to match imported float value %v with expected %v for key %q", sfv, vv, kk)
}
}
if iv, err := pv.Int64(); err == nil {
if siv := strconv.FormatInt(iv, 10); siv != vv {
t.Errorf("failed to match imported int value %v with expected %v for key %q", siv, vv, kk)
}
}
if pv.String() != vv {
t.Errorf("failed to match imported string value %q with expected %q for key %q", pv, vv, kk)
}
default:
if pv != vv {
t.Errorf("failed to match imported string value %q with expected %q for key %q", pv, vv, kk)
@ -356,10 +341,6 @@ func TestProcessDependencyImportValuesMultiLevelPrecedence(t *testing.T) {
if s := strconv.FormatFloat(pv, 'f', -1, 64); s != vv {
t.Errorf("failed to match imported float value %v with expected %v", s, vv)
}
case json.Number:
if pv.String() != vv {
t.Errorf("failed to match imported string value %q with expected %q", pv, vv)
}
default:
if pv != vv {
t.Errorf("failed to match imported string value %q with expected %q", pv, vv)
@ -558,6 +539,7 @@ func TestDependentChartsWithSomeSubchartsSpecifiedInDependency(t *testing.T) {
}
func validateDependencyTree(t *testing.T, c *chart.Chart) {
t.Helper()
for _, dependency := range c.Dependencies() {
if dependency.Parent() != c {
if dependency.Parent() != c {

@ -55,8 +55,8 @@ func TestValidateAgainstInvalidSingleSchema(t *testing.T) {
errString = err.Error()
}
expectedErrString := "unable to validate schema: runtime error: invalid " +
"memory address or nil pointer dereference"
expectedErrString := `"file:///values.schema.json#" is not valid against metaschema: jsonschema validation failed with 'https://json-schema.org/draft/2020-12/schema#'
- at '': got number, want boolean or object`
if errString != expectedErrString {
t.Errorf("Error string :\n`%s`\ndoes not match expected\n`%s`", errString, expectedErrString)
}

@ -17,7 +17,6 @@ limitations under the License.
package util
import (
"encoding/json"
"errors"
"fmt"
"io"
@ -106,10 +105,7 @@ func tableLookup(v Values, simple string) (Values, error) {
// ReadValues will parse YAML byte data into a Values.
func ReadValues(data []byte) (vals Values, err error) {
err = yaml.Unmarshal(data, &vals, func(d *json.Decoder) *json.Decoder {
d.UseNumber()
return d
})
err = yaml.Unmarshal(data, &vals)
if len(vals) == 0 {
vals = Values{}
}

@ -224,6 +224,7 @@ chapter:
}
func matchValues(t *testing.T, data map[string]interface{}) {
t.Helper()
if data["poet"] != "Coleridge" {
t.Errorf("Unexpected poet: %s", data["poet"])
}

@ -38,7 +38,6 @@ func TestSetNamespace(t *testing.T) {
if settings.namespace != "testns" {
t.Errorf("Expected namespace testns, got %s", settings.namespace)
}
}
func TestEnvSettings(t *testing.T) {
@ -126,7 +125,7 @@ func TestEnvSettings(t *testing.T) {
defer resetEnv()()
for k, v := range tt.envvars {
os.Setenv(k, v)
t.Setenv(k, v)
}
flags := pflag.NewFlagSet("testing", pflag.ContinueOnError)
@ -233,10 +232,7 @@ func TestEnvOrBool(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.env != "" {
t.Cleanup(func() {
os.Unsetenv(tt.env)
})
os.Setenv(tt.env, tt.val)
t.Setenv(tt.env, tt.val)
}
actual := envBoolOr(tt.env, tt.def)
if actual != tt.expected {

@ -27,6 +27,7 @@ import (
// Check if file completion should be performed according to parameter 'shouldBePerformed'
func checkFileCompletion(t *testing.T, cmdName string, shouldBePerformed bool) {
t.Helper()
storage := storageFixture()
storage.Create(&release.Release{
Name: "myrelease",
@ -64,6 +65,7 @@ func TestCompletionFileCompletion(t *testing.T) {
}
func checkReleaseCompletion(t *testing.T, cmdName string, multiReleasesAllowed bool) {
t.Helper()
multiReleaseTestGolden := "output/empty_nofile_comp.txt"
if multiReleasesAllowed {
multiReleaseTestGolden = "output/release_list_repeat_comp.txt"

@ -33,7 +33,7 @@ func TestCreateCmd(t *testing.T) {
ensure.HelmHome(t)
cname := "testchart"
dir := t.TempDir()
defer testChdir(t, dir)()
defer t.Chdir(dir)
// Run a create
if _, _, err := executeActionCommand("create " + cname); err != nil {
@ -64,19 +64,19 @@ func TestCreateStarterCmd(t *testing.T) {
ensure.HelmHome(t)
cname := "testchart"
defer resetEnv()()
os.MkdirAll(helmpath.CachePath(), 0755)
defer testChdir(t, helmpath.CachePath())()
os.MkdirAll(helmpath.CachePath(), 0o755)
defer t.Chdir(helmpath.CachePath())
// Create a starter.
starterchart := helmpath.DataPath("starters")
os.MkdirAll(starterchart, 0755)
os.MkdirAll(starterchart, 0o755)
if dest, err := chartutil.Create("starterchart", starterchart); err != nil {
t.Fatalf("Could not create chart: %s", err)
} else {
t.Logf("Created %s", dest)
}
tplpath := filepath.Join(starterchart, "starterchart", "templates", "foo.tpl")
if err := os.WriteFile(tplpath, []byte("test"), 0644); err != nil {
if err := os.WriteFile(tplpath, []byte("test"), 0o644); err != nil {
t.Fatalf("Could not write template: %s", err)
}
@ -122,7 +122,6 @@ func TestCreateStarterCmd(t *testing.T) {
if !found {
t.Error("Did not find foo.tpl")
}
}
func TestCreateStarterAbsoluteCmd(t *testing.T) {
@ -132,19 +131,19 @@ func TestCreateStarterAbsoluteCmd(t *testing.T) {
// Create a starter.
starterchart := helmpath.DataPath("starters")
os.MkdirAll(starterchart, 0755)
os.MkdirAll(starterchart, 0o755)
if dest, err := chartutil.Create("starterchart", starterchart); err != nil {
t.Fatalf("Could not create chart: %s", err)
} else {
t.Logf("Created %s", dest)
}
tplpath := filepath.Join(starterchart, "starterchart", "templates", "foo.tpl")
if err := os.WriteFile(tplpath, []byte("test"), 0644); err != nil {
if err := os.WriteFile(tplpath, []byte("test"), 0o644); err != nil {
t.Fatalf("Could not write template: %s", err)
}
os.MkdirAll(helmpath.CachePath(), 0755)
defer testChdir(t, helmpath.CachePath())()
os.MkdirAll(helmpath.CachePath(), 0o755)
defer t.Chdir(helmpath.CachePath())
starterChartPath := filepath.Join(starterchart, "starterchart")

@ -250,6 +250,7 @@ func TestDependencyUpdateCmd_WithRepoThatWasNotAdded(t *testing.T) {
}
func setupMockRepoServer(t *testing.T) *repotest.Server {
t.Helper()
srv := repotest.NewTempServer(
t,
repotest.WithChartSourceGlob("testdata/testcharts/*.tgz"),

@ -29,6 +29,7 @@ import (
)
func outputFlagCompletionTest(t *testing.T, cmdName string) {
t.Helper()
releasesMockWithStatus := func(info *release.Info, hooks ...*release.Hook) []*release.Release {
info.LastDeployed = helmtime.Unix(1452902400, 0).UTC()
return []*release.Release{{

@ -75,6 +75,7 @@ func TestHistoryOutputCompletion(t *testing.T) {
}
func revisionFlagCompletionTest(t *testing.T, cmdName string) {
t.Helper()
mk := func(name string, vers int, status release.Status) *release.Release {
return release.Mock(&release.MockReleaseOptions{
Name: name,

@ -111,9 +111,9 @@ func TestPackage(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cachePath := t.TempDir()
defer testChdir(t, cachePath)()
defer t.Chdir(cachePath)
if err := os.MkdirAll("toot", 0777); err != nil {
if err := os.MkdirAll("toot", 0o777); err != nil {
t.Fatal(err)
}

@ -79,7 +79,6 @@ func TestManuallyProcessArgs(t *testing.T) {
t.Errorf("expected unknown flag %d to be %q, got %q", i, expectUnknown[i], k)
}
}
}
func TestLoadPlugins(t *testing.T) {
@ -276,6 +275,7 @@ func TestLoadPluginsForCompletion(t *testing.T) {
}
func checkCommand(t *testing.T, plugins []*cobra.Command, tests []staticCompletionDetails) {
t.Helper()
if len(plugins) != len(tests) {
t.Fatalf("Expected commands %v, got %v", tests, plugins)
}
@ -326,7 +326,6 @@ func checkCommand(t *testing.T, plugins []*cobra.Command, tests []staticCompleti
}
func TestPluginDynamicCompletion(t *testing.T) {
tests := []cmdTestCase{{
name: "completion for plugin",
cmd: "__complete args ''",
@ -363,7 +362,7 @@ func TestLoadPlugins_HelmNoPlugins(t *testing.T) {
settings.PluginsDirectory = "testdata/helmhome/helm/plugins"
settings.RepositoryConfig = "testdata/helmhome/helm/repository"
os.Setenv("HELM_NO_PLUGINS", "1")
t.Setenv("HELM_NO_PLUGINS", "1")
out := bytes.NewBuffer(nil)
cmd := &cobra.Command{}
@ -376,7 +375,6 @@ func TestLoadPlugins_HelmNoPlugins(t *testing.T) {
}
func TestPluginCmdsCompletion(t *testing.T) {
tests := []cmdTestCase{{
name: "completion for plugin update",
cmd: "__complete plugin update ''",

@ -34,6 +34,10 @@ import (
const registryLoginDesc = `
Authenticate to a remote registry.
For example for Github Container Registry:
echo "$GITHUB_TOKEN" | helm registry login ghcr.io -u $GITHUB_USER --password-stdin
`
type registryLoginOptions struct {

@ -17,6 +17,7 @@ limitations under the License.
package cmd
import (
"errors"
"fmt"
"io"
"regexp"
@ -85,7 +86,7 @@ func newReleaseTestCmd(cfg *action.Configuration, out io.Writer) *cobra.Command
// Print a newline to stdout to separate the output
fmt.Fprintln(out)
if err := client.GetPodLogs(out, rel); err != nil {
return err
return errors.Join(runErr, err)
}
}

@ -50,7 +50,7 @@ func TestRepoAddCmd(t *testing.T) {
defer srv2.Stop()
tmpdir := filepath.Join(t.TempDir(), "path-component.yaml/data")
if err := os.MkdirAll(tmpdir, 0777); err != nil {
if err := os.MkdirAll(tmpdir, 0o777); err != nil {
t.Fatal(err)
}
repoFile := filepath.Join(tmpdir, "repositories.yaml")
@ -99,7 +99,7 @@ func TestRepoAdd(t *testing.T) {
forceUpdate: false,
repoFile: repoFile,
}
os.Setenv(xdg.CacheHomeEnvVar, rootDir)
t.Setenv(xdg.CacheHomeEnvVar, rootDir)
if err := o.run(io.Discard); err != nil {
t.Error(err)
@ -153,7 +153,7 @@ func TestRepoAddCheckLegalName(t *testing.T) {
forceUpdate: false,
repoFile: repoFile,
}
os.Setenv(xdg.CacheHomeEnvVar, rootDir)
t.Setenv(xdg.CacheHomeEnvVar, rootDir)
wantErrorMsg := fmt.Sprintf("repository name (%s) contains '/', please specify a different name without '/'", testRepoName)
@ -191,6 +191,7 @@ func TestRepoAddConcurrentHiddenFile(t *testing.T) {
}
func repoAddConcurrent(t *testing.T, testName, repoFile string) {
t.Helper()
ts := repotest.NewTempServer(
t,
repotest.WithChartSourceGlob("testdata/testserver/*.*"),

@ -17,7 +17,6 @@ limitations under the License.
package cmd
import (
"errors"
"fmt"
"io"
@ -37,10 +36,14 @@ func newRepoListCmd(out io.Writer) *cobra.Command {
Short: "list chart repositories",
Args: require.NoArgs,
ValidArgsFunction: noMoreArgsCompFunc,
RunE: func(_ *cobra.Command, _ []string) error {
RunE: func(cmd *cobra.Command, _ []string) error {
// The error is silently ignored. If no repository file exists, it cannot be loaded,
// or the file isn't the right format to be parsed the error is ignored. The
// repositories will be 0.
f, _ := repo.LoadFile(settings.RepositoryConfig)
if len(f.Repositories) == 0 && outfmt != output.JSON && outfmt != output.YAML {
return errors.New("no repositories to show")
fmt.Fprintln(cmd.ErrOrStderr(), "no repositories to show")
return nil
}
return outfmt.Write(out, &repoListWriter{f.Repositories})

@ -17,6 +17,8 @@ limitations under the License.
package cmd
import (
"fmt"
"path/filepath"
"testing"
)
@ -27,3 +29,26 @@ func TestRepoListOutputCompletion(t *testing.T) {
func TestRepoListFileCompletion(t *testing.T) {
checkFileCompletion(t, "repo list", false)
}
func TestRepoList(t *testing.T) {
rootDir := t.TempDir()
repoFile := filepath.Join(rootDir, "repositories.yaml")
repoFile2 := "testdata/repositories.yaml"
tests := []cmdTestCase{
{
name: "list with no repos",
cmd: fmt.Sprintf("repo list --repository-config %s --repository-cache %s", repoFile, rootDir),
golden: "output/repo-list-empty.txt",
wantError: false,
},
{
name: "list with repos",
cmd: fmt.Sprintf("repo list --repository-config %s --repository-cache %s", repoFile2, rootDir),
golden: "output/repo-list.txt",
wantError: false,
},
}
runTestCmd(t, tests)
}

@ -153,6 +153,7 @@ func createCacheFiles(rootDir string, repoName string) (cacheIndexFile string, c
}
func testCacheFiles(t *testing.T, cacheIndexFile string, cacheChartsFile string, repoName string) {
t.Helper()
if _, err := os.Stat(cacheIndexFile); err == nil {
t.Errorf("Error cache index file was not removed for repository %s", repoName)
}

@ -113,14 +113,19 @@ func updateCharts(repos []*repo.ChartRepository, out io.Writer) error {
var wg sync.WaitGroup
failRepoURLChan := make(chan string, len(repos))
writeMutex := sync.Mutex{}
for _, re := range repos {
wg.Add(1)
go func(re *repo.ChartRepository) {
defer wg.Done()
if _, err := re.DownloadIndexFile(); err != nil {
writeMutex.Lock()
defer writeMutex.Unlock()
fmt.Fprintf(out, "...Unable to get an update from the %q chart repository (%s):\n\t%s\n", re.Config.Name, re.Config.URL, err)
failRepoURLChan <- re.Config.URL
} else {
writeMutex.Lock()
defer writeMutex.Unlock()
fmt.Fprintf(out, "...Successfully got an update from the %q chart repository\n", re.Config.Name)
}
}(re)

@ -63,6 +63,7 @@ type testCase struct {
}
func runTestCases(t *testing.T, testCases []testCase) {
t.Helper()
for i, tc := range testCases {
t.Run(fmt.Sprint(i), func(t *testing.T) {
cmd := &cobra.Command{

@ -80,7 +80,7 @@ func TestRootCmd(t *testing.T) {
ensure.HelmHome(t)
for k, v := range tt.envvars {
os.Setenv(k, v)
t.Setenv(k, v)
}
if _, _, err := executeActionCommand(tt.args); err != nil {

@ -22,18 +22,6 @@ import (
"testing"
)
func TestTemplateCmdWithToml(t *testing.T) {
tests := []cmdTestCase{
{
name: "check toToml function rendering",
cmd: fmt.Sprintf("template '%s'", "testdata/testcharts/issue-totoml"),
golden: "output/issue-totoml.txt",
},
}
runTestCmd(t, tests)
}
var chartPath = "testdata/testcharts/subchart"
func TestTemplateCmd(t *testing.T) {

@ -1,8 +0,0 @@
---
# Source: issue-totoml/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: issue-totoml
data: |
key = 13

@ -0,0 +1 @@
no repositories to show

@ -0,0 +1,4 @@
NAME URL
charts https://charts.helm.sh/stable
firstexample http://firstexample.com
secondexample http://secondexample.com

@ -1,3 +0,0 @@
apiVersion: v2
name: issue-totoml
version: 0.1.0

@ -1,6 +0,0 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: issue-totoml
data: |
{{ .Values.global | toToml }}

@ -193,7 +193,7 @@ func TestUpgradeCmd(t *testing.T) {
func TestUpgradeWithValue(t *testing.T) {
releaseName := "funny-bunny-v2"
relMock, ch, chartPath := prepareMockRelease(releaseName, t)
relMock, ch, chartPath := prepareMockRelease(t, releaseName)
defer resetEnv()()
@ -220,7 +220,7 @@ func TestUpgradeWithValue(t *testing.T) {
func TestUpgradeWithStringValue(t *testing.T) {
releaseName := "funny-bunny-v3"
relMock, ch, chartPath := prepareMockRelease(releaseName, t)
relMock, ch, chartPath := prepareMockRelease(t, releaseName)
defer resetEnv()()
@ -248,7 +248,7 @@ func TestUpgradeWithStringValue(t *testing.T) {
func TestUpgradeInstallWithSubchartNotes(t *testing.T) {
releaseName := "wacky-bunny-v1"
relMock, ch, _ := prepareMockRelease(releaseName, t)
relMock, ch, _ := prepareMockRelease(t, releaseName)
defer resetEnv()()
@ -280,7 +280,7 @@ func TestUpgradeInstallWithSubchartNotes(t *testing.T) {
func TestUpgradeWithValuesFile(t *testing.T) {
releaseName := "funny-bunny-v4"
relMock, ch, chartPath := prepareMockRelease(releaseName, t)
relMock, ch, chartPath := prepareMockRelease(t, releaseName)
defer resetEnv()()
@ -308,7 +308,7 @@ func TestUpgradeWithValuesFile(t *testing.T) {
func TestUpgradeWithValuesFromStdin(t *testing.T) {
releaseName := "funny-bunny-v5"
relMock, ch, chartPath := prepareMockRelease(releaseName, t)
relMock, ch, chartPath := prepareMockRelease(t, releaseName)
defer resetEnv()()
@ -340,7 +340,7 @@ func TestUpgradeWithValuesFromStdin(t *testing.T) {
func TestUpgradeInstallWithValuesFromStdin(t *testing.T) {
releaseName := "funny-bunny-v6"
_, _, chartPath := prepareMockRelease(releaseName, t)
_, _, chartPath := prepareMockRelease(t, releaseName)
defer resetEnv()()
@ -368,7 +368,8 @@ func TestUpgradeInstallWithValuesFromStdin(t *testing.T) {
}
func prepareMockRelease(releaseName string, t *testing.T) (func(n string, v int, ch *chart.Chart) *release.Release, *chart.Chart, string) {
func prepareMockRelease(t *testing.T, releaseName string) (func(n string, v int, ch *chart.Chart) *release.Release, *chart.Chart, string) {
t.Helper()
tmpChart := t.TempDir()
configmapData, err := os.ReadFile("testdata/testcharts/upgradetest/templates/configmap.yaml")
if err != nil {
@ -445,7 +446,7 @@ func TestUpgradeFileCompletion(t *testing.T) {
func TestUpgradeInstallWithLabels(t *testing.T) {
releaseName := "funny-bunny-labels"
_, _, chartPath := prepareMockRelease(releaseName, t)
_, _, chartPath := prepareMockRelease(t, releaseName)
defer resetEnv()()
@ -471,7 +472,8 @@ func TestUpgradeInstallWithLabels(t *testing.T) {
}
}
func prepareMockReleaseWithSecret(releaseName string, t *testing.T) (func(n string, v int, ch *chart.Chart) *release.Release, *chart.Chart, string) {
func prepareMockReleaseWithSecret(t *testing.T, releaseName string) (func(n string, v int, ch *chart.Chart) *release.Release, *chart.Chart, string) {
t.Helper()
tmpChart := t.TempDir()
configmapData, err := os.ReadFile("testdata/testcharts/chart-with-secret/templates/configmap.yaml")
if err != nil {
@ -512,7 +514,7 @@ func prepareMockReleaseWithSecret(releaseName string, t *testing.T) (func(n stri
func TestUpgradeWithDryRun(t *testing.T) {
releaseName := "funny-bunny-labels"
_, _, chartPath := prepareMockReleaseWithSecret(releaseName, t)
_, _, chartPath := prepareMockReleaseWithSecret(t, releaseName)
defer resetEnv()()

@ -46,6 +46,7 @@ func TestResolveChartRef(t *testing.T) {
{name: "reference, querystring repo", ref: "testing-querystring/alpine", expect: "http://example.com/alpine-1.2.3.tgz?key=value"},
{name: "reference, testing-relative repo", ref: "testing-relative/foo", expect: "http://example.com/helm/charts/foo-1.2.3.tgz"},
{name: "reference, testing-relative repo", ref: "testing-relative/bar", expect: "http://example.com/helm/bar-1.2.3.tgz"},
{name: "reference, testing-relative repo", ref: "testing-relative/baz", expect: "http://example.com/path/to/baz-1.2.3.tgz"},
{name: "reference, testing-relative-trailing-slash repo", ref: "testing-relative-trailing-slash/foo", expect: "http://example.com/helm/charts/foo-1.2.3.tgz"},
{name: "reference, testing-relative-trailing-slash repo", ref: "testing-relative-trailing-slash/bar", expect: "http://example.com/helm/bar-1.2.3.tgz"},
{name: "encoded URL", ref: "encoded-url/foobar", expect: "http://example.com/with%2Fslash/charts/foobar-4.2.1.tgz"},

@ -25,7 +25,6 @@ import (
"log"
"net/url"
"os"
"path"
"path/filepath"
"regexp"
"strings"
@ -750,7 +749,6 @@ func (m *Manager) findChartURL(name, version, repoURL string, repos map[string]*
}
for _, cr := range repos {
if urlutil.Equal(repoURL, cr.Config.URL) {
var entry repo.ChartVersions
entry, err = findEntryByName(name, cr)
@ -767,7 +765,7 @@ func (m *Manager) findChartURL(name, version, repoURL string, repos map[string]*
//nolint:nakedret
return
}
url, err = normalizeURL(repoURL, ve.URLs[0])
url, err = repo.ResolveReferenceURL(repoURL, ve.URLs[0])
if err != nil {
//nolint:nakedret
return
@ -888,24 +886,6 @@ func versionEquals(v1, v2 string) bool {
return sv1.Equal(sv2)
}
func normalizeURL(baseURL, urlOrPath string) (string, error) {
u, err := url.Parse(urlOrPath)
if err != nil {
return urlOrPath, err
}
if u.IsAbs() {
return u.String(), nil
}
u2, err := url.Parse(baseURL)
if err != nil {
return urlOrPath, fmt.Errorf("base URL failed to parse: %w", err)
}
u2.RawPath = path.Join(u2.RawPath, urlOrPath)
u2.Path = path.Join(u2.Path, urlOrPath)
return u2.String(), nil
}
// loadChartRepositories reads the repositories.yaml, and then builds a map of
// ChartRepositories.
//

@ -53,26 +53,6 @@ func TestVersionEquals(t *testing.T) {
}
}
func TestNormalizeURL(t *testing.T) {
tests := []struct {
name, base, path, expect string
}{
{name: "basic URL", base: "https://example.com", path: "http://helm.sh/foo", expect: "http://helm.sh/foo"},
{name: "relative path", base: "https://helm.sh/charts", path: "foo", expect: "https://helm.sh/charts/foo"},
{name: "Encoded path", base: "https://helm.sh/a%2Fb/charts", path: "foo", expect: "https://helm.sh/a%2Fb/charts/foo"},
}
for _, tt := range tests {
got, err := normalizeURL(tt.base, tt.path)
if err != nil {
t.Errorf("%s: error %s", tt.name, err)
continue
} else if got != tt.expect {
t.Errorf("%s: expected %q, got %q", tt.name, tt.expect, got)
}
}
}
func TestFindChartURL(t *testing.T) {
var b bytes.Buffer
m := &Manager{
@ -134,6 +114,31 @@ func TestFindChartURL(t *testing.T) {
if passcredentialsall != false {
t.Errorf("Unexpected passcredentialsall %t", passcredentialsall)
}
name = "foo"
version = "1.2.3"
repoURL = "http://example.com/helm"
churl, username, password, insecureSkipTLSVerify, passcredentialsall, _, _, _, err = m.findChartURL(name, version, repoURL, repos)
if err != nil {
t.Fatal(err)
}
if churl != "http://example.com/helm/charts/foo-1.2.3.tgz" {
t.Errorf("Unexpected URL %q", churl)
}
if username != "" {
t.Errorf("Unexpected username %q", username)
}
if password != "" {
t.Errorf("Unexpected password %q", password)
}
if passcredentialsall != false {
t.Errorf("Unexpected passcredentialsall %t", passcredentialsall)
}
if insecureSkipTLSVerify {
t.Errorf("Unexpected insecureSkipTLSVerify %t", insecureSkipTLSVerify)
}
}
func TestGetRepoNames(t *testing.T) {
@ -440,6 +445,7 @@ func TestUpdateWithNoRepo(t *testing.T) {
// Parent chart includes local-subchart 0.1.0 subchart from a fake repository, by default.
// If each of these main fields (name, version, repository) is not supplied by dep param, default value will be used.
func checkBuildWithOptionalFields(t *testing.T, chartName string, dep chart.Dependency) {
t.Helper()
// Set up a fake repo
srv := repotest.NewTempServer(
t,

@ -26,3 +26,16 @@ entries:
version: 1.2.3
checksum: 0e6661f193211d7a5206918d42f5c2a9470b737d
apiVersion: v2
baz:
- name: baz
description: Baz Chart With Absolute Path
home: https://helm.sh/helm
keywords: []
maintainers: []
sources:
- https://github.com/helm/charts
urls:
- /path/to/baz-1.2.3.tgz
version: 1.2.3
checksum: 0e6661f193211d7a5206918d42f5c2a9470b737d
apiVersion: v2

@ -34,6 +34,18 @@ import (
chartutil "helm.sh/helm/v4/pkg/chart/v2/util"
)
// taken from https://cs.opensource.google/go/go/+/refs/tags/go1.23.6:src/text/template/exec.go;l=141
// > "template: %s: executing %q at <%s>: %s"
var execErrFmt = regexp.MustCompile(`^template: (?P<templateName>(?U).+): executing (?P<functionName>(?U).+) at (?P<location>(?U).+): (?P<errMsg>(?U).+)(?P<nextErr>( template:.*)?)$`)
// taken from https://cs.opensource.google/go/go/+/refs/tags/go1.23.6:src/text/template/exec.go;l=138
// > "template: %s: %s"
var execErrFmtWithoutTemplate = regexp.MustCompile(`^template: (?P<templateName>(?U).+): (?P<errMsg>.*)(?P<nextErr>( template:.*)?)$`)
// taken from https://cs.opensource.google/go/go/+/refs/tags/go1.23.6:src/text/template/exec.go;l=191
// > "template: no template %q associated with template %q"
var execErrNoTemplateAssociated = regexp.MustCompile(`^template: no template (?P<location>.*) associated with template (?P<functionName>(.*)?)$`)
// Engine is an implementation of the Helm rendering implementation for templates.
type Engine struct {
// If strict is enabled, template rendering will fail if a template references
@ -303,7 +315,7 @@ func (e Engine) render(tpls map[string]renderable) (rendered map[string]string,
vals["Template"] = chartutil.Values{"Name": filename, "BasePath": tpls[filename].basePath}
var buf strings.Builder
if err := t.ExecuteTemplate(&buf, filename, vals); err != nil {
return map[string]string{}, cleanupExecError(filename, err)
return map[string]string{}, reformatExecErrorMsg(filename, err)
}
// Work around the issue where Go will emit "<no value>" even if Options(missing=zero)
@ -329,7 +341,33 @@ func cleanupParseError(filename string, err error) error {
return fmt.Errorf("parse error at (%s): %s", string(location), errMsg)
}
func cleanupExecError(filename string, err error) error {
type TraceableError struct {
location string
message string
executedFunction string
}
func (t TraceableError) String() string {
var errorString strings.Builder
if t.location != "" {
fmt.Fprintf(&errorString, "%s\n ", t.location)
}
if t.executedFunction != "" {
fmt.Fprintf(&errorString, "%s\n ", t.executedFunction)
}
if t.message != "" {
fmt.Fprintf(&errorString, "%s\n", t.message)
}
return errorString.String()
}
// reformatExecErrorMsg takes an error message for template rendering and formats it into a formatted
// multi-line error string
func reformatExecErrorMsg(filename string, err error) error {
// This function matches the error message against regex's for the text/template package.
// If the regex's can parse out details from that error message such as the line number, template it failed on,
// and error description, then it will construct a new error that displays these details in a structured way.
// If there are issues with parsing the error message, the err passed into the function should return instead.
if _, isExecError := err.(template.ExecError); !isExecError {
return err
}
@ -348,9 +386,47 @@ func cleanupExecError(filename string, err error) error {
if len(parts) >= 2 {
return fmt.Errorf("execution error at (%s): %s", string(location), parts[1])
}
current := err
fileLocations := []TraceableError{}
for current != nil {
var traceable TraceableError
if matches := execErrFmt.FindStringSubmatch(current.Error()); matches != nil {
templateName := matches[execErrFmt.SubexpIndex("templateName")]
functionName := matches[execErrFmt.SubexpIndex("functionName")]
locationName := matches[execErrFmt.SubexpIndex("location")]
errMsg := matches[execErrFmt.SubexpIndex("errMsg")]
traceable = TraceableError{
location: templateName,
message: errMsg,
executedFunction: "executing " + functionName + " at " + locationName + ":",
}
} else if matches := execErrFmtWithoutTemplate.FindStringSubmatch(current.Error()); matches != nil {
templateName := matches[execErrFmt.SubexpIndex("templateName")]
errMsg := matches[execErrFmt.SubexpIndex("errMsg")]
traceable = TraceableError{
location: templateName,
message: errMsg,
}
} else if matches := execErrNoTemplateAssociated.FindStringSubmatch(current.Error()); matches != nil {
traceable = TraceableError{
message: current.Error(),
}
} else {
return err
}
if len(fileLocations) == 0 || fileLocations[len(fileLocations)-1] != traceable {
fileLocations = append(fileLocations, traceable)
}
current = errors.Unwrap(current)
}
var finalErrorString strings.Builder
for _, fileLocation := range fileLocations {
fmt.Fprintf(&finalErrorString, "%s", fileLocation.String())
}
return errors.New(strings.TrimSpace(finalErrorString.String()))
}
func sortTemplates(tpls map[string]renderable) []string {
keys := make([]string, len(tpls))

@ -24,6 +24,8 @@ import (
"testing"
"text/template"
"github.com/stretchr/testify/assert"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
@ -1289,16 +1291,82 @@ func TestRenderTplMissingKeyString(t *testing.T) {
t.Errorf("Expected error, got %v", out)
return
}
switch err.(type) {
case (template.ExecError):
errTxt := fmt.Sprint(err)
if !strings.Contains(errTxt, "noSuchKey") {
t.Errorf("Expected error to contain 'noSuchKey', got %s", errTxt)
}
default:
// Some unexpected error.
t.Fatal(err)
}
func TestNestedHelpersProducesMultilineStacktrace(t *testing.T) {
c := &chart.Chart{
Metadata: &chart.Metadata{Name: "NestedHelperFunctions"},
Templates: []*chart.File{
{Name: "templates/svc.yaml", Data: []byte(
`name: {{ include "nested_helper.name" . }}`,
)},
{Name: "templates/_helpers_1.tpl", Data: []byte(
`{{- define "nested_helper.name" -}}{{- include "common.names.get_name" . -}}{{- end -}}`,
)},
{Name: "charts/common/templates/_helpers_2.tpl", Data: []byte(
`{{- define "common.names.get_name" -}}{{- .Values.nonexistant.key | trunc 63 | trimSuffix "-" -}}{{- end -}}`,
)},
},
}
expectedErrorMessage := `NestedHelperFunctions/templates/svc.yaml:1:9
executing "NestedHelperFunctions/templates/svc.yaml" at <include "nested_helper.name" .>:
error calling include:
NestedHelperFunctions/templates/_helpers_1.tpl:1:39
executing "nested_helper.name" at <include "common.names.get_name" .>:
error calling include:
NestedHelperFunctions/charts/common/templates/_helpers_2.tpl:1:49
executing "common.names.get_name" at <.Values.nonexistant.key>:
nil pointer evaluating interface {}.key`
v := chartutil.Values{}
val, _ := chartutil.CoalesceValues(c, v)
vals := map[string]interface{}{
"Values": val.AsMap(),
}
_, err := Render(c, vals)
assert.NotNil(t, err)
assert.Equal(t, expectedErrorMessage, err.Error())
}
func TestMultilineNoTemplateAssociatedError(t *testing.T) {
c := &chart.Chart{
Metadata: &chart.Metadata{Name: "multiline"},
Templates: []*chart.File{
{Name: "templates/svc.yaml", Data: []byte(
`name: {{ include "nested_helper.name" . }}`,
)},
{Name: "templates/test.yaml", Data: []byte(
`{{ toYaml .Values }}`,
)},
{Name: "charts/common/templates/_helpers_2.tpl", Data: []byte(
`{{ toYaml .Values }}`,
)},
},
}
expectedErrorMessage := `multiline/templates/svc.yaml:1:9
executing "multiline/templates/svc.yaml" at <include "nested_helper.name" .>:
error calling include:
template: no template "nested_helper.name" associated with template "gotpl"`
v := chartutil.Values{}
val, _ := chartutil.CoalesceValues(c, v)
vals := map[string]interface{}{
"Values": val.AsMap(),
}
_, err := Render(c, vals)
assert.NotNil(t, err)
assert.Equal(t, expectedErrorMessage, err.Error())
}
func TestRenderCustomTemplateFuncs(t *testing.T) {

@ -23,14 +23,13 @@ import (
const name string = "HELM_EXPERIMENTAL_FEATURE"
func TestIsEnabled(t *testing.T) {
os.Unsetenv(name)
g := Gate(name)
if g.IsEnabled() {
t.Errorf("feature gate shows as available, but the environment variable %s was not set", name)
}
os.Setenv(name, "1")
t.Setenv(name, "1")
if !g.IsEnabled() {
t.Errorf("feature gate shows as disabled, but the environment variable %s was set", name)

@ -576,6 +576,7 @@ func TestHttpClientInsecureSkipVerify(t *testing.T) {
}
func verifyInsecureSkipVerify(t *testing.T, g *HTTPGetter, caseName string, expectedValue bool) *http.Transport {
t.Helper()
returnVal, err := g.httpClient()
if err != nil {

@ -16,7 +16,6 @@
package helmpath
import (
"os"
"runtime"
"testing"
@ -24,9 +23,9 @@ import (
)
func TestHelmHome(t *testing.T) {
os.Setenv(xdg.CacheHomeEnvVar, "/cache")
os.Setenv(xdg.ConfigHomeEnvVar, "/config")
os.Setenv(xdg.DataHomeEnvVar, "/data")
t.Setenv(xdg.CacheHomeEnvVar, "/cache")
t.Setenv(xdg.ConfigHomeEnvVar, "/config")
t.Setenv(xdg.DataHomeEnvVar, "/data")
isEq := func(t *testing.T, got, expected string) {
t.Helper()
if expected != got {
@ -40,7 +39,7 @@ func TestHelmHome(t *testing.T) {
isEq(t, DataPath(), "/data/helm")
// test to see if lazy-loading environment variables at runtime works
os.Setenv(xdg.CacheHomeEnvVar, "/cache2")
t.Setenv(xdg.CacheHomeEnvVar, "/cache2")
isEq(t, CachePath(), "/cache2/helm")
}

@ -40,7 +40,7 @@ func TestDataPath(t *testing.T) {
t.Errorf("expected '%s', got '%s'", expected, lazy.dataPath(testFile))
}
os.Setenv(xdg.DataHomeEnvVar, "/tmp")
t.Setenv(xdg.DataHomeEnvVar, "/tmp")
expected = filepath.Join("/tmp", appName, testFile)
@ -58,7 +58,7 @@ func TestConfigPath(t *testing.T) {
t.Errorf("expected '%s', got '%s'", expected, lazy.configPath(testFile))
}
os.Setenv(xdg.ConfigHomeEnvVar, "/tmp")
t.Setenv(xdg.ConfigHomeEnvVar, "/tmp")
expected = filepath.Join("/tmp", appName, testFile)
@ -76,7 +76,7 @@ func TestCachePath(t *testing.T) {
t.Errorf("expected '%s', got '%s'", expected, lazy.cachePath(testFile))
}
os.Setenv(xdg.CacheHomeEnvVar, "/tmp")
t.Setenv(xdg.CacheHomeEnvVar, "/tmp")
expected = filepath.Join("/tmp", appName, testFile)

@ -16,7 +16,6 @@
package helmpath
import (
"os"
"path/filepath"
"testing"
@ -32,15 +31,13 @@ const (
)
func TestDataPath(t *testing.T) {
os.Unsetenv(xdg.DataHomeEnvVar)
expected := filepath.Join(homedir.HomeDir(), ".local", "share", appName, testFile)
if lazy.dataPath(testFile) != expected {
t.Errorf("expected '%s', got '%s'", expected, lazy.dataPath(testFile))
}
os.Setenv(xdg.DataHomeEnvVar, "/tmp")
t.Setenv(xdg.DataHomeEnvVar, "/tmp")
expected = filepath.Join("/tmp", appName, testFile)
@ -50,15 +47,13 @@ func TestDataPath(t *testing.T) {
}
func TestConfigPath(t *testing.T) {
os.Unsetenv(xdg.ConfigHomeEnvVar)
expected := filepath.Join(homedir.HomeDir(), ".config", appName, testFile)
if lazy.configPath(testFile) != expected {
t.Errorf("expected '%s', got '%s'", expected, lazy.configPath(testFile))
}
os.Setenv(xdg.ConfigHomeEnvVar, "/tmp")
t.Setenv(xdg.ConfigHomeEnvVar, "/tmp")
expected = filepath.Join("/tmp", appName, testFile)
@ -68,15 +63,13 @@ func TestConfigPath(t *testing.T) {
}
func TestCachePath(t *testing.T) {
os.Unsetenv(xdg.CacheHomeEnvVar)
expected := filepath.Join(homedir.HomeDir(), ".cache", appName, testFile)
if lazy.cachePath(testFile) != expected {
t.Errorf("expected '%s', got '%s'", expected, lazy.cachePath(testFile))
}
os.Setenv(xdg.CacheHomeEnvVar, "/tmp")
t.Setenv(xdg.CacheHomeEnvVar, "/tmp")
expected = filepath.Join("/tmp", appName, testFile)

@ -170,10 +170,10 @@ func (r *Rules) parseRule(rule string) error {
rule = strings.TrimSuffix(rule, "/")
}
if strings.HasPrefix(rule, "/") {
if after, ok := strings.CutPrefix(rule, "/"); ok {
// Require path matches the root path.
p.match = func(n string, _ os.FileInfo) bool {
rule = strings.TrimPrefix(rule, "/")
rule = after
ok, err := filepath.Match(rule, n)
if err != nil {
slog.Error("failed to compile", "rule", rule, slog.Any("error", err))

@ -30,7 +30,7 @@ import (
"strings"
"sync"
jsonpatch "github.com/evanphx/json-patch"
jsonpatch "github.com/evanphx/json-patch/v5"
v1 "k8s.io/api/core/v1"
apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
apiextv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
@ -585,10 +585,14 @@ func batchPerform(infos ResourceList, fn func(*resource.Info) error, errs chan<-
}
}
var createMutex sync.Mutex
func createResource(info *resource.Info) error {
return retry.RetryOnConflict(
retry.DefaultRetry,
func() error {
createMutex.Lock()
defer createMutex.Unlock()
obj, err := resource.NewHelper(info.Client, info.Mapping).WithFieldManager(getManagedFieldsManager()).Create(info.Namespace, true, info.Object)
if err != nil {
return err

@ -109,6 +109,7 @@ func newResponseJSON(code int, json []byte) (*http.Response, error) {
}
func newTestClient(t *testing.T) *Client {
t.Helper()
testFactory := cmdtesting.NewTestFactory()
t.Cleanup(testFactory.Cleanup)
@ -215,6 +216,7 @@ func TestCreate(t *testing.T) {
}
func testUpdate(t *testing.T, threeWayMerge bool) {
t.Helper()
listA := newPodList("starfish", "otter", "squid")
listB := newPodList("starfish", "otter", "dolphin")
listC := newPodList("starfish", "otter", "dolphin")

@ -60,7 +60,7 @@ func Test_ReadyChecker_IsReady_Pod(t *testing.T) {
pausedAsReady: false,
},
args: args{
ctx: context.TODO(),
ctx: t.Context(),
resource: &resource.Info{Object: &corev1.Pod{}, Name: "foo", Namespace: defaultNamespace},
},
pod: newPodWithCondition("foo", corev1.ConditionTrue),
@ -75,7 +75,7 @@ func Test_ReadyChecker_IsReady_Pod(t *testing.T) {
pausedAsReady: false,
},
args: args{
ctx: context.TODO(),
ctx: t.Context(),
resource: &resource.Info{Object: &corev1.Pod{}, Name: "foo", Namespace: defaultNamespace},
},
pod: newPodWithCondition("bar", corev1.ConditionTrue),
@ -90,7 +90,7 @@ func Test_ReadyChecker_IsReady_Pod(t *testing.T) {
checkJobs: tt.fields.checkJobs,
pausedAsReady: tt.fields.pausedAsReady,
}
if _, err := c.client.CoreV1().Pods(defaultNamespace).Create(context.TODO(), tt.pod, metav1.CreateOptions{}); err != nil {
if _, err := c.client.CoreV1().Pods(defaultNamespace).Create(t.Context(), tt.pod, metav1.CreateOptions{}); err != nil {
t.Errorf("Failed to create Pod error: %v", err)
return
}
@ -132,7 +132,7 @@ func Test_ReadyChecker_IsReady_Job(t *testing.T) {
pausedAsReady: false,
},
args: args{
ctx: context.TODO(),
ctx: t.Context(),
resource: &resource.Info{Object: &batchv1.Job{}, Name: "foo", Namespace: defaultNamespace},
},
job: newJob("bar", 1, intToInt32(1), 1, 0),
@ -147,7 +147,7 @@ func Test_ReadyChecker_IsReady_Job(t *testing.T) {
pausedAsReady: false,
},
args: args{
ctx: context.TODO(),
ctx: t.Context(),
resource: &resource.Info{Object: &batchv1.Job{}, Name: "foo", Namespace: defaultNamespace},
},
job: newJob("foo", 1, intToInt32(1), 1, 0),
@ -162,7 +162,7 @@ func Test_ReadyChecker_IsReady_Job(t *testing.T) {
checkJobs: tt.fields.checkJobs,
pausedAsReady: tt.fields.pausedAsReady,
}
if _, err := c.client.BatchV1().Jobs(defaultNamespace).Create(context.TODO(), tt.job, metav1.CreateOptions{}); err != nil {
if _, err := c.client.BatchV1().Jobs(defaultNamespace).Create(t.Context(), tt.job, metav1.CreateOptions{}); err != nil {
t.Errorf("Failed to create Job error: %v", err)
return
}
@ -204,7 +204,7 @@ func Test_ReadyChecker_IsReady_Deployment(t *testing.T) {
pausedAsReady: false,
},
args: args{
ctx: context.TODO(),
ctx: t.Context(),
resource: &resource.Info{Object: &appsv1.Deployment{}, Name: "foo", Namespace: defaultNamespace},
},
replicaSet: newReplicaSet("foo", 0, 0, true),
@ -220,7 +220,7 @@ func Test_ReadyChecker_IsReady_Deployment(t *testing.T) {
pausedAsReady: false,
},
args: args{
ctx: context.TODO(),
ctx: t.Context(),
resource: &resource.Info{Object: &appsv1.Deployment{}, Name: "foo", Namespace: defaultNamespace},
},
replicaSet: newReplicaSet("foo", 0, 0, true),
@ -236,11 +236,11 @@ func Test_ReadyChecker_IsReady_Deployment(t *testing.T) {
checkJobs: tt.fields.checkJobs,
pausedAsReady: tt.fields.pausedAsReady,
}
if _, err := c.client.AppsV1().Deployments(defaultNamespace).Create(context.TODO(), tt.deployment, metav1.CreateOptions{}); err != nil {
if _, err := c.client.AppsV1().Deployments(defaultNamespace).Create(t.Context(), tt.deployment, metav1.CreateOptions{}); err != nil {
t.Errorf("Failed to create Deployment error: %v", err)
return
}
if _, err := c.client.AppsV1().ReplicaSets(defaultNamespace).Create(context.TODO(), tt.replicaSet, metav1.CreateOptions{}); err != nil {
if _, err := c.client.AppsV1().ReplicaSets(defaultNamespace).Create(t.Context(), tt.replicaSet, metav1.CreateOptions{}); err != nil {
t.Errorf("Failed to create ReplicaSet error: %v", err)
return
}
@ -281,7 +281,7 @@ func Test_ReadyChecker_IsReady_PersistentVolumeClaim(t *testing.T) {
pausedAsReady: false,
},
args: args{
ctx: context.TODO(),
ctx: t.Context(),
resource: &resource.Info{Object: &corev1.PersistentVolumeClaim{}, Name: "foo", Namespace: defaultNamespace},
},
pvc: newPersistentVolumeClaim("foo", corev1.ClaimPending),
@ -296,7 +296,7 @@ func Test_ReadyChecker_IsReady_PersistentVolumeClaim(t *testing.T) {
pausedAsReady: false,
},
args: args{
ctx: context.TODO(),
ctx: t.Context(),
resource: &resource.Info{Object: &corev1.PersistentVolumeClaim{}, Name: "foo", Namespace: defaultNamespace},
},
pvc: newPersistentVolumeClaim("bar", corev1.ClaimPending),
@ -311,7 +311,7 @@ func Test_ReadyChecker_IsReady_PersistentVolumeClaim(t *testing.T) {
checkJobs: tt.fields.checkJobs,
pausedAsReady: tt.fields.pausedAsReady,
}
if _, err := c.client.CoreV1().PersistentVolumeClaims(defaultNamespace).Create(context.TODO(), tt.pvc, metav1.CreateOptions{}); err != nil {
if _, err := c.client.CoreV1().PersistentVolumeClaims(defaultNamespace).Create(t.Context(), tt.pvc, metav1.CreateOptions{}); err != nil {
t.Errorf("Failed to create PersistentVolumeClaim error: %v", err)
return
}
@ -352,7 +352,7 @@ func Test_ReadyChecker_IsReady_Service(t *testing.T) {
pausedAsReady: false,
},
args: args{
ctx: context.TODO(),
ctx: t.Context(),
resource: &resource.Info{Object: &corev1.Service{}, Name: "foo", Namespace: defaultNamespace},
},
svc: newService("foo", corev1.ServiceSpec{Type: corev1.ServiceTypeLoadBalancer, ClusterIP: ""}),
@ -367,7 +367,7 @@ func Test_ReadyChecker_IsReady_Service(t *testing.T) {
pausedAsReady: false,
},
args: args{
ctx: context.TODO(),
ctx: t.Context(),
resource: &resource.Info{Object: &corev1.Service{}, Name: "foo", Namespace: defaultNamespace},
},
svc: newService("bar", corev1.ServiceSpec{Type: corev1.ServiceTypeExternalName, ClusterIP: ""}),
@ -382,7 +382,7 @@ func Test_ReadyChecker_IsReady_Service(t *testing.T) {
checkJobs: tt.fields.checkJobs,
pausedAsReady: tt.fields.pausedAsReady,
}
if _, err := c.client.CoreV1().Services(defaultNamespace).Create(context.TODO(), tt.svc, metav1.CreateOptions{}); err != nil {
if _, err := c.client.CoreV1().Services(defaultNamespace).Create(t.Context(), tt.svc, metav1.CreateOptions{}); err != nil {
t.Errorf("Failed to create Service error: %v", err)
return
}
@ -423,7 +423,7 @@ func Test_ReadyChecker_IsReady_DaemonSet(t *testing.T) {
pausedAsReady: false,
},
args: args{
ctx: context.TODO(),
ctx: t.Context(),
resource: &resource.Info{Object: &appsv1.DaemonSet{}, Name: "foo", Namespace: defaultNamespace},
},
ds: newDaemonSet("foo", 0, 0, 1, 0, true),
@ -438,7 +438,7 @@ func Test_ReadyChecker_IsReady_DaemonSet(t *testing.T) {
pausedAsReady: false,
},
args: args{
ctx: context.TODO(),
ctx: t.Context(),
resource: &resource.Info{Object: &appsv1.DaemonSet{}, Name: "foo", Namespace: defaultNamespace},
},
ds: newDaemonSet("bar", 0, 1, 1, 1, true),
@ -453,7 +453,7 @@ func Test_ReadyChecker_IsReady_DaemonSet(t *testing.T) {
checkJobs: tt.fields.checkJobs,
pausedAsReady: tt.fields.pausedAsReady,
}
if _, err := c.client.AppsV1().DaemonSets(defaultNamespace).Create(context.TODO(), tt.ds, metav1.CreateOptions{}); err != nil {
if _, err := c.client.AppsV1().DaemonSets(defaultNamespace).Create(t.Context(), tt.ds, metav1.CreateOptions{}); err != nil {
t.Errorf("Failed to create DaemonSet error: %v", err)
return
}
@ -494,7 +494,7 @@ func Test_ReadyChecker_IsReady_StatefulSet(t *testing.T) {
pausedAsReady: false,
},
args: args{
ctx: context.TODO(),
ctx: t.Context(),
resource: &resource.Info{Object: &appsv1.StatefulSet{}, Name: "foo", Namespace: defaultNamespace},
},
ss: newStatefulSet("foo", 1, 0, 0, 1, true),
@ -509,7 +509,7 @@ func Test_ReadyChecker_IsReady_StatefulSet(t *testing.T) {
pausedAsReady: false,
},
args: args{
ctx: context.TODO(),
ctx: t.Context(),
resource: &resource.Info{Object: &appsv1.StatefulSet{}, Name: "foo", Namespace: defaultNamespace},
},
ss: newStatefulSet("bar", 1, 0, 1, 1, true),
@ -524,7 +524,7 @@ func Test_ReadyChecker_IsReady_StatefulSet(t *testing.T) {
checkJobs: tt.fields.checkJobs,
pausedAsReady: tt.fields.pausedAsReady,
}
if _, err := c.client.AppsV1().StatefulSets(defaultNamespace).Create(context.TODO(), tt.ss, metav1.CreateOptions{}); err != nil {
if _, err := c.client.AppsV1().StatefulSets(defaultNamespace).Create(t.Context(), tt.ss, metav1.CreateOptions{}); err != nil {
t.Errorf("Failed to create StatefulSet error: %v", err)
return
}
@ -565,7 +565,7 @@ func Test_ReadyChecker_IsReady_ReplicationController(t *testing.T) {
pausedAsReady: false,
},
args: args{
ctx: context.TODO(),
ctx: t.Context(),
resource: &resource.Info{Object: &corev1.ReplicationController{}, Name: "foo", Namespace: defaultNamespace},
},
rc: newReplicationController("foo", false),
@ -580,7 +580,7 @@ func Test_ReadyChecker_IsReady_ReplicationController(t *testing.T) {
pausedAsReady: false,
},
args: args{
ctx: context.TODO(),
ctx: t.Context(),
resource: &resource.Info{Object: &corev1.ReplicationController{}, Name: "foo", Namespace: defaultNamespace},
},
rc: newReplicationController("bar", false),
@ -595,7 +595,7 @@ func Test_ReadyChecker_IsReady_ReplicationController(t *testing.T) {
pausedAsReady: false,
},
args: args{
ctx: context.TODO(),
ctx: t.Context(),
resource: &resource.Info{Object: &corev1.ReplicationController{}, Name: "foo", Namespace: defaultNamespace},
},
rc: newReplicationController("foo", true),
@ -610,7 +610,7 @@ func Test_ReadyChecker_IsReady_ReplicationController(t *testing.T) {
checkJobs: tt.fields.checkJobs,
pausedAsReady: tt.fields.pausedAsReady,
}
if _, err := c.client.CoreV1().ReplicationControllers(defaultNamespace).Create(context.TODO(), tt.rc, metav1.CreateOptions{}); err != nil {
if _, err := c.client.CoreV1().ReplicationControllers(defaultNamespace).Create(t.Context(), tt.rc, metav1.CreateOptions{}); err != nil {
t.Errorf("Failed to create ReplicationController error: %v", err)
return
}
@ -651,7 +651,7 @@ func Test_ReadyChecker_IsReady_ReplicaSet(t *testing.T) {
pausedAsReady: false,
},
args: args{
ctx: context.TODO(),
ctx: t.Context(),
resource: &resource.Info{Object: &appsv1.ReplicaSet{}, Name: "foo", Namespace: defaultNamespace},
},
rs: newReplicaSet("foo", 1, 1, true),
@ -666,7 +666,7 @@ func Test_ReadyChecker_IsReady_ReplicaSet(t *testing.T) {
pausedAsReady: false,
},
args: args{
ctx: context.TODO(),
ctx: t.Context(),
resource: &resource.Info{Object: &appsv1.ReplicaSet{}, Name: "foo", Namespace: defaultNamespace},
},
rs: newReplicaSet("bar", 1, 1, false),
@ -1014,12 +1014,12 @@ func Test_ReadyChecker_podsReadyForObject(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
c := NewReadyChecker(fake.NewClientset())
for _, pod := range tt.existPods {
if _, err := c.client.CoreV1().Pods(defaultNamespace).Create(context.TODO(), &pod, metav1.CreateOptions{}); err != nil {
if _, err := c.client.CoreV1().Pods(defaultNamespace).Create(t.Context(), &pod, metav1.CreateOptions{}); err != nil {
t.Errorf("Failed to create Pod error: %v", err)
return
}
}
got, err := c.podsReadyForObject(context.TODO(), tt.args.namespace, tt.args.obj)
got, err := c.podsReadyForObject(t.Context(), tt.args.namespace, tt.args.obj)
if (err != nil) != tt.wantErr {
t.Errorf("podsReadyForObject() error = %v, wantErr %v", err, tt.wantErr)
return

@ -81,5 +81,5 @@ func (r ResourceList) Intersect(rs ResourceList) ResourceList {
// isMatchingInfo returns true if infos match on Name and GroupVersionKind.
func isMatchingInfo(a, b *resource.Info) bool {
return a.Name == b.Name && a.Namespace == b.Namespace && a.Mapping.GroupVersionKind.Kind == b.Mapping.GroupVersionKind.Kind && a.Mapping.GroupVersionKind.Group == b.Mapping.GroupVersionKind.Group
return a.Name == b.Name && a.Namespace == b.Namespace && a.Mapping.GroupVersionKind == b.Mapping.GroupVersionKind
}

@ -59,3 +59,42 @@ func TestResourceList(t *testing.T) {
t.Error("expected intersect to return bar")
}
}
func TestIsMatchingInfo(t *testing.T) {
gvk := schema.GroupVersionKind{Group: "group1", Version: "version1", Kind: "pod"}
resourceInfo := resource.Info{Name: "name1", Namespace: "namespace1", Mapping: &meta.RESTMapping{GroupVersionKind: gvk}}
gvkDiffGroup := schema.GroupVersionKind{Group: "diff", Version: "version1", Kind: "pod"}
resourceInfoDiffGroup := resource.Info{Name: "name1", Namespace: "namespace1", Mapping: &meta.RESTMapping{GroupVersionKind: gvkDiffGroup}}
if isMatchingInfo(&resourceInfo, &resourceInfoDiffGroup) {
t.Error("expected resources not equal")
}
gvkDiffVersion := schema.GroupVersionKind{Group: "group1", Version: "diff", Kind: "pod"}
resourceInfoDiffVersion := resource.Info{Name: "name1", Namespace: "namespace1", Mapping: &meta.RESTMapping{GroupVersionKind: gvkDiffVersion}}
if isMatchingInfo(&resourceInfo, &resourceInfoDiffVersion) {
t.Error("expected resources not equal")
}
gvkDiffKind := schema.GroupVersionKind{Group: "group1", Version: "version1", Kind: "deployment"}
resourceInfoDiffKind := resource.Info{Name: "name1", Namespace: "namespace1", Mapping: &meta.RESTMapping{GroupVersionKind: gvkDiffKind}}
if isMatchingInfo(&resourceInfo, &resourceInfoDiffKind) {
t.Error("expected resources not equal")
}
resourceInfoDiffName := resource.Info{Name: "diff", Namespace: "namespace1", Mapping: &meta.RESTMapping{GroupVersionKind: gvk}}
if isMatchingInfo(&resourceInfo, &resourceInfoDiffName) {
t.Error("expected resources not equal")
}
resourceInfoDiffNamespace := resource.Info{Name: "name1", Namespace: "diff", Mapping: &meta.RESTMapping{GroupVersionKind: gvk}}
if isMatchingInfo(&resourceInfo, &resourceInfoDiffNamespace) {
t.Error("expected resources not equal")
}
gvkEqual := schema.GroupVersionKind{Group: "group1", Version: "version1", Kind: "pod"}
resourceInfoEqual := resource.Info{Name: "name1", Namespace: "namespace1", Mapping: &meta.RESTMapping{GroupVersionKind: gvkEqual}}
if !isMatchingInfo(&resourceInfo, &resourceInfoEqual) {
t.Error("expected resources to be equal")
}
}

@ -154,6 +154,7 @@ spec:
`
func getGVR(t *testing.T, mapper meta.RESTMapper, obj *unstructured.Unstructured) schema.GroupVersionResource {
t.Helper()
gvk := obj.GroupVersionKind()
mapping, err := mapper.RESTMapping(gvk.GroupKind(), gvk.Version)
require.NoError(t, err)
@ -161,6 +162,7 @@ func getGVR(t *testing.T, mapper meta.RESTMapper, obj *unstructured.Unstructured
}
func getRuntimeObjFromManifests(t *testing.T, manifests []string) []runtime.Object {
t.Helper()
objects := []runtime.Object{}
for _, manifest := range manifests {
m := make(map[string]interface{})
@ -173,6 +175,7 @@ func getRuntimeObjFromManifests(t *testing.T, manifests []string) []runtime.Obje
}
func getResourceListFromRuntimeObjs(t *testing.T, c *Client, objs []runtime.Object) ResourceList {
t.Helper()
resourceList := ResourceList{}
for _, obj := range objs {
list, err := c.Build(objBody(obj), false)

@ -14,7 +14,6 @@ limitations under the License.
package installer // import "helm.sh/helm/v4/pkg/plugin/installer"
import (
"os"
"testing"
)
@ -37,12 +36,11 @@ func TestPath(t *testing.T) {
for _, tt := range tests {
os.Setenv("HELM_PLUGINS", tt.helmPluginsDir)
t.Setenv("HELM_PLUGINS", tt.helmPluginsDir)
baseIns := newBase(tt.source)
baseInsPath := baseIns.Path()
if baseInsPath != tt.expectPath {
t.Errorf("expected name %s, got %s", tt.expectPath, baseInsPath)
}
os.Unsetenv("HELM_PLUGINS")
}
}

@ -20,12 +20,14 @@ import (
"path/filepath"
"testing"
"helm.sh/helm/v4/internal/test/ensure"
"helm.sh/helm/v4/pkg/helmpath"
)
var _ Installer = new(LocalInstaller)
func TestLocalInstaller(t *testing.T) {
ensure.HelmHome(t)
// Make a temp dir
tdir := t.TempDir()
if err := os.WriteFile(filepath.Join(tdir, "plugin.yaml"), []byte{}, 0644); err != nil {

@ -19,6 +19,7 @@ import (
"fmt"
"os"
"path/filepath"
"strings"
"testing"
"github.com/Masterminds/vcs"
@ -119,6 +120,8 @@ func TestVCSInstallerNonExistentVersion(t *testing.T) {
if err := Install(i); err == nil {
t.Fatalf("expected error for version does not exists, got none")
} else if strings.Contains(err.Error(), "Could not resolve host: github.com") {
t.Skip("Unable to run test without Internet access")
} else if err.Error() != fmt.Sprintf("requested version %q does not exist for plugin %q", version, source) {
t.Fatalf("expected error for version does not exists, got (%v)", err)
}
@ -146,8 +149,12 @@ func TestVCSInstallerUpdate(t *testing.T) {
// Install plugin before update
if err := Install(i); err != nil {
if strings.Contains(err.Error(), "Could not resolve host: github.com") {
t.Skip("Unable to run test without Internet access")
} else {
t.Fatal(err)
}
}
// Test FindSource method for positive result
pluginInfo, err := FindSource(i.Path())

@ -60,11 +60,7 @@ func TestGetFullPath(t *testing.T) {
t.Run("binary in PATH resolves correctly", func(t *testing.T) {
testpath := setupTestingScript(t)
realPath := os.Getenv("PATH")
os.Setenv("PATH", filepath.Dir(testpath))
defer func() {
os.Setenv("PATH", realPath)
}()
t.Setenv("PATH", filepath.Dir(testpath))
fullPath, err := getFullPath(filepath.Base(testpath))
is.NoError(err)
@ -183,7 +179,7 @@ func setupTestingScript(t *testing.T) (filepath string) {
t.Fatalf("unable to write tempfile for testing: %s", err)
}
err = f.Chmod(0755)
err = f.Chmod(0o755)
if err != nil {
t.Fatalf("unable to make tempfile executable for testing: %s", err)
}

@ -276,7 +276,7 @@ func TestDecodeSignature(t *testing.T) {
t.Fatal(err)
}
f, err := os.CreateTemp("", "helm-test-sig-")
f, err := os.CreateTemp(t.TempDir(), "helm-test-sig-")
if err != nil {
t.Fatal(err)
}

@ -100,27 +100,8 @@ func NewClient(options ...ClientOption) (*Client, error) {
client.credentialsFile = helmpath.ConfigPath(CredentialsFileBasename)
}
if client.httpClient == nil {
type cloner[T any] interface {
Clone() T
}
// try to copy (clone) the http.DefaultTransport so any mutations we
// perform on it (e.g. TLS config) are not reflected globally
// follow https://github.com/golang/go/issues/39299 for a more elegant
// solution in the future
transport := http.DefaultTransport
if t, ok := transport.(cloner[*http.Transport]); ok {
transport = t.Clone()
} else if t, ok := transport.(cloner[http.RoundTripper]); ok {
// this branch will not be used with go 1.20, it was added
// optimistically to try to clone if the http.DefaultTransport
// implementation changes, still the Clone method in that case
// might not return http.RoundTripper...
transport = t.Clone()
}
client.httpClient = &http.Client{
Transport: retry.NewTransport(transport),
Transport: NewTransport(client.debug),
}
}
@ -262,6 +243,7 @@ func (c *Client) Login(host string, options ...LoginOption) error {
}
key := credentials.ServerAddressFromRegistry(host)
key = credentials.ServerAddressFromHostname(key)
if err := c.credentialsStore.Put(ctx, key, cred); err != nil {
return err
}
@ -294,10 +276,15 @@ func ensureTLSConfig(client *auth.Client) (*tls.Config, error) {
transport = t
case *retry.Transport:
switch t := t.Base.(type) {
case *http.Transport:
transport = t
case *LoggingTransport:
switch t := t.RoundTripper.(type) {
case *http.Transport:
transport = t
}
}
}
if transport == nil {
// we don't know how to access the http.Transport, most likely the
@ -463,7 +450,7 @@ func (c *Client) Pull(ref string, options ...PullOption) (*PullResult, error) {
PreCopy: func(_ context.Context, desc ocispec.Descriptor) error {
mediaType := desc.MediaType
if i := sort.SearchStrings(allowedMediaTypes, mediaType); i >= len(allowedMediaTypes) || allowedMediaTypes[i] != mediaType {
return fmt.Errorf("media type %q is not allowed, found in descriptor with digest: %q", mediaType, desc.Digest)
return oras.SkipNode
}
mu.Lock()
@ -477,7 +464,6 @@ func (c *Client) Pull(ref string, options ...PullOption) (*PullResult, error) {
return nil, err
}
descriptors = append(descriptors, manifest)
descriptors = append(descriptors, layers...)
numDescriptors := len(descriptors)
@ -685,19 +671,9 @@ func (c *Client) Push(data []byte, ref string, options ...PushOption) (*PushResu
})
ociAnnotations := generateOCIAnnotations(meta, operation.creationTime)
manifest := ocispec.Manifest{
Versioned: specs.Versioned{SchemaVersion: 2},
Config: configDescriptor,
Layers: layers,
Annotations: ociAnnotations,
}
manifestData, err := json.Marshal(manifest)
if err != nil {
return nil, err
}
manifestDescriptor, err := oras.TagBytes(ctx, memoryStore, ocispec.MediaTypeImageManifest, manifestData, ref)
manifestDescriptor, err := c.tagManifest(ctx, memoryStore, configDescriptor,
layers, ociAnnotations, parsedRef)
if err != nil {
return nil, err
}
@ -898,3 +874,24 @@ func (c *Client) ValidateReference(ref, version string, u *url.URL) (*url.URL, e
return u, err
}
// tagManifest prepares and tags a manifest in memory storage
func (c *Client) tagManifest(ctx context.Context, memoryStore *memory.Store,
configDescriptor ocispec.Descriptor, layers []ocispec.Descriptor,
ociAnnotations map[string]string, parsedRef reference) (ocispec.Descriptor, error) {
manifest := ocispec.Manifest{
Versioned: specs.Versioned{SchemaVersion: 2},
Config: configDescriptor,
Layers: layers,
Annotations: ociAnnotations,
}
manifestData, err := json.Marshal(manifest)
if err != nil {
return ocispec.Descriptor{}, err
}
return oras.TagBytes(ctx, memoryStore, ocispec.MediaTypeImageManifest,
manifestData, parsedRef.String())
}

@ -0,0 +1,53 @@
/*
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 registry
import (
"io"
"testing"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/stretchr/testify/require"
"oras.land/oras-go/v2/content/memory"
)
// Inspired by oras test
// https://github.com/oras-project/oras-go/blob/05a2b09cbf2eab1df691411884dc4df741ec56ab/content_test.go#L1802
func TestTagManifestTransformsReferences(t *testing.T) {
memStore := memory.New()
client := &Client{out: io.Discard}
ctx := t.Context()
refWithPlus := "test-registry.io/charts/test:1.0.0+metadata"
expectedRef := "test-registry.io/charts/test:1.0.0_metadata" // + becomes _
configDesc := ocispec.Descriptor{MediaType: ConfigMediaType, Digest: "sha256:config", Size: 100}
layers := []ocispec.Descriptor{{MediaType: ChartLayerMediaType, Digest: "sha256:layer", Size: 200}}
parsedRef, err := newReference(refWithPlus)
require.NoError(t, err)
desc, err := client.tagManifest(ctx, memStore, configDesc, layers, nil, parsedRef)
require.NoError(t, err)
transformedDesc, err := memStore.Resolve(ctx, expectedRef)
require.NoError(t, err, "Should find the reference with _ instead of +")
require.Equal(t, desc.Digest, transformedDesc.Digest)
_, err = memStore.Resolve(ctx, refWithPlus)
require.Error(t, err, "Should NOT find the reference with the original +")
}

@ -19,6 +19,7 @@ package registry
import "testing"
func verify(t *testing.T, actual reference, registry, repository, tag, digest string) {
t.Helper()
if registry != actual.orasReference.Registry {
t.Errorf("Oras reference registry expected %v actual %v", registry, actual.Registry)
}

@ -0,0 +1,175 @@
/*
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 registry
import (
"bytes"
"fmt"
"io"
"log/slog"
"mime"
"net/http"
"strings"
"sync/atomic"
"oras.land/oras-go/v2/registry/remote/retry"
)
var (
// requestCount records the number of logged request-response pairs and will
// be used as the unique id for the next pair.
requestCount uint64
// toScrub is a set of headers that should be scrubbed from the log.
toScrub = []string{
"Authorization",
"Set-Cookie",
}
)
// payloadSizeLimit limits the maximum size of the response body to be printed.
const payloadSizeLimit int64 = 16 * 1024 // 16 KiB
// LoggingTransport is an http.RoundTripper that keeps track of the in-flight
// request and add hooks to report HTTP tracing events.
type LoggingTransport struct {
http.RoundTripper
}
// NewTransport creates and returns a new instance of LoggingTransport
func NewTransport(debug bool) *retry.Transport {
type cloner[T any] interface {
Clone() T
}
// try to copy (clone) the http.DefaultTransport so any mutations we
// perform on it (e.g. TLS config) are not reflected globally
// follow https://github.com/golang/go/issues/39299 for a more elegant
// solution in the future
transport := http.DefaultTransport
if t, ok := transport.(cloner[*http.Transport]); ok {
transport = t.Clone()
} else if t, ok := transport.(cloner[http.RoundTripper]); ok {
// this branch will not be used with go 1.20, it was added
// optimistically to try to clone if the http.DefaultTransport
// implementation changes, still the Clone method in that case
// might not return http.RoundTripper...
transport = t.Clone()
}
if debug {
transport = &LoggingTransport{RoundTripper: transport}
}
return retry.NewTransport(transport)
}
// RoundTrip calls base round trip while keeping track of the current request.
func (t *LoggingTransport) RoundTrip(req *http.Request) (resp *http.Response, err error) {
id := atomic.AddUint64(&requestCount, 1) - 1
slog.Debug("Request", "id", id, "url", req.URL, "method", req.Method, "header", logHeader(req.Header))
resp, err = t.RoundTripper.RoundTrip(req)
if err != nil {
slog.Debug("Response", "id", id, "error", err)
} else if resp != nil {
slog.Debug("Response", "id", id, "status", resp.Status, "header", logHeader(resp.Header), "body", logResponseBody(resp))
} else {
slog.Debug("Response", "id", id, "response", "nil")
}
return resp, err
}
// logHeader prints out the provided header keys and values, with auth header scrubbed.
func logHeader(header http.Header) string {
if len(header) > 0 {
headers := []string{}
for k, v := range header {
for _, h := range toScrub {
if strings.EqualFold(k, h) {
v = []string{"*****"}
}
}
headers = append(headers, fmt.Sprintf(" %q: %q", k, strings.Join(v, ", ")))
}
return strings.Join(headers, "\n")
}
return " Empty header"
}
// logResponseBody prints out the response body if it is printable and within size limit.
func logResponseBody(resp *http.Response) string {
if resp.Body == nil || resp.Body == http.NoBody {
return " No response body to print"
}
// non-applicable body is not printed and remains untouched for subsequent processing
contentType := resp.Header.Get("Content-Type")
if contentType == "" {
return " Response body without a content type is not printed"
}
if !isPrintableContentType(contentType) {
return fmt.Sprintf(" Response body of content type %q is not printed", contentType)
}
buf := bytes.NewBuffer(nil)
body := resp.Body
// restore the body by concatenating the read body with the remaining body
resp.Body = struct {
io.Reader
io.Closer
}{
Reader: io.MultiReader(buf, body),
Closer: body,
}
// read the body up to limit+1 to check if the body exceeds the limit
if _, err := io.CopyN(buf, body, payloadSizeLimit+1); err != nil && err != io.EOF {
return fmt.Sprintf(" Error reading response body: %v", err)
}
readBody := buf.String()
if len(readBody) == 0 {
return " Response body is empty"
}
if containsCredentials(readBody) {
return " Response body redacted due to potential credentials"
}
if len(readBody) > int(payloadSizeLimit) {
return readBody[:payloadSizeLimit] + "\n...(truncated)"
}
return readBody
}
// isPrintableContentType returns true if the contentType is printable.
func isPrintableContentType(contentType string) bool {
mediaType, _, err := mime.ParseMediaType(contentType)
if err != nil {
return false
}
switch mediaType {
case "application/json", // JSON types
"text/plain", "text/html": // text types
return true
}
return strings.HasSuffix(mediaType, "+json")
}
// containsCredentials returns true if the body contains potential credentials.
func containsCredentials(body string) bool {
return strings.Contains(body, `"token"`) || strings.Contains(body, `"access_token"`)
}

@ -0,0 +1,399 @@
/*
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 registry
import (
"bytes"
"errors"
"io"
"net/http"
"testing"
)
var errMockRead = errors.New("mock read error")
type errorReader struct{}
func (e *errorReader) Read(_ []byte) (n int, err error) {
return 0, errMockRead
}
func Test_isPrintableContentType(t *testing.T) {
tests := []struct {
name string
contentType string
want bool
}{
{
name: "Empty content type",
contentType: "",
want: false,
},
{
name: "General JSON type",
contentType: "application/json",
want: true,
},
{
name: "General JSON type with charset",
contentType: "application/json; charset=utf-8",
want: true,
},
{
name: "Random type with application/json prefix",
contentType: "application/jsonwhatever",
want: false,
},
{
name: "Manifest type in JSON",
contentType: "application/vnd.oci.image.manifest.v1+json",
want: true,
},
{
name: "Manifest type in JSON with charset",
contentType: "application/vnd.oci.image.manifest.v1+json; charset=utf-8",
want: true,
},
{
name: "Random content type in JSON",
contentType: "application/whatever+json",
want: true,
},
{
name: "Plain text type",
contentType: "text/plain",
want: true,
},
{
name: "Plain text type with charset",
contentType: "text/plain; charset=utf-8",
want: true,
},
{
name: "Random type with text/plain prefix",
contentType: "text/plainnnnn",
want: false,
},
{
name: "HTML type",
contentType: "text/html",
want: true,
},
{
name: "Plain text type with charset",
contentType: "text/html; charset=utf-8",
want: true,
},
{
name: "Random type with text/html prefix",
contentType: "text/htmlllll",
want: false,
},
{
name: "Binary type",
contentType: "application/octet-stream",
want: false,
},
{
name: "Unknown type",
contentType: "unknown/unknown",
want: false,
},
{
name: "Invalid type",
contentType: "text/",
want: false,
},
{
name: "Random string",
contentType: "random123!@#",
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := isPrintableContentType(tt.contentType); got != tt.want {
t.Errorf("isPrintableContentType() = %v, want %v", got, tt.want)
}
})
}
}
func Test_logResponseBody(t *testing.T) {
tests := []struct {
name string
resp *http.Response
want string
wantData []byte
}{
{
name: "Nil body",
resp: &http.Response{
Body: nil,
Header: http.Header{"Content-Type": []string{"application/json"}},
},
want: " No response body to print",
},
{
name: "No body",
wantData: nil,
resp: &http.Response{
Body: http.NoBody,
ContentLength: 100, // in case of HEAD response, the content length is set but the body is empty
Header: http.Header{"Content-Type": []string{"application/json"}},
},
want: " No response body to print",
},
{
name: "Empty body",
wantData: []byte(""),
resp: &http.Response{
Body: io.NopCloser(bytes.NewReader([]byte(""))),
ContentLength: 0,
Header: http.Header{"Content-Type": []string{"text/plain"}},
},
want: " Response body is empty",
},
{
name: "Unknown content length",
wantData: []byte("whatever"),
resp: &http.Response{
Body: io.NopCloser(bytes.NewReader([]byte("whatever"))),
ContentLength: -1,
Header: http.Header{"Content-Type": []string{"text/plain"}},
},
want: "whatever",
},
{
name: "Missing content type header",
wantData: []byte("whatever"),
resp: &http.Response{
Body: io.NopCloser(bytes.NewReader([]byte("whatever"))),
ContentLength: 8,
},
want: " Response body without a content type is not printed",
},
{
name: "Empty content type header",
wantData: []byte("whatever"),
resp: &http.Response{
Body: io.NopCloser(bytes.NewReader([]byte("whatever"))),
ContentLength: 8,
Header: http.Header{"Content-Type": []string{""}},
},
want: " Response body without a content type is not printed",
},
{
name: "Non-printable content type",
wantData: []byte("binary data"),
resp: &http.Response{
Body: io.NopCloser(bytes.NewReader([]byte("binary data"))),
ContentLength: 11,
Header: http.Header{"Content-Type": []string{"application/octet-stream"}},
},
want: " Response body of content type \"application/octet-stream\" is not printed",
},
{
name: "Body at the limit",
wantData: bytes.Repeat([]byte("a"), int(payloadSizeLimit)),
resp: &http.Response{
Body: io.NopCloser(bytes.NewReader(bytes.Repeat([]byte("a"), int(payloadSizeLimit)))),
ContentLength: payloadSizeLimit,
Header: http.Header{"Content-Type": []string{"text/plain"}},
},
want: string(bytes.Repeat([]byte("a"), int(payloadSizeLimit))),
},
{
name: "Body larger than limit",
wantData: bytes.Repeat([]byte("a"), int(payloadSizeLimit+1)),
resp: &http.Response{
Body: io.NopCloser(bytes.NewReader(bytes.Repeat([]byte("a"), int(payloadSizeLimit+1)))), // 1 byte larger than limit
ContentLength: payloadSizeLimit + 1,
Header: http.Header{"Content-Type": []string{"text/plain"}},
},
want: string(bytes.Repeat([]byte("a"), int(payloadSizeLimit))) + "\n...(truncated)",
},
{
name: "Printable content type within limit",
wantData: []byte("data"),
resp: &http.Response{
Body: io.NopCloser(bytes.NewReader([]byte("data"))),
ContentLength: 4,
Header: http.Header{"Content-Type": []string{"text/plain"}},
},
want: "data",
},
{
name: "Actual body size is larger than content length",
wantData: []byte("data"),
resp: &http.Response{
Body: io.NopCloser(bytes.NewReader([]byte("data"))),
ContentLength: 3, // mismatched content length
Header: http.Header{"Content-Type": []string{"text/plain"}},
},
want: "data",
},
{
name: "Actual body size is larger than content length and exceeds limit",
wantData: bytes.Repeat([]byte("a"), int(payloadSizeLimit+1)),
resp: &http.Response{
Body: io.NopCloser(bytes.NewReader(bytes.Repeat([]byte("a"), int(payloadSizeLimit+1)))), // 1 byte larger than limit
ContentLength: 1, // mismatched content length
Header: http.Header{"Content-Type": []string{"text/plain"}},
},
want: string(bytes.Repeat([]byte("a"), int(payloadSizeLimit))) + "\n...(truncated)",
},
{
name: "Actual body size is smaller than content length",
wantData: []byte("data"),
resp: &http.Response{
Body: io.NopCloser(bytes.NewReader([]byte("data"))),
ContentLength: 5, // mismatched content length
Header: http.Header{"Content-Type": []string{"text/plain"}},
},
want: "data",
},
{
name: "Body contains token",
resp: &http.Response{
Body: io.NopCloser(bytes.NewReader([]byte(`{"token":"12345"}`))),
ContentLength: 17,
Header: http.Header{"Content-Type": []string{"application/json"}},
},
wantData: []byte(`{"token":"12345"}`),
want: " Response body redacted due to potential credentials",
},
{
name: "Body contains access_token",
resp: &http.Response{
Body: io.NopCloser(bytes.NewReader([]byte(`{"access_token":"12345"}`))),
ContentLength: 17,
Header: http.Header{"Content-Type": []string{"application/json"}},
},
wantData: []byte(`{"access_token":"12345"}`),
want: " Response body redacted due to potential credentials",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := logResponseBody(tt.resp); got != tt.want {
t.Errorf("logResponseBody() = %v, want %v", got, tt.want)
}
// validate the response body
if tt.resp.Body != nil {
readBytes, err := io.ReadAll(tt.resp.Body)
if err != nil {
t.Errorf("failed to read body after logResponseBody(), err= %v", err)
}
if !bytes.Equal(readBytes, tt.wantData) {
t.Errorf("resp.Body after logResponseBody() = %v, want %v", readBytes, tt.wantData)
}
if closeErr := tt.resp.Body.Close(); closeErr != nil {
t.Errorf("failed to close body after logResponseBody(), err= %v", closeErr)
}
}
})
}
}
func Test_logResponseBody_error(t *testing.T) {
tests := []struct {
name string
resp *http.Response
want string
}{
{
name: "Error reading body",
resp: &http.Response{
Body: io.NopCloser(&errorReader{}),
ContentLength: 10,
Header: http.Header{"Content-Type": []string{"text/plain"}},
},
want: " Error reading response body: mock read error",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := logResponseBody(tt.resp); got != tt.want {
t.Errorf("logResponseBody() = %v, want %v", got, tt.want)
}
if closeErr := tt.resp.Body.Close(); closeErr != nil {
t.Errorf("failed to close body after logResponseBody(), err= %v", closeErr)
}
})
}
}
func Test_containsCredentials(t *testing.T) {
tests := []struct {
name string
body string
want bool
}{
{
name: "Contains token keyword",
body: `{"token": "12345"}`,
want: true,
},
{
name: "Contains quoted token keyword",
body: `whatever "token" blah`,
want: true,
},
{
name: "Contains unquoted token keyword",
body: `whatever token blah`,
want: false,
},
{
name: "Contains access_token keyword",
body: `{"access_token": "12345"}`,
want: true,
},
{
name: "Contains quoted access_token keyword",
body: `whatever "access_token" blah`,
want: true,
},
{
name: "Contains unquoted access_token keyword",
body: `whatever access_token blah`,
want: false,
},
{
name: "Does not contain credentials",
body: `{"key": "value"}`,
want: false,
},
{
name: "Empty body",
body: ``,
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := containsCredentials(tt.body); got != tt.want {
t.Errorf("containsCredentials() = %v, want %v", got, tt.want)
}
})
}
}

@ -141,7 +141,7 @@ func setup(suite *TestSuite, tlsEnabled, insecure bool) *registry.Registry {
suite.Nil(err, "no error creating mock DNS server")
suite.srv.PatchNet(net.DefaultResolver)
config.HTTP.Addr = fmt.Sprintf(":%d", port)
config.HTTP.Addr = fmt.Sprintf("127.0.0.1:%d", port)
config.HTTP.DrainTimeout = time.Duration(10) * time.Second
config.Storage = map[string]configuration.Parameters{"inmemory": map[string]interface{}{}}

@ -43,6 +43,7 @@ func tsRelease(name string, vers int, dur time.Duration, status rspb.Status) *rs
}
func check(t *testing.T, by string, fn func(int, int) bool) {
t.Helper()
for i := len(releases) - 1; i > 0; i-- {
if fn(i, i-1) {
t.Errorf("release at positions '(%d,%d)' not sorted by %s", i-1, i, by)

@ -70,7 +70,7 @@ func TestIndexCustomSchemeDownload(t *testing.T) {
}
repo.CachePath = t.TempDir()
tempIndexFile, err := os.CreateTemp("", "test-repo")
tempIndexFile, err := os.CreateTemp(t.TempDir(), "test-repo")
if err != nil {
t.Fatalf("Failed to create temp index file: %v", err)
}
@ -224,11 +224,15 @@ func TestResolveReferenceURL(t *testing.T) {
for _, tt := range []struct {
baseURL, refURL, chartURL string
}{
{"http://localhost:8123/", "/nginx-0.2.0.tgz", "http://localhost:8123/nginx-0.2.0.tgz"},
{"http://localhost:8123/charts/", "nginx-0.2.0.tgz", "http://localhost:8123/charts/nginx-0.2.0.tgz"},
{"http://localhost:8123/charts/", "/nginx-0.2.0.tgz", "http://localhost:8123/nginx-0.2.0.tgz"},
{"http://localhost:8123/charts-with-no-trailing-slash", "nginx-0.2.0.tgz", "http://localhost:8123/charts-with-no-trailing-slash/nginx-0.2.0.tgz"},
{"http://localhost:8123", "https://charts.helm.sh/stable/nginx-0.2.0.tgz", "https://charts.helm.sh/stable/nginx-0.2.0.tgz"},
{"http://localhost:8123/charts%2fwith%2fescaped%2fslash", "nginx-0.2.0.tgz", "http://localhost:8123/charts%2fwith%2fescaped%2fslash/nginx-0.2.0.tgz"},
{"http://localhost:8123/charts%2fwith%2fescaped%2fslash", "/nginx-0.2.0.tgz", "http://localhost:8123/nginx-0.2.0.tgz"},
{"http://localhost:8123/charts?with=queryparameter", "nginx-0.2.0.tgz", "http://localhost:8123/charts/nginx-0.2.0.tgz?with=queryparameter"},
{"http://localhost:8123/charts?with=queryparameter", "/nginx-0.2.0.tgz", "http://localhost:8123/nginx-0.2.0.tgz?with=queryparameter"},
} {
chartURL, err := ResolveReferenceURL(tt.baseURL, tt.refURL)
if err != nil {

@ -352,6 +352,7 @@ func TestDownloadIndexFile(t *testing.T) {
}
func verifyLocalIndex(t *testing.T, i *IndexFile) {
t.Helper()
numEntries := len(i.Entries)
if numEntries != 3 {
t.Errorf("Expected 3 entries in index file but got %d", numEntries)
@ -450,6 +451,7 @@ func verifyLocalIndex(t *testing.T, i *IndexFile) {
}
func verifyLocalChartsFile(t *testing.T, chartsContent []byte, indexContent *IndexFile) {
t.Helper()
var expected, reald []string
for chart := range indexContent.Entries {
expected = append(expected, chart)

@ -197,7 +197,7 @@ func TestWriteFile(t *testing.T) {
},
)
file, err := os.CreateTemp("", "helm-repo")
file, err := os.CreateTemp(t.TempDir(), "helm-repo")
if err != nil {
t.Errorf("failed to create test-file (%v)", err)
}

@ -16,7 +16,6 @@ limitations under the License.
package repotest
import (
"context"
"crypto/tls"
"fmt"
"net/http"
@ -42,6 +41,7 @@ import (
)
func BasicAuthMiddleware(t *testing.T) http.HandlerFunc {
t.Helper()
return http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) {
username, password, ok := r.BasicAuth()
if !ok || username != "username" || password != "password" {
@ -89,11 +89,8 @@ type Server struct {
//
// The temp dir will be removed by testing package automatically when test finished.
func NewTempServer(t *testing.T, options ...ServerOption) *Server {
docrootTempDir, err := os.MkdirTemp("", "helm-repotest-")
if err != nil {
t.Fatal(err)
}
t.Helper()
docrootTempDir := t.TempDir()
srv := newServer(t, docrootTempDir, options...)
@ -110,6 +107,7 @@ func NewTempServer(t *testing.T, options ...ServerOption) *Server {
// Create the server, but don't yet start it
func newServer(t *testing.T, docroot string, options ...ServerOption) *Server {
t.Helper()
absdocroot, err := filepath.Abs(docroot)
if err != nil {
t.Fatal(err)
@ -162,6 +160,7 @@ func WithDependingChart(c *chart.Chart) OCIServerOpt {
}
func NewOCIServer(t *testing.T, dir string) (*OCIServer, error) {
t.Helper()
testHtpasswdFileBasename := "authtest.htpasswd"
testUsername, testPassword := "username", "password"
@ -170,7 +169,7 @@ func NewOCIServer(t *testing.T, dir string) (*OCIServer, error) {
t.Fatal("error generating bcrypt password for test htpasswd file")
}
htpasswdPath := filepath.Join(dir, testHtpasswdFileBasename)
err = os.WriteFile(htpasswdPath, []byte(fmt.Sprintf("%s:%s\n", testUsername, string(pwBytes))), 0644)
err = os.WriteFile(htpasswdPath, []byte(fmt.Sprintf("%s:%s\n", testUsername, string(pwBytes))), 0o644)
if err != nil {
t.Fatalf("error creating test htpasswd file")
}
@ -194,7 +193,7 @@ func NewOCIServer(t *testing.T, dir string) (*OCIServer, error) {
registryURL := fmt.Sprintf("localhost:%d", port)
r, err := registry.NewRegistry(context.Background(), config)
r, err := registry.NewRegistry(t.Context(), config)
if err != nil {
t.Fatal(err)
}
@ -209,6 +208,7 @@ func NewOCIServer(t *testing.T, dir string) (*OCIServer, error) {
}
func (srv *OCIServer) Run(t *testing.T, opts ...OCIServerOpt) {
t.Helper()
cfg := &OCIServerRunConfig{}
for _, fn := range opts {
fn(cfg)
@ -327,7 +327,7 @@ func (s *Server) CopyCharts(origin string) ([]string, error) {
if err != nil {
return []string{}, err
}
if err := os.WriteFile(newname, data, 0644); err != nil {
if err := os.WriteFile(newname, data, 0o644); err != nil {
return []string{}, err
}
copied[i] = newname
@ -351,7 +351,7 @@ func (s *Server) CreateIndex() error {
}
ifile := filepath.Join(s.docroot, "index.yaml")
return os.WriteFile(ifile, d, 0644)
return os.WriteFile(ifile, d, 0o644)
}
func (s *Server) start() {
@ -403,5 +403,5 @@ func setTestingRepository(url, fname string) error {
Name: "test",
URL: url,
})
return r.WriteFile(fname, 0640)
return r.WriteFile(fname, 0o640)
}

@ -26,6 +26,7 @@ import (
)
func MakeTestTLSConfig(t *testing.T, path string) *tls.Config {
t.Helper()
ca, pub, priv := filepath.Join(path, "rootca.crt"), filepath.Join(path, "crt.pem"), filepath.Join(path, "key.pem")
insecure := false

@ -52,6 +52,7 @@ func testKey(name string, vers int) string {
}
func tsFixtureMemory(t *testing.T) *Memory {
t.Helper()
hs := []*rspb.Release{
// rls-a
releaseStub("rls-a", 4, "default", rspb.StatusDeployed),
@ -83,6 +84,7 @@ func tsFixtureMemory(t *testing.T) *Memory {
// newTestFixtureCfgMaps initializes a MockConfigMapsInterface.
// ConfigMaps are created for each release provided.
func newTestFixtureCfgMaps(t *testing.T, releases ...*rspb.Release) *ConfigMaps {
t.Helper()
var mock MockConfigMapsInterface
mock.Init(t, releases...)
@ -98,6 +100,7 @@ type MockConfigMapsInterface struct {
// Init initializes the MockConfigMapsInterface with the set of releases.
func (mock *MockConfigMapsInterface) Init(t *testing.T, releases ...*rspb.Release) {
t.Helper()
mock.objects = map[string]*v1.ConfigMap{}
for _, rls := range releases {
@ -169,6 +172,7 @@ func (mock *MockConfigMapsInterface) Delete(_ context.Context, name string, _ me
// newTestFixtureSecrets initializes a MockSecretsInterface.
// Secrets are created for each release provided.
func newTestFixtureSecrets(t *testing.T, releases ...*rspb.Release) *Secrets {
t.Helper()
var mock MockSecretsInterface
mock.Init(t, releases...)
@ -184,6 +188,7 @@ type MockSecretsInterface struct {
// Init initializes the MockSecretsInterface with the set of releases.
func (mock *MockSecretsInterface) Init(t *testing.T, releases ...*rspb.Release) {
t.Helper()
mock.objects = map[string]*v1.Secret{}
for _, rls := range releases {
@ -254,6 +259,7 @@ func (mock *MockSecretsInterface) Delete(_ context.Context, name string, _ metav
// newTestFixtureSQL mocks the SQL database (for testing purposes)
func newTestFixtureSQL(t *testing.T, _ ...*rspb.Release) (*SQL, sqlmock.Sqlmock) {
t.Helper()
sqlDB, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("error when opening stub database connection: %v", err)

@ -31,6 +31,7 @@ var (
)
func givenTime(t *testing.T) Time {
t.Helper()
result, err := Parse(time.RFC3339, "1977-09-02T22:04:05Z")
require.NoError(t, err)
return result

Loading…
Cancel
Save