Merge branch 'helm:main' into issue392_fix_plugin_arg_parsing

pull/30614/head
Artem Mikhalitsin 2 weeks ago committed by GitHub
commit a50b70c4ac
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -18,11 +18,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout source code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # pin@v5.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # pin@v6.0.1
- name: Add variables to environment file
run: cat ".github/env" >> "$GITHUB_ENV"
- name: Setup Go
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # pin@5.5.0
uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # pin@6.1.0
with:
go-version: '${{ env.GOLANG_VERSION }}'
check-latest: true

@ -43,7 +43,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # pin@v5.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # pin@v6.0.1
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL

@ -13,15 +13,15 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # pin@v5.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # pin@v6.0.1
- name: Add variables to environment file
run: cat ".github/env" >> "$GITHUB_ENV"
- name: Setup Go
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # pin@5.5.0
uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # pin@6.1.0
with:
go-version: '${{ env.GOLANG_VERSION }}'
check-latest: true
- name: golangci-lint
uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 #pin@8.0.0
uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20 #pin@9.2.0
with:
version: ${{ env.GOLANGCI_LINT_VERSION }}

@ -3,6 +3,11 @@ on:
push:
paths:
- go.sum
- .github/workflows/govulncheck.yml
pull_request:
paths:
- go.sum
- .github/workflows/govulncheck.yml
schedule:
- cron: "0 0 * * *"
@ -14,11 +19,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # pin@v5.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # pin@v6.0.1
with:
persist-credentials: false
- name: Add variables to environment file
run: cat ".github/env" >> "$GITHUB_ENV"
- name: Setup Go
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # pin@5.5.0
uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # pin@6.1.0
with:
go-version: '${{ env.GOLANG_VERSION }}'
check-latest: true

@ -16,11 +16,11 @@ permissions: read-all
# job is triggered by a tag push, VERSION should be the tag ref.
jobs:
release:
if: startsWith(github.ref, 'refs/tags/v')
if: startsWith(github.ref, 'refs/tags/v') && github.repository == 'helm/helm'
runs-on: ubuntu-latest-16-cores
steps:
- name: Checkout source code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # pin@v5.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # pin@v6.0.1
with:
fetch-depth: 0
@ -28,9 +28,10 @@ jobs:
run: cat ".github/env" >> "$GITHUB_ENV"
- name: Setup Go
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # pin@5.5.0
uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # pin@6.1.0
with:
go-version: '${{ env.GOLANG_VERSION }}'
check-latest: true
- name: Run unit tests
run: make test-coverage
- name: Build Helm Binaries
@ -49,8 +50,13 @@ jobs:
# Push the latest semver tag, excluding prerelease tags
LATEST_VERSION="$(git tag | sort -r --version-sort | grep '^v[0-9]' | grep -v '-' | head -n1)"
echo "LATEST_VERSION=${LATEST_VERSION}"
if [[ "${LATEST_VERSION}" != v4.* ]]; then
echo "Error: Latest version ${LATEST_VERSION} is not a v4 release"
exit 1
fi
echo "${LATEST_VERSION}" > _dist_versions/helm-latest-version
echo "${LATEST_VERSION}" > _dist_versions/helm3-latest-version
echo "${LATEST_VERSION}" > _dist_versions/helm4-latest-version
- name: Upload Binaries
uses: bacongobbler/azure-blob-storage-upload@50f7d898b7697e864130ea04c303ca38b5751c50 # pin@3.0.0
@ -76,16 +82,16 @@ jobs:
canary-release:
runs-on: ubuntu-latest-16-cores
if: github.ref == 'refs/heads/main'
if: github.ref == 'refs/heads/main' && github.repository == 'helm/helm'
steps:
- name: Checkout source code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # pin@v5.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # pin@v6.0.1
- name: Add variables to environment file
run: cat ".github/env" >> "$GITHUB_ENV"
- name: Setup Go
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # pin@5.5.0
uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # pin@6.1.0
with:
go-version: '${{ env.GOLANG_VERSION }}'
check-latest: true

@ -28,7 +28,7 @@ jobs:
steps:
- name: "Checkout code"
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
persist-credentials: false
@ -55,7 +55,7 @@ jobs:
# Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
# format to the repository Actions tab.
- name: "Upload artifact"
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: SARIF file
path: results.sarif

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

@ -26,11 +26,13 @@ linters:
- misspell
- nakedret
- revive
- sloglint
- staticcheck
- thelper
- unused
- usestdlibvars
- usetesting
- exhaustive
exclusions:
@ -72,6 +74,9 @@ linters:
recommendations:
- github.com/evanphx/json-patch/v5
exhaustive:
default-signifies-exhaustive: true
run:
timeout: 10m

@ -1,4 +1,4 @@
# Copilot Instructions for Helm
# AGENTS.md
## Overview
Helm is a package manager for Kubernetes written in Go, supporting v3 (stable) and v4 (unstable) APIs.

