Merge branch 'helm:main' into issue392_fix_plugin_arg_parsing

pull/30614/head
Artem Mikhalitsin 10 months 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 runs-on: ubuntu-latest
steps: steps:
- name: Checkout source code - 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 - name: Add variables to environment file
run: cat ".github/env" >> "$GITHUB_ENV" run: cat ".github/env" >> "$GITHUB_ENV"
- name: Setup Go - name: Setup Go
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # pin@5.5.0 uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # pin@6.1.0
with: with:
go-version: '${{ env.GOLANG_VERSION }}' go-version: '${{ env.GOLANG_VERSION }}'
check-latest: true check-latest: true

@ -43,7 +43,7 @@ jobs:
steps: steps:
- name: Checkout repository - 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. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL

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

@ -3,6 +3,11 @@ on:
push: push:
paths: paths:
- go.sum - go.sum
- .github/workflows/govulncheck.yml
pull_request:
paths:
- go.sum
- .github/workflows/govulncheck.yml
schedule: schedule:
- cron: "0 0 * * *" - cron: "0 0 * * *"
@ -14,11 +19,13 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - 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 - name: Add variables to environment file
run: cat ".github/env" >> "$GITHUB_ENV" run: cat ".github/env" >> "$GITHUB_ENV"
- name: Setup Go - name: Setup Go
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # pin@5.5.0 uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # pin@6.1.0
with: with:
go-version: '${{ env.GOLANG_VERSION }}' go-version: '${{ env.GOLANG_VERSION }}'
check-latest: true check-latest: true

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

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

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

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

