Merge branch 'main' into em/ignore-git-dirs

Signed-off-by: Evans Mungai <mbuevans@gmail.com>
pull/31504/head
Evans Mungai 5 months ago committed by GitHub
commit 002ac7844e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

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

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

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

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

@ -16,11 +16,11 @@ permissions: read-all
# job is triggered by a tag push, VERSION should be the tag ref.
jobs:
release:
if: startsWith(github.ref, 'refs/tags/v')
if: startsWith(github.ref, 'refs/tags/v') && github.repository == 'helm/helm'
runs-on: ubuntu-latest-16-cores
steps:
- name: Checkout source code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # pin@v5.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # pin@v6.0.1
with:
fetch-depth: 0
@ -28,9 +28,10 @@ jobs:
run: cat ".github/env" >> "$GITHUB_ENV"
- name: Setup Go
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # pin@5.5.0
uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # pin@6.1.0
with:
go-version: '${{ env.GOLANG_VERSION }}'
check-latest: true
- name: Run unit tests
run: make test-coverage
- name: Build Helm Binaries
@ -81,16 +82,16 @@ jobs:
canary-release:
runs-on: ubuntu-latest-16-cores
if: github.ref == 'refs/heads/main'
if: github.ref == 'refs/heads/main' && github.repository == 'helm/helm'
steps:
- name: Checkout source code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # pin@v5.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # pin@v6.0.1
- name: Add variables to environment file
run: cat ".github/env" >> "$GITHUB_ENV"
- name: Setup Go
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # pin@5.5.0
uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # pin@6.1.0
with:
go-version: '${{ env.GOLANG_VERSION }}'
check-latest: true

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

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

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

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

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

@ -1,4 +1,5 @@
maintainers:
- banjoh
- gjenkins8
- joejulian
- marckhouzam
@ -7,9 +8,8 @@ maintainers:
- sabre1041
- scottrigby
- technosophos
triage:
- banjoh
- TerryHowe
triage:
- yxxhero
- zonggen
- z4ce