@ -120,7 +120,7 @@ specific upcoming bugfix or feature release could fall into one of two different
Issues and PRs which are deemed backwards-incompatible may be added to the discussion items for
Helm 4 with [label:v4.x](https://github.com/helm/helm/labels/v4.x). An issue or PR that we are not
sure we will be addressing will not be added to any milestone.
sure if we will be addressing will not be added to any milestone.
A milestone (and hence release) can be closed when all outstanding issues/PRs have been closed
or moved to another milestone and the associated release has been published.
@ -160,8 +160,8 @@ There are 5 types of issues (each with their own corresponding [label](#labels))
discussion, these can turn into `feature` or `bug` issues.
- `proposal`: Used for items (like this one) that propose a new ideas or functionality that require
a larger community discussion. This allows for feedback from others in the community before a
feature is actually developed. This is not needed for small additions. Final word on whether or
not a feature needs a proposal is up to the core maintainers. All issues that are proposals should
feature is actually developed. This is not needed for small additions. Final word on whether
a feature needs a proposal is up to the core maintainers. All issues that are proposals should
both have a label and an issue title of "Proposal: [the rest of the title]." A proposal can become
a `feature` and does not require a milestone.
- `feature`: These track specific feature requests and ideas until they are complete. They can
@ -179,7 +179,7 @@ below.
2. Triage
- The maintainer in charge of triaging will apply the proper labels for the issue. This includes
labels for priority, type, and metadata (such as `good first issue`). The only issue priority
we will be tracking is whether or not the issue is "critical." If additional levels are needed
we will be tracking is whether the issue is "critical." If additional levels are needed
in the future, we will add them.
- (If needed) Clean up the title to succinctly and clearly state the issue. Also ensure that
proposals are prefaced with "Proposal: [the rest of the title]".

@ -58,20 +58,6 @@ LDFLAGS += -X helm.sh/helm/v4/internal/version.gitCommit=${GIT_COMMIT}
LDFLAGS += -X helm.sh/helm/v4/internal/version.gitTreeState=${GIT_DIRTY}
LDFLAGS += $(EXT_LDFLAGS)
# Define constants based on the client-go version
K8S_MODULES_VER=$(subst ., ,$(subst v,,$(shell go list -f '{{.Version}}' -m k8s.io/client-go)))
K8S_MODULES_MAJOR_VER=$(shell echo $$(($(firstword $(K8S_MODULES_VER)) + 1)))
K8S_MODULES_MINOR_VER=$(word 2,$(K8S_MODULES_VER))
LDFLAGS += -X helm.sh/helm/v4/pkg/chart/v2/lint/rules.k8sVersionMajor=$(K8S_MODULES_MAJOR_VER)
LDFLAGS += -X helm.sh/helm/v4/pkg/chart/v2/lint/rules.k8sVersionMinor=$(K8S_MODULES_MINOR_VER)
LDFLAGS += -X helm.sh/helm/v4/pkg/internal/v3/lint/rules.k8sVersionMajor=$(K8S_MODULES_MAJOR_VER)
LDFLAGS += -X helm.sh/helm/v4/pkg/internal/v3/lint/rules.k8sVersionMinor=$(K8S_MODULES_MINOR_VER)
LDFLAGS += -X helm.sh/helm/v4/pkg/chart/common/util.k8sVersionMajor=$(K8S_MODULES_MAJOR_VER)
LDFLAGS += -X helm.sh/helm/v4/pkg/chart/common/util.k8sVersionMinor=$(K8S_MODULES_MINOR_VER)
LDFLAGS += -X helm.sh/helm/v4/internal/version.kubeClientVersionMajor=$(K8S_MODULES_MAJOR_VER)
LDFLAGS += -X helm.sh/helm/v4/internal/version.kubeClientVersionMinor=$(K8S_MODULES_MINOR_VER)
.PHONY: all
all: build
@ -129,6 +115,13 @@ test-coverage:
.PHONY: test-style
test-style:
@EXPECTED_VERSION=$$(grep GOLANGCI_LINT_VERSION .github/env | cut -d= -f2); \
ACTUAL_VERSION=$$(golangci-lint --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1); \
if [ "v$$ACTUAL_VERSION" != "$$EXPECTED_VERSION" ]; then \
echo "Warning: golangci-lint version is v$$ACTUAL_VERSION (expected $$EXPECTED_VERSION from CI)"; \
echo "To install the correct version, run:"; \
echo " curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b \$$(go env GOPATH)/bin $$EXPECTED_VERSION"; \
fi
golangci-lint run ./...
@scripts/validate-license.sh

@ -7,9 +7,9 @@ maintainers:
- sabre1041
- scottrigby
- technosophos
- TerryHowe
triage:
- banjoh
- TerryHowe
- yxxhero
- zonggen
- z4ce

@ -5,7 +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://insights.production.lfx.dev/api/badge/health-score?project=helm)](https://insights.linuxfoundation.org/project/helm)
[![LFX Health Score](https://insights.linuxfoundation.org/api/badge/health-score?project=helm)](https://insights.linuxfoundation.org/project/helm)
Helm is a tool for managing Charts. Charts are packages of pre-configured Kubernetes resources.

@ -1,10 +1,10 @@
module helm.sh/helm/v4
go 1.24.0
go 1.25.0
require (
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24
github.com/BurntSushi/toml v1.5.0
github.com/BurntSushi/toml v1.6.0
github.com/DATA-DOG/go-sqlmock v1.5.2
github.com/Masterminds/semver/v3 v3.4.0
github.com/Masterminds/sprig/v3 v3.3.0
@ -12,12 +12,12 @@ require (
github.com/Masterminds/vcs v1.13.3
github.com/ProtonMail/go-crypto v1.3.0
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2
github.com/cyphar/filepath-securejoin v0.6.0
github.com/cyphar/filepath-securejoin v0.6.1
github.com/distribution/distribution/v3 v3.0.0
github.com/evanphx/json-patch/v5 v5.9.11
github.com/extism/go-sdk v1.7.1
github.com/fatih/color v1.18.0
github.com/fluxcd/cli-utils v0.36.0-flux.14
github.com/fluxcd/cli-utils v0.36.0-flux.15
github.com/foxcpp/go-mockdns v1.1.0
github.com/gobwas/glob v0.2.3
github.com/gofrs/flock v0.13.0
@ -25,32 +25,31 @@ require (
github.com/jmoiron/sqlx v1.4.0
github.com/lib/pq v1.10.9
github.com/mattn/go-shellwords v1.0.12
github.com/mitchellh/copystructure v1.2.0
github.com/moby/term v0.5.2
github.com/opencontainers/go-digest v1.0.0
github.com/opencontainers/image-spec v1.1.1
github.com/rubenv/sql-migrate v1.8.0
github.com/rubenv/sql-migrate v1.8.1
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2
github.com/spf13/cobra v1.10.1
github.com/spf13/cobra v1.10.2
github.com/spf13/pflag v1.0.10
github.com/stretchr/testify v1.11.1
github.com/tetratelabs/wazero v1.9.0
github.com/tetratelabs/wazero v1.11.0
go.yaml.in/yaml/v3 v3.0.4
golang.org/x/crypto v0.43.0
golang.org/x/term v0.36.0
golang.org/x/text v0.30.0
golang.org/x/crypto v0.46.0
golang.org/x/term v0.38.0
golang.org/x/text v0.32.0
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/api v0.34.1
k8s.io/apiextensions-apiserver v0.34.1
k8s.io/apimachinery v0.34.1
k8s.io/apiserver v0.34.1
k8s.io/cli-runtime v0.34.1
k8s.io/client-go v0.34.1
k8s.io/api v0.34.3
k8s.io/apiextensions-apiserver v0.34.3
k8s.io/apimachinery v0.34.3
k8s.io/apiserver v0.34.3
k8s.io/cli-runtime v0.34.3
k8s.io/client-go v0.34.3
k8s.io/klog/v2 v2.130.1
k8s.io/kubectl v0.34.1
k8s.io/kubectl v0.34.3
oras.land/oras-go/v2 v2.6.0
sigs.k8s.io/controller-runtime v0.22.4
sigs.k8s.io/kustomize/kyaml v0.20.1
sigs.k8s.io/kustomize/kyaml v0.21.0
sigs.k8s.io/yaml v1.6.0
)
@ -112,6 +111,7 @@ require (
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.9 // indirect
github.com/miekg/dns v1.1.57 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/moby/spdystream v0.5.0 // indirect
@ -120,7 +120,7 @@ require (
github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect
github.com/onsi/gomega v1.37.0 // indirect
github.com/onsi/gomega v1.38.2 // indirect
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
@ -162,21 +162,21 @@ require (
go.opentelemetry.io/otel/trace v1.37.0 // indirect
go.opentelemetry.io/proto/otlp v1.5.0 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect
golang.org/x/mod v0.28.0 // indirect
golang.org/x/net v0.45.0 // indirect
golang.org/x/mod v0.30.0 // indirect
golang.org/x/net v0.47.0 // indirect
golang.org/x/oauth2 v0.30.0 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.37.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.39.0 // indirect
golang.org/x/time v0.12.0 // indirect
golang.org/x/tools v0.37.0 // indirect
golang.org/x/tools v0.39.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb // indirect
google.golang.org/grpc v1.72.1 // indirect
google.golang.org/protobuf v1.36.6 // indirect
google.golang.org/protobuf v1.36.7 // indirect
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
k8s.io/component-base v0.34.1 // indirect
k8s.io/component-base v0.34.3 // indirect
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b // indirect
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect

112
go.sum

@ -6,8 +6,8 @@ github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
@ -59,8 +59,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/cyphar/filepath-securejoin v0.6.0 h1:BtGB77njd6SVO6VztOHfPxKitJvd/VPT+OFBFMOi1Is=
github.com/cyphar/filepath-securejoin v0.6.0/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc=
github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE=
github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
@ -93,8 +93,8 @@ github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fluxcd/cli-utils v0.36.0-flux.14 h1:I//AMVUXTc+M04UtIXArMXQZCazGMwfemodV1j/yG8c=
github.com/fluxcd/cli-utils v0.36.0-flux.14/go.mod h1:uDo7BYOfbdmk/asnHuI0IQPl6u0FCgcN54AHDu3Y5As=
github.com/fluxcd/cli-utils v0.36.0-flux.15 h1:Et5QLnIpRjj+oZtM9gEybkAaoNsjysHq0y1253Ai94Y=
github.com/fluxcd/cli-utils v0.36.0-flux.15/go.mod h1:AqRUmWIfNE7cdL6NWSGF0bAlypGs+9x5UQ2qOtlEzv4=
github.com/foxcpp/go-mockdns v1.1.0 h1:jI0rD8M0wuYAxL7r/ynTrCQQq0BVqfB99Vgk7DlmewI=
github.com/foxcpp/go-mockdns v1.1.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
@ -244,10 +244,10 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8m
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus=
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus=
github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8=
github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y=
github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0=
github.com/onsi/ginkgo/v2 v2.25.2 h1:hepmgwx1D+llZleKQDMEvy8vIlCxMGt7W5ZxDjIEhsw=
github.com/onsi/ginkgo/v2 v2.25.2/go.mod h1:43uiyQC4Ed2tkOzLsEYm7hnrb7UJTWHYNsuy3bG/snE=
github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A=
github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
@ -287,16 +287,16 @@ github.com/redis/go-redis/extra/redisotel/v9 v9.0.5/go.mod h1:WZjPDy7VNzn77AAfnA
github.com/redis/go-redis/v9 v9.0.5/go.mod h1:WqMKv5vnQbRuZstUwxQI195wHy+t4PuXDOjzMvcuQHk=
github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM=
github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/rubenv/sql-migrate v1.8.0 h1:dXnYiJk9k3wetp7GfQbKJcPHjVJL6YK19tKj8t2Ns0o=
github.com/rubenv/sql-migrate v1.8.0/go.mod h1:F2bGFBwCU+pnmbtNYDeKvSuvL6lBVtXDXUUv5t+u1qw=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rubenv/sql-migrate v1.8.1 h1:EPNwCvjAowHI3TnZ+4fQu3a915OpnQoPAjTXCGOy2U0=
github.com/rubenv/sql-migrate v1.8.1/go.mod h1:BTIKBORjzyxZDS6dzoiw6eAFYJ1iNlGAtjn4LGeVjS8=
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.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/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
@ -304,8 +304,8 @@ github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w=
github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
@ -321,8 +321,8 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834 h1:ZF+QBjOI+tILZjBaFj3HgFonKXUcwgJ4djLb6i42S3Q=
github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834/go.mod h1:m9ymHTgNSEjuxvw8E7WWe4Pl4hZQHXONY8wE6dMLaRk=
github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I=
github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM=
github.com/tetratelabs/wazero v1.11.0 h1:+gKemEuKCTevU4d7ZTzlsvgd1uaToIDtlQlmNbwqYhA=
github.com/tetratelabs/wazero v1.11.0/go.mod h1:eV28rsN8Q+xwjogd7f4/Pp4xFxO7uOGbLcD/LzB1wiU=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ=
@ -396,16 +396,16 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g=
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
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.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
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=
@ -419,8 +419,8 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ=
golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM=
golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -433,8 +433,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.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -454,8 +454,8 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@ -463,8 +463,8 @@ golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
golang.org/x/term v0.14.0/go.mod h1:TySc+nGkYR6qt8km8wUhuFRTVSMIX3XPR58y2lC8vww=
golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
@ -472,8 +472,8 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.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.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@ -484,8 +484,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.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.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
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=
@ -496,8 +496,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb h1:
google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I=
google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA=
google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A=
google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
@ -512,26 +512,26 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
k8s.io/api v0.34.1 h1:jC+153630BMdlFukegoEL8E/yT7aLyQkIVuwhmwDgJM=
k8s.io/api v0.34.1/go.mod h1:SB80FxFtXn5/gwzCoN6QCtPD7Vbu5w2n1S0J5gFfTYk=
k8s.io/apiextensions-apiserver v0.34.1 h1:NNPBva8FNAPt1iSVwIE0FsdrVriRXMsaWFMqJbII2CI=
k8s.io/apiextensions-apiserver v0.34.1/go.mod h1:hP9Rld3zF5Ay2Of3BeEpLAToP+l4s5UlxiHfqRaRcMc=
k8s.io/apimachinery v0.34.1 h1:dTlxFls/eikpJxmAC7MVE8oOeP1zryV7iRyIjB0gky4=
k8s.io/apimachinery v0.34.1/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw=
k8s.io/apiserver v0.34.1 h1:U3JBGdgANK3dfFcyknWde1G6X1F4bg7PXuvlqt8lITA=
k8s.io/apiserver v0.34.1/go.mod h1:eOOc9nrVqlBI1AFCvVzsob0OxtPZUCPiUJL45JOTBG0=
k8s.io/cli-runtime v0.34.1 h1:btlgAgTrYd4sk8vJTRG6zVtqBKt9ZMDeQZo2PIzbL7M=
k8s.io/cli-runtime v0.34.1/go.mod h1:aVA65c+f0MZiMUPbseU/M9l1Wo2byeaGwUuQEQVVveE=
k8s.io/client-go v0.34.1 h1:ZUPJKgXsnKwVwmKKdPfw4tB58+7/Ik3CrjOEhsiZ7mY=
k8s.io/client-go v0.34.1/go.mod h1:kA8v0FP+tk6sZA0yKLRG67LWjqufAoSHA2xVGKw9Of8=
k8s.io/component-base v0.34.1 h1:v7xFgG+ONhytZNFpIz5/kecwD+sUhVE6HU7qQUiRM4A=
k8s.io/component-base v0.34.1/go.mod h1:mknCpLlTSKHzAQJJnnHVKqjxR7gBeHRv0rPXA7gdtQ0=
k8s.io/api v0.34.3 h1:D12sTP257/jSH2vHV2EDYrb16bS7ULlHpdNdNhEw2S4=
k8s.io/api v0.34.3/go.mod h1:PyVQBF886Q5RSQZOim7DybQjAbVs8g7gwJNhGtY5MBk=
k8s.io/apiextensions-apiserver v0.34.3 h1:p10fGlkDY09eWKOTeUSioxwLukJnm+KuDZdrW71y40g=
k8s.io/apiextensions-apiserver v0.34.3/go.mod h1:aujxvqGFRdb/cmXYfcRTeppN7S2XV/t7WMEc64zB5A0=
k8s.io/apimachinery v0.34.3 h1:/TB+SFEiQvN9HPldtlWOTp0hWbJ+fjU+wkxysf/aQnE=
k8s.io/apimachinery v0.34.3/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw=
k8s.io/apiserver v0.34.3 h1:uGH1qpDvSiYG4HVFqc6A3L4CKiX+aBWDrrsxHYK0Bdo=
k8s.io/apiserver v0.34.3/go.mod h1:QPnnahMO5C2m3lm6fPW3+JmyQbvHZQ8uudAu/493P2w=
k8s.io/cli-runtime v0.34.3 h1:YRyMhiwX0dT9lmG0AtZDaeG33Nkxgt9OlCTZhRXj9SI=
k8s.io/cli-runtime v0.34.3/go.mod h1:GVwL1L5uaGEgM7eGeKjaTG2j3u134JgG4dAI6jQKhMc=
k8s.io/client-go v0.34.3 h1:wtYtpzy/OPNYf7WyNBTj3iUA0XaBHVqhv4Iv3tbrF5A=
k8s.io/client-go v0.34.3/go.mod h1:OxxeYagaP9Kdf78UrKLa3YZixMCfP6bgPwPwNBQBzpM=
k8s.io/component-base v0.34.3 h1:zsEgw6ELqK0XncCQomgO9DpUIzlrYuZYA0Cgo+JWpVk=
k8s.io/component-base v0.34.3/go.mod h1:5iIlD8wPfWE/xSHTRfbjuvUul2WZbI2nOUK65XL0E/c=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b h1:MloQ9/bdJyIu9lb1PzujOPolHyvO06MXG5TUIj2mNAA=
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts=
k8s.io/kubectl v0.34.1 h1:1qP1oqT5Xc93K+H8J7ecpBjaz511gan89KO9Vbsh/OI=
k8s.io/kubectl v0.34.1/go.mod h1:JRYlhJpGPyk3dEmJ+BuBiOB9/dAvnrALJEiY/C5qa6A=
k8s.io/kubectl v0.34.3 h1:vpM6//153gh5gvsYHXWHVJ4l4xmN5QFwTSmlfd8icm8=
k8s.io/kubectl v0.34.3/go.mod h1:zZQHtIZoUqTP1bAnPzq/3W1jfc0NeOeunFgcswrfg1c=
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y=
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
oras.land/oras-go/v2 v2.6.0 h1:X4ELRsiGkrbeox69+9tzTu492FMUu7zJQW6eJU+I2oc=
@ -542,8 +542,8 @@ sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7np
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
sigs.k8s.io/kustomize/api v0.20.1 h1:iWP1Ydh3/lmldBnH/S5RXgT98vWYMaTUL1ADcr+Sv7I=
sigs.k8s.io/kustomize/api v0.20.1/go.mod h1:t6hUFxO+Ph0VxIk1sKp1WS0dOjbPCtLJ4p8aADLwqjM=
sigs.k8s.io/kustomize/kyaml v0.20.1 h1:PCMnA2mrVbRP3NIB6v9kYCAc38uvFLVs8j/CD567A78=
sigs.k8s.io/kustomize/kyaml v0.20.1/go.mod h1:0EmkQHRUsJxY8Ug9Niig1pUMSCGHxQ5RklbpV/Ri6po=
sigs.k8s.io/kustomize/kyaml v0.21.0 h1:7mQAf3dUwf0wBerWJd8rXhVcnkk5Tvn/q91cGkaP6HQ=
sigs.k8s.io/kustomize/kyaml v0.21.0/go.mod h1:hmxADesM3yUN2vbA5z1/YTBnzLJ1dajdqpQonwBL1FQ=
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco=

@ -175,16 +175,6 @@ func TestHelmCreateChart(t *testing.T) {
//
// Resources like hpa and ingress, which are disabled by default in values.yaml are enabled here using the equivalent
// of the `--set` flag.
//
// Note: This test requires the following ldflags to be set per the current Kubernetes version to avoid false-positive
// results.
// 1. -X helm.sh/helm/v4/pkg/lint/rules.k8sVersionMajor=<k8s-major-version>
// 2. -X helm.sh/helm/v4/pkg/lint/rules.k8sVersionMinor=<k8s-minor-version>
// or directly use '$(LDFLAGS)' in Makefile.
//
// When run without ldflags, the test passes giving a false-positive result. This is because the variables
// `k8sVersionMajor` and `k8sVersionMinor` by default are set to an older version of Kubernetes, with which, there
// might not be the deprecation warning.
func TestHelmCreateChart_CheckDeprecatedWarnings(t *testing.T) {
createdChart, err := chartutil.Create("checkdeprecatedwarnings", t.TempDir())
if err != nil {

@ -70,7 +70,7 @@ func Crds(linter *support.Linter) {
var yamlStruct *k8sYamlStruct
err := decoder.Decode(&yamlStruct)
if err == io.EOF {
if errors.Is(err, io.EOF) {
break
}
@ -80,8 +80,10 @@ func Crds(linter *support.Linter) {
return
}
linter.RunLinterRule(support.ErrorSev, fpath, validateCrdAPIVersion(yamlStruct))
linter.RunLinterRule(support.ErrorSev, fpath, validateCrdKind(yamlStruct))
if yamlStruct != nil {
linter.RunLinterRule(support.ErrorSev, fpath, validateCrdAPIVersion(yamlStruct))
linter.RunLinterRule(support.ErrorSev, fpath, validateCrdKind(yamlStruct))
}
}
}
}

@ -17,6 +17,8 @@ limitations under the License.
package rules
import (
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
@ -34,3 +36,31 @@ func TestInvalidCrdsDir(t *testing.T) {
assert.Len(t, res, 1)
assert.ErrorContains(t, res[0].Err, "not a directory")
}
// multi-document YAML with empty documents would panic
func TestCrdWithEmptyDocument(t *testing.T) {
chartDir := t.TempDir()
os.WriteFile(filepath.Join(chartDir, "Chart.yaml"), []byte(
`apiVersion: v1
name: test
version: 0.1.0
`), 0644)
// CRD with comments before --- (creates empty document)
crdsDir := filepath.Join(chartDir, "crds")
os.Mkdir(crdsDir, 0755)
os.WriteFile(filepath.Join(crdsDir, "test.yaml"), []byte(
`# Comments create empty document
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: test.example.io
`), 0644)
linter := support.Linter{ChartDir: chartDir}
Crds(&linter)
assert.Len(t, linter.Messages, 0)
}

@ -28,14 +28,6 @@ import (
kscheme "k8s.io/client-go/kubernetes/scheme"
)
var (
// This should be set in the Makefile based on the version of client-go being imported.
// These constants will be overwritten with LDFLAGS. The version components must be
// strings in order for LDFLAGS to set them.
k8sVersionMajor = "1"
k8sVersionMinor = "20"
)
// deprecatedAPIError indicates than an API is deprecated in Kubernetes
type deprecatedAPIError struct {
Deprecated string
@ -56,33 +48,29 @@ func validateNoDeprecations(resource *k8sYamlStruct, kubeVersion *common.KubeVer
return nil
}
majorVersion := k8sVersionMajor
minorVersion := k8sVersionMinor
if kubeVersion != nil {
majorVersion = kubeVersion.Major
minorVersion = kubeVersion.Minor
if kubeVersion == nil {
kubeVersion = &common.DefaultCapabilities.KubeVersion
}
runtimeObject, err := resourceToRuntimeObject(resource)
kubeVersionMajor, err := strconv.Atoi(kubeVersion.Major)
if err != nil {
// do not error for non-kubernetes resources
if runtime.IsNotRegisteredError(err) {
return nil
}
return err
}
major, err := strconv.Atoi(majorVersion)
kubeVersionMinor, err := strconv.Atoi(kubeVersion.Minor)
if err != nil {
return err
}
minor, err := strconv.Atoi(minorVersion)
runtimeObject, err := resourceToRuntimeObject(resource)
if err != nil {
// do not error for non-kubernetes resources
if runtime.IsNotRegisteredError(err) {
return nil
}
return err
}
if !deprecation.IsDeprecated(runtimeObject, major, minor) {
if !deprecation.IsDeprecated(runtimeObject, kubeVersionMajor, kubeVersionMinor) {
return nil
}
gvk := fmt.Sprintf("%s %s", resource.APIVersion, resource.Kind)

@ -150,7 +150,7 @@ func TemplatesWithSkipSchemaValidation(linter *support.Linter, values map[string
var yamlStruct *k8sYamlStruct
err := decoder.Decode(&yamlStruct)
if err == io.EOF {
if errors.Is(err, io.EOF) {
break
}

@ -56,7 +56,7 @@ func LoadFile(name string) (*chart.Chart, error) {
c, err := LoadArchive(raw)
if err != nil {
if err == gzip.ErrHeader {
if errors.Is(err, gzip.ErrHeader) {
return nil, fmt.Errorf("file '%s' does not appear to be a valid chart file (details: %s)", name, err)
}
}

@ -181,7 +181,7 @@ func LoadFiles(files []*archive.BufferedFile) (*chart.Chart, error) {
// LoadValues loads values from a reader.
//
// The reader is expected to contain one or more YAML documents, the values of which are merged.
// And the values can be either a chart's default values or a user-supplied values.
// And the values can be either a chart's default values or user-supplied values.
func LoadValues(data io.Reader) (map[string]interface{}, error) {
values := map[string]interface{}{}
reader := utilyaml.NewYAMLReader(bufio.NewReader(data))
@ -189,7 +189,7 @@ func LoadValues(data io.Reader) (map[string]interface{}, error) {
currentMap := map[string]interface{}{}
raw, err := reader.Read()
if err != nil {
if err == io.EOF {
if errors.Is(err, io.EOF) {
break
}
return nil, fmt.Errorf("error reading yaml document: %w", err)

@ -20,6 +20,7 @@ import (
"archive/tar"
"bytes"
"compress/gzip"
"errors"
"io"
"log"
"os"
@ -116,7 +117,7 @@ func TestBomTestData(t *testing.T) {
tr := tar.NewReader(unzipped)
for {
file, err := tr.Next()
if err == io.EOF {
if errors.Is(err, io.EOF) {
break
}
if err != nil {

@ -20,9 +20,8 @@ import (
"log/slog"
"strings"
"github.com/mitchellh/copystructure"
chart "helm.sh/helm/v4/internal/chart/v3"
"helm.sh/helm/v4/internal/copystructure"
"helm.sh/helm/v4/pkg/chart/common"
"helm.sh/helm/v4/pkg/chart/common/util"
)
@ -281,7 +280,12 @@ func processImportValues(c *chart.Chart, merge bool) error {
// get child table
vv, err := cvals.Table(r.Name + "." + child)
if err != nil {
slog.Warn("ImportValues missing table from chart", "chart", r.Name, slog.Any("error", err))
slog.Warn(
"ImportValues missing table from chart",
slog.String("chart", "chart"),
slog.String("name", r.Name),
slog.Any("error", err),
)
continue
}
// create value map from child to be merged into parent

@ -21,6 +21,7 @@ import (
"bytes"
"compress/gzip"
"crypto/sha256"
"errors"
"fmt"
"io"
"os"
@ -201,7 +202,7 @@ func retrieveAllHeadersFromTar(path string) ([]*tar.Header, error) {
headers := []*tar.Header{}
for {
hd, err := tr.Next()
if err == io.EOF {
if errors.Is(err, io.EOF) {
break
}

@ -0,0 +1,120 @@
/*
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 copystructure
import (
"fmt"
"reflect"
)
// Copy performs a deep copy of the given src.
// This implementation handles the specific use cases needed by Helm.
func Copy(src any) (any, error) {
if src == nil {
return make(map[string]any), nil
}
return copyValue(reflect.ValueOf(src))
}
// copyValue handles copying using reflection for non-map types
func copyValue(original reflect.Value) (any, error) {
switch original.Kind() {
case reflect.Bool, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32,
reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32,
reflect.Uint64, reflect.Uintptr, reflect.Float32, reflect.Float64,
reflect.Complex64, reflect.Complex128, reflect.String, reflect.Array:
return original.Interface(), nil
case reflect.Interface:
if original.IsNil() {
return original.Interface(), nil
}
return copyValue(original.Elem())
case reflect.Map:
if original.IsNil() {
return original.Interface(), nil
}
copied := reflect.MakeMap(original.Type())
var err error
var child any
iter := original.MapRange()
for iter.Next() {
key := iter.Key()
value := iter.Value()
if value.Kind() == reflect.Interface && value.IsNil() {
copied.SetMapIndex(key, value)
continue
}
child, err = copyValue(value)
if err != nil {
return nil, err
}
copied.SetMapIndex(key, reflect.ValueOf(child))
}
return copied.Interface(), nil
case reflect.Pointer:
if original.IsNil() {
return original.Interface(), nil
}
copied, err := copyValue(original.Elem())
if err != nil {
return nil, err
}
ptr := reflect.New(original.Type().Elem())
ptr.Elem().Set(reflect.ValueOf(copied))
return ptr.Interface(), nil
case reflect.Slice:
if original.IsNil() {
return original.Interface(), nil
}
copied := reflect.MakeSlice(original.Type(), original.Len(), original.Cap())
for i := 0; i < original.Len(); i++ {
val, err := copyValue(original.Index(i))
if err != nil {
return nil, err
}
copied.Index(i).Set(reflect.ValueOf(val))
}
return copied.Interface(), nil
case reflect.Struct:
copied := reflect.New(original.Type()).Elem()
for i := 0; i < original.NumField(); i++ {
elem, err := copyValue(original.Field(i))
if err != nil {
return nil, err
}
copied.Field(i).Set(reflect.ValueOf(elem))
}
return copied.Interface(), nil
case reflect.Func, reflect.Chan, reflect.UnsafePointer:
if original.IsNil() {
return original.Interface(), nil
}
return original.Interface(), nil
default:
return original.Interface(), fmt.Errorf("unsupported type %v", original)
}
}

@ -0,0 +1,374 @@
/*
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 copystructure
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestCopy_Nil(t *testing.T) {
result, err := Copy(nil)
require.NoError(t, err)
assert.Equal(t, map[string]any{}, result)
}
func TestCopy_PrimitiveTypes(t *testing.T) {
tests := []struct {
name string
input any
}{
{"bool", true},
{"int", 42},
{"int8", int8(8)},
{"int16", int16(16)},
{"int32", int32(32)},
{"int64", int64(64)},
{"uint", uint(42)},
{"uint8", uint8(8)},
{"uint16", uint16(16)},
{"uint32", uint32(32)},
{"uint64", uint64(64)},
{"float32", float32(3.14)},
{"float64", 3.14159},
{"complex64", complex64(1 + 2i)},
{"complex128", 1 + 2i},
{"string", "hello world"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := Copy(tt.input)
require.NoError(t, err)
assert.Equal(t, tt.input, result)
})
}
}
func TestCopy_Array(t *testing.T) {
input := [3]int{1, 2, 3}
result, err := Copy(input)
require.NoError(t, err)
assert.Equal(t, input, result)
}
func TestCopy_Slice(t *testing.T) {
t.Run("slice of ints", func(t *testing.T) {
input := []int{1, 2, 3, 4, 5}
result, err := Copy(input)
require.NoError(t, err)
resultSlice, ok := result.([]int)
require.True(t, ok)
assert.Equal(t, input, resultSlice)
// Verify it's a deep copy by modifying original
input[0] = 999
assert.Equal(t, 1, resultSlice[0])
})
t.Run("slice of strings", func(t *testing.T) {
input := []string{"a", "b", "c"}
result, err := Copy(input)
require.NoError(t, err)
assert.Equal(t, input, result)
})
t.Run("nil slice", func(t *testing.T) {
var input []int
result, err := Copy(input)
require.NoError(t, err)
assert.Nil(t, result)
})
t.Run("slice of maps", func(t *testing.T) {
input := []map[string]any{
{"key1": "value1"},
{"key2": "value2"},
}
result, err := Copy(input)
require.NoError(t, err)
resultSlice, ok := result.([]map[string]any)
require.True(t, ok)
assert.Equal(t, input, resultSlice)
// Verify deep copy
input[0]["key1"] = "modified"
assert.Equal(t, "value1", resultSlice[0]["key1"])
})
}
func TestCopy_Map(t *testing.T) {
t.Run("map[string]any", func(t *testing.T) {
input := map[string]any{
"string": "value",
"int": 42,
"bool": true,
"nested": map[string]any{
"inner": "value",
},
}
result, err := Copy(input)
require.NoError(t, err)
resultMap, ok := result.(map[string]any)
require.True(t, ok)
assert.Equal(t, input, resultMap)
// Verify deep copy
input["string"] = "modified"
assert.Equal(t, "value", resultMap["string"])
nestedInput := input["nested"].(map[string]any)
nestedResult := resultMap["nested"].(map[string]any)
nestedInput["inner"] = "modified"
assert.Equal(t, "value", nestedResult["inner"])
})
t.Run("map[string]string", func(t *testing.T) {
input := map[string]string{
"key1": "value1",
"key2": "value2",
}
result, err := Copy(input)
require.NoError(t, err)
assert.Equal(t, input, result)
})
t.Run("nil map", func(t *testing.T) {
var input map[string]any
result, err := Copy(input)
require.NoError(t, err)
assert.Nil(t, result)
})
t.Run("map with nil values", func(t *testing.T) {
input := map[string]any{
"key1": "value1",
"key2": nil,
}
result, err := Copy(input)
require.NoError(t, err)
resultMap, ok := result.(map[string]any)
require.True(t, ok)
assert.Equal(t, input, resultMap)
assert.Nil(t, resultMap["key2"])
})
}
func TestCopy_Struct(t *testing.T) {
type TestStruct struct {
Name string
Age int
Active bool
Scores []int
Metadata map[string]any
}
input := TestStruct{
Name: "John",
Age: 30,
Active: true,
Scores: []int{95, 87, 92},
Metadata: map[string]any{
"level": "advanced",
"tags": []string{"go", "programming"},
},
}
result, err := Copy(input)
require.NoError(t, err)
resultStruct, ok := result.(TestStruct)
require.True(t, ok)
assert.Equal(t, input, resultStruct)
// Verify deep copy
input.Name = "Modified"
input.Scores[0] = 999
assert.Equal(t, "John", resultStruct.Name)
assert.Equal(t, 95, resultStruct.Scores[0])
}
func TestCopy_Pointer(t *testing.T) {
t.Run("pointer to int", func(t *testing.T) {
value := 42
input := &value
result, err := Copy(input)
require.NoError(t, err)
resultPtr, ok := result.(*int)
require.True(t, ok)
assert.Equal(t, *input, *resultPtr)
// Verify they point to different memory locations
assert.NotSame(t, input, resultPtr)
// Verify deep copy
*input = 999
assert.Equal(t, 42, *resultPtr)
})
t.Run("pointer to struct", func(t *testing.T) {
type Person struct {
Name string
Age int
}
input := &Person{Name: "Alice", Age: 25}
result, err := Copy(input)
require.NoError(t, err)
resultPtr, ok := result.(*Person)
require.True(t, ok)
assert.Equal(t, *input, *resultPtr)
assert.NotSame(t, input, resultPtr)
})
t.Run("nil pointer", func(t *testing.T) {
var input *int
result, err := Copy(input)
require.NoError(t, err)
assert.Nil(t, result)
})
}
func TestCopy_Interface(t *testing.T) {
t.Run("any with value", func(t *testing.T) {
var input any = "hello"
result, err := Copy(input)
require.NoError(t, err)
assert.Equal(t, input, result)
})
t.Run("nil any", func(t *testing.T) {
var input any
result, err := Copy(input)
require.NoError(t, err)
// Copy(nil) returns an empty map according to the implementation
assert.Equal(t, map[string]any{}, result)
})
t.Run("any with complex value", func(t *testing.T) {
var input any = map[string]any{
"key": "value",
"nested": map[string]any{
"inner": 42,
},
}
result, err := Copy(input)
require.NoError(t, err)
assert.Equal(t, input, result)
})
}
func TestCopy_ComplexNested(t *testing.T) {
input := map[string]any{
"users": []map[string]any{
{
"name": "Alice",
"age": 30,
"addresses": []map[string]any{
{"type": "home", "city": "NYC"},
{"type": "work", "city": "SF"},
},
},
{
"name": "Bob",
"age": 25,
"addresses": []map[string]any{
{"type": "home", "city": "LA"},
},
},
},
"metadata": map[string]any{
"version": "1.0",
"flags": []bool{true, false, true},
},
}
result, err := Copy(input)
require.NoError(t, err)
resultMap, ok := result.(map[string]any)
require.True(t, ok)
assert.Equal(t, input, resultMap)
// Verify deep copy by modifying nested values
users := input["users"].([]map[string]any)
addresses := users[0]["addresses"].([]map[string]any)
addresses[0]["city"] = "Modified"
resultUsers := resultMap["users"].([]map[string]any)
resultAddresses := resultUsers[0]["addresses"].([]map[string]any)
assert.Equal(t, "NYC", resultAddresses[0]["city"])
}
func TestCopy_Functions(t *testing.T) {
t.Run("function", func(t *testing.T) {
input := func() string { return "hello" }
result, err := Copy(input)
require.NoError(t, err)
// Functions should be copied as-is (same reference)
resultFunc, ok := result.(func() string)
require.True(t, ok)
assert.Equal(t, input(), resultFunc())
})
t.Run("nil function", func(t *testing.T) {
var input func()
result, err := Copy(input)
require.NoError(t, err)
assert.Nil(t, result)
})
}
func TestCopy_Channels(t *testing.T) {
t.Run("channel", func(t *testing.T) {
input := make(chan int, 1)
input <- 42
result, err := Copy(input)
require.NoError(t, err)
// Channels should be copied as-is (same reference)
resultChan, ok := result.(chan int)
require.True(t, ok)
// Since channels are copied as references, verify we can read from the result channel
value := <-resultChan
assert.Equal(t, 42, value)
})
t.Run("nil channel", func(t *testing.T) {
var input chan int
result, err := Copy(input)
require.NoError(t, err)
assert.Nil(t, result)
})
}

@ -36,6 +36,9 @@ type DebugCheckHandler struct {
// Enabled implements slog.Handler.Enabled
func (h *DebugCheckHandler) Enabled(_ context.Context, level slog.Level) bool {
if level == slog.LevelDebug {
if h.debugEnabled == nil {
return false
}
return h.debugEnabled()
}
return true // Always log other levels

@ -18,8 +18,10 @@ package logging
import (
"bytes"
"context"
"log/slog"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
@ -113,3 +115,259 @@ func TestLogHolder_InterfaceCompliance(t *testing.T) {
assert.Equal(t, handler, logger.Handler())
})
}
func TestDebugCheckHandler_Enabled(t *testing.T) {
t.Run("returns debugEnabled function result for debug level", func(t *testing.T) {
// Test with debug enabled
debugEnabled := func() bool { return true }
buf := &bytes.Buffer{}
baseHandler := slog.NewTextHandler(buf, &slog.HandlerOptions{Level: slog.LevelDebug})
handler := &DebugCheckHandler{
handler: baseHandler,
debugEnabled: debugEnabled,
}
assert.True(t, handler.Enabled(t.Context(), slog.LevelDebug))
})
t.Run("returns false for debug level when debug disabled", func(t *testing.T) {
// Test with debug disabled
debugEnabled := func() bool { return false }
buf := &bytes.Buffer{}
baseHandler := slog.NewTextHandler(buf, &slog.HandlerOptions{Level: slog.LevelDebug})
handler := &DebugCheckHandler{
handler: baseHandler,
debugEnabled: debugEnabled,
}
assert.False(t, handler.Enabled(t.Context(), slog.LevelDebug))
})
t.Run("always returns true for non-debug levels", func(t *testing.T) {
debugEnabled := func() bool { return false } // Debug disabled
buf := &bytes.Buffer{}
baseHandler := slog.NewTextHandler(buf, &slog.HandlerOptions{Level: slog.LevelDebug})
handler := &DebugCheckHandler{
handler: baseHandler,
debugEnabled: debugEnabled,
}
// Even with debug disabled, other levels should always be enabled
assert.True(t, handler.Enabled(t.Context(), slog.LevelInfo))
assert.True(t, handler.Enabled(t.Context(), slog.LevelWarn))
assert.True(t, handler.Enabled(t.Context(), slog.LevelError))
})
t.Run("calls debugEnabled function dynamically", func(t *testing.T) {
callCount := 0
debugEnabled := func() bool {
callCount++
return callCount%2 == 1 // Alternates between true and false
}
buf := &bytes.Buffer{}
baseHandler := slog.NewTextHandler(buf, &slog.HandlerOptions{Level: slog.LevelDebug})
handler := &DebugCheckHandler{
handler: baseHandler,
debugEnabled: debugEnabled,
}
// First call should return true
assert.True(t, handler.Enabled(t.Context(), slog.LevelDebug))
assert.Equal(t, 1, callCount)
// Second call should return false
assert.False(t, handler.Enabled(t.Context(), slog.LevelDebug))
assert.Equal(t, 2, callCount)
// Third call should return true again
assert.True(t, handler.Enabled(t.Context(), slog.LevelDebug))
assert.Equal(t, 3, callCount)
})
}
func TestDebugCheckHandler_Handle(t *testing.T) {
t.Run("delegates to underlying handler", func(t *testing.T) {
buf := &bytes.Buffer{}
baseHandler := slog.NewTextHandler(buf, &slog.HandlerOptions{Level: slog.LevelDebug})
handler := &DebugCheckHandler{
handler: baseHandler,
debugEnabled: func() bool { return true },
}
record := slog.NewRecord(time.Now(), slog.LevelInfo, "test message", 0)
err := handler.Handle(t.Context(), record)
assert.NoError(t, err)
assert.Contains(t, buf.String(), "test message")
})
t.Run("handles context correctly", func(t *testing.T) {
buf := &bytes.Buffer{}
baseHandler := slog.NewTextHandler(buf, &slog.HandlerOptions{Level: slog.LevelDebug})
handler := &DebugCheckHandler{
handler: baseHandler,
debugEnabled: func() bool { return true },
}
type testKey string
ctx := context.WithValue(t.Context(), testKey("test"), "value")
record := slog.NewRecord(time.Now(), slog.LevelInfo, "context test", 0)
err := handler.Handle(ctx, record)
assert.NoError(t, err)
assert.Contains(t, buf.String(), "context test")
})
}
func TestDebugCheckHandler_WithAttrs(t *testing.T) {
t.Run("returns new DebugCheckHandler with attributes", func(t *testing.T) {
logger := NewLogger(func() bool { return true })
handler := logger.Handler()
newHandler := handler.WithAttrs([]slog.Attr{
slog.String("key1", "value1"),
slog.Int("key2", 42),
})
// Should return a DebugCheckHandler
debugHandler, ok := newHandler.(*DebugCheckHandler)
assert.True(t, ok)
assert.NotNil(t, debugHandler)
// Should preserve the debugEnabled function
assert.True(t, debugHandler.Enabled(t.Context(), slog.LevelDebug))
// Should have the attributes applied to the underlying handler
assert.NotEqual(t, handler, debugHandler.handler)
})
t.Run("preserves debugEnabled function", func(t *testing.T) {
callCount := 0
debugEnabled := func() bool {
callCount++
return callCount%2 == 1
}
buf := &bytes.Buffer{}
baseHandler := slog.NewTextHandler(buf, &slog.HandlerOptions{Level: slog.LevelDebug})
handler := &DebugCheckHandler{
handler: baseHandler,
debugEnabled: debugEnabled,
}
attrs := []slog.Attr{slog.String("test", "value")}
newHandler := handler.WithAttrs(attrs)
// The new handler should use the same debugEnabled function
assert.True(t, newHandler.Enabled(t.Context(), slog.LevelDebug))
assert.Equal(t, 1, callCount)
assert.False(t, newHandler.Enabled(t.Context(), slog.LevelDebug))
assert.Equal(t, 2, callCount)
})
}
func TestDebugCheckHandler_WithGroup(t *testing.T) {
t.Run("returns new DebugCheckHandler with group", func(t *testing.T) {
buf := &bytes.Buffer{}
baseHandler := slog.NewTextHandler(buf, &slog.HandlerOptions{Level: slog.LevelDebug})
handler := &DebugCheckHandler{
handler: baseHandler,
debugEnabled: func() bool { return true },
}
newHandler := handler.WithGroup("testgroup")
// Should return a DebugCheckHandler
debugHandler, ok := newHandler.(*DebugCheckHandler)
assert.True(t, ok)
assert.NotNil(t, debugHandler)
// Should preserve the debugEnabled function
assert.True(t, debugHandler.Enabled(t.Context(), slog.LevelDebug))
// Should have the group applied to the underlying handler
assert.NotEqual(t, handler.handler, debugHandler.handler)
})
t.Run("preserves debugEnabled function", func(t *testing.T) {
callCount := 0
debugEnabled := func() bool {
callCount++
return callCount%2 == 1
}
buf := &bytes.Buffer{}
baseHandler := slog.NewTextHandler(buf, &slog.HandlerOptions{Level: slog.LevelDebug})
handler := &DebugCheckHandler{
handler: baseHandler,
debugEnabled: debugEnabled,
}
newHandler := handler.WithGroup("testgroup")
// The new handler should use the same debugEnabled function
assert.True(t, newHandler.Enabled(t.Context(), slog.LevelDebug))
assert.Equal(t, 1, callCount)
assert.False(t, newHandler.Enabled(t.Context(), slog.LevelDebug))
assert.Equal(t, 2, callCount)
})
}
func TestDebugCheckHandler_Integration(t *testing.T) {
t.Run("works with NewLogger function", func(t *testing.T) {
debugEnabled := func() bool { return true }
logger := NewLogger(debugEnabled)
assert.NotNil(t, logger)
// The logger should have a DebugCheckHandler
handler := logger.Handler()
debugHandler, ok := handler.(*DebugCheckHandler)
assert.True(t, ok)
// Should enable debug when debugEnabled returns true
assert.True(t, debugHandler.Enabled(t.Context(), slog.LevelDebug))
// Should enable other levels regardless
assert.True(t, debugHandler.Enabled(t.Context(), slog.LevelInfo))
})
t.Run("dynamic debug checking works in practice", func(t *testing.T) {
debugState := false
debugEnabled := func() bool { return debugState }
logger := NewLogger(debugEnabled)
// Initially debug should be disabled
assert.False(t, logger.Handler().(*DebugCheckHandler).Enabled(t.Context(), slog.LevelDebug))
// Enable debug
debugState = true
assert.True(t, logger.Handler().(*DebugCheckHandler).Enabled(t.Context(), slog.LevelDebug))
// Disable debug again
debugState = false
assert.False(t, logger.Handler().(*DebugCheckHandler).Enabled(t.Context(), slog.LevelDebug))
})
t.Run("handles nil debugEnabled function", func(t *testing.T) {
logger := NewLogger(nil)
assert.NotNil(t, logger)
// The logger should have a DebugCheckHandler
handler := logger.Handler()
debugHandler, ok := handler.(*DebugCheckHandler)
assert.True(t, ok)
// When debugEnabled is nil, debug level should be disabled (default behavior)
assert.False(t, debugHandler.Enabled(t.Context(), slog.LevelDebug))
// Other levels should always be enabled
assert.True(t, debugHandler.Enabled(t.Context(), slog.LevelInfo))
assert.True(t, debugHandler.Enabled(t.Context(), slog.LevelWarn))
assert.True(t, debugHandler.Enabled(t.Context(), slog.LevelError))
})
}

@ -140,7 +140,7 @@ func (g *TarGzExtractor) Extract(buffer *bytes.Buffer, targetDir string) error {
tarReader := tar.NewReader(uncompressedStream)
for {
header, err := tarReader.Next()
if err == io.EOF {
if errors.Is(err, io.EOF) {
break
}
if err != nil {

@ -368,7 +368,7 @@ func TestExtractWithNestedDirectories(t *testing.T) {
}{
{"plugin.yaml", "plugin metadata", 0600, tar.TypeReg},
{"bin/", "", 0755, tar.TypeDir},
{"bin/plugin", "#!/bin/bash\necho plugin", 0755, tar.TypeReg},
{"bin/plugin", "#!/usr/bin/env sh\necho plugin", 0755, tar.TypeReg},
{"docs/", "", 0755, tar.TypeDir},
{"docs/README.md", "readme content", 0644, tar.TypeReg},
{"docs/examples/", "", 0755, tar.TypeDir},
@ -531,7 +531,7 @@ func TestExtractPluginInSubdirectory(t *testing.T) {
{"my-plugin/", "", 0755, tar.TypeDir},
{"my-plugin/plugin.yaml", "name: my-plugin\nversion: 1.0.0\nusage: test\ndescription: test plugin\ncommand: $HELM_PLUGIN_DIR/bin/my-plugin", 0644, tar.TypeReg},
{"my-plugin/bin/", "", 0755, tar.TypeDir},
{"my-plugin/bin/my-plugin", "#!/bin/bash\necho test", 0755, tar.TypeReg},
{"my-plugin/bin/my-plugin", "#!/usr/bin/env sh\necho test", 0755, tar.TypeReg},
}
for _, file := range files {

@ -31,9 +31,6 @@ import (
// ErrMissingMetadata indicates that plugin.yaml is missing.
var ErrMissingMetadata = errors.New("plugin metadata (plugin.yaml) missing")
// Debug enables verbose output.
var Debug bool
// Options contains options for plugin installation.
type Options struct {
// Verify enables signature verification before installation
@ -80,7 +77,7 @@ func InstallWithOptions(i Installer, opts Options) (*VerificationResult, error)
return nil, err
}
if _, pathErr := os.Stat(i.Path()); !os.IsNotExist(pathErr) {
slog.Warn("plugin already exists", "path", i.Path(), slog.Any("error", pathErr))
slog.Warn("plugin already exists", slog.String("path", i.Path()), slog.Any("error", pathErr))
return nil, errors.New("plugin already exists")
}
@ -132,7 +129,7 @@ func InstallWithOptions(i Installer, opts Options) (*VerificationResult, error)
// Update updates a plugin.
func Update(i Installer) error {
if _, pathErr := os.Stat(i.Path()); os.IsNotExist(pathErr) {
slog.Warn("plugin does not exist", "path", i.Path(), slog.Any("error", pathErr))
slog.Warn("plugin does not exist", slog.String("path", i.Path()), slog.Any("error", pathErr))
return errors.New("plugin does not exist")
}
return i.Update()
@ -163,7 +160,11 @@ func NewForSource(source, version string) (installer Installer, err error) {
func FindSource(location string) (Installer, error) {
installer, err := existingVCSRepo(location)
if err != nil && err.Error() == "Cannot detect VCS" {
slog.Warn("cannot get information about plugin source", "location", location, slog.Any("error", err))
slog.Warn(
"cannot get information about plugin source",
slog.String("location", location),
slog.Any("error", err),
)
return installer, errors.New("cannot get information about plugin source")
}
return installer, err

@ -87,7 +87,7 @@ func TestLocalInstallerTarball(t *testing.T) {
Mode int64
}{
{"test-plugin/plugin.yaml", "name: test-plugin\napiVersion: v1\ntype: cli/v1\nruntime: subprocess\nversion: 1.0.0\nconfig:\n shortHelp: test\n longHelp: test\nruntimeConfig:\n platformCommand:\n - command: echo", 0644},
{"test-plugin/bin/test-plugin", "#!/bin/bash\necho test", 0755},
{"test-plugin/bin/test-plugin", "#!/usr/bin/env sh\necho test", 0755},
}
for _, file := range files {

@ -19,6 +19,7 @@ import (
"archive/tar"
"bytes"
"compress/gzip"
"errors"
"fmt"
"io"
"log/slog"
@ -214,7 +215,7 @@ func extractTar(r io.Reader, targetDir string) error {
for {
header, err := tarReader.Next()
if err == io.EOF {
if errors.Is(err, io.EOF) {
break
}
if err != nil {

@ -47,6 +47,7 @@ func loadMetadataLegacy(metadataData []byte) (*Metadata, error) {
var ml MetadataLegacy
d := yaml.NewDecoder(bytes.NewReader(metadataData))
// NOTE: No strict unmarshalling for legacy plugins - maintain backwards compatibility
if err := d.Decode(&ml); err != nil {
return nil, err
}
@ -66,6 +67,7 @@ func loadMetadataV1(metadataData []byte) (*Metadata, error) {
var mv1 MetadataV1
d := yaml.NewDecoder(bytes.NewReader(metadataData))
d.KnownFields(true)
if err := d.Decode(&mv1); err != nil {
return nil, err
}

@ -268,3 +268,96 @@ func TestFindPlugins(t *testing.T) {
})
}
}
func TestLoadMetadataLegacy(t *testing.T) {
testCases := map[string]struct {
yaml string
expectError bool
errorContains string
expectedName string
logNote string
}{
"capital name field": {
yaml: `Name: my-plugin
version: 1.0.0
usage: test plugin
description: test description
command: echo test`,
expectError: true,
errorContains: `invalid plugin name "": must contain only a-z, A-Z, 0-9, _ and -`,
// Legacy plugins: No strict unmarshalling (backwards compatibility)
// YAML decoder silently ignores "Name:", then validation catches empty name
logNote: "NOTE: V1 plugins use strict unmarshalling and would get: yaml: field Name not found",
},
"correct name field": {
yaml: `name: my-plugin
version: 1.0.0
usage: test plugin
description: test description
command: echo test`,
expectError: false,
expectedName: "my-plugin",
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
m, err := loadMetadataLegacy([]byte(tc.yaml))
if tc.expectError {
require.Error(t, err)
assert.Contains(t, err.Error(), tc.errorContains)
t.Logf("Legacy error (validation catches empty name): %v", err)
if tc.logNote != "" {
t.Log(tc.logNote)
}
} else {
require.NoError(t, err)
assert.Equal(t, tc.expectedName, m.Name)
}
})
}
}
func TestLoadMetadataV1(t *testing.T) {
testCases := map[string]struct {
yaml string
expectError bool
errorContains string
expectedName string
}{
"capital name field": {
yaml: `apiVersion: v1
Name: my-plugin
type: cli/v1
runtime: subprocess
`,
expectError: true,
errorContains: "field Name not found in type plugin.MetadataV1",
},
"correct name field": {
yaml: `apiVersion: v1
name: my-plugin
type: cli/v1
runtime: subprocess
`,
expectError: false,
expectedName: "my-plugin",
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
m, err := loadMetadataV1([]byte(tc.yaml))
if tc.expectError {
require.Error(t, err)
assert.Contains(t, err.Error(), tc.errorContains)
t.Logf("V1 error (strict unmarshalling): %v", err)
} else {
require.NoError(t, err)
assert.Equal(t, tc.expectedName, m.Name)
}
})
}
}

@ -54,7 +54,7 @@ func (m Metadata) Validate() error {
var errs []error
if !validPluginName.MatchString(m.Name) {
errs = append(errs, fmt.Errorf("invalid name"))
errs = append(errs, fmt.Errorf("invalid plugin name %q: must contain only a-z, A-Z, 0-9, _ and -", m.Name))
}
if m.APIVersion == "" {

@ -69,7 +69,7 @@ type MetadataLegacy struct {
func (m *MetadataLegacy) Validate() error {
if !validPluginName.MatchString(m.Name) {
return fmt.Errorf("invalid plugin name")
return fmt.Errorf("invalid plugin name %q: must contain only a-z, A-Z, 0-9, _ and -", m.Name)
}
m.Usage = sanitizeString(m.Usage)

@ -0,0 +1,126 @@
/*
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 plugin
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestMetadataLegacyValidate(t *testing.T) {
testsValid := map[string]MetadataLegacy{
"valid metadata": {
Name: "myplugin",
},
"valid with command": {
Name: "myplugin",
Command: "echo hello",
},
"valid with platformCommand": {
Name: "myplugin",
PlatformCommand: []PlatformCommand{
{OperatingSystem: "linux", Architecture: "amd64", Command: "echo hello"},
},
},
"valid with hooks": {
Name: "myplugin",
Hooks: Hooks{
"install": "echo install",
},
},
"valid with platformHooks": {
Name: "myplugin",
PlatformHooks: PlatformHooks{
"install": []PlatformCommand{
{OperatingSystem: "linux", Architecture: "amd64", Command: "echo install"},
},
},
},
"valid with downloaders": {
Name: "myplugin",
Downloaders: []Downloaders{
{
Protocols: []string{"myproto"},
Command: "echo download",
},
},
},
}
for testName, metadata := range testsValid {
t.Run(testName, func(t *testing.T) {
assert.NoError(t, metadata.Validate())
})
}
testsInvalid := map[string]MetadataLegacy{
"invalid name": {
Name: "my plugin", // further tested in TestValidPluginName
},
"both command and platformCommand": {
Name: "myplugin",
Command: "echo hello",
PlatformCommand: []PlatformCommand{
{OperatingSystem: "linux", Architecture: "amd64", Command: "echo hello"},
},
},
"both hooks and platformHooks": {
Name: "myplugin",
Hooks: Hooks{
"install": "echo install",
},
PlatformHooks: PlatformHooks{
"install": []PlatformCommand{
{OperatingSystem: "linux", Architecture: "amd64", Command: "echo install"},
},
},
},
"downloader with empty command": {
Name: "myplugin",
Downloaders: []Downloaders{
{
Protocols: []string{"myproto"},
Command: "",
},
},
},
"downloader with no protocols": {
Name: "myplugin",
Downloaders: []Downloaders{
{
Protocols: []string{},
Command: "echo download",
},
},
},
"downloader with empty protocol": {
Name: "myplugin",
Downloaders: []Downloaders{
{
Protocols: []string{""},
Command: "echo download",
},
},
},
}
for testName, metadata := range testsInvalid {
t.Run(testName, func(t *testing.T) {
assert.Error(t, metadata.Validate())
})
}
}

@ -53,10 +53,10 @@ func TestValidatePluginData(t *testing.T) {
}{
{true, mockSubprocessCLIPlugin(t, "abcdefghijklmnopqrstuvwxyz0123456789_-ABC"), ""},
{true, mockSubprocessCLIPlugin(t, "foo-bar-FOO-BAR_1234"), ""},
{false, mockSubprocessCLIPlugin(t, "foo -bar"), "invalid name"},
{false, mockSubprocessCLIPlugin(t, "$foo -bar"), "invalid name"}, // Test leading chars
{false, mockSubprocessCLIPlugin(t, "foo -bar "), "invalid name"}, // Test trailing chars
{false, mockSubprocessCLIPlugin(t, "foo\nbar"), "invalid name"}, // Test newline
{false, mockSubprocessCLIPlugin(t, "foo -bar"), "invalid plugin name"},
{false, mockSubprocessCLIPlugin(t, "$foo -bar"), "invalid plugin name"}, // Test leading chars
{false, mockSubprocessCLIPlugin(t, "foo -bar "), "invalid plugin name"}, // Test trailing chars
{false, mockSubprocessCLIPlugin(t, "foo\nbar"), "invalid plugin name"}, // Test newline
{true, mockNoCommand, ""}, // Test no command metadata works
{true, mockLegacyCommand, ""}, // Test legacy command metadata works
} {
@ -66,8 +66,8 @@ func TestValidatePluginData(t *testing.T) {
} else if !item.pass && err == nil {
t.Errorf("expected case %d to fail", i)
}
if !item.pass && err.Error() != item.errString {
t.Errorf("index [%d]: expected the following error: %s, but got: %s", i, item.errString, err.Error())
if !item.pass && !strings.Contains(err.Error(), item.errString) {
t.Errorf("index [%d]: expected error to contain: %s, but got: %s", i, item.errString, err.Error())
}
}
}
@ -92,7 +92,7 @@ func TestMetadataValidateMultipleErrors(t *testing.T) {
// Check that all expected errors are present in the joined error
expectedErrors := []string{
"invalid name",
"invalid plugin name",
"empty APIVersion",
"empty type field",
"empty runtime field",

@ -21,6 +21,44 @@ import (
"helm.sh/helm/v4/internal/plugin/schema"
)
func TestValidPluginName(t *testing.T) {
validNames := map[string]string{
"lowercase": "myplugin",
"uppercase": "MYPLUGIN",
"mixed case": "MyPlugin",
"with digits": "plugin123",
"with hyphen": "my-plugin",
"with underscore": "my_plugin",
"mixed chars": "my-awesome_plugin_123",
}
for name, pluginName := range validNames {
t.Run("valid/"+name, func(t *testing.T) {
if !validPluginName.MatchString(pluginName) {
t.Errorf("expected %q to match validPluginName regex", pluginName)
}
})
}
invalidNames := map[string]string{
"empty": "",
"space": "my plugin",
"colon": "plugin:",
"period": "my.plugin",
"slash": "my/plugin",
"dollar": "$plugin",
"unicode": "plügîn",
}
for name, pluginName := range invalidNames {
t.Run("invalid/"+name, func(t *testing.T) {
if validPluginName.MatchString(pluginName) {
t.Errorf("expected %q to not match validPluginName regex", pluginName)
}
})
}
}
func mockSubprocessCLIPlugin(t *testing.T, pluginName string) *SubprocessPluginRuntime {
t.Helper()

@ -63,7 +63,7 @@ func ExtractTgzPluginMetadata(r io.Reader) (*Metadata, error) {
tr := tar.NewReader(gzr)
for {
header, err := tr.Next()
if err == io.EOF {
if errors.Is(err, io.EOF) {
break
}
if err != nil {

@ -82,15 +82,16 @@ func PrepareCommands(cmds []PlatformCommand, expandArgs bool, extraArgs []string
if len(cmdParts) == 0 || cmdParts[0] == "" {
return "", nil, fmt.Errorf("no plugin command is applicable")
}
main := os.Expand(cmdParts[0], func(key string) string {
envMappingFunc := func(key string) string {
return env[key]
})
}
main := os.Expand(cmdParts[0], envMappingFunc)
baseArgs := []string{}
if len(cmdParts) > 1 {
for _, cmdPart := range cmdParts[1:] {
if expandArgs {
baseArgs = append(baseArgs, os.ExpandEnv(cmdPart))
baseArgs = append(baseArgs, os.Expand(cmdPart, envMappingFunc))
} else {
baseArgs = append(baseArgs, cmdPart)
}
@ -99,7 +100,7 @@ func PrepareCommands(cmds []PlatformCommand, expandArgs bool, extraArgs []string
for _, arg := range args {
if expandArgs {
baseArgs = append(baseArgs, os.ExpandEnv(arg))
baseArgs = append(baseArgs, os.Expand(arg, envMappingFunc))
} else {
baseArgs = append(baseArgs, arg)
}

@ -224,16 +224,19 @@ func TestPrepareCommandsNoCommands(t *testing.T) {
}
func TestPrepareCommandsExpand(t *testing.T) {
t.Setenv("TEST", "test")
cmdMain := "sh"
cmdArgs := []string{"-c", "echo \"${TEST}\""}
cmdArgs := []string{"-c", "echo \"${TESTX}${TESTY}\""}
cmds := []PlatformCommand{
{OperatingSystem: "", Architecture: "", Command: cmdMain, Args: cmdArgs},
}
expectedArgs := []string{"-c", "echo \"test\""}
expectedArgs := []string{"-c", "echo \"testxtesty\""}
env := map[string]string{
"TESTX": "testx",
"TESTY": "testy",
}
env := map[string]string{}
cmd, args, err := PrepareCommands(cmds, true, []string{}, env)
if err != nil {
t.Fatal(err)
@ -247,14 +250,16 @@ func TestPrepareCommandsExpand(t *testing.T) {
}
func TestPrepareCommandsNoExpand(t *testing.T) {
t.Setenv("TEST", "test")
cmdMain := "sh"
cmdArgs := []string{"-c", "echo \"${TEST}\""}
cmds := []PlatformCommand{
{OperatingSystem: "", Architecture: "", Command: cmdMain, Args: cmdArgs},
}
env := map[string]string{}
env := map[string]string{
"TEST": "test",
}
cmd, args, err := PrepareCommands(cmds, false, []string{}, env)
if err != nil {
t.Fatal(err)

@ -1,9 +1,9 @@
#!/bin/bash
#!/usr/bin/env sh
echo "Hello from a Helm plugin"
echo "PARAMS"
echo $*
echo "$@"
$HELM_BIN ls --all

@ -1,9 +1,9 @@
#!/bin/bash
#!/usr/bin/env sh
echo "Hello from a Helm plugin"
echo "PARAMS"
echo $*
echo "$@"
$HELM_BIN ls --all

@ -86,19 +86,19 @@ func podConditions(u *unstructured.Unstructured) (*status.Result, error) {
},
},
}, nil
}
message := "Pod in progress"
return &status.Result{
Status: status.InProgressStatus,
Message: message,
Conditions: []status.Condition{
{
Type: status.ConditionReconciling,
Status: corev1.ConditionTrue,
Reason: "PodInProgress",
Message: message,
default:
message := "Pod in progress"
return &status.Result{
Status: status.InProgressStatus,
Message: message,
Conditions: []status.Condition{
{
Type: status.ConditionReconciling,
Status: corev1.ConditionTrue,
Reason: "PodInProgress",
Message: message,
},
},
},
}, nil
}, nil
}
}

@ -26,16 +26,16 @@ import (
)
type TLSConfigOptions struct {
insecureSkipTLSverify bool
insecureSkipTLSVerify bool
certPEMBlock, keyPEMBlock []byte
caPEMBlock []byte
}
type TLSConfigOption func(options *TLSConfigOptions) error
func WithInsecureSkipVerify(insecureSkipTLSverify bool) TLSConfigOption {
func WithInsecureSkipVerify(insecureSkipTLSVerify bool) TLSConfigOption {
return func(options *TLSConfigOptions) error {
options.insecureSkipTLSverify = insecureSkipTLSverify
options.insecureSkipTLSVerify = insecureSkipTLSVerify
return nil
}
@ -97,7 +97,7 @@ func NewTLSConfig(options ...TLSConfigOption) (*tls.Config, error) {
}
config := tls.Config{
InsecureSkipVerify: to.insecureSkipTLSverify,
InsecureSkipVerify: to.insecureSkipTLSVerify,
}
if len(to.certPEMBlock) > 0 && len(to.keyPEMBlock) > 0 {

@ -42,11 +42,11 @@ func TestNewTLSConfig(t *testing.T) {
certFile := testfile(t, testCertFile)
keyFile := testfile(t, testKeyFile)
caCertFile := testfile(t, testCaCertFile)
insecureSkipTLSverify := false
insecureSkipTLSVerify := false
{
cfg, err := NewTLSConfig(
WithInsecureSkipVerify(insecureSkipTLSverify),
WithInsecureSkipVerify(insecureSkipTLSVerify),
WithCertKeyPairFiles(certFile, keyFile),
WithCAFile(caCertFile),
)
@ -66,7 +66,7 @@ func TestNewTLSConfig(t *testing.T) {
}
{
cfg, err := NewTLSConfig(
WithInsecureSkipVerify(insecureSkipTLSverify),
WithInsecureSkipVerify(insecureSkipTLSVerify),
WithCAFile(caCertFile),
)
if err != nil {
@ -86,7 +86,7 @@ func TestNewTLSConfig(t *testing.T) {
{
cfg, err := NewTLSConfig(
WithInsecureSkipVerify(insecureSkipTLSverify),
WithInsecureSkipVerify(insecureSkipTLSVerify),
WithCertKeyPairFiles(certFile, keyFile),
)
if err != nil {

@ -0,0 +1,44 @@
/*
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 version
import (
"fmt"
"runtime/debug"
"slices"
_ "k8s.io/client-go/kubernetes" // Force k8s.io/client-go to be included in the build
)
func K8sIOClientGoModVersion() (string, error) {
info, ok := debug.ReadBuildInfo()
if !ok {
return "", fmt.Errorf("failed to read build info")
}
idx := slices.IndexFunc(info.Deps, func(m *debug.Module) bool {
return m.Path == "k8s.io/client-go"
})
if idx == -1 {
return "", fmt.Errorf("k8s.io/client-go not found in build info")
}
m := info.Deps[idx]
return m.Version, nil
}

@ -0,0 +1,30 @@
/*
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 version
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestK8sClientGoModVersion(t *testing.T) {
// Unfortunately, test builds don't include debug info / module info
// So we expect "K8sIOClientGoModVersion" to return error
_, err := K8sIOClientGoModVersion()
require.ErrorContains(t, err, "k8s.io/client-go not found in build info")
}

@ -14,13 +14,17 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package version // import "helm.sh/helm/v4/internal/version"
package version
import (
"flag"
"fmt"
"log/slog"
"runtime"
"strings"
"testing"
"github.com/Masterminds/semver/v3"
)
var (
@ -30,7 +34,7 @@ var (
//
// Increment major number for new feature additions and behavioral changes.
// Increment minor number for bug fixes and performance enhancements.
version = "v4.0"
version = "v4.1"
// metadata is extra build time data
metadata = ""
@ -38,11 +42,10 @@ var (
gitCommit = ""
// gitTreeState is the state of the git tree
gitTreeState = ""
)
// The Kubernetes version can be set by LDFLAGS. In order to do that the value
// must be a string.
kubeClientVersionMajor = ""
kubeClientVersionMinor = ""
const (
kubeClientGoVersionTesting = "v1.20"
)
// BuildInfo describes the compile time information.
@ -74,12 +77,39 @@ func GetUserAgent() string {
// Get returns build info
func Get() BuildInfo {
makeKubeClientVersionString := func() string {
// Test builds don't include debug info / module info
// (And even if they did, we probably want a stable version during tests anyway)
// Return a default value for test builds
if testing.Testing() {
return kubeClientGoVersionTesting
}
vstr, err := K8sIOClientGoModVersion()
if err != nil {
slog.Error("failed to retrieve k8s.io/client-go version", slog.Any("error", err))
return ""
}
v, err := semver.NewVersion(vstr)
if err != nil {
slog.Error("unable to parse k8s.io/client-go version", slog.String("version", vstr), slog.Any("error", err))
return ""
}
kubeClientVersionMajor := v.Major() + 1
kubeClientVersionMinor := v.Minor()
return fmt.Sprintf("v%d.%d", kubeClientVersionMajor, kubeClientVersionMinor)
}
v := BuildInfo{
Version: GetVersion(),
GitCommit: gitCommit,
GitTreeState: gitTreeState,
GoVersion: runtime.Version(),
KubeClientVersion: fmt.Sprintf("v%s.%s", kubeClientVersionMajor, kubeClientVersionMinor),
KubeClientVersion: makeKubeClientVersionString(),
}
// HACK(bacongobbler): strip out GoVersion during a test run for consistent test output

@ -230,7 +230,7 @@ func (cfg *Configuration) renderResources(ch *chart.Chart, values common.Values,
if ch.Metadata.KubeVersion != "" {
if !chartutil.IsCompatibleRange(ch.Metadata.KubeVersion, caps.KubeVersion.String()) {
return hs, b, "", fmt.Errorf("chart requires kubeVersion: %s which is incompatible with Kubernetes %s", ch.Metadata.KubeVersion, caps.KubeVersion.String())
return hs, b, "", fmt.Errorf("chart requires kubeVersion: %s which is incompatible with Kubernetes %s", ch.Metadata.KubeVersion, caps.KubeVersion.Version)
}
}
@ -502,7 +502,12 @@ func GetVersionSet(client discovery.ServerResourcesInterface) (common.VersionSet
// recordRelease with an update operation in case reuse has been set.
func (cfg *Configuration) recordRelease(r *release.Release) {
if err := cfg.Releases.Update(r); err != nil {
cfg.Logger().Warn("failed to update release", "name", r.Name, "revision", r.Version, slog.Any("error", err))
cfg.Logger().Warn(
"failed to update release",
slog.String("name", r.Name),
slog.Int("revision", r.Version),
slog.Any("error", err),
)
}
}

@ -43,7 +43,7 @@ type Dependency struct {
CertFile string
KeyFile string
CaFile string
InsecureSkipTLSverify bool
InsecureSkipTLSVerify bool
PlainHTTP bool
}

@ -410,3 +410,35 @@ data:
})
}
}
func TestConfiguration_hookSetDeletePolicy(t *testing.T) {
tests := map[string]struct {
policies []release.HookDeletePolicy
expected []release.HookDeletePolicy
}{
"no polices specified result in the default policy": {
policies: nil,
expected: []release.HookDeletePolicy{
release.HookBeforeHookCreation,
},
},
"unknown policy is untouched": {
policies: []release.HookDeletePolicy{
release.HookDeletePolicy("never"),
},
expected: []release.HookDeletePolicy{
release.HookDeletePolicy("never"),
},
},
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
cfg := &Configuration{}
h := &release.Hook{
DeletePolicies: tt.policies,
}
cfg.hookSetDeletePolicy(h)
assert.Equal(t, tt.expected, h.DeletePolicies)
})
}
}

@ -62,8 +62,8 @@ import (
"helm.sh/helm/v4/pkg/storage/driver"
)
// notesFileSuffix that we want to treat special. It goes through the templating engine
// but it's not a yaml file (resource) hence can't have hooks, etc. And the user actually
// notesFileSuffix that we want to treat specially. It goes through the templating engine
// but it's not a YAML file (resource) hence can't have hooks, etc. And the user actually
// wants to see this file after rendering in the status command. However, it must be a suffix
// since there can be filepath in front of it.
const notesFileSuffix = "NOTES.txt"
@ -139,7 +139,7 @@ type ChartPathOptions struct {
CaFile string // --ca-file
CertFile string // --cert-file
KeyFile string // --key-file
InsecureSkipTLSverify bool // --insecure-skip-verify
InsecureSkipTLSVerify bool // --insecure-skip-verify
PlainHTTP bool // --plain-http
Keyring string // --keyring
Password string // --password
@ -419,6 +419,7 @@ func (i *Install) RunWithContext(ctx context.Context, ch ci.Charter, vals map[st
if err != nil {
return nil, err
}
if _, err := i.cfg.KubeClient.Create(
resourceList,
kube.ClientCreateOptionServerSideApply(i.ServerSideApply, false)); err != nil && !apierrors.IsAlreadyExists(err) {
@ -886,7 +887,7 @@ func (c *ChartPathOptions) LocateChart(name string, settings *cli.EnvSettings) (
Options: []getter.Option{
getter.WithPassCredentialsAll(c.PassCredentialsAll),
getter.WithTLSClientConfig(c.CertFile, c.KeyFile, c.CaFile),
getter.WithInsecureSkipVerifyTLS(c.InsecureSkipTLSverify),
getter.WithInsecureSkipVerifyTLS(c.InsecureSkipTLSVerify),
getter.WithPlainHTTP(c.PlainHTTP),
getter.WithBasicAuth(c.Username, c.Password),
},
@ -911,7 +912,7 @@ func (c *ChartPathOptions) LocateChart(name string, settings *cli.EnvSettings) (
repo.WithChartVersion(version),
repo.WithClientTLS(c.CertFile, c.KeyFile, c.CaFile),
repo.WithUsernamePassword(c.Username, c.Password),
repo.WithInsecureSkipTLSverify(c.InsecureSkipTLSverify),
repo.WithInsecureSkipTLSVerify(c.InsecureSkipTLSVerify),
repo.WithPassCredentialsAll(c.PassCredentialsAll),
)
if err != nil {

@ -572,7 +572,7 @@ func TestInstallRelease_KubeVersion(t *testing.T) {
vals = map[string]interface{}{}
_, err = instAction.Run(buildChart(withKube(">=99.0.0")), vals)
is.Error(err)
is.Contains(err.Error(), "chart requires kubeVersion")
is.Contains(err.Error(), "chart requires kubeVersion: >=99.0.0 which is incompatible with Kubernetes v1.20.")
}
func TestInstallRelease_Wait(t *testing.T) {

@ -57,7 +57,7 @@ type Package struct {
CertFile string
KeyFile string
CaFile string
InsecureSkipTLSverify bool
InsecureSkipTLSVerify bool
}
const (
@ -103,7 +103,7 @@ func (p *Package) Run(path string, _ map[string]interface{}) (string, error) {
ch.Metadata.AppVersion = p.AppVersion
}
if reqs := ac.MetaDependencies(); reqs != nil {
if reqs := ac.MetaDependencies(); len(reqs) > 0 {
if err := CheckDependencies(ch, reqs); err != nil {
return "", err
}

@ -93,7 +93,7 @@ func TestPassphraseFileFetcher_WithStdinAndMultipleFetches(t *testing.T) {
w.Write([]byte(passphrase + "\n"))
}()
for i := 0; i < 4; i++ {
for range 4 {
fetcher, err := testPkg.passphraseFileFetcher("-", stdin)
if err != nil {
t.Errorf("Expected passphraseFileFetcher to not return an error, but got %v", err)

@ -82,7 +82,7 @@ func (p *Pull) Run(chartRef string) (string, error) {
getter.WithBasicAuth(p.Username, p.Password),
getter.WithPassCredentialsAll(p.PassCredentialsAll),
getter.WithTLSClientConfig(p.CertFile, p.KeyFile, p.CaFile),
getter.WithInsecureSkipVerifyTLS(p.InsecureSkipTLSverify),
getter.WithInsecureSkipVerifyTLS(p.InsecureSkipTLSVerify),
getter.WithPlainHTTP(p.PlainHTTP),
},
RegistryClient: p.cfg.RegistryClient,
@ -124,7 +124,7 @@ func (p *Pull) Run(chartRef string) (string, error) {
repo.WithChartVersion(p.Version),
repo.WithClientTLS(p.CertFile, p.KeyFile, p.CaFile),
repo.WithUsernamePassword(p.Username, p.Password),
repo.WithInsecureSkipTLSverify(p.InsecureSkipTLSverify),
repo.WithInsecureSkipTLSVerify(p.InsecureSkipTLSVerify),
repo.WithPassCredentialsAll(p.PassCredentialsAll),
)
if err != nil {

@ -35,7 +35,7 @@ type Push struct {
certFile string
keyFile string
caFile string
insecureSkipTLSverify bool
insecureSkipTLSVerify bool
plainHTTP bool
out io.Writer
}
@ -62,7 +62,7 @@ func WithTLSClientConfig(certFile, keyFile, caFile string) PushOpt {
// WithInsecureSkipTLSVerify determines if a TLS Certificate will be checked
func WithInsecureSkipTLSVerify(insecureSkipTLSVerify bool) PushOpt {
return func(p *Push) {
p.insecureSkipTLSverify = insecureSkipTLSVerify
p.insecureSkipTLSVerify = insecureSkipTLSVerify
}
}
@ -98,7 +98,7 @@ func (p *Push) Run(chartRef string, remote string) (string, error) {
Pushers: pusher.All(p.Settings),
Options: []pusher.Option{
pusher.WithTLSClientConfig(p.certFile, p.keyFile, p.caFile),
pusher.WithInsecureSkipTLSVerify(p.insecureSkipTLSverify),
pusher.WithInsecureSkipTLSVerify(p.insecureSkipTLSVerify),
pusher.WithPlainHTTP(p.plainHTTP),
},
}

@ -18,8 +18,8 @@ package action
import (
"bytes"
"errors"
"fmt"
"strings"
"time"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@ -28,6 +28,7 @@ import (
"helm.sh/helm/v4/pkg/kube"
"helm.sh/helm/v4/pkg/release/common"
release "helm.sh/helm/v4/pkg/release/v1"
"helm.sh/helm/v4/pkg/storage/driver"
)
// Rollback is the action for rolling back to a given release.
@ -278,7 +279,7 @@ func (r *Rollback) performRollback(currentRelease, targetRelease *release.Releas
}
deployed, err := r.cfg.Releases.DeployedAll(currentRelease.Name)
if err != nil && !strings.Contains(err.Error(), "has no deployed releases") {
if err != nil && !errors.Is(err, driver.ErrNoDeployedReleases) {
return nil, err
}
// Supersede all previous deployments, see issue #2941.

@ -188,6 +188,25 @@ func (u *Uninstall) Run(name string) (*releasei.UninstallReleaseResponse, error)
u.cfg.Logger().Debug("uninstall: Failed to store updated release", slog.Any("error", err))
}
// Supersede all previous deployments, see issue #12556 (which is a
// variation on #2941).
deployed, err := u.cfg.Releases.DeployedAll(name)
if err != nil && !errors.Is(err, driver.ErrNoDeployedReleases) {
return nil, err
}
for _, reli := range deployed {
rel, err := releaserToV1Release(reli)
if err != nil {
return nil, err
}
u.cfg.Logger().Debug("superseding previous deployment", "version", rel.Version)
rel.Info.Status = common.StatusSuperseded
if err := u.cfg.Releases.Update(rel); err != nil {
u.cfg.Logger().Debug("uninstall: Failed to store updated release", slog.Any("error", err))
}
}
if len(errs) > 0 {
return res, fmt.Errorf("uninstallation completed with %d error(s): %w", len(errs), joinErrors(errs, "; "))
}
@ -242,9 +261,9 @@ func (u *Uninstall) deleteRelease(rel *release.Release) (kube.ResourceList, stri
}
filesToKeep, filesToDelete := filterManifestsToKeep(files)
var kept string
var kept strings.Builder
for _, f := range filesToKeep {
kept += "[" + f.Head.Kind + "] " + f.Head.Metadata.Name + "\n"
fmt.Fprintf(&kept, "[%s] %s\n", f.Head.Kind, f.Head.Metadata.Name)
}
var builder strings.Builder
@ -259,7 +278,7 @@ func (u *Uninstall) deleteRelease(rel *release.Release) (kube.ResourceList, stri
if len(resources) > 0 {
_, errs = u.cfg.KubeClient.Delete(resources, parseCascadingFlag(u.DeletionPropagation))
}
return resources, kept, errs
return resources, kept.String(), errs
}
func parseCascadingFlag(cascadingFlag string) v1.DeletionPropagation {

@ -515,7 +515,11 @@ func (u *Upgrade) releasingUpgrade(c chan<- resultMessage, upgradedRelease *rele
func (u *Upgrade) failRelease(rel *release.Release, created kube.ResourceList, err error) (*release.Release, error) {
msg := fmt.Sprintf("Upgrade %q failed: %s", rel.Name, err)
u.cfg.Logger().Warn("upgrade failed", "name", rel.Name, slog.Any("error", err))
u.cfg.Logger().Warn(
"upgrade failed",
slog.String("name", rel.Name),
slog.Any("error", err),
)
rel.Info.Status = rcommon.StatusFailed
rel.Info.Description = msg

@ -55,7 +55,8 @@ func requireAdoption(resources kube.ResourceList) (kube.ResourceList, error) {
return fmt.Errorf("could not get information about the resource %s: %w", resourceString(info), err)
}
requireUpdate.Append(info)
infoCopy := *info
requireUpdate.Append(&infoCopy)
return nil
})
@ -84,7 +85,8 @@ func existingResourceConflict(resources kube.ResourceList, releaseName, releaseN
return fmt.Errorf("%s exists and cannot be imported into the current release: %s", resourceString(info), err)
}
requireUpdate.Append(info)
infoCopy := *info
requireUpdate.Append(&infoCopy)
return nil
})

@ -132,6 +132,7 @@ func TestRequireAdoption(t *testing.T) {
assert.NoError(t, err)
assert.Len(t, found, 1)
assert.Equal(t, found[0], existing)
assert.NotSame(t, found[0], existing)
}
func TestExistingResourceConflict(t *testing.T) {
@ -156,6 +157,7 @@ func TestExistingResourceConflict(t *testing.T) {
assert.NoError(t, err)
assert.Len(t, found, 1)
assert.Equal(t, found[0], existing)
assert.NotSame(t, found[0], existing)
// Verify that an existing resource that lacks labels/annotations results in an error
resources = append(resources, conflict)

@ -19,7 +19,10 @@ import (
"fmt"
"slices"
"strconv"
"strings"
"testing"
"github.com/Masterminds/semver/v3"
"k8s.io/client-go/kubernetes/scheme"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
@ -29,25 +32,23 @@ import (
helmversion "helm.sh/helm/v4/internal/version"
)
var (
// The Kubernetes version can be set by LDFLAGS. In order to do that the value
// must be a string.
k8sVersionMajor = "1"
k8sVersionMinor = "20"
const (
kubeVersionMajorTesting = 1
kubeVersionMinorTesting = 20
)
var (
// DefaultVersionSet is the default version set, which includes only Core V1 ("v1").
DefaultVersionSet = allKnownVersions()
// DefaultCapabilities is the default set of capabilities.
DefaultCapabilities = &Capabilities{
KubeVersion: KubeVersion{
Version: fmt.Sprintf("v%s.%s.0", k8sVersionMajor, k8sVersionMinor),
Major: k8sVersionMajor,
Minor: k8sVersionMinor,
},
APIVersions: DefaultVersionSet,
HelmVersion: helmversion.Get(),
}
DefaultCapabilities = func() *Capabilities {
caps, err := makeDefaultCapabilities()
if err != nil {
panic(fmt.Sprintf("failed to create default capabilities: %v", err))
}
return caps
}()
)
// Capabilities describes the capabilities of the Kubernetes cluster.
@ -70,15 +71,22 @@ func (capabilities *Capabilities) Copy() *Capabilities {
// KubeVersion is the Kubernetes version.
type KubeVersion struct {
Version string // Kubernetes version
Major string // Kubernetes major version
Minor string // Kubernetes minor version
Version string // Full version (e.g., v1.33.4-gke.1245000)
normalizedVersion string // Normalized for constraint checking (e.g., v1.33.4)
Major string // Kubernetes major version
Minor string // Kubernetes minor version
}
// String implements fmt.Stringer
func (kv *KubeVersion) String() string { return kv.Version }
// String implements fmt.Stringer.
// Returns the normalized version used for constraint checking.
func (kv *KubeVersion) String() string {
if kv.normalizedVersion != "" {
return kv.normalizedVersion
}
return kv.Version
}
// GitVersion returns the Kubernetes version string.
// GitVersion returns the full Kubernetes version string.
//
// Deprecated: use KubeVersion.Version.
func (kv *KubeVersion) GitVersion() string { return kv.Version }
@ -91,10 +99,21 @@ func ParseKubeVersion(version string) (*KubeVersion, error) {
if err != nil {
return nil, err
}
// Preserve original input (e.g., v1.33.4-gke.1245000)
gitVersion := version
if !strings.HasPrefix(version, "v") {
gitVersion = "v" + version
}
// Normalize for constraint checking (strips all suffixes)
normalizedVer := "v" + sv.String()
return &KubeVersion{
Version: "v" + sv.String(),
Major: strconv.FormatUint(uint64(sv.Major()), 10),
Minor: strconv.FormatUint(uint64(sv.Minor()), 10),
Version: gitVersion,
normalizedVersion: normalizedVer,
Major: strconv.FormatUint(uint64(sv.Major()), 10),
Minor: strconv.FormatUint(uint64(sv.Minor()), 10),
}, nil
}
@ -122,3 +141,42 @@ func allKnownVersions() VersionSet {
}
return vs
}
func makeDefaultCapabilities() (*Capabilities, error) {
// Test builds don't include debug info / module info
// (And even if they did, we probably want stable capabilities for tests anyway)
// Return a default value for test builds
if testing.Testing() {
return newCapabilities(kubeVersionMajorTesting, kubeVersionMinorTesting)
}
vstr, err := helmversion.K8sIOClientGoModVersion()
if err != nil {
return nil, fmt.Errorf("failed to retrieve k8s.io/client-go version: %w", err)
}
v, err := semver.NewVersion(vstr)
if err != nil {
return nil, fmt.Errorf("unable to parse k8s.io/client-go version %q: %v", vstr, err)
}
kubeVersionMajor := v.Major() + 1
kubeVersionMinor := v.Minor()
return newCapabilities(kubeVersionMajor, kubeVersionMinor)
}
func newCapabilities(kubeVersionMajor, kubeVersionMinor uint64) (*Capabilities, error) {
version := fmt.Sprintf("v%d.%d.0", kubeVersionMajor, kubeVersionMinor)
return &Capabilities{
KubeVersion: KubeVersion{
Version: version,
normalizedVersion: version,
Major: fmt.Sprintf("%d", kubeVersionMajor),
Minor: fmt.Sprintf("%d", kubeVersionMinor),
},
APIVersions: DefaultVersionSet,
HelmVersion: helmversion.Get(),
}, nil
}

@ -41,7 +41,8 @@ func TestDefaultVersionSet(t *testing.T) {
}
func TestDefaultCapabilities(t *testing.T) {
kv := DefaultCapabilities.KubeVersion
caps := DefaultCapabilities
kv := caps.KubeVersion
if kv.String() != "v1.20.0" {
t.Errorf("Expected default KubeVersion.String() to be v1.20.0, got %q", kv.String())
}
@ -57,13 +58,10 @@ func TestDefaultCapabilities(t *testing.T) {
if kv.Minor != "20" {
t.Errorf("Expected default KubeVersion.Minor to be 20, got %q", kv.Minor)
}
}
func TestDefaultCapabilitiesHelmVersion(t *testing.T) {
hv := DefaultCapabilities.HelmVersion
if hv.Version != "v4.0" {
t.Errorf("Expected default HelmVersion to be v4.0, got %q", hv.Version)
hv := caps.HelmVersion
if hv.Version != "v4.1" {
t.Errorf("Expected default HelmVersion to be v4.1, got %q", hv.Version)
}
}
@ -83,18 +81,41 @@ func TestParseKubeVersion(t *testing.T) {
}
}
func TestParseKubeVersionSuffix(t *testing.T) {
kv, err := ParseKubeVersion("v1.28+")
if err != nil {
t.Errorf("Expected v1.28+ to parse successfully")
}
if kv.Version != "v1.28" {
t.Errorf("Expected parsed KubeVersion.Version to be v1.28, got %q", kv.String())
func TestParseKubeVersionWithVendorSuffixes(t *testing.T) {
tests := []struct {
name string
input string
wantVer string
wantString string
wantMajor string
wantMinor string
}{
{"GKE vendor suffix", "v1.33.4-gke.1245000", "v1.33.4-gke.1245000", "v1.33.4", "1", "33"},
{"GKE without v", "1.30.2-gke.1587003", "v1.30.2-gke.1587003", "v1.30.2", "1", "30"},
{"EKS trailing +", "v1.28+", "v1.28+", "v1.28", "1", "28"},
{"EKS + without v", "1.28+", "v1.28+", "v1.28", "1", "28"},
{"Standard version", "v1.31.0", "v1.31.0", "v1.31.0", "1", "31"},
{"Standard without v", "1.29.0", "v1.29.0", "v1.29.0", "1", "29"},
}
if kv.Major != "1" {
t.Errorf("Expected parsed KubeVersion.Major to be 1, got %q", kv.Major)
}
if kv.Minor != "28" {
t.Errorf("Expected parsed KubeVersion.Minor to be 28, got %q", kv.Minor)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
kv, err := ParseKubeVersion(tt.input)
if err != nil {
t.Fatalf("ParseKubeVersion() error = %v", err)
}
if kv.Version != tt.wantVer {
t.Errorf("Version = %q, want %q", kv.Version, tt.wantVer)
}
if kv.String() != tt.wantString {
t.Errorf("String() = %q, want %q", kv.String(), tt.wantString)
}
if kv.Major != tt.wantMajor {
t.Errorf("Major = %q, want %q", kv.Major, tt.wantMajor)
}
if kv.Minor != tt.wantMinor {
t.Errorf("Minor = %q, want %q", kv.Minor, tt.wantMinor)
}
})
}
}

@ -21,8 +21,7 @@ import (
"log"
"maps"
"github.com/mitchellh/copystructure"
"helm.sh/helm/v4/internal/copystructure"
chart "helm.sh/helm/v4/pkg/chart"
"helm.sh/helm/v4/pkg/chart/common"
)

@ -24,6 +24,7 @@ import (
"log/slog"
"net/http"
"strings"
"sync"
"time"
"github.com/santhosh-tekuri/jsonschema/v6"
@ -86,7 +87,7 @@ func ValidateAgainstSchema(ch chart.Charter, values map[string]interface{}) erro
sb.WriteString(err.Error())
}
}
slog.Debug("number of dependencies in the chart", "dependencies", len(chrt.Dependencies()))
slog.Debug("number of dependencies in the chart", "chart", chrt.Name(), "dependencies", len(chrt.Dependencies()))
// For each dependency, recursively call this function with the coalesced values
for _, subchart := range chrt.Dependencies() {
sub, err := chart.NewAccessor(subchart)
@ -142,6 +143,7 @@ func ValidateAgainstSingleSchema(values common.Values, schemaJSON []byte) (reter
"file": jsonschema.FileLoader{},
"http": newHTTPURLLoader(),
"https": newHTTPURLLoader(),
"urn": urnLoader{},
}
compiler := jsonschema.NewCompiler()
@ -164,6 +166,35 @@ func ValidateAgainstSingleSchema(values common.Values, schemaJSON []byte) (reter
return nil
}
// URNResolverFunc allows SDK to plug a URN resolver. It must return a
// schema document compatible with the validator (e.g., result of
// jsonschema.UnmarshalJSON).
type URNResolverFunc func(urn string) (any, error)
// URNResolver is the default resolver used by the URN loader. By default it
// returns a clear error.
var URNResolver URNResolverFunc = func(urn string) (any, error) {
return nil, fmt.Errorf("URN not resolved: %s", urn)
}
// urnLoader implements resolution for the urn: scheme by delegating to
// URNResolver. If unresolved, it logs a warning and returns a permissive
// boolean-true schema to avoid hard failures (back-compat behavior).
type urnLoader struct{}
// warnedURNs ensures we log the unresolved-URN warning only once per URN.
var warnedURNs sync.Map
func (l urnLoader) Load(urlStr string) (any, error) {
if doc, err := URNResolver(urlStr); err == nil && doc != nil {
return doc, nil
}
if _, loaded := warnedURNs.LoadOrStore(urlStr, struct{}{}); !loaded {
slog.Warn("unresolved URN reference ignored; using permissive schema", "urn", urlStr)
}
return jsonschema.UnmarshalJSON(strings.NewReader("true"))
}
// Note, JSONSchemaValidationError is used to wrap the error from the underlying
// validation package so that Helm has a clean interface and the validation package
// could be replaced without changing the Helm SDK API.

@ -287,6 +287,19 @@ func TestHTTPURLLoader_Load(t *testing.T) {
})
}
// Test that an unresolved URN $ref is soft-ignored and validation succeeds.
// it mimics the behavior of Helm 3.18.4
func TestValidateAgainstSingleSchema_UnresolvedURN_Ignored(t *testing.T) {
schema := []byte(`{
"$schema": "https://json-schema.org/draft-07/schema#",
"$ref": "urn:example:helm:schemas:v1:helm-schema-validation-conditions:v1/helmSchemaValidation-true"
}`)
vals := map[string]interface{}{"any": "value"}
if err := ValidateAgainstSingleSchema(vals, schema); err != nil {
t.Fatalf("expected no error when URN unresolved is ignored, got: %v", err)
}
}
// Non-regression tests for https://github.com/helm/helm/issues/31202
// Ensure ValidateAgainstSchema does not panic when:
// - subchart key is missing

@ -68,7 +68,7 @@ func LoadArchiveFiles(in io.Reader) ([]*BufferedFile, error) {
for {
b := bytes.NewBuffer(nil)
hd, err := tr.Next()
if err == io.EOF {
if errors.Is(err, io.EOF) {
break
}
if err != nil {

@ -20,6 +20,7 @@ import (
"compress/gzip"
"errors"
"fmt"
"io"
"os"
"path/filepath"
@ -130,8 +131,8 @@ func LoadFile(name string) (chart.Charter, error) {
files, err := archive.LoadArchiveFiles(raw)
if err != nil {
if err == gzip.ErrHeader {
return nil, fmt.Errorf("file '%s' does not appear to be a valid chart file (details: %s)", name, err)
if errors.Is(err, gzip.ErrHeader) {
return nil, fmt.Errorf("file '%s' does not appear to be a valid chart file (details: %w)", name, err)
}
return nil, errors.New("unable to load chart archive")
}
@ -156,6 +157,38 @@ func LoadFile(name string) (chart.Charter, error) {
return nil, errors.New("unable to detect chart version, no Chart.yaml found")
}
// LoadArchive loads from a reader containing a compressed tar archive.
func LoadArchive(in io.Reader) (chart.Charter, error) {
// Note: This function is for use by SDK users such as Flux.
files, err := archive.LoadArchiveFiles(in)
if err != nil {
if errors.Is(err, gzip.ErrHeader) {
return nil, fmt.Errorf("stream does not appear to be a valid chart file (details: %w)", err)
}
return nil, fmt.Errorf("unable to load chart archive: %w", err)
}
for _, f := range files {
if f.Name == "Chart.yaml" {
c := new(chartBase)
if err := yaml.Unmarshal(f.Data, c); err != nil {
return c, fmt.Errorf("cannot load Chart.yaml: %w", err)
}
switch c.APIVersion {
case c2.APIVersionV1, c2.APIVersionV2, "":
return c2load.LoadFiles(files)
case c3.APIVersionV3:
return c3load.LoadFiles(files)
default:
return nil, errors.New("unsupported chart version")
}
}
}
return nil, errors.New("unable to detect chart version, no Chart.yaml found")
}
// chartBase is used to detect the API Version for the chart to run it through the
// loader for that type.
type chartBase struct {

@ -0,0 +1,186 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package loader
import (
"archive/tar"
"bytes"
"compress/gzip"
"fmt"
"io"
"maps"
"path/filepath"
"strings"
"testing"
"time"
c3 "helm.sh/helm/v4/internal/chart/v3"
"helm.sh/helm/v4/pkg/chart"
c2 "helm.sh/helm/v4/pkg/chart/v2"
)
// createChartArchive is a helper function to create a gzipped tar archive in memory
func createChartArchive(t *testing.T, chartName, apiVersion string, extraFiles map[string][]byte, createChartYaml bool) io.Reader {
t.Helper()
var buf bytes.Buffer
gw := gzip.NewWriter(&buf)
tw := tar.NewWriter(gw)
files := make(map[string][]byte)
maps.Copy(files, extraFiles)
if createChartYaml {
chartYAMLContent := fmt.Sprintf(`apiVersion: %s
name: %s
version: 0.1.0
description: A test chart
`, apiVersion, chartName)
files["Chart.yaml"] = []byte(chartYAMLContent)
}
for name, data := range files {
header := &tar.Header{
Name: filepath.Join(chartName, name),
Mode: 0644,
Size: int64(len(data)),
ModTime: time.Now(),
}
if err := tw.WriteHeader(header); err != nil {
t.Fatalf("Failed to write tar header for %s: %v", name, err)
}
if _, err := tw.Write(data); err != nil {
t.Fatalf("Failed to write tar data for %s: %v", name, err)
}
}
if err := tw.Close(); err != nil {
t.Fatalf("Failed to close tar writer: %v", err)
}
if err := gw.Close(); err != nil {
t.Fatalf("Failed to close gzip writer: %v", err)
}
return &buf
}
func TestLoadArchive(t *testing.T) {
testCases := []struct {
name string
chartName string
apiVersion string
extraFiles map[string][]byte
inputReader io.Reader
expectedChart chart.Charter
expectedError string
createChartYaml bool
}{
{
name: "valid v2 chart archive",
chartName: "mychart-v2",
apiVersion: c2.APIVersionV2,
extraFiles: map[string][]byte{"templates/config.yaml": []byte("key: value")},
expectedChart: &c2.Chart{
Metadata: &c2.Metadata{APIVersion: c2.APIVersionV2, Name: "mychart-v2", Version: "0.1.0", Description: "A test chart"},
},
createChartYaml: true,
},
{
name: "valid v3 chart archive",
chartName: "mychart-v3",
apiVersion: c3.APIVersionV3,
extraFiles: map[string][]byte{"templates/config.yaml": []byte("key: value")},
expectedChart: &c3.Chart{
Metadata: &c3.Metadata{APIVersion: c3.APIVersionV3, Name: "mychart-v3", Version: "0.1.0", Description: "A test chart"},
},
createChartYaml: true,
},
{
name: "invalid gzip header",
inputReader: bytes.NewBufferString("not a gzip file"),
expectedError: "stream does not appear to be a valid chart file (details: gzip: invalid header)",
},
{
name: "archive without Chart.yaml",
chartName: "no-chart-yaml",
apiVersion: c2.APIVersionV2, // This will be ignored as Chart.yaml is missing
extraFiles: map[string][]byte{"values.yaml": []byte("foo: bar")},
expectedError: "unable to detect chart version, no Chart.yaml found",
createChartYaml: false,
},
{
name: "archive with malformed Chart.yaml",
chartName: "malformed-chart-yaml",
apiVersion: c2.APIVersionV2,
extraFiles: map[string][]byte{"Chart.yaml": []byte("apiVersion: v2\nname: mychart\nversion: 0.1.0\ndescription: A test chart\ninvalid: :")},
expectedError: "cannot load Chart.yaml: error converting YAML to JSON: yaml: line 5: mapping values are not allowed in this context",
createChartYaml: false,
},
{
name: "unsupported API version",
chartName: "unsupported-api",
apiVersion: "v99",
expectedError: "unsupported chart version",
createChartYaml: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
var reader io.Reader
if tc.inputReader != nil {
reader = tc.inputReader
} else {
reader = createChartArchive(t, tc.chartName, tc.apiVersion, tc.extraFiles, tc.createChartYaml)
}
loadedChart, err := LoadArchive(reader)
if tc.expectedError != "" {
if err == nil || !strings.Contains(err.Error(), tc.expectedError) {
t.Errorf("Expected error containing %q, but got %v", tc.expectedError, err)
}
return
}
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
lac, err := chart.NewAccessor(loadedChart)
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
eac, err := chart.NewAccessor(tc.expectedChart)
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
if lac.Name() != eac.Name() {
t.Errorf("Expected chart name %q, got %q", eac.Name(), lac.Name())
}
var loadedAPIVersion string
switch lc := loadedChart.(type) {
case *c2.Chart:
loadedAPIVersion = lc.Metadata.APIVersion
case *c3.Chart:
loadedAPIVersion = lc.Metadata.APIVersion
}
if loadedAPIVersion != tc.apiVersion {
t.Errorf("Expected API version %q, got %q", tc.apiVersion, loadedAPIVersion)
}
})
}
}

@ -179,16 +179,6 @@ func TestHelmCreateChart(t *testing.T) {
//
// Resources like hpa and ingress, which are disabled by default in values.yaml are enabled here using the equivalent
// of the `--set` flag.
//
// Note: This test requires the following ldflags to be set per the current Kubernetes version to avoid false-positive
// results.
// 1. -X helm.sh/helm/v4/pkg/lint/rules.k8sVersionMajor=<k8s-major-version>
// 2. -X helm.sh/helm/v4/pkg/lint/rules.k8sVersionMinor=<k8s-minor-version>
// or directly use '$(LDFLAGS)' in Makefile.
//
// When run without ldflags, the test passes giving a false-positive result. This is because the variables
// `k8sVersionMajor` and `k8sVersionMinor` by default are set to an older version of Kubernetes, with which, there
// might not be the deprecation warning.
func TestHelmCreateChart_CheckDeprecatedWarnings(t *testing.T) {
createdChart, err := chartutil.Create("checkdeprecatedwarnings", t.TempDir())
if err != nil {

@ -70,7 +70,7 @@ func Crds(linter *support.Linter) {
var yamlStruct *k8sYamlStruct
err := decoder.Decode(&yamlStruct)
if err == io.EOF {
if errors.Is(err, io.EOF) {
break
}
@ -80,8 +80,10 @@ func Crds(linter *support.Linter) {
return
}
linter.RunLinterRule(support.ErrorSev, fpath, validateCrdAPIVersion(yamlStruct))
linter.RunLinterRule(support.ErrorSev, fpath, validateCrdKind(yamlStruct))
if yamlStruct != nil {
linter.RunLinterRule(support.ErrorSev, fpath, validateCrdAPIVersion(yamlStruct))
linter.RunLinterRule(support.ErrorSev, fpath, validateCrdKind(yamlStruct))
}
}
}
}

@ -17,6 +17,8 @@ limitations under the License.
package rules
import (
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
@ -34,3 +36,31 @@ func TestInvalidCrdsDir(t *testing.T) {
assert.Len(t, res, 1)
assert.ErrorContains(t, res[0].Err, "not a directory")
}
// multi-document YAML with empty documents would panic
func TestCrdWithEmptyDocument(t *testing.T) {
chartDir := t.TempDir()
os.WriteFile(filepath.Join(chartDir, "Chart.yaml"), []byte(
`apiVersion: v1
name: test
version: 0.1.0
`), 0644)
// CRD with comments before --- (creates empty document)
crdsDir := filepath.Join(chartDir, "crds")
os.Mkdir(crdsDir, 0755)
os.WriteFile(filepath.Join(crdsDir, "test.yaml"), []byte(
`# Comments create empty document
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: test.example.io
`), 0644)
linter := support.Linter{ChartDir: chartDir}
Crds(&linter)
assert.Len(t, linter.Messages, 0)
}

@ -28,14 +28,6 @@ import (
kscheme "k8s.io/client-go/kubernetes/scheme"
)
var (
// This should be set in the Makefile based on the version of client-go being imported.
// These constants will be overwritten with LDFLAGS. The version components must be
// strings in order for LDFLAGS to set them.
k8sVersionMajor = "1"
k8sVersionMinor = "20"
)
// deprecatedAPIError indicates than an API is deprecated in Kubernetes
type deprecatedAPIError struct {
Deprecated string
@ -56,12 +48,8 @@ func validateNoDeprecations(resource *k8sYamlStruct, kubeVersion *common.KubeVer
return nil
}
majorVersion := k8sVersionMajor
minorVersion := k8sVersionMinor
if kubeVersion != nil {
majorVersion = kubeVersion.Major
minorVersion = kubeVersion.Minor
if kubeVersion == nil {
kubeVersion = &common.DefaultCapabilities.KubeVersion
}
runtimeObject, err := resourceToRuntimeObject(resource)
@ -73,16 +61,16 @@ func validateNoDeprecations(resource *k8sYamlStruct, kubeVersion *common.KubeVer
return err
}
major, err := strconv.Atoi(majorVersion)
kubeVersionMajor, err := strconv.Atoi(kubeVersion.Major)
if err != nil {
return err
}
minor, err := strconv.Atoi(minorVersion)
kubeVersionMinor, err := strconv.Atoi(kubeVersion.Minor)
if err != nil {
return err
}
if !deprecation.IsDeprecated(runtimeObject, major, minor) {
if !deprecation.IsDeprecated(runtimeObject, kubeVersionMajor, kubeVersionMinor) {
return nil
}
gvk := fmt.Sprintf("%s %s", resource.APIVersion, resource.Kind)

@ -180,7 +180,7 @@ func (t *templateLinter) Lint() {
var yamlStruct *k8sYamlStruct
err := decoder.Decode(&yamlStruct)
if err == io.EOF {
if errors.Is(err, io.EOF) {
break
}

@ -56,8 +56,8 @@ func LoadFile(name string) (*chart.Chart, error) {
c, err := LoadArchive(raw)
if err != nil {
if err == gzip.ErrHeader {
return nil, fmt.Errorf("file '%s' does not appear to be a valid chart file (details: %s)", name, err)
if errors.Is(err, gzip.ErrHeader) {
return nil, fmt.Errorf("file '%s' does not appear to be a valid chart file (details: %w)", name, err)
}
}
return c, err

@ -208,7 +208,7 @@ func LoadFiles(files []*archive.BufferedFile) (*chart.Chart, error) {
// LoadValues loads values from a reader.
//
// The reader is expected to contain one or more YAML documents, the values of which are merged.
// And the values can be either a chart's default values or a user-supplied values.
// And the values can be either a chart's default values or user-supplied values.
func LoadValues(data io.Reader) (map[string]interface{}, error) {
values := map[string]interface{}{}
reader := utilyaml.NewYAMLReader(bufio.NewReader(data))
@ -216,7 +216,7 @@ func LoadValues(data io.Reader) (map[string]interface{}, error) {
currentMap := map[string]interface{}{}
raw, err := reader.Read()
if err != nil {
if err == io.EOF {
if errors.Is(err, io.EOF) {
break
}
return nil, fmt.Errorf("error reading yaml document: %w", err)

@ -20,6 +20,7 @@ import (
"archive/tar"
"bytes"
"compress/gzip"
"errors"
"io"
"log"
"os"
@ -116,7 +117,7 @@ func TestBomTestData(t *testing.T) {
tr := tar.NewReader(unzipped)
for {
file, err := tr.Next()
if err == io.EOF {
if errors.Is(err, io.EOF) {
break
}
if err != nil {

@ -20,8 +20,7 @@ import (
"log/slog"
"strings"
"github.com/mitchellh/copystructure"
"helm.sh/helm/v4/internal/copystructure"
"helm.sh/helm/v4/pkg/chart/common"
"helm.sh/helm/v4/pkg/chart/common/util"
chart "helm.sh/helm/v4/pkg/chart/v2"
@ -281,7 +280,11 @@ func processImportValues(c *chart.Chart, merge bool) error {
// get child table
vv, err := cvals.Table(r.Name + "." + child)
if err != nil {
slog.Warn("ImportValues missing table from chart", "chart", r.Name, slog.Any("error", err))
slog.Warn(
"ImportValues missing table from chart",
slog.String("chart", r.Name),
slog.Any("error", err),
)
continue
}
// create value map from child to be merged into parent

@ -21,6 +21,7 @@ import (
"bytes"
"compress/gzip"
"crypto/sha256"
"errors"
"fmt"
"io"
"os"
@ -205,7 +206,7 @@ func retrieveAllHeadersFromTar(path string) ([]*tar.Header, error) {
headers := []*tar.Header{}
for {
hd, err := tr.Next()
if err == io.EOF {
if errors.Is(err, io.EOF) {
break
}

@ -130,7 +130,7 @@ func addDependencySubcommandFlags(f *pflag.FlagSet, client *action.Dependency) {
f.StringVar(&client.Password, "password", "", "chart repository password where to locate the requested chart")
f.StringVar(&client.CertFile, "cert-file", "", "identify HTTPS client using this SSL certificate file")
f.StringVar(&client.KeyFile, "key-file", "", "identify HTTPS client using this SSL key file")
f.BoolVar(&client.InsecureSkipTLSverify, "insecure-skip-tls-verify", false, "skip tls certificate checks for the chart download")
f.BoolVar(&client.InsecureSkipTLSVerify, "insecure-skip-tls-verify", false, "skip tls certificate checks for the chart download")
f.BoolVar(&client.PlainHTTP, "plain-http", false, "use insecure HTTP connections for the chart download")
f.StringVar(&client.CaFile, "ca-file", "", "verify certificates of HTTPS-enabled servers using this CA bundle")
}

@ -55,7 +55,7 @@ func newDependencyBuildCmd(out io.Writer) *cobra.Command {
chartpath = filepath.Clean(args[0])
}
registryClient, err := newRegistryClient(client.CertFile, client.KeyFile, client.CaFile,
client.InsecureSkipTLSverify, client.PlainHTTP, client.Username, client.Password)
client.InsecureSkipTLSVerify, client.PlainHTTP, client.Username, client.Password)
if err != nil {
return fmt.Errorf("missing registry client: %w", err)
}

@ -59,7 +59,7 @@ func newDependencyUpdateCmd(_ *action.Configuration, out io.Writer) *cobra.Comma
chartpath = filepath.Clean(args[0])
}
registryClient, err := newRegistryClient(client.CertFile, client.KeyFile, client.CaFile,
client.InsecureSkipTLSverify, client.PlainHTTP, client.Username, client.Password)
client.InsecureSkipTLSVerify, client.PlainHTTP, client.Username, client.Password)
if err != nil {
return fmt.Errorf("missing registry client: %w", err)
}

@ -59,7 +59,7 @@ func AddWaitFlag(cmd *cobra.Command, wait *kube.WaitStrategy) {
cmd.Flags().Var(
newWaitValue(kube.HookOnlyStrategy, wait),
"wait",
"if specified, will wait until all resources are in the expected state before marking the operation as successful. It will wait for as long as --timeout. Valid inputs are 'watcher' and 'legacy'",
"if specified, wait until resources are ready (up to --timeout). Values: 'watcher', 'hookOnly', and 'legacy'.",
)
// Sets the strategy to use the watcher strategy if `--wait` is used without an argument
cmd.Flags().Lookup("wait").NoOptDefVal = string(kube.StatusWatcherStrategy)
@ -81,7 +81,7 @@ func (ws *waitValue) String() string {
func (ws *waitValue) Set(s string) error {
switch s {
case string(kube.StatusWatcherStrategy), string(kube.LegacyStrategy):
case string(kube.StatusWatcherStrategy), string(kube.LegacyStrategy), string(kube.HookOnlyStrategy):
*ws = waitValue(s)
return nil
case "true":
@ -89,11 +89,11 @@ func (ws *waitValue) Set(s string) error {
*ws = waitValue(kube.StatusWatcherStrategy)
return nil
case "false":
slog.Warn("--wait=false is deprecated (boolean value) and can be replaced by omitting the --wait flag")
slog.Warn("--wait=false is deprecated (boolean value) and can be replaced with --wait=hookOnly")
*ws = waitValue(kube.HookOnlyStrategy)
return nil
default:
return fmt.Errorf("invalid wait input %q. Valid inputs are %s, and %s", s, kube.StatusWatcherStrategy, kube.LegacyStrategy)
return fmt.Errorf("invalid wait input %q. Valid inputs are %s, %s, and %s", s, kube.StatusWatcherStrategy, kube.HookOnlyStrategy, kube.LegacyStrategy)
}
}
@ -110,7 +110,7 @@ func addChartPathOptionsFlags(f *pflag.FlagSet, c *action.ChartPathOptions) {
f.StringVar(&c.Password, "password", "", "chart repository password where to locate the requested chart")
f.StringVar(&c.CertFile, "cert-file", "", "identify HTTPS client using this SSL certificate file")
f.StringVar(&c.KeyFile, "key-file", "", "identify HTTPS client using this SSL key file")
f.BoolVar(&c.InsecureSkipTLSverify, "insecure-skip-tls-verify", false, "skip tls certificate checks for the chart download")
f.BoolVar(&c.InsecureSkipTLSVerify, "insecure-skip-tls-verify", false, "skip tls certificate checks for the chart download")
f.BoolVar(&c.PlainHTTP, "plain-http", false, "use insecure HTTP connections for the chart download")
f.StringVar(&c.CaFile, "ca-file", "", "verify certificates of HTTPS-enabled servers using this CA bundle")
f.BoolVar(&c.PassCredentialsAll, "pass-credentials", false, "pass credentials to all domains")

@ -144,7 +144,7 @@ func newInstallCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
},
RunE: func(cmd *cobra.Command, args []string) error {
registryClient, err := newRegistryClient(client.CertFile, client.KeyFile, client.CaFile,
client.InsecureSkipTLSverify, client.PlainHTTP, client.Username, client.Password)
client.InsecureSkipTLSVerify, client.PlainHTTP, client.Username, client.Password)
if err != nil {
return fmt.Errorf("missing registry client: %w", err)
}
@ -275,7 +275,7 @@ func runInstall(args []string, client *action.Install, valueOpts *values.Options
slog.Warn("this chart is deprecated")
}
if req := ac.MetaDependencies(); req != nil {
if req := ac.MetaDependencies(); len(req) > 0 {
// If CheckDependencies returns an error, we have unfulfilled dependencies.
// As of Helm 2.4.0, this is treated as a stopping condition:
// https://github.com/helm/helm/issues/2209

@ -76,7 +76,7 @@ func TestLintCmdWithKubeVersionFlag(t *testing.T) {
golden: "output/lint-chart-with-deprecated-api-strict.txt",
wantError: true,
}, {
// the test builds will use the default k8sVersionMinor const in deprecations.go and capabilities.go
// the test builds will use the kubeVersionMinorTesting const in capabilities.go
// which is "20"
name: "lint chart with deprecated api version without kube version",
cmd: fmt.Sprintf("lint %s", testChart),

@ -20,7 +20,7 @@ import (
"context"
"fmt"
"io"
"log"
"log/slog"
"os"
"path/filepath"
"slices"
@ -63,7 +63,7 @@ func loadCLIPlugins(baseCmd *cobra.Command, out io.Writer) {
}
found, err := plugin.FindPlugins(dirs, descriptor)
if err != nil {
fmt.Fprintf(os.Stderr, "failed to load plugins: %s\n", err)
slog.Error("failed to load plugins", slog.String("error", err.Error()))
return
}
@ -263,9 +263,7 @@ func loadCompletionForPlugin(pluginCmd *cobra.Command, plug plugin.Plugin) {
if err != nil {
// The file could be missing or invalid. No static completion for this plugin.
if settings.Debug {
log.Output(2, fmt.Sprintf("[info] %s\n", err.Error()))
}
slog.Debug("plugin completion file loading", slog.String("error", err.Error()))
// Continue to setup dynamic completion.
cmds = &pluginCommand{}
}
@ -284,10 +282,7 @@ func addPluginCommands(plug plugin.Plugin, baseCmd *cobra.Command, cmds *pluginC
}
if len(cmds.Name) == 0 {
// Missing name for a command
if settings.Debug {
log.Output(2, fmt.Sprintf("[info] sub-command name field missing for %s", baseCmd.CommandPath()))
}
slog.Debug("sub-command name field missing", slog.String("commandPath", baseCmd.CommandPath()))
return
}

@ -76,12 +76,12 @@ func newPackageCmd(out io.Writer) *cobra.Command {
}
registryClient, err := newRegistryClient(client.CertFile, client.KeyFile, client.CaFile,
client.InsecureSkipTLSverify, client.PlainHTTP, client.Username, client.Password)
client.InsecureSkipTLSVerify, client.PlainHTTP, client.Username, client.Password)
if err != nil {
return fmt.Errorf("missing registry client: %w", err)
}
for i := 0; i < len(args); i++ {
for i := range args {
path, err := filepath.Abs(args[i])
if err != nil {
return err
@ -130,7 +130,7 @@ func newPackageCmd(out io.Writer) *cobra.Command {
f.StringVar(&client.Password, "password", "", "chart repository password where to locate the requested chart")
f.StringVar(&client.CertFile, "cert-file", "", "identify HTTPS client using this SSL certificate file")
f.StringVar(&client.KeyFile, "key-file", "", "identify HTTPS client using this SSL key file")
f.BoolVar(&client.InsecureSkipTLSverify, "insecure-skip-tls-verify", false, "skip tls certificate checks for the chart download")
f.BoolVar(&client.InsecureSkipTLSVerify, "insecure-skip-tls-verify", false, "skip tls certificate checks for the chart download")
f.BoolVar(&client.PlainHTTP, "plain-http", false, "use insecure HTTP connections for the chart download")
f.StringVar(&client.CaFile, "ca-file", "", "verify certificates of HTTPS-enabled servers using this CA bundle")

@ -40,7 +40,7 @@ type pluginInstallOptions struct {
certFile string
keyFile string
caFile string
insecureSkipTLSverify bool
insecureSkipTLSVerify bool
plainHTTP bool
password string
username string
@ -88,7 +88,7 @@ func newPluginInstallCmd(out io.Writer) *cobra.Command {
cmd.Flags().StringVar(&o.certFile, "cert-file", "", "identify registry client using this SSL certificate file")
cmd.Flags().StringVar(&o.keyFile, "key-file", "", "identify registry client using this SSL key file")
cmd.Flags().StringVar(&o.caFile, "ca-file", "", "verify certificates of HTTPS-enabled servers using this CA bundle")
cmd.Flags().BoolVar(&o.insecureSkipTLSverify, "insecure-skip-tls-verify", false, "skip tls certificate checks for the plugin download")
cmd.Flags().BoolVar(&o.insecureSkipTLSVerify, "insecure-skip-tls-verify", false, "skip tls certificate checks for the plugin download")
cmd.Flags().BoolVar(&o.plainHTTP, "plain-http", false, "use insecure HTTP connections for the plugin download")
cmd.Flags().StringVar(&o.username, "username", "", "registry username")
cmd.Flags().StringVar(&o.password, "password", "", "registry password")
@ -106,7 +106,7 @@ func (o *pluginInstallOptions) newInstallerForSource() (installer.Installer, err
// Build getter options for OCI
options := []getter.Option{
getter.WithTLSClientConfig(o.certFile, o.keyFile, o.caFile),
getter.WithInsecureSkipVerifyTLS(o.insecureSkipTLSverify),
getter.WithInsecureSkipVerifyTLS(o.insecureSkipTLSVerify),
getter.WithPlainHTTP(o.plainHTTP),
getter.WithBasicAuth(o.username, o.password),
}
@ -119,8 +119,6 @@ func (o *pluginInstallOptions) newInstallerForSource() (installer.Installer, err
}
func (o *pluginInstallOptions) run(out io.Writer) error {
installer.Debug = settings.Debug
i, err := o.newInstallerForSource()
if err != nil {
return err

@ -223,7 +223,7 @@ func TestLoadPluginsWithSpace(t *testing.T) {
t.Fatalf("Expected %d plugins, got %d", len(tests), len(plugins))
}
for i := 0; i < len(plugins); i++ {
for i := range plugins {
out.Reset()
tt := tests[i]
pp := plugins[i]

@ -61,7 +61,6 @@ func (o *pluginUpdateOptions) complete(args []string) error {
}
func (o *pluginUpdateOptions) run(out io.Writer) error {
installer.Debug = settings.Debug
slog.Debug("loading installed plugins", "path", settings.PluginsDirectory)
plugins, err := plugin.LoadAll(settings.PluginsDirectory)
if err != nil {

@ -66,13 +66,13 @@ func newPullCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
}
registryClient, err := newRegistryClient(client.CertFile, client.KeyFile, client.CaFile,
client.InsecureSkipTLSverify, client.PlainHTTP, client.Username, client.Password)
client.InsecureSkipTLSVerify, client.PlainHTTP, client.Username, client.Password)
if err != nil {
return fmt.Errorf("missing registry client: %w", err)
}
client.SetRegistryClient(registryClient)
for i := 0; i < len(args); i++ {
for i := range args {
output, err := client.Run(args[i])
if err != nil {
return err

@ -106,16 +106,16 @@ func TestPullCmd(t *testing.T) {
{
name: "Fetch untar when file with same name existed",
args: "test/test1 --untar --untardir test1",
existFile: "test1",
existFile: "test1/test1",
wantError: true,
wantErrorMsg: fmt.Sprintf("failed to untar: a file or directory with the name %s already exists", filepath.Join(srv.Root(), "test1")),
wantErrorMsg: fmt.Sprintf("failed to untar: a file or directory with the name %s already exists", filepath.Join(srv.Root(), "test1", "test1")),
},
{
name: "Fetch untar when dir with same name existed",
args: "test/test2 --untar --untardir test2",
existDir: "test2",
args: "test/test --untar --untardir test2",
existDir: "test2/test",
wantError: true,
wantErrorMsg: fmt.Sprintf("failed to untar: a file or directory with the name %s already exists", filepath.Join(srv.Root(), "test2")),
wantErrorMsg: fmt.Sprintf("failed to untar: a file or directory with the name %s already exists", filepath.Join(srv.Root(), "test2", "test")),
},
{
name: "Fetch, verify, untar",
@ -178,9 +178,10 @@ func TestPullCmd(t *testing.T) {
},
{
name: "OCI Fetch untar when dir with same name existed",
args: fmt.Sprintf("oci-test-chart oci://%s/u/ocitestuser/oci-dependent-chart --version 0.1.0 --untar --untardir ocitest2 --untar --untardir ocitest2", ociSrv.RegistryURL),
args: fmt.Sprintf("oci://%s/u/ocitestuser/oci-dependent-chart --version 0.1.0 --untar --untardir ocitest2", ociSrv.RegistryURL),
existDir: "ocitest2/oci-dependent-chart",
wantError: true,
wantErrorMsg: fmt.Sprintf("failed to untar: a file or directory with the name %s already exists", filepath.Join(srv.Root(), "ocitest2")),
wantErrorMsg: fmt.Sprintf("failed to untar: a file or directory with the name %s already exists", filepath.Join(srv.Root(), "ocitest2", "oci-dependent-chart")),
},
{
name: "Fail fetching non-existent OCI chart",
@ -189,10 +190,9 @@ func TestPullCmd(t *testing.T) {
wantError: true,
},
{
name: "Fail fetching OCI chart without version specified",
args: fmt.Sprintf("oci://%s/u/ocitestuser/nosuchthing", ociSrv.RegistryURL),
wantErrorMsg: "Error: --version flag is explicitly required for OCI registries",
wantError: true,
name: "Fail fetching OCI chart without version specified",
args: fmt.Sprintf("oci://%s/u/ocitestuser/nosuchthing", ociSrv.RegistryURL),
wantError: true,
},
{
name: "Fetching OCI chart without version option specified",
@ -207,7 +207,7 @@ func TestPullCmd(t *testing.T) {
{
name: "Fail fetching OCI chart with version mismatch",
args: fmt.Sprintf("oci://%s/u/ocitestuser/oci-dependent-chart:0.2.0 --version 0.1.0", ociSrv.RegistryURL),
wantErrorMsg: "Error: chart reference and version mismatch: 0.2.0 is not 0.1.0",
wantErrorMsg: "chart reference and version mismatch: 0.1.0 is not 0.2.0",
wantError: true,
},
}
@ -228,6 +228,9 @@ func TestPullCmd(t *testing.T) {
// Create file or Dir before helm pull --untar, see: https://github.com/helm/helm/issues/7182
if tt.existFile != "" {
file := filepath.Join(outdir, tt.existFile)
if err := os.MkdirAll(filepath.Dir(file), 0755); err != nil {
t.Fatal(err)
}
_, err := os.Create(file)
if err != nil {
t.Fatal(err)
@ -235,7 +238,7 @@ func TestPullCmd(t *testing.T) {
}
if tt.existDir != "" {
file := filepath.Join(outdir, tt.existDir)
err := os.Mkdir(file, 0755)
err := os.MkdirAll(file, 0755)
if err != nil {
t.Fatal(err)
}
@ -243,8 +246,8 @@ func TestPullCmd(t *testing.T) {
_, out, err := executeActionCommand(cmd)
if err != nil {
if tt.wantError {
if tt.wantErrorMsg != "" && tt.wantErrorMsg == err.Error() {
t.Fatalf("Actual error %s, not equal to expected error %s", err, tt.wantErrorMsg)
if tt.wantErrorMsg != "" && tt.wantErrorMsg != err.Error() {
t.Fatalf("Actual error '%s', not equal to expected error '%s'", err, tt.wantErrorMsg)
}
return
}
@ -303,16 +306,19 @@ func runPullTests(t *testing.T, tests []struct {
}
if tt.existDir != "" {
file := filepath.Join(outdir, tt.existDir)
err := os.Mkdir(file, 0755)
err := os.MkdirAll(file, 0755)
if err != nil {
t.Fatal(err)
}
}
_, _, err := executeActionCommand(cmd)
if tt.wantError && err == nil {
t.Fatalf("%q: expected error but got none", tt.name)
}
if err != nil {
if tt.wantError {
if tt.wantErrorMsg != "" && tt.wantErrorMsg == err.Error() {
t.Fatalf("Actual error %s, not equal to expected error %s", err, tt.wantErrorMsg)
if tt.wantErrorMsg != "" && tt.wantErrorMsg != err.Error() {
t.Fatalf("Actual error '%s', not equal to expected error '%s'", err, tt.wantErrorMsg)
}
return
}
@ -487,10 +493,9 @@ func TestPullWithCredentialsCmdOCIRegistry(t *testing.T) {
wantError: true,
},
{
name: "Fail fetching OCI chart without version specified",
args: buildOCIURL(ociSrv.RegistryURL, "nosuchthing", "", ociSrv.TestUsername, ociSrv.TestPassword),
wantErrorMsg: "Error: --version flag is explicitly required for OCI registries",
wantError: true,
name: "Fail fetching OCI chart without version specified",
args: buildOCIURL(ociSrv.RegistryURL, "nosuchthing", "", ociSrv.TestUsername, ociSrv.TestPassword),
wantError: true,
},
}

@ -38,7 +38,7 @@ type registryPushOptions struct {
certFile string
keyFile string
caFile string
insecureSkipTLSverify bool
insecureSkipTLSVerify bool
plainHTTP bool
password string
username string
@ -71,7 +71,7 @@ func newPushCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
},
RunE: func(_ *cobra.Command, args []string) error {
registryClient, err := newRegistryClient(
o.certFile, o.keyFile, o.caFile, o.insecureSkipTLSverify, o.plainHTTP, o.username, o.password,
o.certFile, o.keyFile, o.caFile, o.insecureSkipTLSVerify, o.plainHTTP, o.username, o.password,
)
if err != nil {
@ -82,7 +82,7 @@ func newPushCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
remote := args[1]
client := action.NewPushWithOpts(action.WithPushConfig(cfg),
action.WithTLSClientConfig(o.certFile, o.keyFile, o.caFile),
action.WithInsecureSkipTLSVerify(o.insecureSkipTLSverify),
action.WithInsecureSkipTLSVerify(o.insecureSkipTLSVerify),
action.WithPlainHTTP(o.plainHTTP),
action.WithPushOptWriter(out))
client.Settings = settings
@ -99,7 +99,7 @@ func newPushCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
f.StringVar(&o.certFile, "cert-file", "", "identify registry client using this SSL certificate file")
f.StringVar(&o.keyFile, "key-file", "", "identify registry client using this SSL key file")
f.StringVar(&o.caFile, "ca-file", "", "verify certificates of HTTPS-enabled servers using this CA bundle")
f.BoolVar(&o.insecureSkipTLSverify, "insecure-skip-tls-verify", false, "skip tls certificate checks for the chart upload")
f.BoolVar(&o.insecureSkipTLSVerify, "insecure-skip-tls-verify", false, "skip tls certificate checks for the chart upload")
f.BoolVar(&o.plainHTTP, "plain-http", false, "use insecure HTTP connections for the chart upload")
f.StringVar(&o.username, "username", "", "chart repository username where to locate the requested chart")
f.StringVar(&o.password, "password", "", "chart repository password where to locate the requested chart")

@ -57,7 +57,7 @@ type repoAddOptions struct {
certFile string
keyFile string
caFile string
insecureSkipTLSverify bool
insecureSkipTLSVerify bool
repoFile string
repoCache string
@ -94,7 +94,7 @@ func newRepoAddCmd(out io.Writer) *cobra.Command {
f.StringVar(&o.certFile, "cert-file", "", "identify HTTPS client using this SSL certificate file")
f.StringVar(&o.keyFile, "key-file", "", "identify HTTPS client using this SSL key file")
f.StringVar(&o.caFile, "ca-file", "", "verify certificates of HTTPS-enabled servers using this CA bundle")
f.BoolVar(&o.insecureSkipTLSverify, "insecure-skip-tls-verify", false, "skip tls certificate checks for the repository")
f.BoolVar(&o.insecureSkipTLSVerify, "insecure-skip-tls-verify", false, "skip tls certificate checks for the repository")
f.BoolVar(&o.allowDeprecatedRepos, "allow-deprecated-repos", false, "by default, this command will not allow adding official repos that have been permanently deleted. This disables that behavior")
f.BoolVar(&o.passCredentialsAll, "pass-credentials", false, "pass credentials to all domains")
f.DurationVar(&o.timeout, "timeout", getter.DefaultHTTPTimeout*time.Second, "time to wait for the index file download to complete")
@ -177,7 +177,7 @@ func (o *repoAddOptions) run(out io.Writer) error {
CertFile: o.certFile,
KeyFile: o.keyFile,
CAFile: o.caFile,
InsecureSkipTLSverify: o.insecureSkipTLSverify,
InsecureSkipTLSVerify: o.insecureSkipTLSVerify,
}
// Check if the repo name is legal

@ -200,7 +200,7 @@ func repoAddConcurrent(t *testing.T, testName, repoFile string) {
var wg sync.WaitGroup
wg.Add(3)
for i := 0; i < 3; i++ {
for i := range 3 {
go func(name string) {
defer wg.Done()
o := &repoAddOptions{
@ -227,7 +227,7 @@ func repoAddConcurrent(t *testing.T, testName, repoFile string) {
}
var name string
for i := 0; i < 3; i++ {
for i := range 3 {
name = fmt.Sprintf("%s-%d", testName, i)
if !f.Has(name) {
t.Errorf("%s was not successfully inserted into %s: %s", name, repoFile, f.Repositories[0])

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

Loading…
Cancel
Save