@ -1,4 +1,4 @@
# Copilot Instructions for Helm # AGENTS.md
## Overview ## Overview
Helm is a package manager for Kubernetes written in Go, supporting v3 (stable) and v4 (unstable) APIs. 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 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 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 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. 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. 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 - `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 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 feature is actually developed. This is not needed for small additions. Final word on whether
not a feature needs a proposal is up to the core maintainers. All issues that are proposals should 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 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. a `feature` and does not require a milestone.
- `feature`: These track specific feature requests and ideas until they are complete. They can - `feature`: These track specific feature requests and ideas until they are complete. They can
@ -179,7 +179,7 @@ below.
2. Triage 2. Triage
- The maintainer in charge of triaging will apply the proper labels for the issue. This includes - 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 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. in the future, we will add them.
- (If needed) Clean up the title to succinctly and clearly state the issue. Also ensure that - (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]". 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 += -X helm.sh/helm/v4/internal/version.gitTreeState=${GIT_DIRTY}
LDFLAGS += $(EXT_LDFLAGS) 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 .PHONY: all
all: build all: build
@ -129,6 +115,13 @@ test-coverage:
.PHONY: test-style .PHONY: test-style
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 ./... golangci-lint run ./...
@scripts/validate-license.sh @scripts/validate-license.sh

@ -7,9 +7,9 @@ maintainers:
- sabre1041 - sabre1041
- scottrigby - scottrigby
- technosophos - technosophos
- TerryHowe
triage: triage:
- banjoh - banjoh
- TerryHowe
- yxxhero - yxxhero
- zonggen - zonggen
- z4ce - 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) [![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) [![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) [![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. Helm is a tool for managing Charts. Charts are packages of pre-configured Kubernetes resources.

@ -1,10 +1,10 @@
module helm.sh/helm/v4 module helm.sh/helm/v4
go 1.24.0 go 1.25.0
require ( require (
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 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/DATA-DOG/go-sqlmock v1.5.2
github.com/Masterminds/semver/v3 v3.4.0 github.com/Masterminds/semver/v3 v3.4.0
github.com/Masterminds/sprig/v3 v3.3.0 github.com/Masterminds/sprig/v3 v3.3.0
@ -12,12 +12,12 @@ require (
github.com/Masterminds/vcs v1.13.3 github.com/Masterminds/vcs v1.13.3
github.com/ProtonMail/go-crypto v1.3.0 github.com/ProtonMail/go-crypto v1.3.0
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2
github.com/cyphar/filepath-securejoin v0.6.0 github.com/cyphar/filepath-securejoin v0.6.1
github.com/distribution/distribution/v3 v3.0.0 github.com/distribution/distribution/v3 v3.0.0
github.com/evanphx/json-patch/v5 v5.9.11 github.com/evanphx/json-patch/v5 v5.9.11
github.com/extism/go-sdk v1.7.1 github.com/extism/go-sdk v1.7.1
github.com/fatih/color v1.18.0 github.com/fatih/color v1.18.0
github.com/fluxcd/cli-utils v0.36.0-flux.14 github.com/fluxcd/cli-utils v0.36.0-flux.15
github.com/foxcpp/go-mockdns v1.1.0 github.com/foxcpp/go-mockdns v1.1.0
github.com/gobwas/glob v0.2.3 github.com/gobwas/glob v0.2.3
github.com/gofrs/flock v0.13.0 github.com/gofrs/flock v0.13.0
@ -25,32 +25,31 @@ require (
github.com/jmoiron/sqlx v1.4.0 github.com/jmoiron/sqlx v1.4.0
github.com/lib/pq v1.10.9 github.com/lib/pq v1.10.9
github.com/mattn/go-shellwords v1.0.12 github.com/mattn/go-shellwords v1.0.12
github.com/mitchellh/copystructure v1.2.0
github.com/moby/term v0.5.2 github.com/moby/term v0.5.2
github.com/opencontainers/go-digest v1.0.0 github.com/opencontainers/go-digest v1.0.0
github.com/opencontainers/image-spec v1.1.1 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/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/spf13/pflag v1.0.10
github.com/stretchr/testify v1.11.1 github.com/stretchr/testify v1.11.1
github.com/tetratelabs/wazero v1.9.0 github.com/tetratelabs/wazero v1.11.0
go.yaml.in/yaml/v3 v3.0.4 go.yaml.in/yaml/v3 v3.0.4
golang.org/x/crypto v0.43.0 golang.org/x/crypto v0.46.0
golang.org/x/term v0.36.0 golang.org/x/term v0.38.0
golang.org/x/text v0.30.0 golang.org/x/text v0.32.0
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/api v0.34.1 k8s.io/api v0.34.3
k8s.io/apiextensions-apiserver v0.34.1 k8s.io/apiextensions-apiserver v0.34.3
k8s.io/apimachinery v0.34.1 k8s.io/apimachinery v0.34.3
k8s.io/apiserver v0.34.1 k8s.io/apiserver v0.34.3
k8s.io/cli-runtime v0.34.1 k8s.io/cli-runtime v0.34.3
k8s.io/client-go v0.34.1 k8s.io/client-go v0.34.3
k8s.io/klog/v2 v2.130.1 k8s.io/klog/v2 v2.130.1
k8s.io/kubectl v0.34.1 k8s.io/kubectl v0.34.3
oras.land/oras-go/v2 v2.6.0 oras.land/oras-go/v2 v2.6.0
sigs.k8s.io/controller-runtime v0.22.4 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 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-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.9 // indirect github.com/mattn/go-runewidth v0.0.9 // indirect
github.com/miekg/dns v1.1.57 // indirect github.com/miekg/dns v1.1.57 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/go-wordwrap v1.0.1 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/moby/spdystream v0.5.0 // 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/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // 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/peterbourgon/diskv v2.0.1+incompatible // indirect
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // 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/otel/trace v1.37.0 // indirect
go.opentelemetry.io/proto/otlp v1.5.0 // indirect go.opentelemetry.io/proto/otlp v1.5.0 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect
golang.org/x/mod v0.28.0 // indirect golang.org/x/mod v0.30.0 // indirect
golang.org/x/net v0.45.0 // indirect golang.org/x/net v0.47.0 // indirect
golang.org/x/oauth2 v0.30.0 // indirect golang.org/x/oauth2 v0.30.0 // indirect
golang.org/x/sync v0.17.0 // indirect golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.37.0 // indirect golang.org/x/sys v0.39.0 // indirect
golang.org/x/time v0.12.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/api v0.0.0-20250303144028-a0af3efb3deb // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb // indirect
google.golang.org/grpc v1.72.1 // indirect google.golang.org/grpc v1.72.1 // indirect
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/evanphx/json-patch.v4 v4.12.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // 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/kube-openapi v0.0.0-20250710124328-f3f2b991d03b // indirect
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // 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/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 h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= 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.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= 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 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= 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= 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/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/cyphar/filepath-securejoin v0.6.0 h1:BtGB77njd6SVO6VztOHfPxKitJvd/VPT+OFBFMOi1Is= github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE=
github.com/cyphar/filepath-securejoin v0.6.0/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc= 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.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
@ -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/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fluxcd/cli-utils v0.36.0-flux.14 h1:I//AMVUXTc+M04UtIXArMXQZCazGMwfemodV1j/yG8c= github.com/fluxcd/cli-utils v0.36.0-flux.15 h1:Et5QLnIpRjj+oZtM9gEybkAaoNsjysHq0y1253Ai94Y=
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/go.mod h1:AqRUmWIfNE7cdL6NWSGF0bAlypGs+9x5UQ2qOtlEzv4=
github.com/foxcpp/go-mockdns v1.1.0 h1:jI0rD8M0wuYAxL7r/ynTrCQQq0BVqfB99Vgk7DlmewI= 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/foxcpp/go-mockdns v1.1.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= 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/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 h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus=
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= 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.25.2 h1:hepmgwx1D+llZleKQDMEvy8vIlCxMGt7W5ZxDjIEhsw=
github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8= github.com/onsi/ginkgo/v2 v2.25.2/go.mod h1:43uiyQC4Ed2tkOzLsEYm7hnrb7UJTWHYNsuy3bG/snE=
github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y= github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A=
github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0= 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 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= 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.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 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM=
github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA= 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.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rubenv/sql-migrate v1.8.0 h1:dXnYiJk9k3wetp7GfQbKJcPHjVJL6YK19tKj8t2Ns0o= github.com/rubenv/sql-migrate v1.8.1 h1:EPNwCvjAowHI3TnZ+4fQu3a915OpnQoPAjTXCGOy2U0=
github.com/rubenv/sql-migrate v1.8.0/go.mod h1:F2bGFBwCU+pnmbtNYDeKvSuvL6lBVtXDXUUv5t+u1qw= 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 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 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 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ=
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= 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.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= 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 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 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/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 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w=
github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= 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.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= 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.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 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/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 h1:ZF+QBjOI+tILZjBaFj3HgFonKXUcwgJ4djLb6i42S3Q=
github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834/go.mod h1:m9ymHTgNSEjuxvw8E7WWe4Pl4hZQHXONY8wE6dMLaRk= 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.11.0 h1:+gKemEuKCTevU4d7ZTzlsvgd1uaToIDtlQlmNbwqYhA=
github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM= 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 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= 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.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g= golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g=
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= 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.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U= golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI= 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-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@ -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.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ= golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ=
golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM= golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= 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 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -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.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.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.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= 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-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-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-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.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 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-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@ -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.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
golang.org/x/term v0.14.0/go.mod h1:TySc+nGkYR6qt8km8wUhuFRTVSMIX3XPR58y2lC8vww= golang.org/x/term v0.14.0/go.mod h1:TySc+nGkYR6qt8km8wUhuFRTVSMIX3XPR58y2lC8vww=
golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= 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.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
@ -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.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= 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 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@ -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.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.15.0/go.mod h1:hpksKq4dtpQWS1uQ61JkdqWM3LscIS6Slf+VVkm+wQk= golang.org/x/tools v0.15.0/go.mod h1:hpksKq4dtpQWS1uQ61JkdqWM3LscIS6Slf+VVkm+wQk=
golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= 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-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@ -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/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 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA=
google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= 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.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= 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/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 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 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.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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 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.3 h1:D12sTP257/jSH2vHV2EDYrb16bS7ULlHpdNdNhEw2S4=
k8s.io/api v0.34.1/go.mod h1:SB80FxFtXn5/gwzCoN6QCtPD7Vbu5w2n1S0J5gFfTYk= k8s.io/api v0.34.3/go.mod h1:PyVQBF886Q5RSQZOim7DybQjAbVs8g7gwJNhGtY5MBk=
k8s.io/apiextensions-apiserver v0.34.1 h1:NNPBva8FNAPt1iSVwIE0FsdrVriRXMsaWFMqJbII2CI= k8s.io/apiextensions-apiserver v0.34.3 h1:p10fGlkDY09eWKOTeUSioxwLukJnm+KuDZdrW71y40g=
k8s.io/apiextensions-apiserver v0.34.1/go.mod h1:hP9Rld3zF5Ay2Of3BeEpLAToP+l4s5UlxiHfqRaRcMc= k8s.io/apiextensions-apiserver v0.34.3/go.mod h1:aujxvqGFRdb/cmXYfcRTeppN7S2XV/t7WMEc64zB5A0=
k8s.io/apimachinery v0.34.1 h1:dTlxFls/eikpJxmAC7MVE8oOeP1zryV7iRyIjB0gky4= k8s.io/apimachinery v0.34.3 h1:/TB+SFEiQvN9HPldtlWOTp0hWbJ+fjU+wkxysf/aQnE=
k8s.io/apimachinery v0.34.1/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= k8s.io/apimachinery v0.34.3/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw=
k8s.io/apiserver v0.34.1 h1:U3JBGdgANK3dfFcyknWde1G6X1F4bg7PXuvlqt8lITA= k8s.io/apiserver v0.34.3 h1:uGH1qpDvSiYG4HVFqc6A3L4CKiX+aBWDrrsxHYK0Bdo=
k8s.io/apiserver v0.34.1/go.mod h1:eOOc9nrVqlBI1AFCvVzsob0OxtPZUCPiUJL45JOTBG0= k8s.io/apiserver v0.34.3/go.mod h1:QPnnahMO5C2m3lm6fPW3+JmyQbvHZQ8uudAu/493P2w=
k8s.io/cli-runtime v0.34.1 h1:btlgAgTrYd4sk8vJTRG6zVtqBKt9ZMDeQZo2PIzbL7M= k8s.io/cli-runtime v0.34.3 h1:YRyMhiwX0dT9lmG0AtZDaeG33Nkxgt9OlCTZhRXj9SI=
k8s.io/cli-runtime v0.34.1/go.mod h1:aVA65c+f0MZiMUPbseU/M9l1Wo2byeaGwUuQEQVVveE= k8s.io/cli-runtime v0.34.3/go.mod h1:GVwL1L5uaGEgM7eGeKjaTG2j3u134JgG4dAI6jQKhMc=
k8s.io/client-go v0.34.1 h1:ZUPJKgXsnKwVwmKKdPfw4tB58+7/Ik3CrjOEhsiZ7mY= k8s.io/client-go v0.34.3 h1:wtYtpzy/OPNYf7WyNBTj3iUA0XaBHVqhv4Iv3tbrF5A=
k8s.io/client-go v0.34.1/go.mod h1:kA8v0FP+tk6sZA0yKLRG67LWjqufAoSHA2xVGKw9Of8= k8s.io/client-go v0.34.3/go.mod h1:OxxeYagaP9Kdf78UrKLa3YZixMCfP6bgPwPwNBQBzpM=
k8s.io/component-base v0.34.1 h1:v7xFgG+ONhytZNFpIz5/kecwD+sUhVE6HU7qQUiRM4A= k8s.io/component-base v0.34.3 h1:zsEgw6ELqK0XncCQomgO9DpUIzlrYuZYA0Cgo+JWpVk=
k8s.io/component-base v0.34.1/go.mod h1:mknCpLlTSKHzAQJJnnHVKqjxR7gBeHRv0rPXA7gdtQ0= 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 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= 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 h1:MloQ9/bdJyIu9lb1PzujOPolHyvO06MXG5TUIj2mNAA=
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts= 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.3 h1:vpM6//153gh5gvsYHXWHVJ4l4xmN5QFwTSmlfd8icm8=
k8s.io/kubectl v0.34.1/go.mod h1:JRYlhJpGPyk3dEmJ+BuBiOB9/dAvnrALJEiY/C5qa6A= 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 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y=
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
oras.land/oras-go/v2 v2.6.0 h1:X4ELRsiGkrbeox69+9tzTu492FMUu7zJQW6eJU+I2oc= oras.land/oras-go/v2 v2.6.0 h1:X4ELRsiGkrbeox69+9tzTu492FMUu7zJQW6eJU+I2oc=
@ -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/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
sigs.k8s.io/kustomize/api v0.20.1 h1:iWP1Ydh3/lmldBnH/S5RXgT98vWYMaTUL1ADcr+Sv7I= sigs.k8s.io/kustomize/api v0.20.1 h1:iWP1Ydh3/lmldBnH/S5RXgT98vWYMaTUL1ADcr+Sv7I=
sigs.k8s.io/kustomize/api v0.20.1/go.mod h1:t6hUFxO+Ph0VxIk1sKp1WS0dOjbPCtLJ4p8aADLwqjM= 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.21.0 h1:7mQAf3dUwf0wBerWJd8rXhVcnkk5Tvn/q91cGkaP6HQ=
sigs.k8s.io/kustomize/kyaml v0.20.1/go.mod h1:0EmkQHRUsJxY8Ug9Niig1pUMSCGHxQ5RklbpV/Ri6po= 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 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= 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= 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 // Resources like hpa and ingress, which are disabled by default in values.yaml are enabled here using the equivalent
// of the `--set` flag. // 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) { func TestHelmCreateChart_CheckDeprecatedWarnings(t *testing.T) {
createdChart, err := chartutil.Create("checkdeprecatedwarnings", t.TempDir()) createdChart, err := chartutil.Create("checkdeprecatedwarnings", t.TempDir())
if err != nil { if err != nil {

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

@ -17,6 +17,8 @@ limitations under the License.
package rules package rules
import ( import (
"os"
"path/filepath"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -34,3 +36,31 @@ func TestInvalidCrdsDir(t *testing.T) {
assert.Len(t, res, 1) assert.Len(t, res, 1)
assert.ErrorContains(t, res[0].Err, "not a directory") 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" 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 // deprecatedAPIError indicates than an API is deprecated in Kubernetes
type deprecatedAPIError struct { type deprecatedAPIError struct {
Deprecated string Deprecated string
@ -56,33 +48,29 @@ func validateNoDeprecations(resource *k8sYamlStruct, kubeVersion *common.KubeVer
return nil return nil
} }
majorVersion := k8sVersionMajor if kubeVersion == nil {
minorVersion := k8sVersionMinor kubeVersion = &common.DefaultCapabilities.KubeVersion
if kubeVersion != nil {
majorVersion = kubeVersion.Major
minorVersion = kubeVersion.Minor
} }
runtimeObject, err := resourceToRuntimeObject(resource) kubeVersionMajor, err := strconv.Atoi(kubeVersion.Major)
if err != nil { if err != nil {
// do not error for non-kubernetes resources
if runtime.IsNotRegisteredError(err) {
return nil
}
return err return err
} }
kubeVersionMinor, err := strconv.Atoi(kubeVersion.Minor)
major, err := strconv.Atoi(majorVersion)
if err != nil { if err != nil {
return err return err
} }
minor, err := strconv.Atoi(minorVersion)
runtimeObject, err := resourceToRuntimeObject(resource)
if err != nil { if err != nil {
// do not error for non-kubernetes resources
if runtime.IsNotRegisteredError(err) {
return nil
}
return err return err
} }
if !deprecation.IsDeprecated(runtimeObject, major, minor) { if !deprecation.IsDeprecated(runtimeObject, kubeVersionMajor, kubeVersionMinor) {
return nil return nil
} }
gvk := fmt.Sprintf("%s %s", resource.APIVersion, resource.Kind) gvk := fmt.Sprintf("%s %s", resource.APIVersion, resource.Kind)

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

@ -56,7 +56,7 @@ func LoadFile(name string) (*chart.Chart, error) {
c, err := LoadArchive(raw) c, err := LoadArchive(raw)
if err != nil { 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) 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. // LoadValues loads values from a reader.
// //
// The reader is expected to contain one or more YAML documents, the values of which are merged. // 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) { func LoadValues(data io.Reader) (map[string]interface{}, error) {
values := map[string]interface{}{} values := map[string]interface{}{}
reader := utilyaml.NewYAMLReader(bufio.NewReader(data)) reader := utilyaml.NewYAMLReader(bufio.NewReader(data))
@ -189,7 +189,7 @@ func LoadValues(data io.Reader) (map[string]interface{}, error) {
currentMap := map[string]interface{}{} currentMap := map[string]interface{}{}
raw, err := reader.Read() raw, err := reader.Read()
if err != nil { if err != nil {
if err == io.EOF { if errors.Is(err, io.EOF) {
break break
} }
return nil, fmt.Errorf("error reading yaml document: %w", err) return nil, fmt.Errorf("error reading yaml document: %w", err)

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

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

@ -21,6 +21,7 @@ import (
"bytes" "bytes"
"compress/gzip" "compress/gzip"
"crypto/sha256" "crypto/sha256"
"errors"
"fmt" "fmt"
"io" "io"
"os" "os"
@ -201,7 +202,7 @@ func retrieveAllHeadersFromTar(path string) ([]*tar.Header, error) {
headers := []*tar.Header{} headers := []*tar.Header{}
for { for {
hd, err := tr.Next() hd, err := tr.Next()
if err == io.EOF { if errors.Is(err, io.EOF) {
break 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 // Enabled implements slog.Handler.Enabled
func (h *DebugCheckHandler) Enabled(_ context.Context, level slog.Level) bool { func (h *DebugCheckHandler) Enabled(_ context.Context, level slog.Level) bool {
if level == slog.LevelDebug { if level == slog.LevelDebug {
if h.debugEnabled == nil {
return false
}
return h.debugEnabled() return h.debugEnabled()
} }
return true // Always log other levels return true // Always log other levels

@ -18,8 +18,10 @@ package logging
import ( import (
"bytes" "bytes"
"context"
"log/slog" "log/slog"
"testing" "testing"
"time"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@ -113,3 +115,259 @@ func TestLogHolder_InterfaceCompliance(t *testing.T) {
assert.Equal(t, handler, logger.Handler()) 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) tarReader := tar.NewReader(uncompressedStream)
for { for {
header, err := tarReader.Next() header, err := tarReader.Next()
if err == io.EOF { if errors.Is(err, io.EOF) {
break break
} }
if err != nil { if err != nil {

@ -368,7 +368,7 @@ func TestExtractWithNestedDirectories(t *testing.T) {
}{ }{
{"plugin.yaml", "plugin metadata", 0600, tar.TypeReg}, {"plugin.yaml", "plugin metadata", 0600, tar.TypeReg},
{"bin/", "", 0755, tar.TypeDir}, {"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/", "", 0755, tar.TypeDir},
{"docs/README.md", "readme content", 0644, tar.TypeReg}, {"docs/README.md", "readme content", 0644, tar.TypeReg},
{"docs/examples/", "", 0755, tar.TypeDir}, {"docs/examples/", "", 0755, tar.TypeDir},
@ -531,7 +531,7 @@ func TestExtractPluginInSubdirectory(t *testing.T) {
{"my-plugin/", "", 0755, tar.TypeDir}, {"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/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/", "", 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 { for _, file := range files {

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

@ -87,7 +87,7 @@ func TestLocalInstallerTarball(t *testing.T) {
Mode int64 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/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 { for _, file := range files {

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

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

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

@ -21,6 +21,44 @@ import (
"helm.sh/helm/v4/internal/plugin/schema" "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 { func mockSubprocessCLIPlugin(t *testing.T, pluginName string) *SubprocessPluginRuntime {
t.Helper() t.Helper()

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

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

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

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

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

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

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

@ -42,11 +42,11 @@ func TestNewTLSConfig(t *testing.T) {
certFile := testfile(t, testCertFile) certFile := testfile(t, testCertFile)
keyFile := testfile(t, testKeyFile) keyFile := testfile(t, testKeyFile)
caCertFile := testfile(t, testCaCertFile) caCertFile := testfile(t, testCaCertFile)
insecureSkipTLSverify := false insecureSkipTLSVerify := false
{ {
cfg, err := NewTLSConfig( cfg, err := NewTLSConfig(
WithInsecureSkipVerify(insecureSkipTLSverify), WithInsecureSkipVerify(insecureSkipTLSVerify),
WithCertKeyPairFiles(certFile, keyFile), WithCertKeyPairFiles(certFile, keyFile),
WithCAFile(caCertFile), WithCAFile(caCertFile),
) )
@ -66,7 +66,7 @@ func TestNewTLSConfig(t *testing.T) {
} }
{ {
cfg, err := NewTLSConfig( cfg, err := NewTLSConfig(
WithInsecureSkipVerify(insecureSkipTLSverify), WithInsecureSkipVerify(insecureSkipTLSVerify),
WithCAFile(caCertFile), WithCAFile(caCertFile),
) )
if err != nil { if err != nil {
@ -86,7 +86,7 @@ func TestNewTLSConfig(t *testing.T) {
{ {
cfg, err := NewTLSConfig( cfg, err := NewTLSConfig(
WithInsecureSkipVerify(insecureSkipTLSverify), WithInsecureSkipVerify(insecureSkipTLSVerify),
WithCertKeyPairFiles(certFile, keyFile), WithCertKeyPairFiles(certFile, keyFile),
) )
if err != nil { 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. limitations under the License.
*/ */
package version // import "helm.sh/helm/v4/internal/version" package version
import ( import (
"flag" "flag"
"fmt" "fmt"
"log/slog"
"runtime" "runtime"
"strings" "strings"
"testing"
"github.com/Masterminds/semver/v3"
) )
var ( var (
@ -30,7 +34,7 @@ var (
// //
// Increment major number for new feature additions and behavioral changes. // Increment major number for new feature additions and behavioral changes.
// Increment minor number for bug fixes and performance enhancements. // Increment minor number for bug fixes and performance enhancements.
version = "v4.0" version = "v4.1"
// metadata is extra build time data // metadata is extra build time data
metadata = "" metadata = ""
@ -38,11 +42,10 @@ var (
gitCommit = "" gitCommit = ""
// gitTreeState is the state of the git tree // gitTreeState is the state of the git tree
gitTreeState = "" gitTreeState = ""
)
// The Kubernetes version can be set by LDFLAGS. In order to do that the value const (
// must be a string. kubeClientGoVersionTesting = "v1.20"
kubeClientVersionMajor = ""
kubeClientVersionMinor = ""
) )
// BuildInfo describes the compile time information. // BuildInfo describes the compile time information.
@ -74,12 +77,39 @@ func GetUserAgent() string {
// Get returns build info // Get returns build info
func Get() BuildInfo { 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{ v := BuildInfo{
Version: GetVersion(), Version: GetVersion(),
GitCommit: gitCommit, GitCommit: gitCommit,
GitTreeState: gitTreeState, GitTreeState: gitTreeState,
GoVersion: runtime.Version(), 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 // 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 ch.Metadata.KubeVersion != "" {
if !chartutil.IsCompatibleRange(ch.Metadata.KubeVersion, caps.KubeVersion.String()) { 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. // recordRelease with an update operation in case reuse has been set.
func (cfg *Configuration) recordRelease(r *release.Release) { func (cfg *Configuration) recordRelease(r *release.Release) {
if err := cfg.Releases.Update(r); err != nil { 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 CertFile string
KeyFile string KeyFile string
CaFile string CaFile string
InsecureSkipTLSverify bool InsecureSkipTLSVerify bool
PlainHTTP 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" "helm.sh/helm/v4/pkg/storage/driver"
) )
// notesFileSuffix that we want to treat special. It goes through the templating engine // 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 // 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 // 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. // since there can be filepath in front of it.
const notesFileSuffix = "NOTES.txt" const notesFileSuffix = "NOTES.txt"
@ -139,7 +139,7 @@ type ChartPathOptions struct {
CaFile string // --ca-file CaFile string // --ca-file
CertFile string // --cert-file CertFile string // --cert-file
KeyFile string // --key-file KeyFile string // --key-file
InsecureSkipTLSverify bool // --insecure-skip-verify InsecureSkipTLSVerify bool // --insecure-skip-verify
PlainHTTP bool // --plain-http PlainHTTP bool // --plain-http
Keyring string // --keyring Keyring string // --keyring
Password string // --password Password string // --password
@ -419,6 +419,7 @@ func (i *Install) RunWithContext(ctx context.Context, ch ci.Charter, vals map[st
if err != nil { if err != nil {
return nil, err return nil, err
} }
if _, err := i.cfg.KubeClient.Create( if _, err := i.cfg.KubeClient.Create(
resourceList, resourceList,
kube.ClientCreateOptionServerSideApply(i.ServerSideApply, false)); err != nil && !apierrors.IsAlreadyExists(err) { 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{ Options: []getter.Option{
getter.WithPassCredentialsAll(c.PassCredentialsAll), getter.WithPassCredentialsAll(c.PassCredentialsAll),
getter.WithTLSClientConfig(c.CertFile, c.KeyFile, c.CaFile), getter.WithTLSClientConfig(c.CertFile, c.KeyFile, c.CaFile),
getter.WithInsecureSkipVerifyTLS(c.InsecureSkipTLSverify), getter.WithInsecureSkipVerifyTLS(c.InsecureSkipTLSVerify),
getter.WithPlainHTTP(c.PlainHTTP), getter.WithPlainHTTP(c.PlainHTTP),
getter.WithBasicAuth(c.Username, c.Password), getter.WithBasicAuth(c.Username, c.Password),
}, },
@ -911,7 +912,7 @@ func (c *ChartPathOptions) LocateChart(name string, settings *cli.EnvSettings) (
repo.WithChartVersion(version), repo.WithChartVersion(version),
repo.WithClientTLS(c.CertFile, c.KeyFile, c.CaFile), repo.WithClientTLS(c.CertFile, c.KeyFile, c.CaFile),
repo.WithUsernamePassword(c.Username, c.Password), repo.WithUsernamePassword(c.Username, c.Password),
repo.WithInsecureSkipTLSverify(c.InsecureSkipTLSverify), repo.WithInsecureSkipTLSVerify(c.InsecureSkipTLSVerify),
repo.WithPassCredentialsAll(c.PassCredentialsAll), repo.WithPassCredentialsAll(c.PassCredentialsAll),
) )
if err != nil { if err != nil {

@ -572,7 +572,7 @@ func TestInstallRelease_KubeVersion(t *testing.T) {
vals = map[string]interface{}{} vals = map[string]interface{}{}
_, err = instAction.Run(buildChart(withKube(">=99.0.0")), vals) _, err = instAction.Run(buildChart(withKube(">=99.0.0")), vals)
is.Error(err) 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) { func TestInstallRelease_Wait(t *testing.T) {

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

@ -93,7 +93,7 @@ func TestPassphraseFileFetcher_WithStdinAndMultipleFetches(t *testing.T) {
w.Write([]byte(passphrase + "\n")) w.Write([]byte(passphrase + "\n"))
}() }()
for i := 0; i < 4; i++ { for range 4 {
fetcher, err := testPkg.passphraseFileFetcher("-", stdin) fetcher, err := testPkg.passphraseFileFetcher("-", stdin)
if err != nil { if err != nil {
t.Errorf("Expected passphraseFileFetcher to not return an error, but got %v", err) 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.WithBasicAuth(p.Username, p.Password),
getter.WithPassCredentialsAll(p.PassCredentialsAll), getter.WithPassCredentialsAll(p.PassCredentialsAll),
getter.WithTLSClientConfig(p.CertFile, p.KeyFile, p.CaFile), getter.WithTLSClientConfig(p.CertFile, p.KeyFile, p.CaFile),
getter.WithInsecureSkipVerifyTLS(p.InsecureSkipTLSverify), getter.WithInsecureSkipVerifyTLS(p.InsecureSkipTLSVerify),
getter.WithPlainHTTP(p.PlainHTTP), getter.WithPlainHTTP(p.PlainHTTP),
}, },
RegistryClient: p.cfg.RegistryClient, RegistryClient: p.cfg.RegistryClient,
@ -124,7 +124,7 @@ func (p *Pull) Run(chartRef string) (string, error) {
repo.WithChartVersion(p.Version), repo.WithChartVersion(p.Version),
repo.WithClientTLS(p.CertFile, p.KeyFile, p.CaFile), repo.WithClientTLS(p.CertFile, p.KeyFile, p.CaFile),
repo.WithUsernamePassword(p.Username, p.Password), repo.WithUsernamePassword(p.Username, p.Password),
repo.WithInsecureSkipTLSverify(p.InsecureSkipTLSverify), repo.WithInsecureSkipTLSVerify(p.InsecureSkipTLSVerify),
repo.WithPassCredentialsAll(p.PassCredentialsAll), repo.WithPassCredentialsAll(p.PassCredentialsAll),
) )
if err != nil { if err != nil {

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

@ -18,8 +18,8 @@ package action
import ( import (
"bytes" "bytes"
"errors"
"fmt" "fmt"
"strings"
"time" "time"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@ -28,6 +28,7 @@ import (
"helm.sh/helm/v4/pkg/kube" "helm.sh/helm/v4/pkg/kube"
"helm.sh/helm/v4/pkg/release/common" "helm.sh/helm/v4/pkg/release/common"
release "helm.sh/helm/v4/pkg/release/v1" release "helm.sh/helm/v4/pkg/release/v1"
"helm.sh/helm/v4/pkg/storage/driver"
) )
// Rollback is the action for rolling back to a given release. // 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) 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 return nil, err
} }
// Supersede all previous deployments, see issue #2941. // 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)) 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 { if len(errs) > 0 {
return res, fmt.Errorf("uninstallation completed with %d error(s): %w", len(errs), joinErrors(errs, "; ")) 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) filesToKeep, filesToDelete := filterManifestsToKeep(files)
var kept string var kept strings.Builder
for _, f := range filesToKeep { 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 var builder strings.Builder
@ -259,7 +278,7 @@ func (u *Uninstall) deleteRelease(rel *release.Release) (kube.ResourceList, stri
if len(resources) > 0 { if len(resources) > 0 {
_, errs = u.cfg.KubeClient.Delete(resources, parseCascadingFlag(u.DeletionPropagation)) _, errs = u.cfg.KubeClient.Delete(resources, parseCascadingFlag(u.DeletionPropagation))
} }
return resources, kept, errs return resources, kept.String(), errs
} }
func parseCascadingFlag(cascadingFlag string) v1.DeletionPropagation { 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) { 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) 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.Status = rcommon.StatusFailed
rel.Info.Description = msg 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) 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 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) 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 return nil
}) })

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

@ -19,7 +19,10 @@ import (
"fmt" "fmt"
"slices" "slices"
"strconv" "strconv"
"strings"
"testing"
"github.com/Masterminds/semver/v3"
"k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/kubernetes/scheme"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
@ -29,25 +32,23 @@ import (
helmversion "helm.sh/helm/v4/internal/version" helmversion "helm.sh/helm/v4/internal/version"
) )
var ( const (
// The Kubernetes version can be set by LDFLAGS. In order to do that the value kubeVersionMajorTesting = 1
// must be a string. kubeVersionMinorTesting = 20
k8sVersionMajor = "1" )
k8sVersionMinor = "20"
var (
// DefaultVersionSet is the default version set, which includes only Core V1 ("v1"). // DefaultVersionSet is the default version set, which includes only Core V1 ("v1").
DefaultVersionSet = allKnownVersions() DefaultVersionSet = allKnownVersions()
// DefaultCapabilities is the default set of capabilities. DefaultCapabilities = func() *Capabilities {
DefaultCapabilities = &Capabilities{ caps, err := makeDefaultCapabilities()
KubeVersion: KubeVersion{ if err != nil {
Version: fmt.Sprintf("v%s.%s.0", k8sVersionMajor, k8sVersionMinor), panic(fmt.Sprintf("failed to create default capabilities: %v", err))
Major: k8sVersionMajor, }
Minor: k8sVersionMinor, return caps
},
APIVersions: DefaultVersionSet, }()
HelmVersion: helmversion.Get(),
}
) )
// Capabilities describes the capabilities of the Kubernetes cluster. // Capabilities describes the capabilities of the Kubernetes cluster.
@ -70,15 +71,22 @@ func (capabilities *Capabilities) Copy() *Capabilities {
// KubeVersion is the Kubernetes version. // KubeVersion is the Kubernetes version.
type KubeVersion struct { type KubeVersion struct {
Version string // Kubernetes version Version string // Full version (e.g., v1.33.4-gke.1245000)
Major string // Kubernetes major version normalizedVersion string // Normalized for constraint checking (e.g., v1.33.4)
Minor string // Kubernetes minor version Major string // Kubernetes major version
Minor string // Kubernetes minor version
} }
// String implements fmt.Stringer // String implements fmt.Stringer.
func (kv *KubeVersion) String() string { return kv.Version } // 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. // Deprecated: use KubeVersion.Version.
func (kv *KubeVersion) GitVersion() string { return kv.Version } func (kv *KubeVersion) GitVersion() string { return kv.Version }
@ -91,10 +99,21 @@ func ParseKubeVersion(version string) (*KubeVersion, error) {
if err != nil { if err != nil {
return nil, err 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{ return &KubeVersion{
Version: "v" + sv.String(), Version: gitVersion,
Major: strconv.FormatUint(uint64(sv.Major()), 10), normalizedVersion: normalizedVer,
Minor: strconv.FormatUint(uint64(sv.Minor()), 10), Major: strconv.FormatUint(uint64(sv.Major()), 10),
Minor: strconv.FormatUint(uint64(sv.Minor()), 10),
}, nil }, nil
} }
@ -122,3 +141,42 @@ func allKnownVersions() VersionSet {
} }
return vs 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) { func TestDefaultCapabilities(t *testing.T) {
kv := DefaultCapabilities.KubeVersion caps := DefaultCapabilities
kv := caps.KubeVersion
if kv.String() != "v1.20.0" { if kv.String() != "v1.20.0" {
t.Errorf("Expected default KubeVersion.String() to be v1.20.0, got %q", kv.String()) 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" { if kv.Minor != "20" {
t.Errorf("Expected default KubeVersion.Minor to be 20, got %q", kv.Minor) 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" { hv := caps.HelmVersion
t.Errorf("Expected default HelmVersion to be v4.0, got %q", hv.Version) 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) { func TestParseKubeVersionWithVendorSuffixes(t *testing.T) {
kv, err := ParseKubeVersion("v1.28+") tests := []struct {
if err != nil { name string
t.Errorf("Expected v1.28+ to parse successfully") input string
} wantVer string
if kv.Version != "v1.28" { wantString string
t.Errorf("Expected parsed KubeVersion.Version to be v1.28, got %q", kv.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) for _, tt := range tests {
} t.Run(tt.name, func(t *testing.T) {
if kv.Minor != "28" { kv, err := ParseKubeVersion(tt.input)
t.Errorf("Expected parsed KubeVersion.Minor to be 28, got %q", kv.Minor) 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" "log"
"maps" "maps"
"github.com/mitchellh/copystructure" "helm.sh/helm/v4/internal/copystructure"
chart "helm.sh/helm/v4/pkg/chart" chart "helm.sh/helm/v4/pkg/chart"
"helm.sh/helm/v4/pkg/chart/common" "helm.sh/helm/v4/pkg/chart/common"
) )

@ -24,6 +24,7 @@ import (
"log/slog" "log/slog"
"net/http" "net/http"
"strings" "strings"
"sync"
"time" "time"
"github.com/santhosh-tekuri/jsonschema/v6" "github.com/santhosh-tekuri/jsonschema/v6"
@ -86,7 +87,7 @@ func ValidateAgainstSchema(ch chart.Charter, values map[string]interface{}) erro
sb.WriteString(err.Error()) 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 each dependency, recursively call this function with the coalesced values
for _, subchart := range chrt.Dependencies() { for _, subchart := range chrt.Dependencies() {
sub, err := chart.NewAccessor(subchart) sub, err := chart.NewAccessor(subchart)
@ -142,6 +143,7 @@ func ValidateAgainstSingleSchema(values common.Values, schemaJSON []byte) (reter
"file": jsonschema.FileLoader{}, "file": jsonschema.FileLoader{},
"http": newHTTPURLLoader(), "http": newHTTPURLLoader(),
"https": newHTTPURLLoader(), "https": newHTTPURLLoader(),
"urn": urnLoader{},
} }
compiler := jsonschema.NewCompiler() compiler := jsonschema.NewCompiler()
@ -164,6 +166,35 @@ func ValidateAgainstSingleSchema(values common.Values, schemaJSON []byte) (reter
return nil 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 // Note, JSONSchemaValidationError is used to wrap the error from the underlying
// validation package so that Helm has a clean interface and the validation package // validation package so that Helm has a clean interface and the validation package
// could be replaced without changing the Helm SDK API. // 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 // Non-regression tests for https://github.com/helm/helm/issues/31202
// Ensure ValidateAgainstSchema does not panic when: // Ensure ValidateAgainstSchema does not panic when:
// - subchart key is missing // - subchart key is missing

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

@ -20,6 +20,7 @@ import (
"compress/gzip" "compress/gzip"
"errors" "errors"
"fmt" "fmt"
"io"
"os" "os"
"path/filepath" "path/filepath"
@ -130,8 +131,8 @@ func LoadFile(name string) (chart.Charter, error) {
files, err := archive.LoadArchiveFiles(raw) files, err := archive.LoadArchiveFiles(raw)
if err != nil { 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) 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") 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") 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 // chartBase is used to detect the API Version for the chart to run it through the
// loader for that type. // loader for that type.
type chartBase struct { 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 // Resources like hpa and ingress, which are disabled by default in values.yaml are enabled here using the equivalent
// of the `--set` flag. // 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) { func TestHelmCreateChart_CheckDeprecatedWarnings(t *testing.T) {
createdChart, err := chartutil.Create("checkdeprecatedwarnings", t.TempDir()) createdChart, err := chartutil.Create("checkdeprecatedwarnings", t.TempDir())
if err != nil { if err != nil {

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

@ -17,6 +17,8 @@ limitations under the License.
package rules package rules
import ( import (
"os"
"path/filepath"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -34,3 +36,31 @@ func TestInvalidCrdsDir(t *testing.T) {
assert.Len(t, res, 1) assert.Len(t, res, 1)
assert.ErrorContains(t, res[0].Err, "not a directory") 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" 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 // deprecatedAPIError indicates than an API is deprecated in Kubernetes
type deprecatedAPIError struct { type deprecatedAPIError struct {
Deprecated string Deprecated string
@ -56,12 +48,8 @@ func validateNoDeprecations(resource *k8sYamlStruct, kubeVersion *common.KubeVer
return nil return nil
} }
majorVersion := k8sVersionMajor if kubeVersion == nil {
minorVersion := k8sVersionMinor kubeVersion = &common.DefaultCapabilities.KubeVersion
if kubeVersion != nil {
majorVersion = kubeVersion.Major
minorVersion = kubeVersion.Minor
} }
runtimeObject, err := resourceToRuntimeObject(resource) runtimeObject, err := resourceToRuntimeObject(resource)
@ -73,16 +61,16 @@ func validateNoDeprecations(resource *k8sYamlStruct, kubeVersion *common.KubeVer
return err return err
} }
major, err := strconv.Atoi(majorVersion) kubeVersionMajor, err := strconv.Atoi(kubeVersion.Major)
if err != nil { if err != nil {
return err return err
} }
minor, err := strconv.Atoi(minorVersion) kubeVersionMinor, err := strconv.Atoi(kubeVersion.Minor)
if err != nil { if err != nil {
return err return err
} }
if !deprecation.IsDeprecated(runtimeObject, major, minor) { if !deprecation.IsDeprecated(runtimeObject, kubeVersionMajor, kubeVersionMinor) {
return nil return nil
} }
gvk := fmt.Sprintf("%s %s", resource.APIVersion, resource.Kind) gvk := fmt.Sprintf("%s %s", resource.APIVersion, resource.Kind)

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

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

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

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

@ -20,8 +20,7 @@ import (
"log/slog" "log/slog"
"strings" "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"
"helm.sh/helm/v4/pkg/chart/common/util" "helm.sh/helm/v4/pkg/chart/common/util"
chart "helm.sh/helm/v4/pkg/chart/v2" chart "helm.sh/helm/v4/pkg/chart/v2"
@ -281,7 +280,11 @@ func processImportValues(c *chart.Chart, merge bool) error {
// get child table // get child table
vv, err := cvals.Table(r.Name + "." + child) vv, err := cvals.Table(r.Name + "." + child)
if err != nil { 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 continue
} }
// create value map from child to be merged into parent // create value map from child to be merged into parent

@ -21,6 +21,7 @@ import (
"bytes" "bytes"
"compress/gzip" "compress/gzip"
"crypto/sha256" "crypto/sha256"
"errors"
"fmt" "fmt"
"io" "io"
"os" "os"
@ -205,7 +206,7 @@ func retrieveAllHeadersFromTar(path string) ([]*tar.Header, error) {
headers := []*tar.Header{} headers := []*tar.Header{}
for { for {
hd, err := tr.Next() hd, err := tr.Next()
if err == io.EOF { if errors.Is(err, io.EOF) {
break 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.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.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.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.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") 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]) chartpath = filepath.Clean(args[0])
} }
registryClient, err := newRegistryClient(client.CertFile, client.KeyFile, client.CaFile, registryClient, err := newRegistryClient(client.CertFile, client.KeyFile, client.CaFile,
client.InsecureSkipTLSverify, client.PlainHTTP, client.Username, client.Password) client.InsecureSkipTLSVerify, client.PlainHTTP, client.Username, client.Password)
if err != nil { if err != nil {
return fmt.Errorf("missing registry client: %w", err) 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]) chartpath = filepath.Clean(args[0])
} }
registryClient, err := newRegistryClient(client.CertFile, client.KeyFile, client.CaFile, registryClient, err := newRegistryClient(client.CertFile, client.KeyFile, client.CaFile,
client.InsecureSkipTLSverify, client.PlainHTTP, client.Username, client.Password) client.InsecureSkipTLSVerify, client.PlainHTTP, client.Username, client.Password)
if err != nil { if err != nil {
return fmt.Errorf("missing registry client: %w", err) return fmt.Errorf("missing registry client: %w", err)
} }

@ -59,7 +59,7 @@ func AddWaitFlag(cmd *cobra.Command, wait *kube.WaitStrategy) {
cmd.Flags().Var( cmd.Flags().Var(
newWaitValue(kube.HookOnlyStrategy, wait), newWaitValue(kube.HookOnlyStrategy, wait),
"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 // Sets the strategy to use the watcher strategy if `--wait` is used without an argument
cmd.Flags().Lookup("wait").NoOptDefVal = string(kube.StatusWatcherStrategy) cmd.Flags().Lookup("wait").NoOptDefVal = string(kube.StatusWatcherStrategy)
@ -81,7 +81,7 @@ func (ws *waitValue) String() string {
func (ws *waitValue) Set(s string) error { func (ws *waitValue) Set(s string) error {
switch s { switch s {
case string(kube.StatusWatcherStrategy), string(kube.LegacyStrategy): case string(kube.StatusWatcherStrategy), string(kube.LegacyStrategy), string(kube.HookOnlyStrategy):
*ws = waitValue(s) *ws = waitValue(s)
return nil return nil
case "true": case "true":
@ -89,11 +89,11 @@ func (ws *waitValue) Set(s string) error {
*ws = waitValue(kube.StatusWatcherStrategy) *ws = waitValue(kube.StatusWatcherStrategy)
return nil return nil
case "false": 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) *ws = waitValue(kube.HookOnlyStrategy)
return nil return nil
default: 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.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.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.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.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.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") 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 { RunE: func(cmd *cobra.Command, args []string) error {
registryClient, err := newRegistryClient(client.CertFile, client.KeyFile, client.CaFile, registryClient, err := newRegistryClient(client.CertFile, client.KeyFile, client.CaFile,
client.InsecureSkipTLSverify, client.PlainHTTP, client.Username, client.Password) client.InsecureSkipTLSVerify, client.PlainHTTP, client.Username, client.Password)
if err != nil { if err != nil {
return fmt.Errorf("missing registry client: %w", err) 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") 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. // If CheckDependencies returns an error, we have unfulfilled dependencies.
// As of Helm 2.4.0, this is treated as a stopping condition: // As of Helm 2.4.0, this is treated as a stopping condition:
// https://github.com/helm/helm/issues/2209 // 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", golden: "output/lint-chart-with-deprecated-api-strict.txt",
wantError: true, 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" // which is "20"
name: "lint chart with deprecated api version without kube version", name: "lint chart with deprecated api version without kube version",
cmd: fmt.Sprintf("lint %s", testChart), cmd: fmt.Sprintf("lint %s", testChart),

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

@ -76,12 +76,12 @@ func newPackageCmd(out io.Writer) *cobra.Command {
} }
registryClient, err := newRegistryClient(client.CertFile, client.KeyFile, client.CaFile, registryClient, err := newRegistryClient(client.CertFile, client.KeyFile, client.CaFile,
client.InsecureSkipTLSverify, client.PlainHTTP, client.Username, client.Password) client.InsecureSkipTLSVerify, client.PlainHTTP, client.Username, client.Password)
if err != nil { if err != nil {
return fmt.Errorf("missing registry client: %w", err) 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]) path, err := filepath.Abs(args[i])
if err != nil { if err != nil {
return err 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.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.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.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.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") 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 certFile string
keyFile string keyFile string
caFile string caFile string
insecureSkipTLSverify bool insecureSkipTLSVerify bool
plainHTTP bool plainHTTP bool
password string password string
username 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.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.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().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().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.username, "username", "", "registry username")
cmd.Flags().StringVar(&o.password, "password", "", "registry password") cmd.Flags().StringVar(&o.password, "password", "", "registry password")
@ -106,7 +106,7 @@ func (o *pluginInstallOptions) newInstallerForSource() (installer.Installer, err
// Build getter options for OCI // Build getter options for OCI
options := []getter.Option{ options := []getter.Option{
getter.WithTLSClientConfig(o.certFile, o.keyFile, o.caFile), getter.WithTLSClientConfig(o.certFile, o.keyFile, o.caFile),
getter.WithInsecureSkipVerifyTLS(o.insecureSkipTLSverify), getter.WithInsecureSkipVerifyTLS(o.insecureSkipTLSVerify),
getter.WithPlainHTTP(o.plainHTTP), getter.WithPlainHTTP(o.plainHTTP),
getter.WithBasicAuth(o.username, o.password), 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 { func (o *pluginInstallOptions) run(out io.Writer) error {
installer.Debug = settings.Debug
i, err := o.newInstallerForSource() i, err := o.newInstallerForSource()
if err != nil { if err != nil {
return err return err

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

@ -61,7 +61,6 @@ func (o *pluginUpdateOptions) complete(args []string) error {
} }
func (o *pluginUpdateOptions) run(out io.Writer) error { func (o *pluginUpdateOptions) run(out io.Writer) error {
installer.Debug = settings.Debug
slog.Debug("loading installed plugins", "path", settings.PluginsDirectory) slog.Debug("loading installed plugins", "path", settings.PluginsDirectory)
plugins, err := plugin.LoadAll(settings.PluginsDirectory) plugins, err := plugin.LoadAll(settings.PluginsDirectory)
if err != nil { 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, registryClient, err := newRegistryClient(client.CertFile, client.KeyFile, client.CaFile,
client.InsecureSkipTLSverify, client.PlainHTTP, client.Username, client.Password) client.InsecureSkipTLSVerify, client.PlainHTTP, client.Username, client.Password)
if err != nil { if err != nil {
return fmt.Errorf("missing registry client: %w", err) return fmt.Errorf("missing registry client: %w", err)
} }
client.SetRegistryClient(registryClient) client.SetRegistryClient(registryClient)
for i := 0; i < len(args); i++ { for i := range args {
output, err := client.Run(args[i]) output, err := client.Run(args[i])
if err != nil { if err != nil {
return err return err

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

@ -38,7 +38,7 @@ type registryPushOptions struct {
certFile string certFile string
keyFile string keyFile string
caFile string caFile string
insecureSkipTLSverify bool insecureSkipTLSVerify bool
plainHTTP bool plainHTTP bool
password string password string
username string username string
@ -71,7 +71,7 @@ func newPushCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
}, },
RunE: func(_ *cobra.Command, args []string) error { RunE: func(_ *cobra.Command, args []string) error {
registryClient, err := newRegistryClient( 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 { if err != nil {
@ -82,7 +82,7 @@ func newPushCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
remote := args[1] remote := args[1]
client := action.NewPushWithOpts(action.WithPushConfig(cfg), client := action.NewPushWithOpts(action.WithPushConfig(cfg),
action.WithTLSClientConfig(o.certFile, o.keyFile, o.caFile), action.WithTLSClientConfig(o.certFile, o.keyFile, o.caFile),
action.WithInsecureSkipTLSVerify(o.insecureSkipTLSverify), action.WithInsecureSkipTLSVerify(o.insecureSkipTLSVerify),
action.WithPlainHTTP(o.plainHTTP), action.WithPlainHTTP(o.plainHTTP),
action.WithPushOptWriter(out)) action.WithPushOptWriter(out))
client.Settings = settings 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.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.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.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.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.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") f.StringVar(&o.password, "password", "", "chart repository password where to locate the requested chart")

@ -57,7 +57,7 @@ type repoAddOptions struct {
certFile string certFile string
keyFile string keyFile string
caFile string caFile string
insecureSkipTLSverify bool insecureSkipTLSVerify bool
repoFile string repoFile string
repoCache 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.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.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.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.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.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") 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, CertFile: o.certFile,
KeyFile: o.keyFile, KeyFile: o.keyFile,
CAFile: o.caFile, CAFile: o.caFile,
InsecureSkipTLSverify: o.insecureSkipTLSverify, InsecureSkipTLSVerify: o.insecureSkipTLSVerify,
} }
// Check if the repo name is legal // Check if the repo name is legal

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