@ -5,7 +5,7 @@
[![GoDoc](https://img.shields.io/static/v1?label=godoc&message=reference&color=blue)](https://pkg.go.dev/helm.sh/helm/v4)
[![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/3131/badge)](https://bestpractices.coreinfrastructure.org/projects/3131)
[![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/helm/helm/badge)](https://scorecard.dev/viewer/?uri=github.com/helm/helm)
[![LFX Health Score](https://insights.production.lfx.dev/api/badge/health-score?project=helm)](https://insights.linuxfoundation.org/project/helm)
[![LFX Health Score](https://insights.linuxfoundation.org/api/badge/health-score?project=helm)](https://insights.linuxfoundation.org/project/helm)
Helm is a tool for managing Charts. Charts are packages of pre-configured Kubernetes resources.
@ -49,6 +49,7 @@ If you want to use a package manager:
- [Scoop](https://scoop.sh/) users can use `scoop install helm`.
- [Snapcraft](https://snapcraft.io/) users can use `snap install helm --classic`.
- [Flox](https://flox.dev) users can use `flox install kubernetes-helm`.
- [Mise-en-place](https://mise.jdx.dev/) users can use `mise use -g helm@latest`
To rapidly get Helm up and running, start with the [Quick Start Guide](https://helm.sh/docs/intro/quickstart/).

@ -1,10 +1,10 @@
module helm.sh/helm/v4
go 1.24.0
go 1.25.0
require (
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24
github.com/BurntSushi/toml v1.5.0
github.com/BurntSushi/toml v1.6.0
github.com/DATA-DOG/go-sqlmock v1.5.2
github.com/Masterminds/semver/v3 v3.4.0
github.com/Masterminds/sprig/v3 v3.3.0
@ -12,42 +12,41 @@ require (
github.com/Masterminds/vcs v1.13.3
github.com/ProtonMail/go-crypto v1.3.0
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2
github.com/cyphar/filepath-securejoin v0.6.0
github.com/cyphar/filepath-securejoin v0.6.1
github.com/distribution/distribution/v3 v3.0.0
github.com/evanphx/json-patch/v5 v5.9.11
github.com/extism/go-sdk v1.7.1
github.com/fatih/color v1.18.0
github.com/fluxcd/cli-utils v0.36.0-flux.14
github.com/foxcpp/go-mockdns v1.1.0
github.com/fluxcd/cli-utils v0.37.0-flux.1
github.com/foxcpp/go-mockdns v1.2.0
github.com/gobwas/glob v0.2.3
github.com/gofrs/flock v0.13.0
github.com/gosuri/uitable v0.0.4
github.com/jmoiron/sqlx v1.4.0
github.com/lib/pq v1.10.9
github.com/mattn/go-shellwords v1.0.12
github.com/mitchellh/copystructure v1.2.0
github.com/moby/term v0.5.2
github.com/opencontainers/go-digest v1.0.0
github.com/opencontainers/image-spec v1.1.1
github.com/rubenv/sql-migrate v1.8.0
github.com/rubenv/sql-migrate v1.8.1
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2
github.com/spf13/cobra v1.10.1
github.com/spf13/cobra v1.10.2
github.com/spf13/pflag v1.0.10
github.com/stretchr/testify v1.11.1
github.com/tetratelabs/wazero v1.10.1
github.com/tetratelabs/wazero v1.11.0
go.yaml.in/yaml/v3 v3.0.4
golang.org/x/crypto v0.43.0
golang.org/x/term v0.37.0
golang.org/x/text v0.30.0
golang.org/x/crypto v0.46.0
golang.org/x/term v0.38.0
golang.org/x/text v0.32.0
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/api v0.34.1
k8s.io/apiextensions-apiserver v0.34.1
k8s.io/apimachinery v0.34.1
k8s.io/apiserver v0.34.1
k8s.io/cli-runtime v0.34.1
k8s.io/client-go v0.34.1
k8s.io/api v0.35.0
k8s.io/apiextensions-apiserver v0.35.0
k8s.io/apimachinery v0.35.0
k8s.io/apiserver v0.35.0
k8s.io/cli-runtime v0.35.0
k8s.io/client-go v0.35.0
k8s.io/klog/v2 v2.130.1
k8s.io/kubectl v0.34.1
k8s.io/kubectl v0.35.0
oras.land/oras-go/v2 v2.6.0
sigs.k8s.io/controller-runtime v0.22.4
sigs.k8s.io/kustomize/kyaml v0.21.0
@ -86,14 +85,12 @@ require (
github.com/go-openapi/jsonpointer v0.21.1 // indirect
github.com/go-openapi/jsonreference v0.21.0 // indirect
github.com/go-openapi/swag v0.23.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/google/btree v1.1.3 // indirect
github.com/google/gnostic-models v0.7.0 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/handlers v1.5.2 // indirect
github.com/gorilla/mux v1.8.1 // indirect
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect
github.com/hashicorp/golang-lru/arc/v2 v2.0.5 // indirect
@ -112,21 +109,19 @@ require (
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.9 // indirect
github.com/miekg/dns v1.1.57 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/moby/spdystream v0.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect
github.com/onsi/gomega v1.37.0 // indirect
github.com/onsi/gomega v1.38.2 // indirect
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_golang v1.22.0 // indirect
github.com/prometheus/client_golang v1.23.2 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.65.0 // indirect
github.com/prometheus/common v0.66.1 // indirect
github.com/prometheus/procfs v0.17.0 // indirect
github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5 // indirect
github.com/redis/go-redis/extra/redisotel/v9 v9.0.5 // indirect
@ -141,7 +136,7 @@ require (
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/bridges/prometheus v0.57.0 // indirect
go.opentelemetry.io/contrib/exporters/autoexport v0.57.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
go.opentelemetry.io/otel v1.37.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.8.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.8.0 // indirect
@ -156,30 +151,30 @@ require (
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.32.0 // indirect
go.opentelemetry.io/otel/log v0.8.0 // indirect
go.opentelemetry.io/otel/metric v1.37.0 // indirect
go.opentelemetry.io/otel/sdk v1.34.0 // indirect
go.opentelemetry.io/otel/sdk v1.36.0 // indirect
go.opentelemetry.io/otel/sdk/log v0.8.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.34.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.36.0 // indirect
go.opentelemetry.io/otel/trace v1.37.0 // indirect
go.opentelemetry.io/proto/otlp v1.5.0 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect
golang.org/x/mod v0.28.0 // indirect
golang.org/x/net v0.45.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
golang.org/x/mod v0.30.0 // indirect
golang.org/x/net v0.47.0 // indirect
golang.org/x/oauth2 v0.30.0 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.39.0 // indirect
golang.org/x/time v0.12.0 // indirect
golang.org/x/tools v0.37.0 // indirect
golang.org/x/tools v0.39.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb // indirect
google.golang.org/grpc v1.72.1 // indirect
google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a // indirect
google.golang.org/grpc v1.72.2 // indirect
google.golang.org/protobuf v1.36.8 // indirect
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
k8s.io/component-base v0.34.1 // indirect
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b // indirect
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect
k8s.io/component-base v0.35.0 // indirect
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
sigs.k8s.io/kustomize/api v0.20.1 // indirect
sigs.k8s.io/randfill v1.0.0 // indirect
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect

186
go.sum

@ -6,8 +6,8 @@ github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
@ -26,8 +26,6 @@ github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBi
github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
@ -59,8 +57,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/cyphar/filepath-securejoin v0.6.0 h1:BtGB77njd6SVO6VztOHfPxKitJvd/VPT+OFBFMOi1Is=
github.com/cyphar/filepath-securejoin v0.6.0/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc=
github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE=
github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
@ -93,10 +91,10 @@ github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fluxcd/cli-utils v0.36.0-flux.14 h1:I//AMVUXTc+M04UtIXArMXQZCazGMwfemodV1j/yG8c=
github.com/fluxcd/cli-utils v0.36.0-flux.14/go.mod h1:uDo7BYOfbdmk/asnHuI0IQPl6u0FCgcN54AHDu3Y5As=
github.com/foxcpp/go-mockdns v1.1.0 h1:jI0rD8M0wuYAxL7r/ynTrCQQq0BVqfB99Vgk7DlmewI=
github.com/foxcpp/go-mockdns v1.1.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk=
github.com/fluxcd/cli-utils v0.37.0-flux.1 h1:k/VvPNT3tGa/l2N+qzHduaQr3GVbgoWS6nw7tGZz16w=
github.com/fluxcd/cli-utils v0.37.0-flux.1/go.mod h1:aND5wX3LuTFtB7eUT7vsWr8mmxRVSPR2Wkvbn0SqPfw=
github.com/foxcpp/go-mockdns v1.2.0 h1:omK3OrHRD1IWJz1FuFBCFquhXslXoF17OvBS6JPzZF0=
github.com/foxcpp/go-mockdns v1.2.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
@ -132,8 +130,6 @@ github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5x
github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=
github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@ -155,8 +151,6 @@ github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyE
github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo=
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA=
github.com/gosuri/uitable v0.0.4 h1:IG2xLKRvErL3uhY6e1BylFzG+aJiwQviDDTfOKeKTpY=
github.com/gosuri/uitable v0.0.4/go.mod h1:tKR86bXuXPZazfOTG1FIzvjIdXzd0mo4Vtn16vt0PJo=
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA=
@ -182,8 +176,6 @@ github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/u
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
@ -225,8 +217,6 @@ github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQ
github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU=
github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI=
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@ -242,12 +232,10 @@ github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus=
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus=
github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8=
github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y=
github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0=
github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns=
github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo=
github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A=
github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
@ -255,8 +243,6 @@ github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgr
github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI=
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@ -265,16 +251,16 @@ github.com/poy/onpar v1.1.2/go.mod h1:6X8FLNoxyr9kkmnlqpK6LSoiOtrO6MICtWwEuWkLjz
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g=
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc=
github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
@ -289,8 +275,8 @@ github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0
github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rubenv/sql-migrate v1.8.0 h1:dXnYiJk9k3wetp7GfQbKJcPHjVJL6YK19tKj8t2Ns0o=
github.com/rubenv/sql-migrate v1.8.0/go.mod h1:F2bGFBwCU+pnmbtNYDeKvSuvL6lBVtXDXUUv5t+u1qw=
github.com/rubenv/sql-migrate v1.8.1 h1:EPNwCvjAowHI3TnZ+4fQu3a915OpnQoPAjTXCGOy2U0=
github.com/rubenv/sql-migrate v1.8.1/go.mod h1:BTIKBORjzyxZDS6dzoiw6eAFYJ1iNlGAtjn4LGeVjS8=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ=
@ -304,8 +290,8 @@ github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w=
github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
@ -321,14 +307,12 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834 h1:ZF+QBjOI+tILZjBaFj3HgFonKXUcwgJ4djLb6i42S3Q=
github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834/go.mod h1:m9ymHTgNSEjuxvw8E7WWe4Pl4hZQHXONY8wE6dMLaRk=
github.com/tetratelabs/wazero v1.10.1 h1:2DugeJf6VVk58KTPszlNfeeN8AhhpwcZqkJj2wwFuH8=
github.com/tetratelabs/wazero v1.10.1/go.mod h1:DRm5twOQ5Gr1AoEdSi0CLjDQF1J9ZAuyqFIjl1KKfQU=
github.com/tetratelabs/wazero v1.11.0 h1:+gKemEuKCTevU4d7ZTzlsvgd1uaToIDtlQlmNbwqYhA=
github.com/tetratelabs/wazero v1.11.0/go.mod h1:eV28rsN8Q+xwjogd7f4/Pp4xFxO7uOGbLcD/LzB1wiU=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ=
github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
@ -336,8 +320,8 @@ go.opentelemetry.io/contrib/bridges/prometheus v0.57.0 h1:UW0+QyeyBVhn+COBec3nGh
go.opentelemetry.io/contrib/bridges/prometheus v0.57.0/go.mod h1:ppciCHRLsyCio54qbzQv0E4Jyth/fLWDTJYfvWpcSVk=
go.opentelemetry.io/contrib/exporters/autoexport v0.57.0 h1:jmTVJ86dP60C01K3slFQa2NQ/Aoi7zA+wy7vMOKD9H4=
go.opentelemetry.io/contrib/exporters/autoexport v0.57.0/go.mod h1:EJBheUMttD/lABFyLXhce47Wr6DPWYReCzaZiXadH7g=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.8.0 h1:WzNab7hOOLzdDF/EoWCt4glhrbMPVMOO5JYTmpz36Ls=
@ -366,52 +350,43 @@ go.opentelemetry.io/otel/log v0.8.0 h1:egZ8vV5atrUWUbnSsHn6vB8R21G2wrKqNiDt3iWer
go.opentelemetry.io/otel/log v0.8.0/go.mod h1:M9qvDdUTRCopJcGRKg57+JSQ9LgLBrwwfC32epk5NX8=
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A=
go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs=
go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY=
go.opentelemetry.io/otel/sdk/log v0.8.0 h1:zg7GUYXqxk1jnGF/dTdLPrK06xJdrXgqgFLnI4Crxvs=
go.opentelemetry.io/otel/sdk/log v0.8.0/go.mod h1:50iXr0UVwQrYS45KbruFrEt4LvAdCaWWgIrsN3ZQggo=
go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk=
go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis=
go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4=
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4=
go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4=
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g=
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
@ -419,28 +394,24 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ=
golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM=
golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@ -454,8 +425,8 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@ -463,8 +434,8 @@ golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
golang.org/x/term v0.14.0/go.mod h1:TySc+nGkYR6qt8km8wUhuFRTVSMIX3XPR58y2lC8vww=
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
@ -472,38 +443,33 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.15.0/go.mod h1:hpksKq4dtpQWS1uQ61JkdqWM3LscIS6Slf+VVkm+wQk=
golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb h1:p31xT4yrYrSM/G4Sn2+TNUkVhFCbG9y8itM2S6Th950=
google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:jbe3Bkdp+Dh2IrslsFCklNhweNTBgSYanP1UXhJDhKg=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb h1:TLPQVbx1GJ8VKZxz52VAxl1EBgKXXbTiU9Fc5fZeLn4=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I=
google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA=
google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a h1:v2PbRU4K3llS09c7zodFpNePeamkAwG3mPrAery9VeE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/grpc v1.72.2 h1:TdbGzwb82ty4OusHWepvFWGLgIbNo1/SUynEN0ssqv8=
google.golang.org/grpc v1.72.2/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4=
gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo=
gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
@ -512,34 +478,34 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
k8s.io/api v0.34.1 h1:jC+153630BMdlFukegoEL8E/yT7aLyQkIVuwhmwDgJM=
k8s.io/api v0.34.1/go.mod h1:SB80FxFtXn5/gwzCoN6QCtPD7Vbu5w2n1S0J5gFfTYk=
k8s.io/apiextensions-apiserver v0.34.1 h1:NNPBva8FNAPt1iSVwIE0FsdrVriRXMsaWFMqJbII2CI=
k8s.io/apiextensions-apiserver v0.34.1/go.mod h1:hP9Rld3zF5Ay2Of3BeEpLAToP+l4s5UlxiHfqRaRcMc=
k8s.io/apimachinery v0.34.1 h1:dTlxFls/eikpJxmAC7MVE8oOeP1zryV7iRyIjB0gky4=
k8s.io/apimachinery v0.34.1/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw=
k8s.io/apiserver v0.34.1 h1:U3JBGdgANK3dfFcyknWde1G6X1F4bg7PXuvlqt8lITA=
k8s.io/apiserver v0.34.1/go.mod h1:eOOc9nrVqlBI1AFCvVzsob0OxtPZUCPiUJL45JOTBG0=
k8s.io/cli-runtime v0.34.1 h1:btlgAgTrYd4sk8vJTRG6zVtqBKt9ZMDeQZo2PIzbL7M=
k8s.io/cli-runtime v0.34.1/go.mod h1:aVA65c+f0MZiMUPbseU/M9l1Wo2byeaGwUuQEQVVveE=
k8s.io/client-go v0.34.1 h1:ZUPJKgXsnKwVwmKKdPfw4tB58+7/Ik3CrjOEhsiZ7mY=
k8s.io/client-go v0.34.1/go.mod h1:kA8v0FP+tk6sZA0yKLRG67LWjqufAoSHA2xVGKw9Of8=
k8s.io/component-base v0.34.1 h1:v7xFgG+ONhytZNFpIz5/kecwD+sUhVE6HU7qQUiRM4A=
k8s.io/component-base v0.34.1/go.mod h1:mknCpLlTSKHzAQJJnnHVKqjxR7gBeHRv0rPXA7gdtQ0=
k8s.io/api v0.35.0 h1:iBAU5LTyBI9vw3L5glmat1njFK34srdLmktWwLTprlY=
k8s.io/api v0.35.0/go.mod h1:AQ0SNTzm4ZAczM03QH42c7l3bih1TbAXYo0DkF8ktnA=
k8s.io/apiextensions-apiserver v0.35.0 h1:3xHk2rTOdWXXJM+RDQZJvdx0yEOgC0FgQ1PlJatA5T4=
k8s.io/apiextensions-apiserver v0.35.0/go.mod h1:E1Ahk9SADaLQ4qtzYFkwUqusXTcaV2uw3l14aqpL2LU=
k8s.io/apimachinery v0.35.0 h1:Z2L3IHvPVv/MJ7xRxHEtk6GoJElaAqDCCU0S6ncYok8=
k8s.io/apimachinery v0.35.0/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns=
k8s.io/apiserver v0.35.0 h1:CUGo5o+7hW9GcAEF3x3usT3fX4f9r8xmgQeCBDaOgX4=
k8s.io/apiserver v0.35.0/go.mod h1:QUy1U4+PrzbJaM3XGu2tQ7U9A4udRRo5cyxkFX0GEds=
k8s.io/cli-runtime v0.35.0 h1:PEJtYS/Zr4p20PfZSLCbY6YvaoLrfByd6THQzPworUE=
k8s.io/cli-runtime v0.35.0/go.mod h1:VBRvHzosVAoVdP3XwUQn1Oqkvaa8facnokNkD7jOTMY=
k8s.io/client-go v0.35.0 h1:IAW0ifFbfQQwQmga0UdoH0yvdqrbwMdq9vIFEhRpxBE=
k8s.io/client-go v0.35.0/go.mod h1:q2E5AAyqcbeLGPdoRB+Nxe3KYTfPce1Dnu1myQdqz9o=
k8s.io/component-base v0.35.0 h1:+yBrOhzri2S1BVqyVSvcM3PtPyx5GUxCK2tinZz1G94=
k8s.io/component-base v0.35.0/go.mod h1:85SCX4UCa6SCFt6p3IKAPej7jSnF3L8EbfSyMZayJR0=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b h1:MloQ9/bdJyIu9lb1PzujOPolHyvO06MXG5TUIj2mNAA=
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts=
k8s.io/kubectl v0.34.1 h1:1qP1oqT5Xc93K+H8J7ecpBjaz511gan89KO9Vbsh/OI=
k8s.io/kubectl v0.34.1/go.mod h1:JRYlhJpGPyk3dEmJ+BuBiOB9/dAvnrALJEiY/C5qa6A=
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y=
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE=
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ=
k8s.io/kubectl v0.35.0 h1:cL/wJKHDe8E8+rP3G7avnymcMg6bH6JEcR5w5uo06wc=
k8s.io/kubectl v0.35.0/go.mod h1:VR5/TSkYyxZwrRwY5I5dDq6l5KXmiCb+9w8IKplk3Qo=
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck=
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
oras.land/oras-go/v2 v2.6.0 h1:X4ELRsiGkrbeox69+9tzTu492FMUu7zJQW6eJU+I2oc=
oras.land/oras-go/v2 v2.6.0/go.mod h1:magiQDfG6H1O9APp+rOsvCPcW1GD2MM7vgnKY0Y+u1o=
sigs.k8s.io/controller-runtime v0.22.4 h1:GEjV7KV3TY8e+tJ2LCTxUTanW4z/FmNB7l327UfMq9A=
sigs.k8s.io/controller-runtime v0.22.4/go.mod h1:+QX1XUpTXN4mLoblf4tqr5CQcyHPAki2HLXqQMY6vh8=
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE=
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
sigs.k8s.io/kustomize/api v0.20.1 h1:iWP1Ydh3/lmldBnH/S5RXgT98vWYMaTUL1ADcr+Sv7I=
sigs.k8s.io/kustomize/api v0.20.1/go.mod h1:t6hUFxO+Ph0VxIk1sKp1WS0dOjbPCtLJ4p8aADLwqjM=
sigs.k8s.io/kustomize/kyaml v0.21.0 h1:7mQAf3dUwf0wBerWJd8rXhVcnkk5Tvn/q91cGkaP6HQ=

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

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

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

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

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

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

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

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

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

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

@ -0,0 +1,120 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package copystructure
import (
"fmt"
"reflect"
)
// Copy performs a deep copy of the given src.
// This implementation handles the specific use cases needed by Helm.
func Copy(src any) (any, error) {
if src == nil {
return make(map[string]any), nil
}
return copyValue(reflect.ValueOf(src))
}
// copyValue handles copying using reflection for non-map types
func copyValue(original reflect.Value) (any, error) {
switch original.Kind() {
case reflect.Bool, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32,
reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32,
reflect.Uint64, reflect.Uintptr, reflect.Float32, reflect.Float64,
reflect.Complex64, reflect.Complex128, reflect.String, reflect.Array:
return original.Interface(), nil
case reflect.Interface:
if original.IsNil() {
return original.Interface(), nil
}
return copyValue(original.Elem())
case reflect.Map:
if original.IsNil() {
return original.Interface(), nil
}
copied := reflect.MakeMap(original.Type())
var err error
var child any
iter := original.MapRange()
for iter.Next() {
key := iter.Key()
value := iter.Value()
if value.Kind() == reflect.Interface && value.IsNil() {
copied.SetMapIndex(key, value)
continue
}
child, err = copyValue(value)
if err != nil {
return nil, err
}
copied.SetMapIndex(key, reflect.ValueOf(child))
}
return copied.Interface(), nil
case reflect.Pointer:
if original.IsNil() {
return original.Interface(), nil
}
copied, err := copyValue(original.Elem())
if err != nil {
return nil, err
}
ptr := reflect.New(original.Type().Elem())
ptr.Elem().Set(reflect.ValueOf(copied))
return ptr.Interface(), nil
case reflect.Slice:
if original.IsNil() {
return original.Interface(), nil
}
copied := reflect.MakeSlice(original.Type(), original.Len(), original.Cap())
for i := 0; i < original.Len(); i++ {
val, err := copyValue(original.Index(i))
if err != nil {
return nil, err
}
copied.Index(i).Set(reflect.ValueOf(val))
}
return copied.Interface(), nil
case reflect.Struct:
copied := reflect.New(original.Type()).Elem()
for i := 0; i < original.NumField(); i++ {
elem, err := copyValue(original.Field(i))
if err != nil {
return nil, err
}
copied.Field(i).Set(reflect.ValueOf(elem))
}
return copied.Interface(), nil
case reflect.Func, reflect.Chan, reflect.UnsafePointer:
if original.IsNil() {
return original.Interface(), nil
}
return original.Interface(), nil
default:
return original.Interface(), fmt.Errorf("unsupported type %v", original)
}
}

@ -0,0 +1,374 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package copystructure
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestCopy_Nil(t *testing.T) {
result, err := Copy(nil)
require.NoError(t, err)
assert.Equal(t, map[string]any{}, result)
}
func TestCopy_PrimitiveTypes(t *testing.T) {
tests := []struct {
name string
input any
}{
{"bool", true},
{"int", 42},
{"int8", int8(8)},
{"int16", int16(16)},
{"int32", int32(32)},
{"int64", int64(64)},
{"uint", uint(42)},
{"uint8", uint8(8)},
{"uint16", uint16(16)},
{"uint32", uint32(32)},
{"uint64", uint64(64)},
{"float32", float32(3.14)},
{"float64", 3.14159},
{"complex64", complex64(1 + 2i)},
{"complex128", 1 + 2i},
{"string", "hello world"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := Copy(tt.input)
require.NoError(t, err)
assert.Equal(t, tt.input, result)
})
}
}
func TestCopy_Array(t *testing.T) {
input := [3]int{1, 2, 3}
result, err := Copy(input)
require.NoError(t, err)
assert.Equal(t, input, result)
}
func TestCopy_Slice(t *testing.T) {
t.Run("slice of ints", func(t *testing.T) {
input := []int{1, 2, 3, 4, 5}
result, err := Copy(input)
require.NoError(t, err)
resultSlice, ok := result.([]int)
require.True(t, ok)
assert.Equal(t, input, resultSlice)
// Verify it's a deep copy by modifying original
input[0] = 999
assert.Equal(t, 1, resultSlice[0])
})
t.Run("slice of strings", func(t *testing.T) {
input := []string{"a", "b", "c"}
result, err := Copy(input)
require.NoError(t, err)
assert.Equal(t, input, result)
})
t.Run("nil slice", func(t *testing.T) {
var input []int
result, err := Copy(input)
require.NoError(t, err)
assert.Nil(t, result)
})
t.Run("slice of maps", func(t *testing.T) {
input := []map[string]any{
{"key1": "value1"},
{"key2": "value2"},
}
result, err := Copy(input)
require.NoError(t, err)
resultSlice, ok := result.([]map[string]any)
require.True(t, ok)
assert.Equal(t, input, resultSlice)
// Verify deep copy
input[0]["key1"] = "modified"
assert.Equal(t, "value1", resultSlice[0]["key1"])
})
}
func TestCopy_Map(t *testing.T) {
t.Run("map[string]any", func(t *testing.T) {
input := map[string]any{
"string": "value",
"int": 42,
"bool": true,
"nested": map[string]any{
"inner": "value",
},
}
result, err := Copy(input)
require.NoError(t, err)
resultMap, ok := result.(map[string]any)
require.True(t, ok)
assert.Equal(t, input, resultMap)
// Verify deep copy
input["string"] = "modified"
assert.Equal(t, "value", resultMap["string"])
nestedInput := input["nested"].(map[string]any)
nestedResult := resultMap["nested"].(map[string]any)
nestedInput["inner"] = "modified"
assert.Equal(t, "value", nestedResult["inner"])
})
t.Run("map[string]string", func(t *testing.T) {
input := map[string]string{
"key1": "value1",
"key2": "value2",
}
result, err := Copy(input)
require.NoError(t, err)
assert.Equal(t, input, result)
})
t.Run("nil map", func(t *testing.T) {
var input map[string]any
result, err := Copy(input)
require.NoError(t, err)
assert.Nil(t, result)
})
t.Run("map with nil values", func(t *testing.T) {
input := map[string]any{
"key1": "value1",
"key2": nil,
}
result, err := Copy(input)
require.NoError(t, err)
resultMap, ok := result.(map[string]any)
require.True(t, ok)
assert.Equal(t, input, resultMap)
assert.Nil(t, resultMap["key2"])
})
}
func TestCopy_Struct(t *testing.T) {
type TestStruct struct {
Name string
Age int
Active bool
Scores []int
Metadata map[string]any
}
input := TestStruct{
Name: "John",
Age: 30,
Active: true,
Scores: []int{95, 87, 92},
Metadata: map[string]any{
"level": "advanced",
"tags": []string{"go", "programming"},
},
}
result, err := Copy(input)
require.NoError(t, err)
resultStruct, ok := result.(TestStruct)
require.True(t, ok)
assert.Equal(t, input, resultStruct)
// Verify deep copy
input.Name = "Modified"
input.Scores[0] = 999
assert.Equal(t, "John", resultStruct.Name)
assert.Equal(t, 95, resultStruct.Scores[0])
}
func TestCopy_Pointer(t *testing.T) {
t.Run("pointer to int", func(t *testing.T) {
value := 42
input := &value
result, err := Copy(input)
require.NoError(t, err)
resultPtr, ok := result.(*int)
require.True(t, ok)
assert.Equal(t, *input, *resultPtr)
// Verify they point to different memory locations
assert.NotSame(t, input, resultPtr)
// Verify deep copy
*input = 999
assert.Equal(t, 42, *resultPtr)
})
t.Run("pointer to struct", func(t *testing.T) {
type Person struct {
Name string
Age int
}
input := &Person{Name: "Alice", Age: 25}
result, err := Copy(input)
require.NoError(t, err)
resultPtr, ok := result.(*Person)
require.True(t, ok)
assert.Equal(t, *input, *resultPtr)
assert.NotSame(t, input, resultPtr)
})
t.Run("nil pointer", func(t *testing.T) {
var input *int
result, err := Copy(input)
require.NoError(t, err)
assert.Nil(t, result)
})
}
func TestCopy_Interface(t *testing.T) {
t.Run("any with value", func(t *testing.T) {
var input any = "hello"
result, err := Copy(input)
require.NoError(t, err)
assert.Equal(t, input, result)
})
t.Run("nil any", func(t *testing.T) {
var input any
result, err := Copy(input)
require.NoError(t, err)
// Copy(nil) returns an empty map according to the implementation
assert.Equal(t, map[string]any{}, result)
})
t.Run("any with complex value", func(t *testing.T) {
var input any = map[string]any{
"key": "value",
"nested": map[string]any{
"inner": 42,
},
}
result, err := Copy(input)
require.NoError(t, err)
assert.Equal(t, input, result)
})
}
func TestCopy_ComplexNested(t *testing.T) {
input := map[string]any{
"users": []map[string]any{
{
"name": "Alice",
"age": 30,
"addresses": []map[string]any{
{"type": "home", "city": "NYC"},
{"type": "work", "city": "SF"},
},
},
{
"name": "Bob",
"age": 25,
"addresses": []map[string]any{
{"type": "home", "city": "LA"},
},
},
},
"metadata": map[string]any{
"version": "1.0",
"flags": []bool{true, false, true},
},
}
result, err := Copy(input)
require.NoError(t, err)
resultMap, ok := result.(map[string]any)
require.True(t, ok)
assert.Equal(t, input, resultMap)
// Verify deep copy by modifying nested values
users := input["users"].([]map[string]any)
addresses := users[0]["addresses"].([]map[string]any)
addresses[0]["city"] = "Modified"
resultUsers := resultMap["users"].([]map[string]any)
resultAddresses := resultUsers[0]["addresses"].([]map[string]any)
assert.Equal(t, "NYC", resultAddresses[0]["city"])
}
func TestCopy_Functions(t *testing.T) {
t.Run("function", func(t *testing.T) {
input := func() string { return "hello" }
result, err := Copy(input)
require.NoError(t, err)
// Functions should be copied as-is (same reference)
resultFunc, ok := result.(func() string)
require.True(t, ok)
assert.Equal(t, input(), resultFunc())
})
t.Run("nil function", func(t *testing.T) {
var input func()
result, err := Copy(input)
require.NoError(t, err)
assert.Nil(t, result)
})
}
func TestCopy_Channels(t *testing.T) {
t.Run("channel", func(t *testing.T) {
input := make(chan int, 1)
input <- 42
result, err := Copy(input)
require.NoError(t, err)
// Channels should be copied as-is (same reference)
resultChan, ok := result.(chan int)
require.True(t, ok)
// Since channels are copied as references, verify we can read from the result channel
value := <-resultChan
assert.Equal(t, 42, value)
})
t.Run("nil channel", func(t *testing.T) {
var input chan int
result, err := Copy(input)
require.NoError(t, err)
assert.Nil(t, result)
})
}

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

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

@ -23,14 +23,13 @@ import (
"go.yaml.in/yaml/v3"
)
// Config represents an plugin type specific configuration
// It is expected to type assert (cast) the a Config to its expected underlying type (schema.ConfigCLIV1, schema.ConfigGetterV1, etc).
// Config represents a plugin type specific configuration
// It is expected to type assert (cast) the Config to its expected underlying type (schema.ConfigCLIV1, schema.ConfigGetterV1, etc).
type Config interface {
Validate() error
}
func unmarshaConfig(pluginType string, configData map[string]any) (Config, error) {
func unmarshalConfig(pluginType string, configData map[string]any) (Config, error) {
pluginTypeMeta, ok := pluginTypesIndex[pluginType]
if !ok {
return nil, fmt.Errorf("unknown plugin type %q", pluginType)

@ -27,7 +27,7 @@ import (
func TestUnmarshaConfig(t *testing.T) {
// Test unmarshalling a CLI plugin config
{
config, err := unmarshaConfig("cli/v1", map[string]any{
config, err := unmarshalConfig("cli/v1", map[string]any{
"usage": "usage string",
"shortHelp": "short help string",
"longHelp": "long help string",
@ -46,7 +46,7 @@ func TestUnmarshaConfig(t *testing.T) {
// Test unmarshalling invalid config data
{
config, err := unmarshaConfig("cli/v1", map[string]any{
config, err := unmarshalConfig("cli/v1", map[string]any{
"invalid field": "foo",
})
require.Error(t, err)

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

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

@ -84,7 +84,7 @@ func InstallWithOptions(i Installer, opts Options) (*VerificationResult, error)
return nil, err
}
if _, pathErr := os.Stat(i.Path()); !os.IsNotExist(pathErr) {
slog.Warn("plugin already exists", "path", i.Path(), slog.Any("error", pathErr))
slog.Warn("plugin already exists", slog.String("path", i.Path()), slog.Any("error", pathErr))
return nil, errors.New("plugin already exists")
}
@ -136,7 +136,7 @@ func InstallWithOptions(i Installer, opts Options) (*VerificationResult, error)
// Update updates a plugin.
func Update(i Installer) error {
if _, pathErr := os.Stat(i.Path()); os.IsNotExist(pathErr) {
slog.Warn("plugin does not exist", "path", i.Path(), slog.Any("error", pathErr))
slog.Warn("plugin does not exist", slog.String("path", i.Path()), slog.Any("error", pathErr))
return errors.New("plugin does not exist")
}
return i.Update()
@ -167,7 +167,11 @@ func NewForSource(source, version string) (installer Installer, err error) {
func FindSource(location string) (Installer, error) {
installer, err := existingVCSRepo(location)
if err != nil && err.Error() == "Cannot detect VCS" {
slog.Warn("cannot get information about plugin source", "location", location, slog.Any("error", err))
slog.Warn(
"cannot get information about plugin source",
slog.String("location", location),
slog.Any("error", err),
)
return installer, errors.New("cannot get information about plugin source")
}
return installer, err

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

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

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

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

@ -54,7 +54,7 @@ func (m Metadata) Validate() error {
var errs []error
if !validPluginName.MatchString(m.Name) {
errs = append(errs, fmt.Errorf("invalid name"))
errs = append(errs, fmt.Errorf("invalid plugin name %q: must contain only a-z, A-Z, 0-9, _ and -", m.Name))
}
if m.APIVersion == "" {
@ -174,13 +174,12 @@ func buildLegacyRuntimeConfig(m MetadataLegacy) RuntimeConfig {
}
func fromMetadataV1(mv1 MetadataV1) (*Metadata, error) {
config, err := unmarshaConfig(mv1.Type, mv1.Config)
config, err := unmarshalConfig(mv1.Type, mv1.Config)
if err != nil {
return nil, err
}
runtimeConfig, err := convertMetdataRuntimeConfig(mv1.Runtime, mv1.RuntimeConfig)
runtimeConfig, err := convertMetadataRuntimeConfig(mv1.Runtime, mv1.RuntimeConfig)
if err != nil {
return nil, err
}
@ -197,7 +196,7 @@ func fromMetadataV1(mv1 MetadataV1) (*Metadata, error) {
}, nil
}
func convertMetdataRuntimeConfig(runtimeType string, runtimeConfigRaw map[string]any) (RuntimeConfig, error) {
func convertMetadataRuntimeConfig(runtimeType string, runtimeConfigRaw map[string]any) (RuntimeConfig, error) {
var runtimeConfig RuntimeConfig
var err error

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

@ -0,0 +1,126 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package plugin
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestMetadataLegacyValidate(t *testing.T) {
testsValid := map[string]MetadataLegacy{
"valid metadata": {
Name: "myplugin",
},
"valid with command": {
Name: "myplugin",
Command: "echo hello",
},
"valid with platformCommand": {
Name: "myplugin",
PlatformCommand: []PlatformCommand{
{OperatingSystem: "linux", Architecture: "amd64", Command: "echo hello"},
},
},
"valid with hooks": {
Name: "myplugin",
Hooks: Hooks{
"install": "echo install",
},
},
"valid with platformHooks": {
Name: "myplugin",
PlatformHooks: PlatformHooks{
"install": []PlatformCommand{
{OperatingSystem: "linux", Architecture: "amd64", Command: "echo install"},
},
},
},
"valid with downloaders": {
Name: "myplugin",
Downloaders: []Downloaders{
{
Protocols: []string{"myproto"},
Command: "echo download",
},
},
},
}
for testName, metadata := range testsValid {
t.Run(testName, func(t *testing.T) {
assert.NoError(t, metadata.Validate())
})
}
testsInvalid := map[string]MetadataLegacy{
"invalid name": {
Name: "my plugin", // further tested in TestValidPluginName
},
"both command and platformCommand": {
Name: "myplugin",
Command: "echo hello",
PlatformCommand: []PlatformCommand{
{OperatingSystem: "linux", Architecture: "amd64", Command: "echo hello"},
},
},
"both hooks and platformHooks": {
Name: "myplugin",
Hooks: Hooks{
"install": "echo install",
},
PlatformHooks: PlatformHooks{
"install": []PlatformCommand{
{OperatingSystem: "linux", Architecture: "amd64", Command: "echo install"},
},
},
},
"downloader with empty command": {
Name: "myplugin",
Downloaders: []Downloaders{
{
Protocols: []string{"myproto"},
Command: "",
},
},
},
"downloader with no protocols": {
Name: "myplugin",
Downloaders: []Downloaders{
{
Protocols: []string{},
Command: "echo download",
},
},
},
"downloader with empty protocol": {
Name: "myplugin",
Downloaders: []Downloaders{
{
Protocols: []string{""},
Command: "echo download",
},
},
},
}
for testName, metadata := range testsInvalid {
t.Run(testName, func(t *testing.T) {
assert.Error(t, metadata.Validate())
})
}
}

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

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

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

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

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

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

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

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

@ -0,0 +1,44 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package version
import (
"fmt"
"runtime/debug"
"slices"
_ "k8s.io/client-go/kubernetes" // Force k8s.io/client-go to be included in the build
)
func K8sIOClientGoModVersion() (string, error) {
info, ok := debug.ReadBuildInfo()
if !ok {
return "", fmt.Errorf("failed to read build info")
}
idx := slices.IndexFunc(info.Deps, func(m *debug.Module) bool {
return m.Path == "k8s.io/client-go"
})
if idx == -1 {
return "", fmt.Errorf("k8s.io/client-go not found in build info")
}
m := info.Deps[idx]
return m.Version, nil
}

@ -0,0 +1,30 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package version
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestK8sClientGoModVersion(t *testing.T) {
// Unfortunately, test builds don't include debug info / module info
// So we expect "K8sIOClientGoModVersion" to return error
_, err := K8sIOClientGoModVersion()
require.ErrorContains(t, err, "k8s.io/client-go not found in build info")
}

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

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

@ -201,6 +201,12 @@ func withMetadataDependency(dependency chart.Dependency) chartOption {
}
}
func withFile(file common.File) chartOption {
return func(opts *chartOptions) {
opts.Files = append(opts.Files, &file)
}
}
func withSampleTemplates() chartOption {
return func(opts *chartOptions) {
modTime := time.Now()

@ -121,7 +121,7 @@ func TestGetMetadata_Run_WithDependencies(t *testing.T) {
Namespace: "default",
}
cfg.Releases.Create(rel)
require.NoError(t, cfg.Releases.Create(rel))
result, err := client.Run(releaseName)
require.NoError(t, err)
@ -180,7 +180,7 @@ func TestGetMetadata_Run_WithDependenciesAliases(t *testing.T) {
Namespace: "default",
}
cfg.Releases.Create(rel)
require.NoError(t, cfg.Releases.Create(rel))
result, err := client.Run(releaseName)
require.NoError(t, err)
@ -251,7 +251,7 @@ func TestGetMetadata_Run_WithMixedDependencies(t *testing.T) {
Namespace: "default",
}
cfg.Releases.Create(rel)
require.NoError(t, cfg.Releases.Create(rel))
result, err := client.Run(releaseName)
require.NoError(t, err)
@ -315,7 +315,7 @@ func TestGetMetadata_Run_WithAnnotations(t *testing.T) {
Namespace: "default",
}
cfg.Releases.Create(rel)
require.NoError(t, cfg.Releases.Create(rel))
result, err := client.Run(releaseName)
require.NoError(t, err)
@ -370,8 +370,8 @@ func TestGetMetadata_Run_SpecificVersion(t *testing.T) {
Namespace: "default",
}
cfg.Releases.Create(rel1)
cfg.Releases.Create(rel2)
require.NoError(t, cfg.Releases.Create(rel1))
require.NoError(t, cfg.Releases.Create(rel2))
result, err := client.Run(releaseName)
require.NoError(t, err)
@ -424,7 +424,7 @@ func TestGetMetadata_Run_DifferentStatuses(t *testing.T) {
Namespace: "default",
}
cfg.Releases.Create(rel)
require.NoError(t, cfg.Releases.Create(rel))
result, err := client.Run(releaseName)
require.NoError(t, err)
@ -480,7 +480,7 @@ func TestGetMetadata_Run_EmptyAppVersion(t *testing.T) {
Namespace: "default",
}
cfg.Releases.Create(rel)
require.NoError(t, cfg.Releases.Create(rel))
result, err := client.Run(releaseName)
require.NoError(t, err)

@ -0,0 +1,69 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package action
import (
"errors"
"io"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
kubefake "helm.sh/helm/v4/pkg/kube/fake"
"helm.sh/helm/v4/pkg/release/common"
)
func TestNewGet(t *testing.T) {
config := actionConfigFixture(t)
client := NewGet(config)
assert.NotNil(t, client)
assert.Equal(t, config, client.cfg)
assert.Equal(t, 0, client.Version)
}
func TestGetRun(t *testing.T) {
config := actionConfigFixture(t)
client := NewGet(config)
simpleRelease := namedReleaseStub("test-release", common.StatusPendingUpgrade)
require.NoError(t, config.Releases.Create(simpleRelease))
releaser, err := client.Run(simpleRelease.Name)
require.NoError(t, err)
result, err := releaserToV1Release(releaser)
require.NoError(t, err)
assert.Equal(t, simpleRelease.Name, result.Name)
assert.Equal(t, simpleRelease.Version, result.Version)
}
func TestGetRun_UnreachableKubeClient(t *testing.T) {
config := actionConfigFixture(t)
failingKubeClient := kubefake.FailingKubeClient{PrintingKubeClient: kubefake.PrintingKubeClient{Out: io.Discard}, DummyResources: nil}
failingKubeClient.ConnectionError = errors.New("connection refused")
config.KubeClient = &failingKubeClient
client := NewGet(config)
simpleRelease := namedReleaseStub("test-release", common.StatusPendingUpgrade)
require.NoError(t, config.Releases.Create(simpleRelease))
result, err := client.Run(simpleRelease.Name)
assert.Nil(t, result)
assert.Error(t, err)
}

@ -79,7 +79,7 @@ func TestGetValues_Run_UserConfigOnly(t *testing.T) {
Namespace: "default",
}
cfg.Releases.Create(rel)
require.NoError(t, cfg.Releases.Create(rel))
result, err := client.Run(releaseName)
require.NoError(t, err)
@ -127,7 +127,7 @@ func TestGetValues_Run_AllValues(t *testing.T) {
Namespace: "default",
}
cfg.Releases.Create(rel)
require.NoError(t, cfg.Releases.Create(rel))
result, err := client.Run(releaseName)
require.NoError(t, err)
@ -161,7 +161,7 @@ func TestGetValues_Run_EmptyValues(t *testing.T) {
Namespace: "default",
}
cfg.Releases.Create(rel)
require.NoError(t, cfg.Releases.Create(rel))
result, err := client.Run(releaseName)
require.NoError(t, err)
@ -212,7 +212,7 @@ func TestGetValues_Run_NilConfig(t *testing.T) {
Namespace: "default",
}
cfg.Releases.Create(rel)
require.NoError(t, cfg.Releases.Create(rel))
result, err := client.Run(releaseName)
require.NoError(t, err)

@ -20,7 +20,7 @@ import (
"fmt"
chartutil "helm.sh/helm/v4/pkg/chart/v2/util"
release "helm.sh/helm/v4/pkg/release"
"helm.sh/helm/v4/pkg/release"
)
// History is the action for checking the release's ledger.

@ -0,0 +1,108 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package action
import (
"errors"
"io"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
kubefake "helm.sh/helm/v4/pkg/kube/fake"
release "helm.sh/helm/v4/pkg/release/v1"
"helm.sh/helm/v4/pkg/release/common"
)
func TestNewHistory(t *testing.T) {
config := actionConfigFixture(t)
client := NewHistory(config)
assert.NotNil(t, client)
assert.Equal(t, config, client.cfg)
}
func TestHistoryRun(t *testing.T) {
releaseName := "test-release"
simpleRelease := namedReleaseStub(releaseName, common.StatusPendingUpgrade)
updatedRelease := namedReleaseStub(releaseName, common.StatusDeployed)
updatedRelease.Chart.Metadata.Version = "0.1.1"
updatedRelease.Version = 2
config := actionConfigFixture(t)
client := NewHistory(config)
client.Max = 3
client.cfg.Releases.MaxHistory = 3
for _, rel := range []*release.Release{simpleRelease, updatedRelease} {
if err := client.cfg.Releases.Create(rel); err != nil {
t.Fatal(err, "Could not add releases to Config")
}
}
releases, err := config.Releases.ListReleases()
require.NoError(t, err)
assert.Len(t, releases, 2, "expected 2 Releases in Config")
releasers, err := client.Run(releaseName)
require.NoError(t, err)
assert.Len(t, releasers, 2, "expected 2 Releases in History result")
release1, err := releaserToV1Release(releasers[0])
require.NoError(t, err)
assert.Equal(t, simpleRelease.Name, release1.Name)
assert.Equal(t, simpleRelease.Version, release1.Version)
release2, err := releaserToV1Release(releasers[1])
require.NoError(t, err)
assert.Equal(t, updatedRelease.Name, release2.Name)
assert.Equal(t, updatedRelease.Version, release2.Version)
}
func TestHistoryRun_UnreachableKubeClient(t *testing.T) {
config := actionConfigFixture(t)
failingKubeClient := kubefake.FailingKubeClient{PrintingKubeClient: kubefake.PrintingKubeClient{Out: io.Discard}, DummyResources: nil}
failingKubeClient.ConnectionError = errors.New("connection refused")
config.KubeClient = &failingKubeClient
client := NewHistory(config)
result, err := client.Run("release-name")
assert.Nil(t, result)
assert.Error(t, err)
}
func TestHistoryRun_InvalidReleaseNames(t *testing.T) {
config := actionConfigFixture(t)
client := NewHistory(config)
invalidReleaseNames := []string{
"",
"too-long-release-name-max-53-characters-abcdefghijklmnopqrstuvwxyz",
"MyRelease",
"release_name",
"release@123",
"-badstart",
"badend-",
".dotstart",
}
for _, name := range invalidReleaseNames {
result, err := client.Run(name)
assert.Nil(t, result)
assert.ErrorContains(t, err, "release name is invalid")
}
}

@ -62,8 +62,8 @@ import (
"helm.sh/helm/v4/pkg/storage/driver"
)
// notesFileSuffix that we want to treat special. It goes through the templating engine
// but it's not a yaml file (resource) hence can't have hooks, etc. And the user actually
// notesFileSuffix that we want to treat specially. It goes through the templating engine
// but it's not a YAML file (resource) hence can't have hooks, etc. And the user actually
// wants to see this file after rendering in the status command. However, it must be a suffix
// since there can be filepath in front of it.
const notesFileSuffix = "NOTES.txt"
@ -116,7 +116,7 @@ type Install struct {
Labels map[string]string
// KubeVersion allows specifying a custom kubernetes version to use and
// APIVersions allows a manual set of supported API Versions to be passed
// (for things like templating). These are ignored if ClientOnly is false
// (for things like templating).
KubeVersion *common.KubeVersion
APIVersions common.VersionSet
// Used by helm template to render charts with .Release.IsUpgrade. Ignored if Dry-Run is false
@ -192,7 +192,7 @@ func (i *Install) installCRDs(crds []chart.CRD) error {
kube.ClientCreateOptionServerSideApply(i.ServerSideApply, i.ForceConflicts)); err != nil {
// If the error is CRD already exists, continue.
if apierrors.IsAlreadyExists(err) {
crdName := res[0].Name
crdName := obj.Name
i.cfg.Logger().Debug("CRD is already present. Skipping", "crd", crdName)
continue
}
@ -419,6 +419,7 @@ func (i *Install) RunWithContext(ctx context.Context, ch ci.Charter, vals map[st
if err != nil {
return nil, err
}
if _, err := i.cfg.KubeClient.Create(
resourceList,
kube.ClientCreateOptionServerSideApply(i.ServerSideApply, false)); err != nil && !apierrors.IsAlreadyExists(err) {

@ -35,6 +35,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
appsv1 "k8s.io/api/apps/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
kuberuntime "k8s.io/apimachinery/pkg/runtime"
@ -43,10 +44,14 @@ import (
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest/fake"
ci "helm.sh/helm/v4/pkg/chart"
"helm.sh/helm/v4/internal/test"
"helm.sh/helm/v4/pkg/chart/common"
chart "helm.sh/helm/v4/pkg/chart/v2"
"helm.sh/helm/v4/pkg/kube"
kubefake "helm.sh/helm/v4/pkg/kube/fake"
"helm.sh/helm/v4/pkg/registry"
rcommon "helm.sh/helm/v4/pkg/release/common"
release "helm.sh/helm/v4/pkg/release/v1"
"helm.sh/helm/v4/pkg/storage/driver"
@ -500,7 +505,7 @@ func TestInstallRelease_NoHooks(t *testing.T) {
instAction := installAction(t)
instAction.DisableHooks = true
instAction.ReleaseName = "no-hooks"
instAction.cfg.Releases.Create(releaseStub())
require.NoError(t, instAction.cfg.Releases.Create(releaseStub()))
vals := map[string]interface{}{}
resi, err := instAction.Run(buildChart(), vals)
@ -540,7 +545,7 @@ func TestInstallRelease_ReplaceRelease(t *testing.T) {
rel := releaseStub()
rel.Info.Status = rcommon.StatusUninstalled
instAction.cfg.Releases.Create(rel)
require.NoError(t, instAction.cfg.Releases.Create(rel))
instAction.ReleaseName = rel.Name
vals := map[string]interface{}{}
@ -572,7 +577,7 @@ func TestInstallRelease_KubeVersion(t *testing.T) {
vals = map[string]interface{}{}
_, err = instAction.Run(buildChart(withKube(">=99.0.0")), vals)
is.Error(err)
is.Contains(err.Error(), "chart requires kubeVersion")
is.Contains(err.Error(), "chart requires kubeVersion: >=99.0.0 which is incompatible with Kubernetes v1.20.")
}
func TestInstallRelease_Wait(t *testing.T) {
@ -1068,3 +1073,116 @@ func TestInstallRun_UnreachableKubeClient(t *testing.T) {
assert.Nil(t, res)
assert.ErrorContains(t, err, "connection refused")
}
func TestInstallSetRegistryClient(t *testing.T) {
config := actionConfigFixture(t)
instAction := NewInstall(config)
registryClient := &registry.Client{}
instAction.SetRegistryClient(registryClient)
assert.Equal(t, registryClient, instAction.GetRegistryClient())
}
func TestInstalLCRDs(t *testing.T) {
config := actionConfigFixture(t)
instAction := NewInstall(config)
mockFile := common.File{
Name: "crds/foo.yaml",
Data: []byte("hello"),
}
mockChart := buildChart(withFile(mockFile))
crdsToInstall := mockChart.CRDObjects()
assert.Len(t, crdsToInstall, 1)
assert.Equal(t, crdsToInstall[0].File.Data, mockFile.Data)
require.NoError(t, instAction.installCRDs(crdsToInstall))
}
func TestInstalLCRDs_KubeClient_BuildError(t *testing.T) {
config := actionConfigFixture(t)
failingKubeClient := kubefake.FailingKubeClient{PrintingKubeClient: kubefake.PrintingKubeClient{Out: io.Discard}, DummyResources: nil}
failingKubeClient.BuildError = errors.New("build error")
config.KubeClient = &failingKubeClient
instAction := NewInstall(config)
mockFile := common.File{
Name: "crds/foo.yaml",
Data: []byte("hello"),
}
mockChart := buildChart(withFile(mockFile))
crdsToInstall := mockChart.CRDObjects()
require.Error(t, instAction.installCRDs(crdsToInstall), "failed to install CRD")
}
func TestInstalLCRDs_KubeClient_CreateError(t *testing.T) {
config := actionConfigFixture(t)
failingKubeClient := kubefake.FailingKubeClient{PrintingKubeClient: kubefake.PrintingKubeClient{Out: io.Discard}, DummyResources: nil}
failingKubeClient.CreateError = errors.New("create error")
config.KubeClient = &failingKubeClient
instAction := NewInstall(config)
mockFile := common.File{
Name: "crds/foo.yaml",
Data: []byte("hello"),
}
mockChart := buildChart(withFile(mockFile))
crdsToInstall := mockChart.CRDObjects()
require.Error(t, instAction.installCRDs(crdsToInstall), "failed to install CRD")
}
func TestInstalLCRDs_AlreadyExist(t *testing.T) {
config := actionConfigFixture(t)
failingKubeClient := kubefake.FailingKubeClient{PrintingKubeClient: kubefake.PrintingKubeClient{Out: io.Discard}, DummyResources: nil}
mockError := &apierrors.StatusError{ErrStatus: metav1.Status{
Status: metav1.StatusFailure,
Reason: metav1.StatusReasonAlreadyExists,
}}
failingKubeClient.CreateError = mockError
config.KubeClient = &failingKubeClient
instAction := NewInstall(config)
mockFile := common.File{
Name: "crds/foo.yaml",
Data: []byte("hello"),
}
mockChart := buildChart(withFile(mockFile))
crdsToInstall := mockChart.CRDObjects()
assert.Nil(t, instAction.installCRDs(crdsToInstall))
}
func TestInstalLCRDs_WaiterError(t *testing.T) {
config := actionConfigFixture(t)
failingKubeClient := kubefake.FailingKubeClient{PrintingKubeClient: kubefake.PrintingKubeClient{Out: io.Discard}, DummyResources: nil}
failingKubeClient.WaitError = errors.New("wait error")
failingKubeClient.BuildDummy = true
config.KubeClient = &failingKubeClient
instAction := NewInstall(config)
mockFile := common.File{
Name: "crds/foo.yaml",
Data: []byte("hello"),
}
mockChart := buildChart(withFile(mockFile))
crdsToInstall := mockChart.CRDObjects()
require.Error(t, instAction.installCRDs(crdsToInstall), "wait error")
}
func TestCheckDependencies(t *testing.T) {
dependency := chart.Dependency{Name: "hello"}
mockChart := buildChart(withDependency())
assert.Nil(t, CheckDependencies(mockChart, []ci.Dependency{&dependency}))
}
func TestCheckDependencies_MissingDependency(t *testing.T) {
dependency := chart.Dependency{Name: "missing"}
mockChart := buildChart(withDependency())
assert.ErrorContains(t, CheckDependencies(mockChart, []ci.Dependency{&dependency}), "missing in charts")
}

@ -17,7 +17,12 @@ limitations under the License.
package action
import (
"errors"
"testing"
"github.com/stretchr/testify/assert"
"helm.sh/helm/v4/pkg/chart/v2/lint/support"
)
var (
@ -163,3 +168,45 @@ func TestLint_ChartWithWarnings(t *testing.T) {
}
})
}
func TestHasWarningsOrErrors(t *testing.T) {
testError := errors.New("test-error")
cases := []struct {
name string
data LintResult
expected bool
}{
{
name: "has no warning messages and no errors",
data: LintResult{TotalChartsLinted: 1, Messages: make([]support.Message, 0), Errors: make([]error, 0)},
expected: false,
},
{
name: "has error",
data: LintResult{TotalChartsLinted: 1, Messages: make([]support.Message, 0), Errors: []error{testError}},
expected: true,
},
{
name: "has info message only",
data: LintResult{TotalChartsLinted: 1, Messages: []support.Message{{Severity: support.InfoSev, Path: "", Err: testError}}, Errors: make([]error, 0)},
expected: false,
},
{
name: "has warning message",
data: LintResult{TotalChartsLinted: 1, Messages: []support.Message{{Severity: support.WarningSev, Path: "", Err: testError}}, Errors: make([]error, 0)},
expected: true,
},
{
name: "has error message",
data: LintResult{TotalChartsLinted: 1, Messages: []support.Message{{Severity: support.ErrorSev, Path: "", Err: testError}}, Errors: make([]error, 0)},
expected: true,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
result := HasWarningsOrErrors(&tc.data)
assert.Equal(t, tc.expected, result)
})
}
}

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

@ -22,6 +22,7 @@ import (
"testing"
"github.com/Masterminds/semver/v3"
"github.com/stretchr/testify/require"
"helm.sh/helm/v4/internal/test/ensure"
)
@ -90,10 +91,11 @@ func TestPassphraseFileFetcher_WithStdinAndMultipleFetches(t *testing.T) {
passphrase := "secret-from-stdin"
go func() {
w.Write([]byte(passphrase + "\n"))
_, err = w.Write([]byte(passphrase + "\n"))
require.NoError(t, err)
}()
for i := 0; i < 4; i++ {
for range 4 {
fetcher, err := testPkg.passphraseFileFetcher("-", stdin)
if err != nil {
t.Errorf("Expected passphraseFileFetcher to not return an error, but got %v", err)
@ -152,3 +154,18 @@ func TestValidateVersion(t *testing.T) {
})
}
}
func TestRun_ErrorPath(t *testing.T) {
client := NewPackage()
_, err := client.Run("err-path", nil)
require.Error(t, err)
}
func TestRun(t *testing.T) {
chartPath := "testdata/charts/chart-with-schema"
client := NewPackage()
filename, err := client.Run(chartPath, nil)
require.NoError(t, err)
require.Equal(t, "empty-0.1.0.tgz", filename)
require.NoError(t, os.Remove(filename))
}

@ -0,0 +1,80 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package action
import (
"net/http"
"net/http/httptest"
"os"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"helm.sh/helm/v4/pkg/cli"
"helm.sh/helm/v4/pkg/registry"
)
func TestNewPull(t *testing.T) {
config := actionConfigFixture(t)
client := NewPull(WithConfig(config))
assert.NotNil(t, client)
assert.Equal(t, config, client.cfg)
}
func TestPullSetRegistryClient(t *testing.T) {
config := actionConfigFixture(t)
client := NewPull(WithConfig(config))
registryClient := &registry.Client{}
client.SetRegistryClient(registryClient)
assert.Equal(t, registryClient, client.cfg.RegistryClient)
}
func TestPullRun_ChartNotFound(t *testing.T) {
srv, err := startLocalServerForTests(t, nil)
if err != nil {
t.Fatal(err)
}
defer srv.Close()
config := actionConfigFixture(t)
client := NewPull(WithConfig(config))
client.Settings = cli.New()
client.RepoURL = srv.URL
chartRef := "nginx"
_, err = client.Run(chartRef)
require.ErrorContains(t, err, "404 Not Found")
}
func startLocalServerForTests(t *testing.T, handler http.Handler) (*httptest.Server, error) {
t.Helper()
if handler == nil {
fileBytes, err := os.ReadFile("../repo/v1/testdata/local-index.yaml")
if err != nil {
return nil, err
}
handler = http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
_, err = w.Write(fileBytes)
require.NoError(t, err)
})
}
return httptest.NewServer(handler), nil
}

@ -0,0 +1,66 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package action
import (
"bytes"
"testing"
"github.com/stretchr/testify/assert"
)
func TestNewPushWithPushConfig(t *testing.T) {
config := actionConfigFixture(t)
client := NewPushWithOpts(WithPushConfig(config))
assert.NotNil(t, client)
assert.Equal(t, config, client.cfg)
}
func TestNewPushWithTLSClientConfig(t *testing.T) {
certFile := "certFile"
keyFile := "keyFile"
caFile := "caFile"
client := NewPushWithOpts(WithTLSClientConfig(certFile, keyFile, caFile))
assert.NotNil(t, client)
assert.Equal(t, certFile, client.certFile)
assert.Equal(t, keyFile, client.keyFile)
assert.Equal(t, caFile, client.caFile)
}
func TestNewPushWithInsecureSkipTLSVerify(t *testing.T) {
client := NewPushWithOpts(WithInsecureSkipTLSVerify(true))
assert.NotNil(t, client)
assert.Equal(t, true, client.insecureSkipTLSVerify)
}
func TestNewPushWithPlainHTTP(t *testing.T) {
client := NewPushWithOpts(WithPlainHTTP(true))
assert.NotNil(t, client)
assert.Equal(t, true, client.plainHTTP)
}
func TestNewPushWithPushOptWriter(t *testing.T) {
buf := new(bytes.Buffer)
client := NewPushWithOpts(WithPushOptWriter(buf))
assert.NotNil(t, client)
assert.Equal(t, buf, client.out)
}

@ -0,0 +1,84 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package action
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestNewRegistryLogin(t *testing.T) {
config := actionConfigFixture(t)
client := NewRegistryLogin(config)
assert.NotNil(t, client)
assert.Equal(t, config, client.cfg)
}
func TestWithCertFile(t *testing.T) {
config := actionConfigFixture(t)
client := NewRegistryLogin(config)
certFile := "testdata/cert.pem"
opt := WithCertFile(certFile)
assert.Nil(t, opt(client))
assert.Equal(t, certFile, client.certFile)
}
func TestWithInsecure(t *testing.T) {
config := actionConfigFixture(t)
client := NewRegistryLogin(config)
opt := WithInsecure(true)
assert.Nil(t, opt(client))
assert.Equal(t, true, client.insecure)
}
func TestWithKeyFile(t *testing.T) {
config := actionConfigFixture(t)
client := NewRegistryLogin(config)
keyFile := "testdata/key.pem"
opt := WithKeyFile(keyFile)
assert.Nil(t, opt(client))
assert.Equal(t, keyFile, client.keyFile)
}
func TestWithCAFile(t *testing.T) {
config := actionConfigFixture(t)
client := NewRegistryLogin(config)
caFile := "testdata/ca.pem"
opt := WithCAFile(caFile)
assert.Nil(t, opt(client))
assert.Equal(t, caFile, client.caFile)
}
func TestWithPlainHTTPLogin(t *testing.T) {
config := actionConfigFixture(t)
client := NewRegistryLogin(config)
opt := WithPlainHTTPLogin(true)
assert.Nil(t, opt(client))
assert.Equal(t, true, client.plainHTTP)
}

@ -0,0 +1,31 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package action
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestNewRegistryLogout(t *testing.T) {
config := actionConfigFixture(t)
client := NewRegistryLogout(config)
assert.NotNil(t, client)
assert.Equal(t, config, client.cfg)
}

@ -0,0 +1,91 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package action
import (
"bytes"
"errors"
"io"
"os"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"helm.sh/helm/v4/pkg/cli"
kubefake "helm.sh/helm/v4/pkg/kube/fake"
release "helm.sh/helm/v4/pkg/release/v1"
)
func TestNewReleaseTesting(t *testing.T) {
config := actionConfigFixture(t)
client := NewReleaseTesting(config)
assert.NotNil(t, client)
assert.Equal(t, config, client.cfg)
}
func TestReleaseTestingRun_UnreachableKubeClient(t *testing.T) {
config := actionConfigFixture(t)
failingKubeClient := kubefake.FailingKubeClient{PrintingKubeClient: kubefake.PrintingKubeClient{Out: io.Discard}, DummyResources: nil}
failingKubeClient.ConnectionError = errors.New("connection refused")
config.KubeClient = &failingKubeClient
client := NewReleaseTesting(config)
result, err := client.Run("")
assert.Nil(t, result)
assert.Error(t, err)
}
func TestReleaseTestingGetPodLogs_FilterEvents(t *testing.T) {
config := actionConfigFixture(t)
require.NoError(t, config.Init(cli.New().RESTClientGetter(), "", os.Getenv("HELM_DRIVER")))
client := NewReleaseTesting(config)
client.Filters[ExcludeNameFilter] = []string{"event-1"}
client.Filters[IncludeNameFilter] = []string{"event-3"}
hooks := []*release.Hook{
{
Name: "event-1",
Events: []release.HookEvent{release.HookTest},
},
{
Name: "event-2",
Events: []release.HookEvent{release.HookTest},
},
}
out := &bytes.Buffer{}
require.NoError(t, client.GetPodLogs(out, &release.Release{Hooks: hooks}))
assert.Empty(t, out.String())
}
func TestReleaseTestingGetPodLogs_PodRetrievalError(t *testing.T) {
config := actionConfigFixture(t)
require.NoError(t, config.Init(cli.New().RESTClientGetter(), "", os.Getenv("HELM_DRIVER")))
client := NewReleaseTesting(config)
hooks := []*release.Hook{
{
Name: "event-1",
Events: []release.HookEvent{release.HookTest},
},
}
require.ErrorContains(t, client.GetPodLogs(&bytes.Buffer{}, &release.Release{Hooks: hooks}), "unable to get pod logs")
}

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

@ -0,0 +1,45 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package action
import (
"errors"
"io"
"testing"
"github.com/stretchr/testify/assert"
kubefake "helm.sh/helm/v4/pkg/kube/fake"
)
func TestNewRollback(t *testing.T) {
config := actionConfigFixture(t)
client := NewRollback(config)
assert.NotNil(t, client)
assert.Equal(t, config, client.cfg)
}
func TestRollbackRun_UnreachableKubeClient(t *testing.T) {
config := actionConfigFixture(t)
failingKubeClient := kubefake.FailingKubeClient{PrintingKubeClient: kubefake.PrintingKubeClient{Out: io.Discard}, DummyResources: nil}
failingKubeClient.ConnectionError = errors.New("connection refused")
config.KubeClient = &failingKubeClient
client := NewRollback(config)
assert.Error(t, client.Run(""))
}

@ -20,8 +20,11 @@ import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"helm.sh/helm/v4/pkg/chart/common"
chart "helm.sh/helm/v4/pkg/chart/v2"
"helm.sh/helm/v4/pkg/registry"
)
func TestShow(t *testing.T) {
@ -168,3 +171,12 @@ bar
t.Errorf("Expected\n%q\nGot\n%q\n", expect, output)
}
}
func TestShowSetRegistryClient(t *testing.T) {
config := actionConfigFixture(t)
client := NewShow(ShowAll, config)
registryClient := &registry.Client{}
client.SetRegistryClient(registryClient)
assert.Equal(t, registryClient, client.registryClient)
}

@ -0,0 +1,143 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package action
import (
"errors"
"io"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
kubefake "helm.sh/helm/v4/pkg/kube/fake"
rcommon "helm.sh/helm/v4/pkg/release/common"
release "helm.sh/helm/v4/pkg/release/v1"
)
func TestNewStatus(t *testing.T) {
config := actionConfigFixture(t)
client := NewStatus(config)
assert.NotNil(t, client)
assert.Equal(t, config, client.cfg)
assert.Equal(t, 0, client.Version)
}
func TestStatusRun(t *testing.T) {
config := actionConfigFixture(t)
failingKubeClient := kubefake.FailingKubeClient{PrintingKubeClient: kubefake.PrintingKubeClient{Out: io.Discard}, DummyResources: nil}
failingKubeClient.BuildDummy = true
config.KubeClient = &failingKubeClient
client := NewStatus(config)
client.ShowResourcesTable = true
releaseName := "test-release"
require.NoError(t, configureReleaseContent(config, releaseName))
releaser, err := client.Run(releaseName)
require.NoError(t, err)
result, err := releaserToV1Release(releaser)
require.NoError(t, err)
assert.Equal(t, releaseName, result.Name)
assert.Equal(t, 1, result.Version)
}
func TestStatusRun_KubeClientNotReachable(t *testing.T) {
config := actionConfigFixture(t)
failingKubeClient := kubefake.FailingKubeClient{PrintingKubeClient: kubefake.PrintingKubeClient{Out: io.Discard}, DummyResources: nil}
failingKubeClient.ConnectionError = errors.New("connection refused")
config.KubeClient = &failingKubeClient
client := NewStatus(config)
result, err := client.Run("")
assert.Nil(t, result)
assert.Error(t, err)
}
func TestStatusRun_KubeClientBuildTableError(t *testing.T) {
config := actionConfigFixture(t)
failingKubeClient := kubefake.FailingKubeClient{PrintingKubeClient: kubefake.PrintingKubeClient{Out: io.Discard}, DummyResources: nil}
failingKubeClient.BuildTableError = errors.New("build table error")
config.KubeClient = &failingKubeClient
releaseName := "test-release"
require.NoError(t, configureReleaseContent(config, releaseName))
client := NewStatus(config)
client.ShowResourcesTable = true
result, err := client.Run(releaseName)
assert.Nil(t, result)
assert.ErrorContains(t, err, "build table error")
}
func TestStatusRun_KubeClientBuildError(t *testing.T) {
config := actionConfigFixture(t)
failingKubeClient := kubefake.FailingKubeClient{PrintingKubeClient: kubefake.PrintingKubeClient{Out: io.Discard}, DummyResources: nil}
failingKubeClient.BuildError = errors.New("build error")
config.KubeClient = &failingKubeClient
releaseName := "test-release"
require.NoError(t, configureReleaseContent(config, releaseName))
client := NewStatus(config)
client.ShowResourcesTable = false
result, err := client.Run(releaseName)
assert.Nil(t, result)
assert.ErrorContains(t, err, "build error")
}
func TestStatusRun_KubeClientGetError(t *testing.T) {
config := actionConfigFixture(t)
failingKubeClient := kubefake.FailingKubeClient{PrintingKubeClient: kubefake.PrintingKubeClient{Out: io.Discard}, DummyResources: nil}
failingKubeClient.BuildError = errors.New("get error")
config.KubeClient = &failingKubeClient
releaseName := "test-release"
require.NoError(t, configureReleaseContent(config, releaseName))
client := NewStatus(config)
result, err := client.Run(releaseName)
assert.Nil(t, result)
assert.ErrorContains(t, err, "get error")
}
func configureReleaseContent(cfg *Configuration, releaseName string) error {
rel := &release.Release{
Name: releaseName,
Info: &release.Info{
Status: rcommon.StatusDeployed,
},
Manifest: testManifest,
Version: 1,
Namespace: "default",
}
return cfg.Releases.Create(rel)
}
const testManifest = `
apiVersion: v1
kind: Pod
metadata:
namespace: default
name: test-application
`

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

@ -82,7 +82,7 @@ func TestUninstallRelease_deleteRelease(t *testing.T) {
"password": "password"
}
}`
unAction.cfg.Releases.Create(rel)
require.NoError(t, unAction.cfg.Releases.Create(rel))
res, err := unAction.Run(rel.Name)
is.NoError(err)
expected := `These resources were kept due to the resource policy:
@ -112,7 +112,7 @@ func TestUninstallRelease_Wait(t *testing.T) {
"password": "password"
}
}`
unAction.cfg.Releases.Create(rel)
require.NoError(t, unAction.cfg.Releases.Create(rel))
failer := unAction.cfg.KubeClient.(*kubefake.FailingKubeClient)
failer.WaitForDeleteError = fmt.Errorf("U timed out")
unAction.cfg.KubeClient = failer
@ -146,7 +146,7 @@ func TestUninstallRelease_Cascade(t *testing.T) {
"password": "password"
}
}`
unAction.cfg.Releases.Create(rel)
require.NoError(t, unAction.cfg.Releases.Create(rel))
failer := unAction.cfg.KubeClient.(*kubefake.FailingKubeClient)
failer.DeleteError = fmt.Errorf("Uninstall with cascade failed")
failer.BuildDummy = true

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

@ -25,16 +25,19 @@ import (
"testing"
"time"
chart "helm.sh/helm/v4/pkg/chart/v2"
"helm.sh/helm/v4/pkg/kube"
"helm.sh/helm/v4/pkg/storage/driver"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
appsv1 "k8s.io/api/apps/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/cli-runtime/pkg/resource"
chart "helm.sh/helm/v4/pkg/chart/v2"
"helm.sh/helm/v4/pkg/kube"
kubefake "helm.sh/helm/v4/pkg/kube/fake"
"helm.sh/helm/v4/pkg/registry"
"helm.sh/helm/v4/pkg/release/common"
release "helm.sh/helm/v4/pkg/release/v1"
"helm.sh/helm/v4/pkg/storage/driver"
)
func upgradeAction(t *testing.T) *Upgrade {
@ -85,7 +88,7 @@ func TestUpgradeRelease_Wait(t *testing.T) {
rel := releaseStub()
rel.Name = "come-fail-away"
rel.Info.Status = common.StatusDeployed
upAction.cfg.Releases.Create(rel)
require.NoError(t, upAction.cfg.Releases.Create(rel))
failer := upAction.cfg.KubeClient.(*kubefake.FailingKubeClient)
failer.WaitError = fmt.Errorf("I timed out")
@ -109,7 +112,7 @@ func TestUpgradeRelease_WaitForJobs(t *testing.T) {
rel := releaseStub()
rel.Name = "come-fail-away"
rel.Info.Status = common.StatusDeployed
upAction.cfg.Releases.Create(rel)
require.NoError(t, upAction.cfg.Releases.Create(rel))
failer := upAction.cfg.KubeClient.(*kubefake.FailingKubeClient)
failer.WaitError = fmt.Errorf("I timed out")
@ -134,7 +137,7 @@ func TestUpgradeRelease_CleanupOnFail(t *testing.T) {
rel := releaseStub()
rel.Name = "come-fail-away"
rel.Info.Status = common.StatusDeployed
upAction.cfg.Releases.Create(rel)
require.NoError(t, upAction.cfg.Releases.Create(rel))
failer := upAction.cfg.KubeClient.(*kubefake.FailingKubeClient)
failer.WaitError = fmt.Errorf("I timed out")
@ -163,7 +166,7 @@ func TestUpgradeRelease_RollbackOnFailure(t *testing.T) {
rel := releaseStub()
rel.Name = "nuketown"
rel.Info.Status = common.StatusDeployed
upAction.cfg.Releases.Create(rel)
require.NoError(t, upAction.cfg.Releases.Create(rel))
failer := upAction.cfg.KubeClient.(*kubefake.FailingKubeClient)
// We can't make Update error because then the rollback won't work
@ -193,7 +196,7 @@ func TestUpgradeRelease_RollbackOnFailure(t *testing.T) {
rel := releaseStub()
rel.Name = "fallout"
rel.Info.Status = common.StatusDeployed
upAction.cfg.Releases.Create(rel)
require.NoError(t, upAction.cfg.Releases.Create(rel))
failer := upAction.cfg.KubeClient.(*kubefake.FailingKubeClient)
failer.UpdateError = fmt.Errorf("update fail")
@ -401,12 +404,12 @@ func TestUpgradeRelease_Pending(t *testing.T) {
rel := releaseStub()
rel.Name = "come-fail-away"
rel.Info.Status = common.StatusDeployed
upAction.cfg.Releases.Create(rel)
require.NoError(t, upAction.cfg.Releases.Create(rel))
rel2 := releaseStub()
rel2.Name = "come-fail-away"
rel2.Info.Status = common.StatusPendingUpgrade
rel2.Version = 2
upAction.cfg.Releases.Create(rel2)
require.NoError(t, upAction.cfg.Releases.Create(rel2))
vals := map[string]interface{}{}
@ -422,7 +425,7 @@ func TestUpgradeRelease_Interrupted_Wait(t *testing.T) {
rel := releaseStub()
rel.Name = "interrupted-release"
rel.Info.Status = common.StatusDeployed
upAction.cfg.Releases.Create(rel)
require.NoError(t, upAction.cfg.Releases.Create(rel))
failer := upAction.cfg.KubeClient.(*kubefake.FailingKubeClient)
failer.WaitDuration = 10 * time.Second
@ -451,7 +454,7 @@ func TestUpgradeRelease_Interrupted_RollbackOnFailure(t *testing.T) {
rel := releaseStub()
rel.Name = "interrupted-release"
rel.Info.Status = common.StatusDeployed
upAction.cfg.Releases.Create(rel)
require.NoError(t, upAction.cfg.Releases.Create(rel))
failer := upAction.cfg.KubeClient.(*kubefake.FailingKubeClient)
failer.WaitDuration = 5 * time.Second
@ -755,3 +758,20 @@ func TestUpgradeRun_UnreachableKubeClient(t *testing.T) {
assert.Nil(t, result)
assert.ErrorContains(t, err, "connection refused")
}
func TestUpgradeSetRegistryClient(t *testing.T) {
config := actionConfigFixture(t)
client := NewUpgrade(config)
registryClient := &registry.Client{}
client.SetRegistryClient(registryClient)
assert.Equal(t, registryClient, client.registryClient)
}
func TestObjectKey(t *testing.T) {
obj := &appsv1.Deployment{}
obj.SetGroupVersionKind(schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"})
info := resource.Info{Name: "name", Namespace: "namespace", Object: obj}
assert.Equal(t, "apps/v1/Deployment/namespace/name", objectKey(&info))
}

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

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

@ -0,0 +1,48 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package action
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestNewVerify(t *testing.T) {
client := NewVerify()
assert.NotNil(t, client)
}
func TestVerifyRun(t *testing.T) {
client := NewVerify()
client.Keyring = "../downloader/testdata/helm-test-key.pub"
output, err := client.Run("../downloader/testdata/signtest-0.1.0.tgz")
assert.Contains(t, output, "Signed by:")
assert.Contains(t, output, "Using Key With Fingerprint:")
assert.Contains(t, output, "Chart Hash Verified:")
require.NoError(t, err)
}
func TestVerifyRun_DownloadError(t *testing.T) {
client := NewVerify()
output, err := client.Run("invalid-chart-path")
require.Error(t, err)
assert.Empty(t, output)
}

@ -186,7 +186,7 @@ func structToMap(obj interface{}) (map[string]interface{}, error) {
objValue := reflect.ValueOf(obj)
// If the value is a pointer, dereference it
if objValue.Kind() == reflect.Ptr {
if objValue.Kind() == reflect.Pointer {
objValue = objValue.Elem()
}
@ -209,7 +209,7 @@ func structToMap(obj interface{}) (map[string]interface{}, error) {
return nil, err
}
result[field.Name] = nestedMap
case reflect.Ptr:
case reflect.Pointer:
// Recurse for pointers by dereferencing
if value.IsNil() {
result[field.Name] = nil
@ -224,7 +224,7 @@ func structToMap(obj interface{}) (map[string]interface{}, error) {
sliceOfMaps := make([]interface{}, value.Len())
for j := 0; j < value.Len(); j++ {
sliceElement := value.Index(j)
if sliceElement.Kind() == reflect.Struct || sliceElement.Kind() == reflect.Ptr {
if sliceElement.Kind() == reflect.Struct || sliceElement.Kind() == reflect.Pointer {
nestedMap, err := structToMap(sliceElement.Interface())
if err != nil {
return nil, err

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

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

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

@ -87,7 +87,7 @@ func ValidateAgainstSchema(ch chart.Charter, values map[string]interface{}) erro
sb.WriteString(err.Error())
}
}
slog.Debug("number of dependencies in the chart", "dependencies", len(chrt.Dependencies()))
slog.Debug("number of dependencies in the chart", "chart", chrt.Name(), "dependencies", len(chrt.Dependencies()))
// For each dependency, recursively call this function with the coalesced values
for _, subchart := range chrt.Dependencies() {
sub, err := chart.NewAccessor(subchart)

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -99,6 +99,7 @@ func New() *EnvSettings {
env := &EnvSettings{
namespace: os.Getenv("HELM_NAMESPACE"),
MaxHistory: envIntOr("HELM_MAX_HISTORY", defaultMaxHistory),
KubeConfig: os.Getenv("KUBECONFIG"),
KubeContext: os.Getenv("HELM_KUBECONTEXT"),
KubeToken: os.Getenv("HELM_KUBETOKEN"),
KubeAsUser: os.Getenv("HELM_KUBEASUSER"),

@ -59,7 +59,7 @@ func AddWaitFlag(cmd *cobra.Command, wait *kube.WaitStrategy) {
cmd.Flags().Var(
newWaitValue(kube.HookOnlyStrategy, wait),
"wait",
"if specified, will wait until all resources are in the expected state before marking the operation as successful. It will wait for as long as --timeout. Valid inputs are 'watcher' and 'legacy'",
"if specified, wait until resources are ready (up to --timeout). Values: 'watcher', 'hookOnly', and 'legacy'.",
)
// Sets the strategy to use the watcher strategy if `--wait` is used without an argument
cmd.Flags().Lookup("wait").NoOptDefVal = string(kube.StatusWatcherStrategy)
@ -81,7 +81,7 @@ func (ws *waitValue) String() string {
func (ws *waitValue) Set(s string) error {
switch s {
case string(kube.StatusWatcherStrategy), string(kube.LegacyStrategy):
case string(kube.StatusWatcherStrategy), string(kube.LegacyStrategy), string(kube.HookOnlyStrategy):
*ws = waitValue(s)
return nil
case "true":
@ -89,11 +89,11 @@ func (ws *waitValue) Set(s string) error {
*ws = waitValue(kube.StatusWatcherStrategy)
return nil
case "false":
slog.Warn("--wait=false is deprecated (boolean value) and can be replaced by omitting the --wait flag")
slog.Warn("--wait=false is deprecated (boolean value) and can be replaced with --wait=hookOnly")
*ws = waitValue(kube.HookOnlyStrategy)
return nil
default:
return fmt.Errorf("invalid wait input %q. Valid inputs are %s, and %s", s, kube.StatusWatcherStrategy, kube.LegacyStrategy)
return fmt.Errorf("invalid wait input %q. Valid inputs are %s, %s, and %s", s, kube.StatusWatcherStrategy, kube.HookOnlyStrategy, kube.LegacyStrategy)
}
}

@ -275,7 +275,7 @@ func runInstall(args []string, client *action.Install, valueOpts *values.Options
slog.Warn("this chart is deprecated")
}
if req := ac.MetaDependencies(); req != nil {
if req := ac.MetaDependencies(); len(req) > 0 {
// If CheckDependencies returns an error, we have unfulfilled dependencies.
// As of Helm 2.4.0, this is treated as a stopping condition:
// https://github.com/helm/helm/issues/2209

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

Loading…
Cancel
Save