merge main and add test back

Signed-off-by: Janit Sriganeshaelankovan <janits_27@hotmail.com>
pull/31062/head
Janit Sriganeshaelankovan 3 months ago
commit 51f369725d
No known key found for this signature in database
GPG Key ID: 82CD06AA03D8CEF1

@ -44,6 +44,7 @@ body:
label: Helm version
value: |
<details>
```console
$ helm version
# paste output here

@ -0,0 +1,48 @@
# Copilot Instructions for Helm
## Overview
Helm is a package manager for Kubernetes written in Go, supporting v3 (stable) and v4 (unstable) APIs.
## Build & Test
```bash
make build # Build binary
make test # Run all tests (style + unit)
make test-unit # Unit tests only
make test-coverage # With coverage
make test-style # Linting
golangci-lint run # Direct linting
go test -run TestName # Specific test
```
## Code Structure
- `/cmd/helm/` - CLI entry point (Cobra-based)
- `/pkg/` - Public API
- `action/` - Core operations (install, upgrade, rollback)
- `chart/v2/` - Stable chart format
- `engine/` - Template rendering (Go templates + Sprig)
- `registry/` - OCI support
- `storage/` - Release backends (Secrets/ConfigMaps/SQL)
- `/internal/` - Private implementation
- `chart/v3/` - Next-gen chart format
## Development Guidelines
### Code Standards
- Use table-driven tests with testify
- Golden files in `testdata/` for complex output
- Mock Kubernetes clients for action tests
- All commits must include DCO sign-off: `git commit -s`
### Branching
- `main` - Helm v4 development
- `dev-v3` - Helm v3 stable (backport from main)
### Dependencies
- `k8s.io/client-go` - Kubernetes interaction
- `github.com/spf13/cobra` - CLI framework
- `github.com/Masterminds/sprig` - Template functions
### Key Patterns
- **Actions**: Operations in `/pkg/action/` use shared Configuration
- **Dual Chart Support**: v2 (stable) in `/pkg/`, v3 (dev) in `/internal/`
- **Storage Abstraction**: Pluggable release storage backends

@ -18,7 +18,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout source code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # pin@v4.2.2
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # pin@v5.0.0
- name: Add variables to environment file
run: cat ".github/env" >> "$GITHUB_ENV"
- name: Setup Go
@ -28,6 +28,8 @@ jobs:
check-latest: true
- name: Test source headers are present
run: make test-source-headers
- name: Check if go modules need to be tidied
run: go mod tidy -diff
- name: Run unit tests
run: make test-coverage
- name: Test build

@ -43,7 +43,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # pin@v4.2.2
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # pin@v5.0.0
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL

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

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

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

@ -28,12 +28,12 @@ jobs:
steps:
- name: "Checkout code"
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- name: "Run analysis"
uses: ossf/scorecard-action@05b42c624433fc40578a4040d5cf5e36ddca8cde # v2.4.2
uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3
with:
results_file: results.sarif
results_format: sarif

@ -2,18 +2,17 @@ name: "Close stale issues"
on:
schedule:
- cron: "0 0 * * *"
permissions:
contents: read
jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9.1.0
- uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
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.'
stale-pr-message: 'This pull request has been marked as stale because it has been open for 90 days with no activity. This pull request will be automatically closed in 30 days if no further activity occurs.'
exempt-issue-labels: 'keep open,v4.x,in progress'
days-before-stale: 90
days-before-close: 30
operations-per-run: 100
operations-per-run: 200

2
.gitignore vendored

@ -2,7 +2,7 @@
*.swp
.DS_Store
.coverage/
.idea/
.idea
.vimrc
.vscode/
.devcontainer/

@ -33,6 +33,7 @@ linters:
- usetesting
exclusions:
generated: lax
presets:
@ -41,7 +42,13 @@ linters:
- legacy
- std-error-handling
rules: []
rules:
# This rule is triggered for packages like 'util'. When changes to those packages
# occur it triggers this rule. This exclusion enables making changes to existing
# packages.
- linters:
- revive
text: 'var-naming: avoid meaningless package names'
warn-unused: true

@ -1,8 +1,8 @@
BINDIR := $(CURDIR)/bin
INSTALL_PATH ?= /usr/local/bin
DIST_DIRS := find * -type d -exec
TARGETS := darwin/amd64 darwin/arm64 linux/amd64 linux/386 linux/arm linux/arm64 linux/ppc64le linux/s390x linux/riscv64 windows/amd64 windows/arm64
TARGET_OBJS ?= darwin-amd64.tar.gz darwin-amd64.tar.gz.sha256 darwin-amd64.tar.gz.sha256sum darwin-arm64.tar.gz darwin-arm64.tar.gz.sha256 darwin-arm64.tar.gz.sha256sum linux-amd64.tar.gz linux-amd64.tar.gz.sha256 linux-amd64.tar.gz.sha256sum linux-386.tar.gz linux-386.tar.gz.sha256 linux-386.tar.gz.sha256sum linux-arm.tar.gz linux-arm.tar.gz.sha256 linux-arm.tar.gz.sha256sum linux-arm64.tar.gz linux-arm64.tar.gz.sha256 linux-arm64.tar.gz.sha256sum linux-ppc64le.tar.gz linux-ppc64le.tar.gz.sha256 linux-ppc64le.tar.gz.sha256sum linux-s390x.tar.gz linux-s390x.tar.gz.sha256 linux-s390x.tar.gz.sha256sum linux-riscv64.tar.gz linux-riscv64.tar.gz.sha256 linux-riscv64.tar.gz.sha256sum windows-amd64.zip windows-amd64.zip.sha256 windows-amd64.zip.sha256sum windows-arm64.zip windows-arm64.zip.sha256 windows-arm64.zip.sha256sum
TARGETS := darwin/amd64 darwin/arm64 linux/amd64 linux/386 linux/arm linux/arm64 linux/loong64 linux/ppc64le linux/s390x linux/riscv64 windows/amd64 windows/arm64
TARGET_OBJS ?= darwin-amd64.tar.gz darwin-amd64.tar.gz.sha256 darwin-amd64.tar.gz.sha256sum darwin-arm64.tar.gz darwin-arm64.tar.gz.sha256 darwin-arm64.tar.gz.sha256sum linux-amd64.tar.gz linux-amd64.tar.gz.sha256 linux-amd64.tar.gz.sha256sum linux-386.tar.gz linux-386.tar.gz.sha256 linux-386.tar.gz.sha256sum linux-arm.tar.gz linux-arm.tar.gz.sha256 linux-arm.tar.gz.sha256sum linux-arm64.tar.gz linux-arm64.tar.gz.sha256 linux-arm64.tar.gz.sha256sum linux-loong64.tar.gz linux-loong64.tar.gz.sha256 linux-loong64.tar.gz.sha256sum linux-ppc64le.tar.gz linux-ppc64le.tar.gz.sha256 linux-ppc64le.tar.gz.sha256sum linux-s390x.tar.gz linux-s390x.tar.gz.sha256 linux-s390x.tar.gz.sha256sum linux-riscv64.tar.gz linux-riscv64.tar.gz.sha256 linux-riscv64.tar.gz.sha256sum windows-amd64.zip windows-amd64.zip.sha256 windows-amd64.zip.sha256sum windows-arm64.zip windows-arm64.zip.sha256 windows-arm64.zip.sha256sum
BINNAME ?= helm
GOBIN = $(shell go env GOBIN)
@ -13,15 +13,15 @@ GOX = $(GOBIN)/gox
GOIMPORTS = $(GOBIN)/goimports
ARCH = $(shell go env GOARCH)
ACCEPTANCE_DIR:=../acceptance-testing
ACCEPTANCE_DIR := ../acceptance-testing
# To specify the subset of acceptance tests to run. '.' means all tests
ACCEPTANCE_RUN_TESTS=.
ACCEPTANCE_RUN_TESTS = .
# go option
PKG := ./...
TAGS :=
TESTS := .
TESTFLAGS :=
TESTFLAGS := -shuffle=on -count=1
LDFLAGS := -w -s
GOFLAGS :=
CGO_ENABLED ?= 0
@ -63,10 +63,14 @@ K8S_MODULES_VER=$(subst ., ,$(subst v,,$(shell go list -f '{{.Version}}' -m k8s.
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/lint/rules.k8sVersionMajor=$(K8S_MODULES_MAJOR_VER)
LDFLAGS += -X helm.sh/helm/v4/pkg/lint/rules.k8sVersionMinor=$(K8S_MODULES_MINOR_VER)
LDFLAGS += -X helm.sh/helm/v4/pkg/chart/v2/util.k8sVersionMajor=$(K8S_MODULES_MAJOR_VER)
LDFLAGS += -X helm.sh/helm/v4/pkg/chart/v2/util.k8sVersionMinor=$(K8S_MODULES_MINOR_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
@ -75,7 +79,7 @@ all: build
# build
.PHONY: build
build: $(BINDIR)/$(BINNAME)
build: $(BINDIR)/$(BINNAME) tidy
$(BINDIR)/$(BINNAME): $(SRC)
CGO_ENABLED=$(CGO_ENABLED) go build $(GOFLAGS) -trimpath -tags '$(TAGS)' -ldflags '$(LDFLAGS)' -o '$(BINDIR)'/$(BINNAME) ./cmd/helm
@ -112,14 +116,16 @@ test-unit:
# based on older versions, this is run separately. When run without the ldflags in the unit test (above) or coverage
# test, it still passes with a false-positive result as the resources shouldnt be deprecated in the older Kubernetes
# version if it only starts failing with the latest.
go test $(GOFLAGS) -run ^TestHelmCreateChart_CheckDeprecatedWarnings$$ ./pkg/lint/ $(TESTFLAGS) -ldflags '$(LDFLAGS)'
go test $(GOFLAGS) -run ^TestHelmCreateChart_CheckDeprecatedWarnings$$ ./pkg/chart/v2/lint/ $(TESTFLAGS) -ldflags '$(LDFLAGS)'
go test $(GOFLAGS) -run ^TestHelmCreateChart_CheckDeprecatedWarnings$$ ./internal/chart/v3/lint/ $(TESTFLAGS) -ldflags '$(LDFLAGS)'
# To run the coverage for a specific package use: make test-coverage PKG=./pkg/action
.PHONY: test-coverage
test-coverage:
@echo
@echo "==> Running unit tests with coverage <=="
@ ./scripts/coverage.sh
@echo "==> Running unit tests with coverage: $(PKG) <=="
@ ./scripts/coverage.sh $(PKG)
.PHONY: test-style
test-style:
@ -145,10 +151,6 @@ test-acceptance: build build-cross
test-acceptance-completion: ACCEPTANCE_RUN_TESTS = shells.robot
test-acceptance-completion: test-acceptance
.PHONY: coverage
coverage:
@scripts/coverage.sh
.PHONY: format
format: $(GOIMPORTS)
go list -f '{{.Dir}}' ./... | xargs $(GOIMPORTS) -w -local helm.sh/helm
@ -227,22 +229,23 @@ clean:
.PHONY: release-notes
release-notes:
@if [ ! -d "./_dist" ]; then \
echo "please run 'make fetch-dist' first" && \
exit 1; \
fi
@if [ -z "${PREVIOUS_RELEASE}" ]; then \
echo "please set PREVIOUS_RELEASE environment variable" \
&& exit 1; \
fi
@./scripts/release-notes.sh ${PREVIOUS_RELEASE} ${VERSION}
@if [ ! -d "./_dist" ]; then \
echo "please run 'make fetch-dist' first" && \
exit 1; \
fi
@if [ -z "${PREVIOUS_RELEASE}" ]; then \
echo "please set PREVIOUS_RELEASE environment variable" && \
exit 1; \
fi
@./scripts/release-notes.sh ${PREVIOUS_RELEASE} ${VERSION}
.PHONY: info
info:
@echo "Version: ${VERSION}"
@echo "Git Tag: ${GIT_TAG}"
@echo "Git Commit: ${GIT_COMMIT}"
@echo "Git Tree State: ${GIT_DIRTY}"
@echo "Version: ${VERSION}"
@echo "Git Tag: ${GIT_TAG}"
@echo "Git Commit: ${GIT_COMMIT}"
@echo "Git Tree State: ${GIT_DIRTY}"
.PHONY: tidy
tidy:
go mod tidy

@ -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://img.shields.io/static/v1?label=Health%20Score&message=Healthy&color=A7F3D0&logo=linuxfoundation&logoColor=white&style=flat)](https://insights.linuxfoundation.org/project/helm)
[![LFX Health Score](https://insights.production.lfx.dev/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.

@ -41,11 +41,9 @@ func main() {
}
if err := cmd.Execute(); err != nil {
switch e := err.(type) {
case helmcmd.PluginError:
os.Exit(e.Code)
default:
os.Exit(1)
if cerr, ok := err.(helmcmd.CommandError); ok {
os.Exit(cerr.ExitCode)
}
os.Exit(1)
}
}

@ -22,11 +22,13 @@ import (
"os/exec"
"runtime"
"testing"
"github.com/stretchr/testify/assert"
)
func TestPluginExitCode(t *testing.T) {
func TestCliPluginExitCode(t *testing.T) {
if os.Getenv("RUN_MAIN_FOR_TESTING") == "1" {
os.Args = []string{"helm", "exitwith", "2"}
os.Args = []string{"helm", "exitwith", "43"}
// We DO call helm's main() here. So this looks like a normal `helm` process.
main()
@ -43,7 +45,7 @@ func TestPluginExitCode(t *testing.T) {
// So that the second run is able to run main() and this first run can verify the exit status returned by that.
//
// This technique originates from https://talks.golang.org/2014/testing.slide#23.
cmd := exec.Command(os.Args[0], "-test.run=TestPluginExitCode")
cmd := exec.Command(os.Args[0], "-test.run=TestCliPluginExitCode")
cmd.Env = append(
os.Environ(),
"RUN_MAIN_FOR_TESTING=1",
@ -57,23 +59,21 @@ func TestPluginExitCode(t *testing.T) {
cmd.Stdout = stdout
cmd.Stderr = stderr
err := cmd.Run()
exiterr, ok := err.(*exec.ExitError)
exiterr, ok := err.(*exec.ExitError)
if !ok {
t.Fatalf("Unexpected error returned by os.Exit: %T", err)
t.Fatalf("Unexpected error type returned by os.Exit: %T", err)
}
if stdout.String() != "" {
t.Errorf("Expected no write to stdout: Got %q", stdout.String())
}
assert.Empty(t, stdout.String())
expectedStderr := "Error: plugin \"exitwith\" exited with error\n"
if stderr.String() != expectedStderr {
t.Errorf("Expected %q written to stderr: Got %q", expectedStderr, stderr.String())
}
if exiterr.ExitCode() != 2 {
t.Errorf("Expected exit code 2: Got %d", exiterr.ExitCode())
if exiterr.ExitCode() != 43 {
t.Errorf("Expected exit code 43: Got %d", exiterr.ExitCode())
}
}
}

@ -10,43 +10,48 @@ require (
github.com/Masterminds/sprig/v3 v3.3.0
github.com/Masterminds/squirrel v1.5.4
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.4.1
github.com/cyphar/filepath-securejoin v0.5.0
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/gobwas/glob v0.2.3
github.com/gofrs/flock v0.12.1
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/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5
github.com/rubenv/sql-migrate v1.8.0
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2
github.com/spf13/cobra v1.9.1
github.com/spf13/pflag v1.0.6
github.com/stretchr/testify v1.10.0
golang.org/x/crypto v0.40.0
golang.org/x/term v0.33.0
golang.org/x/text v0.27.0
gopkg.in/yaml.v3 v3.0.1
k8s.io/api v0.33.2
k8s.io/apiextensions-apiserver v0.33.2
k8s.io/apimachinery v0.33.2
k8s.io/apiserver v0.33.2
k8s.io/cli-runtime v0.33.2
k8s.io/client-go v0.33.2
github.com/spf13/cobra v1.10.1
github.com/spf13/pflag v1.0.10
github.com/stretchr/testify v1.11.1
github.com/tetratelabs/wazero v1.9.0
go.yaml.in/yaml/v3 v3.0.4
golang.org/x/crypto v0.43.0
golang.org/x/term v0.36.0
golang.org/x/text v0.30.0
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/klog/v2 v2.130.1
k8s.io/kubectl v0.33.2
k8s.io/kubectl v0.34.1
oras.land/oras-go/v2 v2.6.0
sigs.k8s.io/controller-runtime v0.21.0
sigs.k8s.io/kustomize/kyaml v0.20.0
sigs.k8s.io/yaml v1.5.0
sigs.k8s.io/controller-runtime v0.22.3
sigs.k8s.io/kustomize/kyaml v0.20.1
sigs.k8s.io/yaml v1.6.0
)
require (
@ -57,10 +62,10 @@ require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/blang/semver/v4 v4.0.0 // indirect
github.com/bshuster-repo/logrus-logstash-hook v1.0.0 // indirect
github.com/carapace-sh/carapace-shlex v1.0.1 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/chai2010/gettext-go v1.0.2 // indirect
github.com/cloudflare/circl v1.6.1 // indirect
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
@ -69,11 +74,11 @@ require (
github.com/docker/docker-credential-helpers v0.8.2 // indirect
github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c // indirect
github.com/docker/go-metrics v0.0.1 // indirect
github.com/dylibso/observe-sdk/go v0.0.0-20240819160327-2d926c5d788a // indirect
github.com/emicklei/go-restful/v3 v3.12.2 // indirect
github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect
github.com/fatih/color v1.13.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fxamacker/cbor/v2 v2.8.0 // indirect
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
github.com/go-errors/errors v1.5.1 // indirect
github.com/go-gorp/gorp/v3 v3.1.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
@ -90,10 +95,11 @@ require (
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.24.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect
github.com/hashicorp/golang-lru/arc/v2 v2.0.5 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.5 // indirect
github.com/huandu/xstrings v1.5.0 // indirect
github.com/ianlancetaylor/demangle v0.0.0-20240805132620-81f5be970eca // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
@ -103,19 +109,18 @@ require (
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect
github.com/mailru/easyjson v0.9.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.9 // indirect
github.com/miekg/dns v1.1.57 // indirect
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
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.2 // 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/opencontainers/go-digest v1.0.0 // 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
@ -130,6 +135,7 @@ require (
github.com/shopspring/decimal v1.4.0 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/spf13/cast v1.7.0 // indirect
github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/xlab/treeprint v1.2.0 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
@ -141,8 +147,8 @@ require (
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.8.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.32.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.32.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0 // indirect
go.opentelemetry.io/otel/exporters/prometheus v0.54.0 // indirect
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.8.0 // indirect
@ -150,32 +156,31 @@ 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.33.0 // indirect
go.opentelemetry.io/otel/sdk v1.34.0 // indirect
go.opentelemetry.io/otel/sdk/log v0.8.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.32.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.34.0 // indirect
go.opentelemetry.io/otel/trace v1.37.0 // indirect
go.opentelemetry.io/proto/otlp v1.4.0 // indirect
go.opentelemetry.io/proto/otlp v1.5.0 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/mod v0.25.0 // indirect
golang.org/x/net v0.41.0 // indirect
golang.org/x/mod v0.28.0 // indirect
golang.org/x/net v0.45.0 // indirect
golang.org/x/oauth2 v0.30.0 // indirect
golang.org/x/sync v0.16.0 // indirect
golang.org/x/sys v0.34.0 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.37.0 // indirect
golang.org/x/time v0.12.0 // indirect
golang.org/x/tools v0.34.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 // indirect
google.golang.org/grpc v1.68.1 // indirect
golang.org/x/tools v0.37.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
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
k8s.io/component-base v0.33.2 // indirect
k8s.io/kube-openapi v0.0.0-20250701173324-9bd5c66d9911 // 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
sigs.k8s.io/kustomize/api v0.20.0 // indirect
sigs.k8s.io/kustomize/api v0.20.1 // indirect
sigs.k8s.io/randfill v1.0.0 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.7.0 // indirect
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect
)

188
go.sum

@ -22,6 +22,8 @@ github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8
github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10=
github.com/Masterminds/vcs v1.13.3 h1:IIA2aBdXvfbIM+yl/eTnL4hb1XwdpvuQLglAix1gweE=
github.com/Masterminds/vcs v1.13.3/go.mod h1:TiE7xuEjl1N4j016moRd6vezp6e6Lz23gypeXfzXeW8=
github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw=
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=
@ -42,8 +44,6 @@ github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdb
github.com/bsm/gomega v1.26.0/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/carapace-sh/carapace-shlex v1.0.1 h1:ww0JCgWpOVuqWG7k3724pJ18Lq8gh5pHQs9j3ojUs1c=
github.com/carapace-sh/carapace-shlex v1.0.1/go.mod h1:lJ4ZsdxytE0wHJ8Ta9S7Qq0XpjgjU0mdfCqiI2FHx7M=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
@ -51,14 +51,16 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chai2010/gettext-go v1.0.2 h1:1Lwwip6Q2QGsAdl/ZKPCwTe9fe0CjlUbqj5bFNSjIRk=
github.com/chai2010/gettext-go v1.0.2/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA=
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0=
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.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s=
github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
github.com/cyphar/filepath-securejoin v0.5.0 h1:hIAhkRBMQ8nIeuVwcAoymp7MY4oherZdAxD+m0u9zaw=
github.com/cyphar/filepath-securejoin v0.5.0/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
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=
@ -77,14 +79,18 @@ github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c h1:+pKlWGMw7gf6bQ
github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA=
github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8=
github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw=
github.com/dylibso/observe-sdk/go v0.0.0-20240819160327-2d926c5d788a h1:UwSIFv5g5lIvbGgtf3tVwC7Ky9rmMFBp0RMs+6f6YqE=
github.com/dylibso/observe-sdk/go v0.0.0-20240819160327-2d926c5d788a/go.mod h1:C8DzXehI4zAbrdlbtOByKX6pfivJTBiV9Jjqv56Yd9Q=
github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU=
github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU=
github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM=
github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f h1:Wl78ApPPB2Wvf/TIe2xdyJxTlb6obmF18d8QdkxNDu4=
github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f/go.mod h1:OSYXu++VVOHnXeitef/D8n/6y4QV8uLHSFXX4NeXMGc=
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/extism/go-sdk v1.7.1 h1:lWJos6uY+tRFdlIHR+SJjwFDApY7OypS/2nMhiVQ9Sw=
github.com/extism/go-sdk v1.7.1/go.mod h1:IT+Xdg5AZM9hVtpFUA+uZCJMge/hbvshl8bwzLtFyKA=
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=
@ -93,8 +99,8 @@ github.com/foxcpp/go-mockdns v1.1.0 h1:jI0rD8M0wuYAxL7r/ynTrCQQq0BVqfB99Vgk7Dlme
github.com/foxcpp/go-mockdns v1.1.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fxamacker/cbor/v2 v2.8.0 h1:fFtUGXUzXPHTIUdne5+zzMPTfffl3RD5qYnkY40vtxU=
github.com/fxamacker/cbor/v2 v2.8.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk=
github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
github.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs=
@ -123,8 +129,8 @@ github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZ
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E=
github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0=
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=
@ -138,7 +144,6 @@ github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl76
github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo=
github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
@ -156,14 +161,16 @@ 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=
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 h1:TmHmbvxPmaegwhDubVz0lICL0J5Ka2vwTzhoePEXsGE=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0/go.mod h1:qztMSjm835F2bXf+5HKAPIS5qsmQDqZna/PgVt4rWtI=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
github.com/hashicorp/golang-lru/arc/v2 v2.0.5 h1:l2zaLDubNhW4XO3LnliVj0GXO3+/CGNJAg1dcN2Fpfw=
github.com/hashicorp/golang-lru/arc/v2 v2.0.5/go.mod h1:ny6zBSQZi2JxIeYcv7kt2sH2PXJtirBN7RDhRpxPkxU=
github.com/hashicorp/golang-lru/v2 v2.0.5 h1:wW7h1TG88eUIJ2i69gaE3uNVtEPIagzhGvHgwfx2Vm4=
github.com/hashicorp/golang-lru/v2 v2.0.5/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/ianlancetaylor/demangle v0.0.0-20240805132620-81f5be970eca h1:T54Ema1DU8ngI+aef9ZhAhNGQhcRTrWxVeG07F+c/Rw=
github.com/ianlancetaylor/demangle v0.0.0-20240805132620-81f5be970eca/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
@ -198,14 +205,11 @@ github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhn
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE=
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk=
@ -230,8 +234,9 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8=
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0=
github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
@ -249,8 +254,6 @@ github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJw
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
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/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 h1:Ii+DKncOVM8Cu1Hc+ETb5K+23HdAMvESYE3ZJ5b5cMI=
github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE=
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=
@ -301,10 +304,11 @@ 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.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
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/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=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
@ -313,8 +317,12 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834 h1:ZF+QBjOI+tILZjBaFj3HgFonKXUcwgJ4djLb6i42S3Q=
github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834/go.mod h1:m9ymHTgNSEjuxvw8E7WWe4Pl4hZQHXONY8wE6dMLaRk=
github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I=
github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM=
github.com/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=
@ -340,10 +348,10 @@ go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.32.0 h1:j7Z
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.32.0/go.mod h1:WXbYJTUaZXAbYd8lbgGuvih0yuCfOFC5RJoYnoLcGz8=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.32.0 h1:t/Qur3vKSkUCcDVaSumWF2PKHt85pc7fRvFuoVT8qFU=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.32.0/go.mod h1:Rl61tySSdcOJWoEgYZVtmnKdA0GeKrSqkHC1t+91CH8=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 h1:Vh5HayB/0HHfOQA7Ctx69E/Y/DcQSMPpKANYVMQ7fBA=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0/go.mod h1:cpgtDBaqD/6ok/UG0jT15/uKjAY8mRA53diogHBg3UI=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0 h1:5pojmb1U1AogINhN3SurB+zm/nIcusopeBNp42f45QM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0/go.mod h1:57gTHJSE5S1tqg+EKsLPlTWhpHMsWlVmer+LA926XiA=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0/go.mod h1:7Bept48yIeqxP2OZ9/AqIpYS94h2or0aB4FypJTc8ZM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 h1:tgJ0uaNS4c98WRNUEx5U3aDlrDOI5Rs+1Vifcw4DJ8U=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0/go.mod h1:U7HYyW0zt/a9x5J1Kjs+r1f/d4ZHnYFclhYY2+YbeoE=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0 h1:cMyu9O88joYEaI47CnQkxO1XZdpoTF9fEnW2duIddhw=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0/go.mod h1:6Am3rn7P9TVVeXYG+wtcGE7IE1tsQ+bP3AuWcKt/gOI=
go.opentelemetry.io/otel/exporters/prometheus v0.54.0 h1:rFwzp68QMgtzu9PgP3jm9XaMICI6TsofWWPcBDKwlsU=
@ -358,16 +366,16 @@ 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.33.0 h1:iax7M131HuAm9QkZotNHEfstof92xM+N8sr3uHXc2IM=
go.opentelemetry.io/otel/sdk v1.33.0/go.mod h1:A1Q5oi7/9XaMlIWzPSxLRWOI8nG3FnzHJNbiENQuihM=
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/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.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU=
go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ=
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/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.4.0 h1:TA9WRvW6zMwP+Ssb6fLoUIuirti1gGbP28GcKG1jgeg=
go.opentelemetry.io/proto/otlp v1.4.0/go.mod h1:PPBWZIP98o2ElSqI35IHfu7hIhSwvc5N38Jw8pXuGFY=
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=
@ -388,16 +396,16 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g=
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
golang.org/x/crypto v0.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/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
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=
@ -411,8 +419,8 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ=
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM=
golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
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=
@ -425,31 +433,29 @@ 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.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.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-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.37.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=
@ -457,8 +463,8 @@ golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
golang.org/x/term v0.14.0/go.mod h1:TySc+nGkYR6qt8km8wUhuFRTVSMIX3XPR58y2lC8vww=
golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg=
golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0=
golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
golang.org/x/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=
@ -466,8 +472,8 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
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/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=
@ -478,18 +484,18 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.15.0/go.mod h1:hpksKq4dtpQWS1uQ61JkdqWM3LscIS6Slf+VVkm+wQk=
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
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-20241209162323-e6fa225c2576 h1:CkkIfIt50+lT6NHAVoRYEyAvQGFM7xEwXUUywFvEb3Q=
google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576/go.mod h1:1R3kvZ1dtP3+4p4d3G8uJ8rFk/fWlScl38vanWACI08=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 h1:8ZmaLZE4XWrtU3MyClkYqqtl6Oegr3235h7jxsDyqCY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU=
google.golang.org/grpc v1.68.1 h1:oI5oTa11+ng8r8XMMN7jAOmWfPZWbYpCFaMUTACxkM0=
google.golang.org/grpc v1.68.1/go.mod h1:+q1XYFJjShcqn0QZHvCyeR4CXPA+llXIeUIfIe00waw=
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=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
@ -506,43 +512,41 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
k8s.io/api v0.33.2 h1:YgwIS5jKfA+BZg//OQhkJNIfie/kmRsO0BmNaVSimvY=
k8s.io/api v0.33.2/go.mod h1:fhrbphQJSM2cXzCWgqU29xLDuks4mu7ti9vveEnpSXs=
k8s.io/apiextensions-apiserver v0.33.2 h1:6gnkIbngnaUflR3XwE1mCefN3YS8yTD631JXQhsU6M8=
k8s.io/apiextensions-apiserver v0.33.2/go.mod h1:IvVanieYsEHJImTKXGP6XCOjTwv2LUMos0YWc9O+QP8=
k8s.io/apimachinery v0.33.2 h1:IHFVhqg59mb8PJWTLi8m1mAoepkUNYmptHsV+Z1m5jY=
k8s.io/apimachinery v0.33.2/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM=
k8s.io/apiserver v0.33.2 h1:KGTRbxn2wJagJowo29kKBp4TchpO1DRO3g+dB/KOJN4=
k8s.io/apiserver v0.33.2/go.mod h1:9qday04wEAMLPWWo9AwqCZSiIn3OYSZacDyu/AcoM/M=
k8s.io/cli-runtime v0.33.2 h1:koNYQKSDdq5AExa/RDudXMhhtFasEg48KLS2KSAU74Y=
k8s.io/cli-runtime v0.33.2/go.mod h1:gnhsAWpovqf1Zj5YRRBBU7PFsRc6NkEkwYNQE+mXL88=
k8s.io/client-go v0.33.2 h1:z8CIcc0P581x/J1ZYf4CNzRKxRvQAwoAolYPbtQes+E=
k8s.io/client-go v0.33.2/go.mod h1:9mCgT4wROvL948w6f6ArJNb7yQd7QsvqavDeZHvNmHo=
k8s.io/component-base v0.33.2 h1:sCCsn9s/dG3ZrQTX/Us0/Sx2R0G5kwa0wbZFYoVp/+0=
k8s.io/component-base v0.33.2/go.mod h1:/41uw9wKzuelhN+u+/C59ixxf4tYQKW7p32ddkYNe2k=
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/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-20250701173324-9bd5c66d9911 h1:gAXU86Fmbr/ktY17lkHwSjw5aoThQvhnstGGIYKlKYc=
k8s.io/kube-openapi v0.0.0-20250701173324-9bd5c66d9911/go.mod h1:GLOk5B+hDbRROvt0X2+hqX64v/zO3vXN7J78OUmBSKw=
k8s.io/kubectl v0.33.2 h1:7XKZ6DYCklu5MZQzJe+CkCjoGZwD1wWl7t/FxzhMz7Y=
k8s.io/kubectl v0.33.2/go.mod h1:8rC67FB8tVTYraovAGNi/idWIK90z2CHFNMmGJZJ3KI=
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=
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.21.0 h1:CYfjpEuicjUecRk+KAeyYh+ouUBn4llGyDYytIGcJS8=
sigs.k8s.io/controller-runtime v0.21.0/go.mod h1:OSg14+F65eWqIu4DceX7k/+QRAbTTvxeQSNSOQpukWM=
sigs.k8s.io/controller-runtime v0.22.3 h1:I7mfqz/a/WdmDCEnXmSPm8/b/yRTy6JsKKENTijTq8Y=
sigs.k8s.io/controller-runtime v0.22.3/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/kustomize/api v0.20.0 h1:xPLqcobHI0bThyRUteO+nCV8G4d1Rlo5HafO57VRcas=
sigs.k8s.io/kustomize/api v0.20.0/go.mod h1:F6CfaV27oevRCMJgehLqyX81dlUnRX/Fc13Uo7+OSo4=
sigs.k8s.io/kustomize/kyaml v0.20.0 h1:tT8KMKi4R3hCJ1+9HDdek2VoXpkerP92ZfF6fDgGw14=
sigs.k8s.io/kustomize/kyaml v0.20.0/go.mod h1:0EmkQHRUsJxY8Ug9Niig1pUMSCGHxQ5RklbpV/Ri6po=
sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
sigs.k8s.io/kustomize/api v0.20.1 h1:iWP1Ydh3/lmldBnH/S5RXgT98vWYMaTUL1ADcr+Sv7I=
sigs.k8s.io/kustomize/api v0.20.1/go.mod h1:t6hUFxO+Ph0VxIk1sKp1WS0dOjbPCtLJ4p8aADLwqjM=
sigs.k8s.io/kustomize/kyaml v0.20.1 h1:PCMnA2mrVbRP3NIB6v9kYCAc38uvFLVs8j/CD567A78=
sigs.k8s.io/kustomize/kyaml v0.20.1/go.mod h1:0EmkQHRUsJxY8Ug9Niig1pUMSCGHxQ5RklbpV/Ri6po=
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
sigs.k8s.io/structured-merge-diff/v4 v4.7.0 h1:qPeWmscJcXP0snki5IYF79Z8xrl8ETFxgMd7wez1XkI=
sigs.k8s.io/structured-merge-diff/v4 v4.7.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps=
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
sigs.k8s.io/yaml v1.5.0 h1:M10b2U7aEUY6hRtU870n2VTPgR5RZiL/I6Lcc2F4NUQ=
sigs.k8s.io/yaml v1.5.0/go.mod h1:wZs27Rbxoai4C0f8/9urLZtZtF3avA3gKvGyPdDqTO4=
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco=
sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=

@ -0,0 +1,174 @@
/*
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 v3
import (
"path/filepath"
"regexp"
"strings"
"helm.sh/helm/v4/pkg/chart/common"
)
// APIVersionV3 is the API version number for version 3.
const APIVersionV3 = "v3"
// aliasNameFormat defines the characters that are legal in an alias name.
var aliasNameFormat = regexp.MustCompile("^[a-zA-Z0-9_-]+$")
// Chart is a helm package that contains metadata, a default config, zero or more
// optionally parameterizable templates, and zero or more charts (dependencies).
type Chart struct {
// Raw contains the raw contents of the files originally contained in the chart archive.
//
// This should not be used except in special cases like `helm show values`,
// where we want to display the raw values, comments and all.
Raw []*common.File `json:"-"`
// Metadata is the contents of the Chartfile.
Metadata *Metadata `json:"metadata"`
// Lock is the contents of Chart.lock.
Lock *Lock `json:"lock"`
// Templates for this chart.
Templates []*common.File `json:"templates"`
// Values are default config for this chart.
Values map[string]interface{} `json:"values"`
// Schema is an optional JSON schema for imposing structure on Values
Schema []byte `json:"schema"`
// Files are miscellaneous files in a chart archive,
// e.g. README, LICENSE, etc.
Files []*common.File `json:"files"`
parent *Chart
dependencies []*Chart
}
type CRD struct {
// Name is the File.Name for the crd file
Name string
// Filename is the File obj Name including (sub-)chart.ChartFullPath
Filename string
// File is the File obj for the crd
File *common.File
}
// SetDependencies replaces the chart dependencies.
func (ch *Chart) SetDependencies(charts ...*Chart) {
ch.dependencies = nil
ch.AddDependency(charts...)
}
// Name returns the name of the chart.
func (ch *Chart) Name() string {
if ch.Metadata == nil {
return ""
}
return ch.Metadata.Name
}
// AddDependency determines if the chart is a subchart.
func (ch *Chart) AddDependency(charts ...*Chart) {
for i, x := range charts {
charts[i].parent = ch
ch.dependencies = append(ch.dependencies, x)
}
}
// Root finds the root chart.
func (ch *Chart) Root() *Chart {
if ch.IsRoot() {
return ch
}
return ch.Parent().Root()
}
// Dependencies are the charts that this chart depends on.
func (ch *Chart) Dependencies() []*Chart { return ch.dependencies }
// IsRoot determines if the chart is the root chart.
func (ch *Chart) IsRoot() bool { return ch.parent == nil }
// Parent returns a subchart's parent chart.
func (ch *Chart) Parent() *Chart { return ch.parent }
// ChartPath returns the full path to this chart in dot notation.
func (ch *Chart) ChartPath() string {
if !ch.IsRoot() {
return ch.Parent().ChartPath() + "." + ch.Name()
}
return ch.Name()
}
// ChartFullPath returns the full path to this chart.
// Note that the path may not correspond to the path where the file can be found on the file system if the path
// points to an aliased subchart.
func (ch *Chart) ChartFullPath() string {
if !ch.IsRoot() {
return ch.Parent().ChartFullPath() + "/charts/" + ch.Name()
}
return ch.Name()
}
// Validate validates the metadata.
func (ch *Chart) Validate() error {
return ch.Metadata.Validate()
}
// AppVersion returns the appversion of the chart.
func (ch *Chart) AppVersion() string {
if ch.Metadata == nil {
return ""
}
return ch.Metadata.AppVersion
}
// CRDs returns a list of File objects in the 'crds/' directory of a Helm chart.
// Deprecated: use CRDObjects()
func (ch *Chart) CRDs() []*common.File {
files := []*common.File{}
// Find all resources in the crds/ directory
for _, f := range ch.Files {
if strings.HasPrefix(f.Name, "crds/") && hasManifestExtension(f.Name) {
files = append(files, f)
}
}
// Get CRDs from dependencies, too.
for _, dep := range ch.Dependencies() {
files = append(files, dep.CRDs()...)
}
return files
}
// CRDObjects returns a list of CRD objects in the 'crds/' directory of a Helm chart & subcharts
func (ch *Chart) CRDObjects() []CRD {
crds := []CRD{}
// Find all resources in the crds/ directory
for _, f := range ch.Files {
if strings.HasPrefix(f.Name, "crds/") && hasManifestExtension(f.Name) {
mycrd := CRD{Name: f.Name, Filename: filepath.Join(ch.ChartFullPath(), f.Name), File: f}
crds = append(crds, mycrd)
}
}
// Get CRDs from dependencies, too.
for _, dep := range ch.Dependencies() {
crds = append(crds, dep.CRDObjects()...)
}
return crds
}
func hasManifestExtension(fname string) bool {
ext := filepath.Ext(fname)
return strings.EqualFold(ext, ".yaml") || strings.EqualFold(ext, ".yml") || strings.EqualFold(ext, ".json")
}

@ -0,0 +1,213 @@
/*
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 v3
import (
"encoding/json"
"testing"
"github.com/stretchr/testify/assert"
"helm.sh/helm/v4/pkg/chart/common"
)
func TestCRDs(t *testing.T) {
chrt := Chart{
Files: []*common.File{
{
Name: "crds/foo.yaml",
Data: []byte("hello"),
},
{
Name: "bar.yaml",
Data: []byte("hello"),
},
{
Name: "crds/foo/bar/baz.yaml",
Data: []byte("hello"),
},
{
Name: "crdsfoo/bar/baz.yaml",
Data: []byte("hello"),
},
{
Name: "crds/README.md",
Data: []byte("# hello"),
},
},
}
is := assert.New(t)
crds := chrt.CRDs()
is.Equal(2, len(crds))
is.Equal("crds/foo.yaml", crds[0].Name)
is.Equal("crds/foo/bar/baz.yaml", crds[1].Name)
}
func TestSaveChartNoRawData(t *testing.T) {
chrt := Chart{
Raw: []*common.File{
{
Name: "fhqwhgads.yaml",
Data: []byte("Everybody to the Limit"),
},
},
}
is := assert.New(t)
data, err := json.Marshal(chrt)
if err != nil {
t.Fatal(err)
}
res := &Chart{}
if err := json.Unmarshal(data, res); err != nil {
t.Fatal(err)
}
is.Equal([]*common.File(nil), res.Raw)
}
func TestMetadata(t *testing.T) {
chrt := Chart{
Metadata: &Metadata{
Name: "foo.yaml",
AppVersion: "1.0.0",
APIVersion: "v3",
Version: "1.0.0",
Type: "application",
},
}
is := assert.New(t)
is.Equal("foo.yaml", chrt.Name())
is.Equal("1.0.0", chrt.AppVersion())
is.Equal(nil, chrt.Validate())
}
func TestIsRoot(t *testing.T) {
chrt1 := Chart{
parent: &Chart{
Metadata: &Metadata{
Name: "foo",
},
},
}
chrt2 := Chart{
Metadata: &Metadata{
Name: "foo",
},
}
is := assert.New(t)
is.Equal(false, chrt1.IsRoot())
is.Equal(true, chrt2.IsRoot())
}
func TestChartPath(t *testing.T) {
chrt1 := Chart{
parent: &Chart{
Metadata: &Metadata{
Name: "foo",
},
},
}
chrt2 := Chart{
Metadata: &Metadata{
Name: "foo",
},
}
is := assert.New(t)
is.Equal("foo.", chrt1.ChartPath())
is.Equal("foo", chrt2.ChartPath())
}
func TestChartFullPath(t *testing.T) {
chrt1 := Chart{
parent: &Chart{
Metadata: &Metadata{
Name: "foo",
},
},
}
chrt2 := Chart{
Metadata: &Metadata{
Name: "foo",
},
}
is := assert.New(t)
is.Equal("foo/charts/", chrt1.ChartFullPath())
is.Equal("foo", chrt2.ChartFullPath())
}
func TestCRDObjects(t *testing.T) {
chrt := Chart{
Files: []*common.File{
{
Name: "crds/foo.yaml",
Data: []byte("hello"),
},
{
Name: "bar.yaml",
Data: []byte("hello"),
},
{
Name: "crds/foo/bar/baz.yaml",
Data: []byte("hello"),
},
{
Name: "crdsfoo/bar/baz.yaml",
Data: []byte("hello"),
},
{
Name: "crds/README.md",
Data: []byte("# hello"),
},
},
}
expected := []CRD{
{
Name: "crds/foo.yaml",
Filename: "crds/foo.yaml",
File: &common.File{
Name: "crds/foo.yaml",
Data: []byte("hello"),
},
},
{
Name: "crds/foo/bar/baz.yaml",
Filename: "crds/foo/bar/baz.yaml",
File: &common.File{
Name: "crds/foo/bar/baz.yaml",
Data: []byte("hello"),
},
},
}
is := assert.New(t)
crds := chrt.CRDObjects()
is.Equal(expected, crds)
}

@ -0,0 +1,82 @@
/*
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 v3
import "time"
// Dependency describes a chart upon which another chart depends.
//
// Dependencies can be used to express developer intent, or to capture the state
// of a chart.
type Dependency struct {
// Name is the name of the dependency.
//
// This must mach the name in the dependency's Chart.yaml.
Name string `json:"name" yaml:"name"`
// Version is the version (range) of this chart.
//
// A lock file will always produce a single version, while a dependency
// may contain a semantic version range.
Version string `json:"version,omitempty" yaml:"version,omitempty"`
// The URL to the repository.
//
// Appending `index.yaml` to this string should result in a URL that can be
// used to fetch the repository index.
Repository string `json:"repository" yaml:"repository"`
// A yaml path that resolves to a boolean, used for enabling/disabling charts (e.g. subchart1.enabled )
Condition string `json:"condition,omitempty" yaml:"condition,omitempty"`
// Tags can be used to group charts for enabling/disabling together
Tags []string `json:"tags,omitempty" yaml:"tags,omitempty"`
// Enabled bool determines if chart should be loaded
Enabled bool `json:"enabled,omitempty" yaml:"enabled,omitempty"`
// ImportValues holds the mapping of source values to parent key to be imported. Each item can be a
// string or pair of child/parent sublist items.
ImportValues []interface{} `json:"import-values,omitempty" yaml:"import-values,omitempty"`
// Alias usable alias to be used for the chart
Alias string `json:"alias,omitempty" yaml:"alias,omitempty"`
}
// Validate checks for common problems with the dependency datastructure in
// the chart. This check must be done at load time before the dependency's charts are
// loaded.
func (d *Dependency) Validate() error {
if d == nil {
return ValidationError("dependencies must not contain empty or null nodes")
}
d.Name = sanitizeString(d.Name)
d.Version = sanitizeString(d.Version)
d.Repository = sanitizeString(d.Repository)
d.Condition = sanitizeString(d.Condition)
for i := range d.Tags {
d.Tags[i] = sanitizeString(d.Tags[i])
}
if d.Alias != "" && !aliasNameFormat.MatchString(d.Alias) {
return ValidationErrorf("dependency %q has disallowed characters in the alias", d.Name)
}
return nil
}
// Lock is a lock file for dependencies.
//
// It represents the state that the dependencies should be in.
type Lock struct {
// Generated is the date the lock file was last generated.
Generated time.Time `json:"generated"`
// Digest is a hash of the dependencies in Chart.yaml.
Digest string `json:"digest"`
// Dependencies is the list of dependencies that this lock file has locked.
Dependencies []*Dependency `json:"dependencies"`
}

@ -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 v3
import (
"testing"
)
func TestValidateDependency(t *testing.T) {
dep := &Dependency{
Name: "example",
}
for value, shouldFail := range map[string]bool{
"abcdefghijklmenopQRSTUVWXYZ-0123456780_": false,
"-okay": false,
"_okay": false,
"- bad": true,
" bad": true,
"bad\nvalue": true,
"bad ": true,
"bad$": true,
} {
dep.Alias = value
res := dep.Validate()
if res != nil && !shouldFail {
t.Errorf("Failed on case %q", dep.Alias)
} else if res == nil && shouldFail {
t.Errorf("Expected failure for %q", dep.Alias)
}
}
}

@ -1,13 +1,10 @@
//go:build !linux
/*
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
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,
@ -15,13 +12,10 @@ 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 ctime
import (
"os"
"time"
)
/*
Package v3 provides chart handling for apiVersion v3 charts
func modified(fi os.FileInfo) time.Time {
return fi.ModTime()
}
This package and its sub-packages provide handling for apiVersion v3 charts.
*/
package v3

@ -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 v3
import "fmt"
// ValidationError represents a data validation error.
type ValidationError string
func (v ValidationError) Error() string {
return "validation: " + string(v)
}
// ValidationErrorf takes a message and formatting options and creates a ValidationError
func ValidationErrorf(msg string, args ...interface{}) ValidationError {
return ValidationError(fmt.Sprintf(msg, args...))
}

@ -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 v3
import (
"testing"
fuzz "github.com/AdaLogics/go-fuzz-headers"
)
func FuzzMetadataValidate(f *testing.F) {
f.Fuzz(func(t *testing.T, data []byte) {
fdp := fuzz.NewConsumer(data)
// Add random values to the metadata
md := &Metadata{}
err := fdp.GenerateStruct(md)
if err != nil {
t.Skip()
}
md.Validate()
})
}
func FuzzDependencyValidate(f *testing.F) {
f.Fuzz(func(t *testing.T, data []byte) {
f := fuzz.NewConsumer(data)
// Add random values to the dependenci
d := &Dependency{}
err := f.GenerateStruct(d)
if err != nil {
t.Skip()
}
d.Validate()
})
}

@ -14,24 +14,24 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package lint // import "helm.sh/helm/v4/pkg/lint"
package lint // import "helm.sh/helm/v4/internal/chart/v3/lint"
import (
"path/filepath"
chartutil "helm.sh/helm/v4/pkg/chart/v2/util"
"helm.sh/helm/v4/pkg/lint/rules"
"helm.sh/helm/v4/pkg/lint/support"
"helm.sh/helm/v4/internal/chart/v3/lint/rules"
"helm.sh/helm/v4/internal/chart/v3/lint/support"
"helm.sh/helm/v4/pkg/chart/common"
)
type linterOptions struct {
KubeVersion *chartutil.KubeVersion
KubeVersion *common.KubeVersion
SkipSchemaValidation bool
}
type LinterOption func(lo *linterOptions)
func WithKubeVersion(kubeVersion *chartutil.KubeVersion) LinterOption {
func WithKubeVersion(kubeVersion *common.KubeVersion) LinterOption {
return func(lo *linterOptions) {
lo.KubeVersion = kubeVersion
}
@ -57,9 +57,10 @@ func RunAll(baseDir string, values map[string]interface{}, namespace string, opt
}
rules.Chartfile(&result)
rules.ValuesWithOverrides(&result, values)
rules.ValuesWithOverrides(&result, values, lo.SkipSchemaValidation)
rules.TemplatesWithSkipSchemaValidation(&result, values, namespace, lo.KubeVersion, lo.SkipSchemaValidation)
rules.Dependencies(&result)
rules.Crds(&result)
return result
}

@ -0,0 +1,253 @@
/*
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 lint
import (
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"helm.sh/helm/v4/internal/chart/v3/lint/support"
chartutil "helm.sh/helm/v4/internal/chart/v3/util"
)
const namespace = "testNamespace"
const badChartDir = "rules/testdata/badchartfile"
const badValuesFileDir = "rules/testdata/badvaluesfile"
const badYamlFileDir = "rules/testdata/albatross"
const badCrdFileDir = "rules/testdata/badcrdfile"
const goodChartDir = "rules/testdata/goodone"
const subChartValuesDir = "rules/testdata/withsubchart"
const malformedTemplate = "rules/testdata/malformed-template"
const invalidChartFileDir = "rules/testdata/invalidchartfile"
func TestBadChartV3(t *testing.T) {
var values map[string]any
m := RunAll(badChartDir, values, namespace).Messages
if len(m) != 8 {
t.Errorf("Number of errors %v", len(m))
t.Errorf("All didn't fail with expected errors, got %#v", m)
}
// There should be one INFO, one WARNING, and 2 ERROR messages, check for them
var i, w, e, e2, e3, e4, e5, e6 bool
for _, msg := range m {
if msg.Severity == support.InfoSev {
if strings.Contains(msg.Err.Error(), "icon is recommended") {
i = true
}
}
if msg.Severity == support.WarningSev {
if strings.Contains(msg.Err.Error(), "does not exist") {
w = true
}
}
if msg.Severity == support.ErrorSev {
if strings.Contains(msg.Err.Error(), "version '0.0.0.0' is not a valid SemVerV2") {
e = true
}
if strings.Contains(msg.Err.Error(), "name is required") {
e2 = true
}
if strings.Contains(msg.Err.Error(), "apiVersion is required. The value must be \"v3\"") {
e3 = true
}
if strings.Contains(msg.Err.Error(), "chart type is not valid in apiVersion") {
e4 = true
}
if strings.Contains(msg.Err.Error(), "dependencies are not valid in the Chart file with apiVersion") {
e5 = true
}
// This comes from the dependency check, which loads dependency info from the Chart.yaml
if strings.Contains(msg.Err.Error(), "unable to load chart") {
e6 = true
}
}
}
if !e || !e2 || !e3 || !e4 || !e5 || !i || !e6 || !w {
t.Errorf("Didn't find all the expected errors, got %#v", m)
}
}
func TestInvalidYaml(t *testing.T) {
var values map[string]any
m := RunAll(badYamlFileDir, values, namespace).Messages
if len(m) != 1 {
t.Fatalf("All didn't fail with expected errors, got %#v", m)
}
if !strings.Contains(m[0].Err.Error(), "deliberateSyntaxError") {
t.Errorf("All didn't have the error for deliberateSyntaxError")
}
}
func TestInvalidChartYamlV3(t *testing.T) {
var values map[string]any
m := RunAll(invalidChartFileDir, values, namespace).Messages
t.Log(m)
if len(m) != 3 {
t.Fatalf("All didn't fail with expected errors, got %#v", m)
}
if !strings.Contains(m[0].Err.Error(), "failed to strictly parse chart metadata file") {
t.Errorf("All didn't have the error for duplicate YAML keys")
}
}
func TestBadValuesV3(t *testing.T) {
var values map[string]any
m := RunAll(badValuesFileDir, values, namespace).Messages
if len(m) < 1 {
t.Fatalf("All didn't fail with expected errors, got %#v", m)
}
if !strings.Contains(m[0].Err.Error(), "unable to parse YAML") {
t.Errorf("All didn't have the error for invalid key format: %s", m[0].Err)
}
}
func TestBadCrdFileV3(t *testing.T) {
var values map[string]any
m := RunAll(badCrdFileDir, values, namespace).Messages
assert.Lenf(t, m, 2, "All didn't fail with expected errors, got %#v", m)
assert.ErrorContains(t, m[0].Err, "apiVersion is not in 'apiextensions.k8s.io'")
assert.ErrorContains(t, m[1].Err, "object kind is not 'CustomResourceDefinition'")
}
func TestGoodChart(t *testing.T) {
var values map[string]any
m := RunAll(goodChartDir, values, namespace).Messages
if len(m) != 0 {
t.Error("All returned linter messages when it shouldn't have")
for i, msg := range m {
t.Logf("Message %d: %s", i, msg)
}
}
}
// TestHelmCreateChart tests that a `helm create` always passes a `helm lint` test.
//
// See https://github.com/helm/helm/issues/7923
func TestHelmCreateChart(t *testing.T) {
var values map[string]any
dir := t.TempDir()
createdChart, err := chartutil.Create("testhelmcreatepasseslint", dir)
if err != nil {
t.Error(err)
// Fatal is bad because of the defer.
return
}
// Note: we test with strict=true here, even though others have
// strict = false.
m := RunAll(createdChart, values, namespace, WithSkipSchemaValidation(true)).Messages
if ll := len(m); ll != 1 {
t.Errorf("All should have had exactly 1 error. Got %d", ll)
for i, msg := range m {
t.Logf("Message %d: %s", i, msg.Error())
}
} else if msg := m[0].Err.Error(); !strings.Contains(msg, "icon is recommended") {
t.Errorf("Unexpected lint error: %s", msg)
}
}
// TestHelmCreateChart_CheckDeprecatedWarnings checks if any default template created by `helm create` throws
// deprecated warnings in the linter check against the current Kubernetes version (provided using ldflags).
//
// See https://github.com/helm/helm/issues/11495
//
// 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 {
t.Error(err)
return
}
// Add values to enable hpa, and ingress which are disabled by default.
// This is the equivalent of:
// helm lint checkdeprecatedwarnings --set 'autoscaling.enabled=true,ingress.enabled=true'
updatedValues := map[string]any{
"autoscaling": map[string]any{
"enabled": true,
},
"ingress": map[string]any{
"enabled": true,
},
}
linterRunDetails := RunAll(createdChart, updatedValues, namespace, WithSkipSchemaValidation(true))
for _, msg := range linterRunDetails.Messages {
if strings.HasPrefix(msg.Error(), "[WARNING]") &&
strings.Contains(msg.Error(), "deprecated") {
// When there is a deprecation warning for an object created
// by `helm create` for the current Kubernetes version, fail.
t.Errorf("Unexpected deprecation warning for %q: %s", msg.Path, msg.Error())
}
}
}
// lint ignores import-values
// See https://github.com/helm/helm/issues/9658
func TestSubChartValuesChart(t *testing.T) {
var values map[string]any
m := RunAll(subChartValuesDir, values, namespace).Messages
if len(m) != 0 {
t.Error("All returned linter messages when it shouldn't have")
for i, msg := range m {
t.Logf("Message %d: %s", i, msg)
}
}
}
// lint stuck with malformed template object
// See https://github.com/helm/helm/issues/11391
func TestMalformedTemplate(t *testing.T) {
var values map[string]any
c := time.After(3 * time.Second)
ch := make(chan int, 1)
var m []support.Message
go func() {
m = RunAll(malformedTemplate, values, namespace).Messages
ch <- 1
}()
select {
case <-c:
t.Fatalf("lint malformed template timeout")
case <-ch:
if len(m) != 1 {
t.Fatalf("All didn't fail with expected errors, got %#v", m)
}
if !strings.Contains(m[0].Err.Error(), "invalid character '{'") {
t.Errorf("All didn't have the error for invalid character '{'")
}
}
}

@ -0,0 +1,225 @@
/*
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 rules // import "helm.sh/helm/v4/internal/chart/v3/lint/rules"
import (
"errors"
"fmt"
"os"
"path/filepath"
"github.com/Masterminds/semver/v3"
"github.com/asaskevich/govalidator"
"sigs.k8s.io/yaml"
chart "helm.sh/helm/v4/internal/chart/v3"
"helm.sh/helm/v4/internal/chart/v3/lint/support"
chartutil "helm.sh/helm/v4/internal/chart/v3/util"
)
// Chartfile runs a set of linter rules related to Chart.yaml file
func Chartfile(linter *support.Linter) {
chartFileName := "Chart.yaml"
chartPath := filepath.Join(linter.ChartDir, chartFileName)
linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartYamlNotDirectory(chartPath))
chartFile, err := chartutil.LoadChartfile(chartPath)
validChartFile := linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartYamlFormat(err))
// Guard clause. Following linter rules require a parsable ChartFile
if !validChartFile {
return
}
_, err = chartutil.StrictLoadChartfile(chartPath)
linter.RunLinterRule(support.WarningSev, chartFileName, validateChartYamlStrictFormat(err))
// type check for Chart.yaml . ignoring error as any parse
// errors would already be caught in the above load function
chartFileForTypeCheck, _ := loadChartFileForTypeCheck(chartPath)
linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartName(chartFile))
// Chart metadata
linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartAPIVersion(chartFile))
linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartVersionType(chartFileForTypeCheck))
linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartVersion(chartFile))
linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartAppVersionType(chartFileForTypeCheck))
linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartMaintainer(chartFile))
linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartSources(chartFile))
linter.RunLinterRule(support.InfoSev, chartFileName, validateChartIconPresence(chartFile))
linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartIconURL(chartFile))
linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartType(chartFile))
linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartDependencies(chartFile))
}
func validateChartVersionType(data map[string]interface{}) error {
return isStringValue(data, "version")
}
func validateChartAppVersionType(data map[string]interface{}) error {
return isStringValue(data, "appVersion")
}
func isStringValue(data map[string]interface{}, key string) error {
value, ok := data[key]
if !ok {
return nil
}
valueType := fmt.Sprintf("%T", value)
if valueType != "string" {
return fmt.Errorf("%s should be of type string but it's of type %s", key, valueType)
}
return nil
}
func validateChartYamlNotDirectory(chartPath string) error {
fi, err := os.Stat(chartPath)
if err == nil && fi.IsDir() {
return errors.New("should be a file, not a directory")
}
return nil
}
func validateChartYamlFormat(chartFileError error) error {
if chartFileError != nil {
return fmt.Errorf("unable to parse YAML\n\t%w", chartFileError)
}
return nil
}
func validateChartYamlStrictFormat(chartFileError error) error {
if chartFileError != nil {
return fmt.Errorf("failed to strictly parse chart metadata file\n\t%w", chartFileError)
}
return nil
}
func validateChartName(cf *chart.Metadata) error {
if cf.Name == "" {
return errors.New("name is required")
}
name := filepath.Base(cf.Name)
if name != cf.Name {
return fmt.Errorf("chart name %q is invalid", cf.Name)
}
return nil
}
func validateChartAPIVersion(cf *chart.Metadata) error {
if cf.APIVersion == "" {
return errors.New("apiVersion is required. The value must be \"v3\"")
}
if cf.APIVersion != chart.APIVersionV3 {
return fmt.Errorf("apiVersion '%s' is not valid. The value must be \"v3\"", cf.APIVersion)
}
return nil
}
func validateChartVersion(cf *chart.Metadata) error {
if cf.Version == "" {
return errors.New("version is required")
}
version, err := semver.StrictNewVersion(cf.Version)
if err != nil {
return fmt.Errorf("version '%s' is not a valid SemVerV2", cf.Version)
}
c, err := semver.NewConstraint(">0.0.0-0")
if err != nil {
return err
}
valid, msg := c.Validate(version)
if !valid && len(msg) > 0 {
return fmt.Errorf("version %v", msg[0])
}
return nil
}
func validateChartMaintainer(cf *chart.Metadata) error {
for _, maintainer := range cf.Maintainers {
if maintainer == nil {
return errors.New("a maintainer entry is empty")
}
if maintainer.Name == "" {
return errors.New("each maintainer requires a name")
} else if maintainer.Email != "" && !govalidator.IsEmail(maintainer.Email) {
return fmt.Errorf("invalid email '%s' for maintainer '%s'", maintainer.Email, maintainer.Name)
} else if maintainer.URL != "" && !govalidator.IsURL(maintainer.URL) {
return fmt.Errorf("invalid url '%s' for maintainer '%s'", maintainer.URL, maintainer.Name)
}
}
return nil
}
func validateChartSources(cf *chart.Metadata) error {
for _, source := range cf.Sources {
if source == "" || !govalidator.IsRequestURL(source) {
return fmt.Errorf("invalid source URL '%s'", source)
}
}
return nil
}
func validateChartIconPresence(cf *chart.Metadata) error {
if cf.Icon == "" {
return errors.New("icon is recommended")
}
return nil
}
func validateChartIconURL(cf *chart.Metadata) error {
if cf.Icon != "" && !govalidator.IsRequestURL(cf.Icon) {
return fmt.Errorf("invalid icon URL '%s'", cf.Icon)
}
return nil
}
func validateChartDependencies(cf *chart.Metadata) error {
if len(cf.Dependencies) > 0 && cf.APIVersion != chart.APIVersionV3 {
return fmt.Errorf("dependencies are not valid in the Chart file with apiVersion '%s'. They are valid in apiVersion '%s'", cf.APIVersion, chart.APIVersionV3)
}
return nil
}
func validateChartType(cf *chart.Metadata) error {
if len(cf.Type) > 0 && cf.APIVersion != chart.APIVersionV3 {
return fmt.Errorf("chart type is not valid in apiVersion '%s'. It is valid in apiVersion '%s'", cf.APIVersion, chart.APIVersionV3)
}
return nil
}
// loadChartFileForTypeCheck loads the Chart.yaml
// in a generic form of a map[string]interface{}, so that the type
// of the values can be checked
func loadChartFileForTypeCheck(filename string) (map[string]interface{}, error) {
b, err := os.ReadFile(filename)
if err != nil {
return nil, err
}
y := make(map[string]interface{})
err = yaml.Unmarshal(b, &y)
return y, err
}

@ -23,9 +23,9 @@ import (
"strings"
"testing"
chart "helm.sh/helm/v4/pkg/chart/v2"
chartutil "helm.sh/helm/v4/pkg/chart/v2/util"
"helm.sh/helm/v4/pkg/lint/support"
chart "helm.sh/helm/v4/internal/chart/v3"
"helm.sh/helm/v4/internal/chart/v3/lint/support"
chartutil "helm.sh/helm/v4/internal/chart/v3/util"
)
const (
@ -121,9 +121,11 @@ func TestValidateChartVersion(t *testing.T) {
ErrorMsg string
}{
{"", "version is required"},
{"1.2.3.4", "version '1.2.3.4' is not a valid SemVer"},
{"waps", "'waps' is not a valid SemVer"},
{"-3", "'-3' is not a valid SemVer"},
{"1.2.3.4", "version '1.2.3.4' is not a valid SemVerV2"},
{"waps", "'waps' is not a valid SemVerV2"},
{"-3", "'-3' is not a valid SemVerV2"},
{"1.1", "'1.1' is not a valid SemVerV2"},
{"1", "'1' is not a valid SemVerV2"},
}
var successTest = []string{"0.0.1", "0.0.1+build", "0.0.1-beta"}
@ -179,6 +181,16 @@ func TestValidateChartMaintainer(t *testing.T) {
t.Errorf("validateChartMaintainer(%s, %s) to return no error, got %s", test.Name, test.Email, err.Error())
}
}
// Testing for an empty maintainer
badChart.Maintainers = []*chart.Maintainer{nil}
err := validateChartMaintainer(badChart)
if err == nil {
t.Errorf("validateChartMaintainer did not return error for nil maintainer as expected")
}
if err.Error() != "a maintainer entry is empty" {
t.Errorf("validateChartMaintainer returned unexpected error for nil maintainer: %s", err.Error())
}
}
func TestValidateChartSources(t *testing.T) {
@ -248,7 +260,7 @@ func TestValidateChartIconURL(t *testing.T) {
}
}
func TestChartfile(t *testing.T) {
func TestV3Chartfile(t *testing.T) {
t.Run("Chart.yaml basic validity issues", func(t *testing.T) {
linter := support.Linter{ChartDir: badChartDir}
Chartfile(&linter)
@ -264,7 +276,7 @@ func TestChartfile(t *testing.T) {
t.Errorf("Unexpected message 0: %s", msgs[0].Err)
}
if !strings.Contains(msgs[1].Err.Error(), "apiVersion is required. The value must be either \"v1\" or \"v2\"") {
if !strings.Contains(msgs[1].Err.Error(), "apiVersion is required. The value must be \"v3\"") {
t.Errorf("Unexpected message 1: %s", msgs[1].Err)
}
@ -275,14 +287,6 @@ func TestChartfile(t *testing.T) {
if !strings.Contains(msgs[3].Err.Error(), "icon is recommended") {
t.Errorf("Unexpected message 3: %s", msgs[3].Err)
}
if !strings.Contains(msgs[4].Err.Error(), "chart type is not valid in apiVersion") {
t.Errorf("Unexpected message 4: %s", msgs[4].Err)
}
if !strings.Contains(msgs[5].Err.Error(), "dependencies are not valid in the Chart file with apiVersion") {
t.Errorf("Unexpected message 5: %s", msgs[5].Err)
}
})
t.Run("Chart.yaml validity issues due to type mismatch", func(t *testing.T) {

@ -0,0 +1,113 @@
/*
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 rules
import (
"bytes"
"errors"
"fmt"
"io"
"io/fs"
"os"
"path/filepath"
"strings"
"k8s.io/apimachinery/pkg/util/yaml"
"helm.sh/helm/v4/internal/chart/v3/lint/support"
"helm.sh/helm/v4/internal/chart/v3/loader"
)
// Crds lints the CRDs in the Linter.
func Crds(linter *support.Linter) {
fpath := "crds/"
crdsPath := filepath.Join(linter.ChartDir, fpath)
// crds directory is optional
if _, err := os.Stat(crdsPath); errors.Is(err, fs.ErrNotExist) {
return
}
crdsDirValid := linter.RunLinterRule(support.ErrorSev, fpath, validateCrdsDir(crdsPath))
if !crdsDirValid {
return
}
// Load chart and parse CRDs
chart, err := loader.Load(linter.ChartDir)
chartLoaded := linter.RunLinterRule(support.ErrorSev, fpath, err)
if !chartLoaded {
return
}
/* Iterate over all the CRDs to check:
1. It is a YAML file and not a template
2. The API version is apiextensions.k8s.io
3. The kind is CustomResourceDefinition
*/
for _, crd := range chart.CRDObjects() {
fileName := crd.Name
fpath = fileName
decoder := yaml.NewYAMLOrJSONDecoder(bytes.NewReader(crd.File.Data), 4096)
for {
var yamlStruct *k8sYamlStruct
err := decoder.Decode(&yamlStruct)
if err == io.EOF {
break
}
// If YAML parsing fails here, it will always fail in the next block as well, so we should return here.
// This also confirms the YAML is not a template, since templates can't be decoded into a K8sYamlStruct.
if !linter.RunLinterRule(support.ErrorSev, fpath, validateYamlContent(err)) {
return
}
linter.RunLinterRule(support.ErrorSev, fpath, validateCrdAPIVersion(yamlStruct))
linter.RunLinterRule(support.ErrorSev, fpath, validateCrdKind(yamlStruct))
}
}
}
// Validation functions
func validateCrdsDir(crdsPath string) error {
fi, err := os.Stat(crdsPath)
if err != nil {
return err
}
if !fi.IsDir() {
return errors.New("not a directory")
}
return nil
}
func validateCrdAPIVersion(obj *k8sYamlStruct) error {
if !strings.HasPrefix(obj.APIVersion, "apiextensions.k8s.io") {
return fmt.Errorf("apiVersion is not in 'apiextensions.k8s.io'")
}
return nil
}
func validateCrdKind(obj *k8sYamlStruct) error {
if obj.Kind != "CustomResourceDefinition" {
return fmt.Errorf("object kind is not 'CustomResourceDefinition'")
}
return nil
}

@ -0,0 +1,36 @@
/*
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 rules
import (
"testing"
"github.com/stretchr/testify/assert"
"helm.sh/helm/v4/internal/chart/v3/lint/support"
)
const invalidCrdsDir = "./testdata/invalidcrdsdir"
func TestInvalidCrdsDir(t *testing.T) {
linter := support.Linter{ChartDir: invalidCrdsDir}
Crds(&linter)
res := linter.Messages
assert.Len(t, res, 1)
assert.ErrorContains(t, res[0].Err, "not a directory")
}

@ -0,0 +1,101 @@
/*
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 rules // import "helm.sh/helm/v4/internal/chart/v3/lint/rules"
import (
"fmt"
"strings"
chart "helm.sh/helm/v4/internal/chart/v3"
"helm.sh/helm/v4/internal/chart/v3/lint/support"
"helm.sh/helm/v4/internal/chart/v3/loader"
)
// Dependencies runs lints against a chart's dependencies
//
// See https://github.com/helm/helm/issues/7910
func Dependencies(linter *support.Linter) {
c, err := loader.LoadDir(linter.ChartDir)
if !linter.RunLinterRule(support.ErrorSev, "", validateChartFormat(err)) {
return
}
linter.RunLinterRule(support.ErrorSev, linter.ChartDir, validateDependencyInMetadata(c))
linter.RunLinterRule(support.ErrorSev, linter.ChartDir, validateDependenciesUnique(c))
linter.RunLinterRule(support.WarningSev, linter.ChartDir, validateDependencyInChartsDir(c))
}
func validateChartFormat(chartError error) error {
if chartError != nil {
return fmt.Errorf("unable to load chart\n\t%w", chartError)
}
return nil
}
func validateDependencyInChartsDir(c *chart.Chart) (err error) {
dependencies := map[string]struct{}{}
missing := []string{}
for _, dep := range c.Dependencies() {
dependencies[dep.Metadata.Name] = struct{}{}
}
for _, dep := range c.Metadata.Dependencies {
if _, ok := dependencies[dep.Name]; !ok {
missing = append(missing, dep.Name)
}
}
if len(missing) > 0 {
err = fmt.Errorf("chart directory is missing these dependencies: %s", strings.Join(missing, ","))
}
return err
}
func validateDependencyInMetadata(c *chart.Chart) (err error) {
dependencies := map[string]struct{}{}
missing := []string{}
for _, dep := range c.Metadata.Dependencies {
dependencies[dep.Name] = struct{}{}
}
for _, dep := range c.Dependencies() {
if _, ok := dependencies[dep.Metadata.Name]; !ok {
missing = append(missing, dep.Metadata.Name)
}
}
if len(missing) > 0 {
err = fmt.Errorf("chart metadata is missing these dependencies: %s", strings.Join(missing, ","))
}
return err
}
func validateDependenciesUnique(c *chart.Chart) (err error) {
dependencies := map[string]*chart.Dependency{}
shadowing := []string{}
for _, dep := range c.Metadata.Dependencies {
key := dep.Name
if dep.Alias != "" {
key = dep.Alias
}
if dependencies[key] != nil {
shadowing = append(shadowing, key)
}
dependencies[key] = dep
}
if len(shadowing) > 0 {
err = fmt.Errorf("multiple dependencies with name or alias: %s", strings.Join(shadowing, ","))
}
return err
}

@ -0,0 +1,157 @@
/*
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 rules
import (
"path/filepath"
"testing"
chart "helm.sh/helm/v4/internal/chart/v3"
"helm.sh/helm/v4/internal/chart/v3/lint/support"
chartutil "helm.sh/helm/v4/internal/chart/v3/util"
)
func chartWithBadDependencies() chart.Chart {
badChartDeps := chart.Chart{
Metadata: &chart.Metadata{
Name: "badchart",
Version: "0.1.0",
APIVersion: "v2",
Dependencies: []*chart.Dependency{
{
Name: "sub2",
},
{
Name: "sub3",
},
},
},
}
badChartDeps.SetDependencies(
&chart.Chart{
Metadata: &chart.Metadata{
Name: "sub1",
Version: "0.1.0",
APIVersion: "v2",
},
},
&chart.Chart{
Metadata: &chart.Metadata{
Name: "sub2",
Version: "0.1.0",
APIVersion: "v2",
},
},
)
return badChartDeps
}
func TestValidateDependencyInChartsDir(t *testing.T) {
c := chartWithBadDependencies()
if err := validateDependencyInChartsDir(&c); err == nil {
t.Error("chart should have been flagged for missing deps in chart directory")
}
}
func TestValidateDependencyInMetadata(t *testing.T) {
c := chartWithBadDependencies()
if err := validateDependencyInMetadata(&c); err == nil {
t.Errorf("chart should have been flagged for missing deps in chart metadata")
}
}
func TestValidateDependenciesUnique(t *testing.T) {
tests := []struct {
chart chart.Chart
}{
{chart.Chart{
Metadata: &chart.Metadata{
Name: "badchart",
Version: "0.1.0",
APIVersion: "v2",
Dependencies: []*chart.Dependency{
{
Name: "foo",
},
{
Name: "foo",
},
},
},
}},
{chart.Chart{
Metadata: &chart.Metadata{
Name: "badchart",
Version: "0.1.0",
APIVersion: "v2",
Dependencies: []*chart.Dependency{
{
Name: "foo",
Alias: "bar",
},
{
Name: "bar",
},
},
},
}},
{chart.Chart{
Metadata: &chart.Metadata{
Name: "badchart",
Version: "0.1.0",
APIVersion: "v2",
Dependencies: []*chart.Dependency{
{
Name: "foo",
Alias: "baz",
},
{
Name: "bar",
Alias: "baz",
},
},
},
}},
}
for _, tt := range tests {
if err := validateDependenciesUnique(&tt.chart); err == nil {
t.Errorf("chart should have been flagged for dependency shadowing")
}
}
}
func TestDependencies(t *testing.T) {
tmp := t.TempDir()
c := chartWithBadDependencies()
err := chartutil.SaveDir(&c, tmp)
if err != nil {
t.Fatal(err)
}
linter := support.Linter{ChartDir: filepath.Join(tmp, c.Metadata.Name)}
Dependencies(&linter)
if l := len(linter.Messages); l != 2 {
t.Errorf("expected 2 linter errors for bad chart dependencies. Got %d.", l)
for i, msg := range linter.Messages {
t.Logf("Message: %d, Error: %#v", i, msg)
}
}
}

@ -14,18 +14,18 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package rules // import "helm.sh/helm/v4/pkg/lint/rules"
package rules // import "helm.sh/helm/v4/internal/chart/v3/lint/rules"
import (
"fmt"
"strconv"
"helm.sh/helm/v4/pkg/chart/common"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/endpoints/deprecation"
kscheme "k8s.io/client-go/kubernetes/scheme"
chartutil "helm.sh/helm/v4/pkg/chart/v2/util"
)
var (
@ -47,7 +47,7 @@ func (e deprecatedAPIError) Error() string {
return msg
}
func validateNoDeprecations(resource *k8sYamlStruct, kubeVersion *chartutil.KubeVersion) error {
func validateNoDeprecations(resource *k8sYamlStruct, kubeVersion *common.KubeVersion) error {
// if `resource` does not have an APIVersion or Kind, we cannot test it for deprecation
if resource.APIVersion == "" {
return nil

@ -0,0 +1,41 @@
/*
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 rules // import "helm.sh/helm/v4/internal/chart/v3/lint/rules"
import "testing"
func TestValidateNoDeprecations(t *testing.T) {
deprecated := &k8sYamlStruct{
APIVersion: "extensions/v1beta1",
Kind: "Deployment",
}
err := validateNoDeprecations(deprecated, nil)
if err == nil {
t.Fatal("Expected deprecated extension to be flagged")
}
depErr := err.(deprecatedAPIError)
if depErr.Message == "" {
t.Fatalf("Expected error message to be non-blank: %v", err)
}
if err := validateNoDeprecations(&k8sYamlStruct{
APIVersion: "v1",
Kind: "Pod",
}, nil); err != nil {
t.Errorf("Expected a v1 Pod to not be deprecated")
}
}

@ -33,10 +33,12 @@ import (
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/apimachinery/pkg/util/yaml"
"helm.sh/helm/v4/pkg/chart/v2/loader"
chartutil "helm.sh/helm/v4/pkg/chart/v2/util"
"helm.sh/helm/v4/internal/chart/v3/lint/support"
"helm.sh/helm/v4/internal/chart/v3/loader"
chartutil "helm.sh/helm/v4/internal/chart/v3/util"
"helm.sh/helm/v4/pkg/chart/common"
"helm.sh/helm/v4/pkg/chart/common/util"
"helm.sh/helm/v4/pkg/engine"
"helm.sh/helm/v4/pkg/lint/support"
)
// Templates lints the templates in the Linter.
@ -45,19 +47,23 @@ func Templates(linter *support.Linter, values map[string]interface{}, namespace
}
// TemplatesWithKubeVersion lints the templates in the Linter, allowing to specify the kubernetes version.
func TemplatesWithKubeVersion(linter *support.Linter, values map[string]interface{}, namespace string, kubeVersion *chartutil.KubeVersion) {
func TemplatesWithKubeVersion(linter *support.Linter, values map[string]interface{}, namespace string, kubeVersion *common.KubeVersion) {
TemplatesWithSkipSchemaValidation(linter, values, namespace, kubeVersion, false)
}
// TemplatesWithSkipSchemaValidation lints the templates in the Linter, allowing to specify the kubernetes version and if schema validation is enabled or not.
func TemplatesWithSkipSchemaValidation(linter *support.Linter, values map[string]interface{}, namespace string, kubeVersion *chartutil.KubeVersion, skipSchemaValidation bool) {
func TemplatesWithSkipSchemaValidation(linter *support.Linter, values map[string]interface{}, namespace string, kubeVersion *common.KubeVersion, skipSchemaValidation bool) {
fpath := "templates/"
templatesPath := filepath.Join(linter.ChartDir, fpath)
templatesDirExist := linter.RunLinterRule(support.WarningSev, fpath, validateTemplatesDir(templatesPath))
// Templates directory is optional for now
if !templatesDirExist {
templatesDirExists := linter.RunLinterRule(support.WarningSev, fpath, templatesDirExists(templatesPath))
if !templatesDirExists {
return
}
validTemplatesDir := linter.RunLinterRule(support.ErrorSev, fpath, validateTemplatesDir(templatesPath))
if !validTemplatesDir {
return
}
@ -70,12 +76,12 @@ func TemplatesWithSkipSchemaValidation(linter *support.Linter, values map[string
return
}
options := chartutil.ReleaseOptions{
options := common.ReleaseOptions{
Name: "test-release",
Namespace: namespace,
}
caps := chartutil.DefaultCapabilities.Copy()
caps := common.DefaultCapabilities.Copy()
if kubeVersion != nil {
caps.KubeVersion = *kubeVersion
}
@ -86,12 +92,12 @@ func TemplatesWithSkipSchemaValidation(linter *support.Linter, values map[string
return
}
cvals, err := chartutil.CoalesceValues(chart, values)
cvals, err := util.CoalesceValues(chart, values)
if err != nil {
return
}
valuesToRender, err := chartutil.ToRenderValuesWithSchemaValidation(chart, cvals, options, caps, skipSchemaValidation)
valuesToRender, err := util.ToRenderValuesWithSchemaValidation(chart, cvals, options, caps, skipSchemaValidation)
if err != nil {
linter.RunLinterRule(support.ErrorSev, fpath, err)
return
@ -120,7 +126,7 @@ func TemplatesWithSkipSchemaValidation(linter *support.Linter, values map[string
linter.RunLinterRule(support.ErrorSev, fpath, validateAllowedExtension(fileName))
// We only apply the following lint rules to yaml files
if filepath.Ext(fileName) != ".yaml" || filepath.Ext(fileName) == ".yml" {
if !isYamlFileExtension(fileName) {
continue
}
@ -194,11 +200,21 @@ func validateTopIndentLevel(content string) error {
}
// Validation functions
func templatesDirExists(templatesPath string) error {
_, err := os.Stat(templatesPath)
if errors.Is(err, os.ErrNotExist) {
return errors.New("directory does not exist")
}
return nil
}
func validateTemplatesDir(templatesPath string) error {
if fi, err := os.Stat(templatesPath); err == nil {
if !fi.IsDir() {
return errors.New("not a directory")
}
fi, err := os.Stat(templatesPath)
if err != nil {
return err
}
if !fi.IsDir() {
return errors.New("not a directory")
}
return nil
}
@ -319,6 +335,11 @@ func validateListAnnotations(yamlStruct *k8sYamlStruct, manifest string) error {
return nil
}
func isYamlFileExtension(fileName string) bool {
ext := strings.ToLower(filepath.Ext(fileName))
return ext == ".yaml" || ext == ".yml"
}
// k8sYamlStruct stubs a Kubernetes YAML file.
type k8sYamlStruct struct {
APIVersion string `json:"apiVersion"`

@ -23,9 +23,10 @@ import (
"strings"
"testing"
chart "helm.sh/helm/v4/pkg/chart/v2"
chartutil "helm.sh/helm/v4/pkg/chart/v2/util"
"helm.sh/helm/v4/pkg/lint/support"
chart "helm.sh/helm/v4/internal/chart/v3"
"helm.sh/helm/v4/internal/chart/v3/lint/support"
chartutil "helm.sh/helm/v4/internal/chart/v3/util"
"helm.sh/helm/v4/pkg/chart/common"
)
const templateTestBasedir = "./testdata/albatross"
@ -189,7 +190,7 @@ func TestDeprecatedAPIFails(t *testing.T) {
Version: "0.1.0",
Icon: "satisfy-the-linting-gods.gif",
},
Templates: []*chart.File{
Templates: []*common.File{
{
Name: "templates/baddeployment.yaml",
Data: []byte("apiVersion: apps/v1beta1\nkind: Deployment\nmetadata:\n name: baddep\nspec: {selector: {matchLabels: {foo: bar}}}"),
@ -249,7 +250,7 @@ func TestStrictTemplateParsingMapError(t *testing.T) {
"key1": "val1",
},
},
Templates: []*chart.File{
Templates: []*common.File{
{
Name: "templates/configmap.yaml",
Data: []byte(manifest),
@ -378,7 +379,7 @@ func TestEmptyWithCommentsManifests(t *testing.T) {
Version: "0.1.0",
Icon: "satisfy-the-linting-gods.gif",
},
Templates: []*chart.File{
Templates: []*common.File{
{
Name: "templates/empty-with-comments.yaml",
Data: []byte("#@formatter:off\n"),
@ -438,3 +439,23 @@ items:
t.Fatalf("List objects keep annotations should pass. got: %s", err)
}
}
func TestIsYamlFileExtension(t *testing.T) {
tests := []struct {
filename string
expected bool
}{
{"test.yaml", true},
{"test.yml", true},
{"test.txt", false},
{"test", false},
}
for _, test := range tests {
result := isYamlFileExtension(test.filename)
if result != test.expected {
t.Errorf("isYamlFileExtension(%s) = %v; want %v", test.filename, result, test.expected)
}
}
}

@ -0,0 +1,5 @@
apiVersion: v3
name: albatross
description: testing chart
version: 199.44.12345-Alpha.1+cafe009
icon: http://riverrun.io

@ -0,0 +1,15 @@
name: "some-chart"
apiVersion: v3
description: A Helm chart for Kubernetes
version: 72445e2
home: ""
type: application
appVersion: 72225e2
icon: "https://some-url.com/icon.jpeg"
dependencies:
- name: mariadb
version: 5.x.x
repository: https://charts.helm.sh/stable/
condition: mariadb.enabled
tags:
- database

@ -0,0 +1,5 @@
apiVersion: v3
description: A Helm chart for Kubernetes
version: 0.1.0
name: "../badchartname"
type: application

@ -0,0 +1,6 @@
apiVersion: v3
description: A Helm chart for Kubernetes
version: 0.1.0
name: badcrdfile
type: application
icon: http://riverrun.io

@ -0,0 +1,2 @@
apiVersion: bad.k8s.io/v1beta1
kind: CustomResourceDefinition

@ -0,0 +1,2 @@
apiVersion: apiextensions.k8s.io/v1beta1
kind: NotACustomResourceDefinition

@ -0,0 +1 @@
# Default values for badcrdfile.

@ -0,0 +1,6 @@
apiVersion: v3
name: badvaluesfile
description: A Helm chart for Kubernetes
version: 0.0.1
home: ""
icon: http://riverrun.io

@ -0,0 +1,5 @@
apiVersion: v3
name: goodone
description: good testing chart
version: 199.44.12345-Alpha.1+cafe009
icon: http://riverrun.io

@ -0,0 +1,19 @@
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
name: tests.test.io
spec:
group: test.io
names:
kind: Test
listKind: TestList
plural: tests
singular: test
scope: Namespaced
versions:
- name : v1alpha2
served: true
storage: true
- name : v1alpha1
served: true
storage: false

@ -0,0 +1,6 @@
apiVersion: v3
description: A Helm chart for Kubernetes
version: 0.1.0
name: invalidcrdsdir
type: application
icon: http://riverrun.io

@ -0,0 +1 @@
# Default values for invalidcrdsdir.

@ -0,0 +1,25 @@
apiVersion: v3
name: test
description: A Helm chart for Kubernetes
# A chart can be either an 'application' or a 'library' chart.
#
# Application charts are a collection of templates that can be packaged into versioned archives
# to be deployed.
#
# Library charts provide useful utilities or functions for the chart developer. They're included as
# a dependency of application charts to inject those utilities and functions into the rendering
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
type: application
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 0.1.0
# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes.
appVersion: "1.16.0"
icon: https://riverrun.io

@ -0,0 +1,21 @@
apiVersion: v3
name: multi-template-fail
description: A Helm chart for Kubernetes
# A chart can be either an 'application' or a 'library' chart.
#
# Application charts are a collection of templates that can be packaged into versioned archives
# to be deployed.
#
# Library charts provide useful utilities or functions for the chart developer. They're included as
# a dependency of application charts to inject those utilities and functions into the rendering
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
type: application
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
version: 0.1.0
# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application and it is recommended to use it with quotes.
appVersion: "1.16.0"

@ -0,0 +1,21 @@
apiVersion: v3
name: v3-fail
description: A Helm chart for Kubernetes
# A chart can be either an 'application' or a 'library' chart.
#
# Application charts are a collection of templates that can be packaged into versioned archives
# to be deployed.
#
# Library charts provide useful utilities or functions for the chart developer. They're included as
# a dependency of application charts to inject those utilities and functions into the rendering
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
type: application
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
version: 0.1.0
# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application and it is recommended to use it with quotes.
appVersion: "1.16.0"

@ -0,0 +1,16 @@
apiVersion: v3
name: withsubchart
description: A Helm chart for Kubernetes
type: application
version: 0.1.0
appVersion: "1.16.0"
icon: http://riverrun.io
dependencies:
- name: subchart
version: 0.1.16
repository: "file://../subchart"
import-values:
- child: subchart
parent: subchart

@ -0,0 +1,6 @@
apiVersion: v3
name: subchart
description: A Helm chart for Kubernetes
type: application
version: 0.1.0
appVersion: "1.16.0"

@ -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 rules
import (
"fmt"
"os"
"path/filepath"
"helm.sh/helm/v4/internal/chart/v3/lint/support"
"helm.sh/helm/v4/pkg/chart/common"
"helm.sh/helm/v4/pkg/chart/common/util"
)
// ValuesWithOverrides tests the values.yaml file.
//
// If a schema is present in the chart, values are tested against that. Otherwise,
// they are only tested for well-formedness.
//
// If additional values are supplied, they are coalesced into the values in values.yaml.
func ValuesWithOverrides(linter *support.Linter, valueOverrides map[string]interface{}, skipSchemaValidation bool) {
file := "values.yaml"
vf := filepath.Join(linter.ChartDir, file)
fileExists := linter.RunLinterRule(support.InfoSev, file, validateValuesFileExistence(vf))
if !fileExists {
return
}
linter.RunLinterRule(support.ErrorSev, file, validateValuesFile(vf, valueOverrides, skipSchemaValidation))
}
func validateValuesFileExistence(valuesPath string) error {
_, err := os.Stat(valuesPath)
if err != nil {
return fmt.Errorf("file does not exist")
}
return nil
}
func validateValuesFile(valuesPath string, overrides map[string]interface{}, skipSchemaValidation bool) error {
values, err := common.ReadValuesFile(valuesPath)
if err != nil {
return fmt.Errorf("unable to parse YAML: %w", err)
}
// Helm 3.0.0 carried over the values linting from Helm 2.x, which only tests the top
// level values against the top-level expectations. Subchart values are not linted.
// We could change that. For now, though, we retain that strategy, and thus can
// coalesce tables (like reuse-values does) instead of doing the full chart
// CoalesceValues
coalescedValues := util.CoalesceTables(make(map[string]interface{}, len(overrides)), overrides)
coalescedValues = util.CoalesceTables(coalescedValues, values)
ext := filepath.Ext(valuesPath)
schemaPath := valuesPath[:len(valuesPath)-len(ext)] + ".schema.json"
schema, err := os.ReadFile(schemaPath)
if len(schema) == 0 {
return nil
}
if err != nil {
return err
}
if !skipSchemaValidation {
return util.ValidateAgainstSingleSchema(coalescedValues, schema)
}
return nil
}

@ -67,7 +67,7 @@ func TestValidateValuesFileWellFormed(t *testing.T) {
`
tmpdir := ensure.TempFile(t, "values.yaml", []byte(badYaml))
valfile := filepath.Join(tmpdir, "values.yaml")
if err := validateValuesFile(valfile, map[string]interface{}{}); err == nil {
if err := validateValuesFile(valfile, map[string]interface{}{}, false); err == nil {
t.Fatal("expected values file to fail parsing")
}
}
@ -78,7 +78,7 @@ func TestValidateValuesFileSchema(t *testing.T) {
createTestingSchema(t, tmpdir)
valfile := filepath.Join(tmpdir, "values.yaml")
if err := validateValuesFile(valfile, map[string]interface{}{}); err != nil {
if err := validateValuesFile(valfile, map[string]interface{}{}, false); err != nil {
t.Fatalf("Failed validation with %s", err)
}
}
@ -91,7 +91,7 @@ func TestValidateValuesFileSchemaFailure(t *testing.T) {
valfile := filepath.Join(tmpdir, "values.yaml")
err := validateValuesFile(valfile, map[string]interface{}{})
err := validateValuesFile(valfile, map[string]interface{}{}, false)
if err == nil {
t.Fatal("expected values file to fail parsing")
}
@ -99,6 +99,20 @@ func TestValidateValuesFileSchemaFailure(t *testing.T) {
assert.Contains(t, err.Error(), "- at '/username': got number, want string")
}
func TestValidateValuesFileSchemaFailureButWithSkipSchemaValidation(t *testing.T) {
// 1234 is an int, not a string. This should fail normally but pass with skipSchemaValidation.
yaml := "username: 1234\npassword: swordfish"
tmpdir := ensure.TempFile(t, "values.yaml", []byte(yaml))
createTestingSchema(t, tmpdir)
valfile := filepath.Join(tmpdir, "values.yaml")
err := validateValuesFile(valfile, map[string]interface{}{}, true)
if err != nil {
t.Fatal("expected values file to pass parsing because of skipSchemaValidation")
}
}
func TestValidateValuesFileSchemaOverrides(t *testing.T) {
yaml := "username: admin"
overrides := map[string]interface{}{
@ -108,7 +122,7 @@ func TestValidateValuesFileSchemaOverrides(t *testing.T) {
createTestingSchema(t, tmpdir)
valfile := filepath.Join(tmpdir, "values.yaml")
if err := validateValuesFile(valfile, overrides); err != nil {
if err := validateValuesFile(valfile, overrides, false); err != nil {
t.Fatalf("Failed validation with %s", err)
}
}
@ -145,7 +159,7 @@ func TestValidateValuesFile(t *testing.T) {
valfile := filepath.Join(tmpdir, "values.yaml")
err := validateValuesFile(valfile, tt.overrides)
err := validateValuesFile(valfile, tt.overrides, false)
switch {
case err != nil && tt.errorMessage == "":

@ -1,5 +1,3 @@
//go:build linux
/*
Copyright The Helm Authors.
@ -7,7 +5,7 @@ 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
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,
@ -15,16 +13,11 @@ 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 ctime
import (
"os"
"syscall"
"time"
)
/*
Package support contains tools for linting charts.
func modified(fi os.FileInfo) time.Time {
st := fi.Sys().(*syscall.Stat_t)
//nolint
return time.Unix(int64(st.Mtim.Sec), int64(st.Mtim.Nsec))
}
Linting is the process of testing charts for errors or warnings regarding
formatting, compilation, or standards compliance.
*/
package support // import "helm.sh/helm/v4/internal/chart/v3/lint/support"

@ -0,0 +1,74 @@
/*
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 (
"compress/gzip"
"errors"
"fmt"
"io"
"os"
chart "helm.sh/helm/v4/internal/chart/v3"
"helm.sh/helm/v4/pkg/chart/loader/archive"
)
// FileLoader loads a chart from a file
type FileLoader string
// Load loads a chart
func (l FileLoader) Load() (*chart.Chart, error) {
return LoadFile(string(l))
}
// LoadFile loads from an archive file.
func LoadFile(name string) (*chart.Chart, error) {
if fi, err := os.Stat(name); err != nil {
return nil, err
} else if fi.IsDir() {
return nil, errors.New("cannot load a directory")
}
raw, err := os.Open(name)
if err != nil {
return nil, err
}
defer raw.Close()
err = archive.EnsureArchive(name, raw)
if err != nil {
return nil, err
}
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)
}
}
return c, err
}
// LoadArchive loads from a reader containing a compressed tar archive.
func LoadArchive(in io.Reader) (*chart.Chart, error) {
files, err := archive.LoadArchiveFiles(in)
if err != nil {
return nil, err
}
return LoadFiles(files)
}

@ -0,0 +1,122 @@
/*
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 (
"bytes"
"fmt"
"os"
"path/filepath"
"strings"
chart "helm.sh/helm/v4/internal/chart/v3"
"helm.sh/helm/v4/internal/sympath"
"helm.sh/helm/v4/pkg/chart/loader/archive"
"helm.sh/helm/v4/pkg/ignore"
)
var utf8bom = []byte{0xEF, 0xBB, 0xBF}
// DirLoader loads a chart from a directory
type DirLoader string
// Load loads the chart
func (l DirLoader) Load() (*chart.Chart, error) {
return LoadDir(string(l))
}
// LoadDir loads from a directory.
//
// This loads charts only from directories.
func LoadDir(dir string) (*chart.Chart, error) {
topdir, err := filepath.Abs(dir)
if err != nil {
return nil, err
}
// Just used for errors.
c := &chart.Chart{}
rules := ignore.Empty()
ifile := filepath.Join(topdir, ignore.HelmIgnore)
if _, err := os.Stat(ifile); err == nil {
r, err := ignore.ParseFile(ifile)
if err != nil {
return c, err
}
rules = r
}
rules.AddDefaults()
files := []*archive.BufferedFile{}
topdir += string(filepath.Separator)
walk := func(name string, fi os.FileInfo, err error) error {
n := strings.TrimPrefix(name, topdir)
if n == "" {
// No need to process top level. Avoid bug with helmignore .* matching
// empty names. See issue 1779.
return nil
}
// Normalize to / since it will also work on Windows
n = filepath.ToSlash(n)
if err != nil {
return err
}
if fi.IsDir() {
// Directory-based ignore rules should involve skipping the entire
// contents of that directory.
if rules.Ignore(n, fi) {
return filepath.SkipDir
}
return nil
}
// If a .helmignore file matches, skip this file.
if rules.Ignore(n, fi) {
return nil
}
// Irregular files include devices, sockets, and other uses of files that
// are not regular files. In Go they have a file mode type bit set.
// See https://golang.org/pkg/os/#FileMode for examples.
if !fi.Mode().IsRegular() {
return fmt.Errorf("cannot load irregular file %s as it has file mode type bits set", name)
}
if fi.Size() > archive.MaxDecompressedFileSize {
return fmt.Errorf("chart file %q is larger than the maximum file size %d", fi.Name(), archive.MaxDecompressedFileSize)
}
data, err := os.ReadFile(name)
if err != nil {
return fmt.Errorf("error reading %s: %w", n, err)
}
data = bytes.TrimPrefix(data, utf8bom)
files = append(files, &archive.BufferedFile{Name: n, Data: data})
return nil
}
if err = sympath.Walk(topdir, walk); err != nil {
return c, err
}
return LoadFiles(files)
}

@ -0,0 +1,215 @@
/*
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 (
"bufio"
"bytes"
"errors"
"fmt"
"io"
"maps"
"os"
"path/filepath"
"strings"
utilyaml "k8s.io/apimachinery/pkg/util/yaml"
"sigs.k8s.io/yaml"
chart "helm.sh/helm/v4/internal/chart/v3"
"helm.sh/helm/v4/pkg/chart/common"
"helm.sh/helm/v4/pkg/chart/loader/archive"
)
// ChartLoader loads a chart.
type ChartLoader interface {
Load() (*chart.Chart, error)
}
// Loader returns a new ChartLoader appropriate for the given chart name
func Loader(name string) (ChartLoader, error) {
fi, err := os.Stat(name)
if err != nil {
return nil, err
}
if fi.IsDir() {
return DirLoader(name), nil
}
return FileLoader(name), nil
}
// Load takes a string name, tries to resolve it to a file or directory, and then loads it.
//
// This is the preferred way to load a chart. It will discover the chart encoding
// and hand off to the appropriate chart reader.
//
// If a .helmignore file is present, the directory loader will skip loading any files
// matching it. But .helmignore is not evaluated when reading out of an archive.
func Load(name string) (*chart.Chart, error) {
l, err := Loader(name)
if err != nil {
return nil, err
}
return l.Load()
}
// LoadFiles loads from in-memory files.
func LoadFiles(files []*archive.BufferedFile) (*chart.Chart, error) {
c := new(chart.Chart)
subcharts := make(map[string][]*archive.BufferedFile)
// do not rely on assumed ordering of files in the chart and crash
// if Chart.yaml was not coming early enough to initialize metadata
for _, f := range files {
c.Raw = append(c.Raw, &common.File{Name: f.Name, Data: f.Data})
if f.Name == "Chart.yaml" {
if c.Metadata == nil {
c.Metadata = new(chart.Metadata)
}
if err := yaml.Unmarshal(f.Data, c.Metadata); err != nil {
return c, fmt.Errorf("cannot load Chart.yaml: %w", err)
}
// While the documentation says the APIVersion is required, in practice there
// are cases where that's not enforced. Since this package set is for v3 charts,
// when this function is used v3 is automatically added when not present.
if c.Metadata.APIVersion == "" {
c.Metadata.APIVersion = chart.APIVersionV3
}
}
}
for _, f := range files {
switch {
case f.Name == "Chart.yaml":
// already processed
continue
case f.Name == "Chart.lock":
c.Lock = new(chart.Lock)
if err := yaml.Unmarshal(f.Data, &c.Lock); err != nil {
return c, fmt.Errorf("cannot load Chart.lock: %w", err)
}
case f.Name == "values.yaml":
values, err := LoadValues(bytes.NewReader(f.Data))
if err != nil {
return c, fmt.Errorf("cannot load values.yaml: %w", err)
}
c.Values = values
case f.Name == "values.schema.json":
c.Schema = f.Data
case strings.HasPrefix(f.Name, "templates/"):
c.Templates = append(c.Templates, &common.File{Name: f.Name, Data: f.Data})
case strings.HasPrefix(f.Name, "charts/"):
if filepath.Ext(f.Name) == ".prov" {
c.Files = append(c.Files, &common.File{Name: f.Name, Data: f.Data})
continue
}
fname := strings.TrimPrefix(f.Name, "charts/")
cname := strings.SplitN(fname, "/", 2)[0]
subcharts[cname] = append(subcharts[cname], &archive.BufferedFile{Name: fname, Data: f.Data})
default:
c.Files = append(c.Files, &common.File{Name: f.Name, Data: f.Data})
}
}
if c.Metadata == nil {
return c, errors.New("Chart.yaml file is missing") //nolint:staticcheck
}
if err := c.Validate(); err != nil {
return c, err
}
for n, files := range subcharts {
var sc *chart.Chart
var err error
switch {
case strings.IndexAny(n, "_.") == 0:
continue
case filepath.Ext(n) == ".tgz":
file := files[0]
if file.Name != n {
return c, fmt.Errorf("error unpacking subchart tar in %s: expected %s, got %s", c.Name(), n, file.Name)
}
// Untar the chart and add to c.Dependencies
sc, err = LoadArchive(bytes.NewBuffer(file.Data))
default:
// We have to trim the prefix off of every file, and ignore any file
// that is in charts/, but isn't actually a chart.
buff := make([]*archive.BufferedFile, 0, len(files))
for _, f := range files {
parts := strings.SplitN(f.Name, "/", 2)
if len(parts) < 2 {
continue
}
f.Name = parts[1]
buff = append(buff, f)
}
sc, err = LoadFiles(buff)
}
if err != nil {
return c, fmt.Errorf("error unpacking subchart %s in %s: %w", n, c.Name(), err)
}
c.AddDependency(sc)
}
return c, nil
}
// 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.
func LoadValues(data io.Reader) (map[string]interface{}, error) {
values := map[string]interface{}{}
reader := utilyaml.NewYAMLReader(bufio.NewReader(data))
for {
currentMap := map[string]interface{}{}
raw, err := reader.Read()
if err != nil {
if err == io.EOF {
break
}
return nil, fmt.Errorf("error reading yaml document: %w", err)
}
if err := yaml.Unmarshal(raw, &currentMap); err != nil {
return nil, fmt.Errorf("cannot unmarshal yaml document: %w", err)
}
values = MergeMaps(values, currentMap)
}
return values, nil
}
// MergeMaps merges two maps. If a key exists in both maps, the value from b will be used.
// If the value is a map, the maps will be merged recursively.
func MergeMaps(a, b map[string]interface{}) map[string]interface{} {
out := make(map[string]interface{}, len(a))
maps.Copy(out, a)
for k, v := range b {
if v, ok := v.(map[string]interface{}); ok {
if bv, ok := out[k]; ok {
if bv, ok := bv.(map[string]interface{}); ok {
out[k] = MergeMaps(bv, v)
continue
}
}
}
out[k] = v
}
return out
}

@ -0,0 +1,713 @@
/*
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"
"io"
"log"
"os"
"path/filepath"
"reflect"
"runtime"
"strings"
"testing"
"time"
chart "helm.sh/helm/v4/internal/chart/v3"
"helm.sh/helm/v4/pkg/chart/common"
"helm.sh/helm/v4/pkg/chart/loader/archive"
)
func TestLoadDir(t *testing.T) {
l, err := Loader("testdata/frobnitz")
if err != nil {
t.Fatalf("Failed to load testdata: %s", err)
}
c, err := l.Load()
if err != nil {
t.Fatalf("Failed to load testdata: %s", err)
}
verifyFrobnitz(t, c)
verifyChart(t, c)
verifyDependencies(t, c)
verifyDependenciesLock(t, c)
}
func TestLoadDirWithDevNull(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("test only works on unix systems with /dev/null present")
}
l, err := Loader("testdata/frobnitz_with_dev_null")
if err != nil {
t.Fatalf("Failed to load testdata: %s", err)
}
if _, err := l.Load(); err == nil {
t.Errorf("packages with an irregular file (/dev/null) should not load")
}
}
func TestLoadDirWithSymlink(t *testing.T) {
sym := filepath.Join("..", "LICENSE")
link := filepath.Join("testdata", "frobnitz_with_symlink", "LICENSE")
if err := os.Symlink(sym, link); err != nil {
t.Fatal(err)
}
defer os.Remove(link)
l, err := Loader("testdata/frobnitz_with_symlink")
if err != nil {
t.Fatalf("Failed to load testdata: %s", err)
}
c, err := l.Load()
if err != nil {
t.Fatalf("Failed to load testdata: %s", err)
}
verifyFrobnitz(t, c)
verifyChart(t, c)
verifyDependencies(t, c)
verifyDependenciesLock(t, c)
}
func TestBomTestData(t *testing.T) {
testFiles := []string{"frobnitz_with_bom/.helmignore", "frobnitz_with_bom/templates/template.tpl", "frobnitz_with_bom/Chart.yaml"}
for _, file := range testFiles {
data, err := os.ReadFile("testdata/" + file)
if err != nil || !bytes.HasPrefix(data, utf8bom) {
t.Errorf("Test file has no BOM or is invalid: testdata/%s", file)
}
}
archive, err := os.ReadFile("testdata/frobnitz_with_bom.tgz")
if err != nil {
t.Fatalf("Error reading archive frobnitz_with_bom.tgz: %s", err)
}
unzipped, err := gzip.NewReader(bytes.NewReader(archive))
if err != nil {
t.Fatalf("Error reading archive frobnitz_with_bom.tgz: %s", err)
}
defer unzipped.Close()
for _, testFile := range testFiles {
data := make([]byte, 3)
err := unzipped.Reset(bytes.NewReader(archive))
if err != nil {
t.Fatalf("Error reading archive frobnitz_with_bom.tgz: %s", err)
}
tr := tar.NewReader(unzipped)
for {
file, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
t.Fatalf("Error reading archive frobnitz_with_bom.tgz: %s", err)
}
if file != nil && strings.EqualFold(file.Name, testFile) {
_, err := tr.Read(data)
if err != nil {
t.Fatalf("Error reading archive frobnitz_with_bom.tgz: %s", err)
} else {
break
}
}
}
if !bytes.Equal(data, utf8bom) {
t.Fatalf("Test file has no BOM or is invalid: frobnitz_with_bom.tgz/%s", testFile)
}
}
}
func TestLoadDirWithUTFBOM(t *testing.T) {
l, err := Loader("testdata/frobnitz_with_bom")
if err != nil {
t.Fatalf("Failed to load testdata: %s", err)
}
c, err := l.Load()
if err != nil {
t.Fatalf("Failed to load testdata: %s", err)
}
verifyFrobnitz(t, c)
verifyChart(t, c)
verifyDependencies(t, c)
verifyDependenciesLock(t, c)
verifyBomStripped(t, c.Files)
}
func TestLoadArchiveWithUTFBOM(t *testing.T) {
l, err := Loader("testdata/frobnitz_with_bom.tgz")
if err != nil {
t.Fatalf("Failed to load testdata: %s", err)
}
c, err := l.Load()
if err != nil {
t.Fatalf("Failed to load testdata: %s", err)
}
verifyFrobnitz(t, c)
verifyChart(t, c)
verifyDependencies(t, c)
verifyDependenciesLock(t, c)
verifyBomStripped(t, c.Files)
}
func TestLoadFile(t *testing.T) {
l, err := Loader("testdata/frobnitz-1.2.3.tgz")
if err != nil {
t.Fatalf("Failed to load testdata: %s", err)
}
c, err := l.Load()
if err != nil {
t.Fatalf("Failed to load testdata: %s", err)
}
verifyFrobnitz(t, c)
verifyChart(t, c)
verifyDependencies(t, c)
}
func TestLoadFiles(t *testing.T) {
goodFiles := []*archive.BufferedFile{
{
Name: "Chart.yaml",
Data: []byte(`apiVersion: v3
name: frobnitz
description: This is a frobnitz.
version: "1.2.3"
keywords:
- frobnitz
- sprocket
- dodad
maintainers:
- name: The Helm Team
email: helm@example.com
- name: Someone Else
email: nobody@example.com
sources:
- https://example.com/foo/bar
home: http://example.com
icon: https://example.com/64x64.png
`),
},
{
Name: "values.yaml",
Data: []byte("var: some values"),
},
{
Name: "values.schema.json",
Data: []byte("type: Values"),
},
{
Name: "templates/deployment.yaml",
Data: []byte("some deployment"),
},
{
Name: "templates/service.yaml",
Data: []byte("some service"),
},
}
c, err := LoadFiles(goodFiles)
if err != nil {
t.Errorf("Expected good files to be loaded, got %v", err)
}
if c.Name() != "frobnitz" {
t.Errorf("Expected chart name to be 'frobnitz', got %s", c.Name())
}
if c.Values["var"] != "some values" {
t.Error("Expected chart values to be populated with default values")
}
if len(c.Raw) != 5 {
t.Errorf("Expected %d files, got %d", 5, len(c.Raw))
}
if !bytes.Equal(c.Schema, []byte("type: Values")) {
t.Error("Expected chart schema to be populated with default values")
}
if len(c.Templates) != 2 {
t.Errorf("Expected number of templates == 2, got %d", len(c.Templates))
}
if _, err = LoadFiles([]*archive.BufferedFile{}); err == nil {
t.Fatal("Expected err to be non-nil")
}
if err.Error() != "Chart.yaml file is missing" {
t.Errorf("Expected chart metadata missing error, got '%s'", err.Error())
}
}
// Test the order of file loading. The Chart.yaml file needs to come first for
// later comparison checks. See https://github.com/helm/helm/pull/8948
func TestLoadFilesOrder(t *testing.T) {
goodFiles := []*archive.BufferedFile{
{
Name: "requirements.yaml",
Data: []byte("dependencies:"),
},
{
Name: "values.yaml",
Data: []byte("var: some values"),
},
{
Name: "templates/deployment.yaml",
Data: []byte("some deployment"),
},
{
Name: "templates/service.yaml",
Data: []byte("some service"),
},
{
Name: "Chart.yaml",
Data: []byte(`apiVersion: v3
name: frobnitz
description: This is a frobnitz.
version: "1.2.3"
keywords:
- frobnitz
- sprocket
- dodad
maintainers:
- name: The Helm Team
email: helm@example.com
- name: Someone Else
email: nobody@example.com
sources:
- https://example.com/foo/bar
home: http://example.com
icon: https://example.com/64x64.png
`),
},
}
// Capture stderr to make sure message about Chart.yaml handle dependencies
// is not present
r, w, err := os.Pipe()
if err != nil {
t.Fatalf("Unable to create pipe: %s", err)
}
stderr := log.Writer()
log.SetOutput(w)
defer func() {
log.SetOutput(stderr)
}()
_, err = LoadFiles(goodFiles)
if err != nil {
t.Errorf("Expected good files to be loaded, got %v", err)
}
w.Close()
var text bytes.Buffer
io.Copy(&text, r)
if text.String() != "" {
t.Errorf("Expected no message to Stderr, got %s", text.String())
}
}
// Packaging the chart on a Windows machine will produce an
// archive that has \\ as delimiters. Test that we support these archives
func TestLoadFileBackslash(t *testing.T) {
c, err := Load("testdata/frobnitz_backslash-1.2.3.tgz")
if err != nil {
t.Fatalf("Failed to load testdata: %s", err)
}
verifyChartFileAndTemplate(t, c, "frobnitz_backslash")
verifyChart(t, c)
verifyDependencies(t, c)
}
func TestLoadV3WithReqs(t *testing.T) {
l, err := Loader("testdata/frobnitz.v3.reqs")
if err != nil {
t.Fatalf("Failed to load testdata: %s", err)
}
c, err := l.Load()
if err != nil {
t.Fatalf("Failed to load testdata: %s", err)
}
verifyDependencies(t, c)
verifyDependenciesLock(t, c)
}
func TestLoadInvalidArchive(t *testing.T) {
tmpdir := t.TempDir()
writeTar := func(filename, internalPath string, body []byte) {
dest, err := os.Create(filename)
if err != nil {
t.Fatal(err)
}
zipper := gzip.NewWriter(dest)
tw := tar.NewWriter(zipper)
h := &tar.Header{
Name: internalPath,
Mode: 0755,
Size: int64(len(body)),
ModTime: time.Now(),
}
if err := tw.WriteHeader(h); err != nil {
t.Fatal(err)
}
if _, err := tw.Write(body); err != nil {
t.Fatal(err)
}
tw.Close()
zipper.Close()
dest.Close()
}
for _, tt := range []struct {
chartname string
internal string
expectError string
}{
{"illegal-dots.tgz", "../../malformed-helm-test", "chart illegally references parent directory"},
{"illegal-dots2.tgz", "/foo/../../malformed-helm-test", "chart illegally references parent directory"},
{"illegal-dots3.tgz", "/../../malformed-helm-test", "chart illegally references parent directory"},
{"illegal-dots4.tgz", "./../../malformed-helm-test", "chart illegally references parent directory"},
{"illegal-name.tgz", "./.", "chart illegally contains content outside the base directory"},
{"illegal-name2.tgz", "/./.", "chart illegally contains content outside the base directory"},
{"illegal-name3.tgz", "missing-leading-slash", "chart illegally contains content outside the base directory"},
{"illegal-name4.tgz", "/missing-leading-slash", "Chart.yaml file is missing"},
{"illegal-abspath.tgz", "//foo", "chart illegally contains absolute paths"},
{"illegal-abspath2.tgz", "///foo", "chart illegally contains absolute paths"},
{"illegal-abspath3.tgz", "\\\\foo", "chart illegally contains absolute paths"},
{"illegal-abspath3.tgz", "\\..\\..\\foo", "chart illegally references parent directory"},
// Under special circumstances, this can get normalized to things that look like absolute Windows paths
{"illegal-abspath4.tgz", "\\.\\c:\\\\foo", "chart contains illegally named files"},
{"illegal-abspath5.tgz", "/./c://foo", "chart contains illegally named files"},
{"illegal-abspath6.tgz", "\\\\?\\Some\\windows\\magic", "chart illegally contains absolute paths"},
} {
illegalChart := filepath.Join(tmpdir, tt.chartname)
writeTar(illegalChart, tt.internal, []byte("hello: world"))
_, err := Load(illegalChart)
if err == nil {
t.Fatal("expected error when unpacking illegal files")
}
if !strings.Contains(err.Error(), tt.expectError) {
t.Errorf("Expected error to contain %q, got %q for %s", tt.expectError, err.Error(), tt.chartname)
}
}
// Make sure that absolute path gets interpreted as relative
illegalChart := filepath.Join(tmpdir, "abs-path.tgz")
writeTar(illegalChart, "/Chart.yaml", []byte("hello: world"))
_, err := Load(illegalChart)
if err.Error() != "validation: chart.metadata.name is required" {
t.Error(err)
}
// And just to validate that the above was not spurious
illegalChart = filepath.Join(tmpdir, "abs-path2.tgz")
writeTar(illegalChart, "files/whatever.yaml", []byte("hello: world"))
_, err = Load(illegalChart)
if err.Error() != "Chart.yaml file is missing" {
t.Errorf("Unexpected error message: %s", err)
}
// Finally, test that drive letter gets stripped off on Windows
illegalChart = filepath.Join(tmpdir, "abs-winpath.tgz")
writeTar(illegalChart, "c:\\Chart.yaml", []byte("hello: world"))
_, err = Load(illegalChart)
if err.Error() != "validation: chart.metadata.name is required" {
t.Error(err)
}
}
func TestLoadValues(t *testing.T) {
testCases := map[string]struct {
data []byte
expctedValues map[string]interface{}
}{
"It should load values correctly": {
data: []byte(`
foo:
image: foo:v1
bar:
version: v2
`),
expctedValues: map[string]interface{}{
"foo": map[string]interface{}{
"image": "foo:v1",
},
"bar": map[string]interface{}{
"version": "v2",
},
},
},
"It should load values correctly with multiple documents in one file": {
data: []byte(`
foo:
image: foo:v1
bar:
version: v2
---
foo:
image: foo:v2
`),
expctedValues: map[string]interface{}{
"foo": map[string]interface{}{
"image": "foo:v2",
},
"bar": map[string]interface{}{
"version": "v2",
},
},
},
}
for testName, testCase := range testCases {
t.Run(testName, func(tt *testing.T) {
values, err := LoadValues(bytes.NewReader(testCase.data))
if err != nil {
tt.Fatal(err)
}
if !reflect.DeepEqual(values, testCase.expctedValues) {
tt.Errorf("Expected values: %v, got %v", testCase.expctedValues, values)
}
})
}
}
func TestMergeValuesV3(t *testing.T) {
nestedMap := map[string]interface{}{
"foo": "bar",
"baz": map[string]string{
"cool": "stuff",
},
}
anotherNestedMap := map[string]interface{}{
"foo": "bar",
"baz": map[string]string{
"cool": "things",
"awesome": "stuff",
},
}
flatMap := map[string]interface{}{
"foo": "bar",
"baz": "stuff",
}
anotherFlatMap := map[string]interface{}{
"testing": "fun",
}
testMap := MergeMaps(flatMap, nestedMap)
equal := reflect.DeepEqual(testMap, nestedMap)
if !equal {
t.Errorf("Expected a nested map to overwrite a flat value. Expected: %v, got %v", nestedMap, testMap)
}
testMap = MergeMaps(nestedMap, flatMap)
equal = reflect.DeepEqual(testMap, flatMap)
if !equal {
t.Errorf("Expected a flat value to overwrite a map. Expected: %v, got %v", flatMap, testMap)
}
testMap = MergeMaps(nestedMap, anotherNestedMap)
equal = reflect.DeepEqual(testMap, anotherNestedMap)
if !equal {
t.Errorf("Expected a nested map to overwrite another nested map. Expected: %v, got %v", anotherNestedMap, testMap)
}
testMap = MergeMaps(anotherFlatMap, anotherNestedMap)
expectedMap := map[string]interface{}{
"testing": "fun",
"foo": "bar",
"baz": map[string]string{
"cool": "things",
"awesome": "stuff",
},
}
equal = reflect.DeepEqual(testMap, expectedMap)
if !equal {
t.Errorf("Expected a map with different keys to merge properly with another map. Expected: %v, got %v", expectedMap, testMap)
}
}
func verifyChart(t *testing.T, c *chart.Chart) {
t.Helper()
if c.Name() == "" {
t.Fatalf("No chart metadata found on %v", c)
}
t.Logf("Verifying chart %s", c.Name())
if len(c.Templates) != 1 {
t.Errorf("Expected 1 template, got %d", len(c.Templates))
}
numfiles := 6
if len(c.Files) != numfiles {
t.Errorf("Expected %d extra files, got %d", numfiles, len(c.Files))
for _, n := range c.Files {
t.Logf("\t%s", n.Name)
}
}
if len(c.Dependencies()) != 2 {
t.Errorf("Expected 2 dependencies, got %d (%v)", len(c.Dependencies()), c.Dependencies())
for _, d := range c.Dependencies() {
t.Logf("\tSubchart: %s\n", d.Name())
}
}
expect := map[string]map[string]string{
"alpine": {
"version": "0.1.0",
},
"mariner": {
"version": "4.3.2",
},
}
for _, dep := range c.Dependencies() {
if dep.Metadata == nil {
t.Fatalf("expected metadata on dependency: %v", dep)
}
exp, ok := expect[dep.Name()]
if !ok {
t.Fatalf("Unknown dependency %s", dep.Name())
}
if exp["version"] != dep.Metadata.Version {
t.Errorf("Expected %s version %s, got %s", dep.Name(), exp["version"], dep.Metadata.Version)
}
}
}
func verifyDependencies(t *testing.T, c *chart.Chart) {
t.Helper()
if len(c.Metadata.Dependencies) != 2 {
t.Errorf("Expected 2 dependencies, got %d", len(c.Metadata.Dependencies))
}
tests := []*chart.Dependency{
{Name: "alpine", Version: "0.1.0", Repository: "https://example.com/charts"},
{Name: "mariner", Version: "4.3.2", Repository: "https://example.com/charts"},
}
for i, tt := range tests {
d := c.Metadata.Dependencies[i]
if d.Name != tt.Name {
t.Errorf("Expected dependency named %q, got %q", tt.Name, d.Name)
}
if d.Version != tt.Version {
t.Errorf("Expected dependency named %q to have version %q, got %q", tt.Name, tt.Version, d.Version)
}
if d.Repository != tt.Repository {
t.Errorf("Expected dependency named %q to have repository %q, got %q", tt.Name, tt.Repository, d.Repository)
}
}
}
func verifyDependenciesLock(t *testing.T, c *chart.Chart) {
t.Helper()
if len(c.Metadata.Dependencies) != 2 {
t.Errorf("Expected 2 dependencies, got %d", len(c.Metadata.Dependencies))
}
tests := []*chart.Dependency{
{Name: "alpine", Version: "0.1.0", Repository: "https://example.com/charts"},
{Name: "mariner", Version: "4.3.2", Repository: "https://example.com/charts"},
}
for i, tt := range tests {
d := c.Metadata.Dependencies[i]
if d.Name != tt.Name {
t.Errorf("Expected dependency named %q, got %q", tt.Name, d.Name)
}
if d.Version != tt.Version {
t.Errorf("Expected dependency named %q to have version %q, got %q", tt.Name, tt.Version, d.Version)
}
if d.Repository != tt.Repository {
t.Errorf("Expected dependency named %q to have repository %q, got %q", tt.Name, tt.Repository, d.Repository)
}
}
}
func verifyFrobnitz(t *testing.T, c *chart.Chart) {
t.Helper()
verifyChartFileAndTemplate(t, c, "frobnitz")
}
func verifyChartFileAndTemplate(t *testing.T, c *chart.Chart, name string) {
t.Helper()
if c.Metadata == nil {
t.Fatal("Metadata is nil")
}
if c.Name() != name {
t.Errorf("Expected %s, got %s", name, c.Name())
}
if len(c.Templates) != 1 {
t.Fatalf("Expected 1 template, got %d", len(c.Templates))
}
if c.Templates[0].Name != "templates/template.tpl" {
t.Errorf("Unexpected template: %s", c.Templates[0].Name)
}
if len(c.Templates[0].Data) == 0 {
t.Error("No template data.")
}
if len(c.Files) != 6 {
t.Fatalf("Expected 6 Files, got %d", len(c.Files))
}
if len(c.Dependencies()) != 2 {
t.Fatalf("Expected 2 Dependency, got %d", len(c.Dependencies()))
}
if len(c.Metadata.Dependencies) != 2 {
t.Fatalf("Expected 2 Dependencies.Dependency, got %d", len(c.Metadata.Dependencies))
}
if len(c.Lock.Dependencies) != 2 {
t.Fatalf("Expected 2 Lock.Dependency, got %d", len(c.Lock.Dependencies))
}
for _, dep := range c.Dependencies() {
switch dep.Name() {
case "mariner":
case "alpine":
if len(dep.Templates) != 1 {
t.Fatalf("Expected 1 template, got %d", len(dep.Templates))
}
if dep.Templates[0].Name != "templates/alpine-pod.yaml" {
t.Errorf("Unexpected template: %s", dep.Templates[0].Name)
}
if len(dep.Templates[0].Data) == 0 {
t.Error("No template data.")
}
if len(dep.Files) != 1 {
t.Fatalf("Expected 1 Files, got %d", len(dep.Files))
}
if len(dep.Dependencies()) != 2 {
t.Fatalf("Expected 2 Dependency, got %d", len(dep.Dependencies()))
}
default:
t.Errorf("Unexpected dependency %s", dep.Name())
}
}
}
func verifyBomStripped(t *testing.T, files []*common.File) {
t.Helper()
for _, file := range files {
if bytes.HasPrefix(file.Data, utf8bom) {
t.Errorf("Byte Order Mark still present in processed file %s", file.Name)
}
}
}

@ -0,0 +1 @@
LICENSE placeholder.

@ -0,0 +1,4 @@
name: albatross
description: A Helm chart for Kubernetes
version: 0.1.0
home: ""

@ -0,0 +1,4 @@
albatross: "true"
global:
author: Coleridge

@ -0,0 +1,27 @@
apiVersion: v3
name: frobnitz
description: This is a frobnitz.
version: "1.2.3"
keywords:
- frobnitz
- sprocket
- dodad
maintainers:
- name: The Helm Team
email: helm@example.com
- name: Someone Else
email: nobody@example.com
sources:
- https://example.com/foo/bar
home: http://example.com
icon: https://example.com/64x64.png
annotations:
extrakey: extravalue
anotherkey: anothervalue
dependencies:
- name: alpine
version: "0.1.0"
repository: https://example.com/charts
- name: mariner
version: "4.3.2"
repository: https://example.com/charts

@ -0,0 +1 @@
This is an install document. The client may display this.

@ -0,0 +1,11 @@
# Frobnitz
This is an example chart.
## Usage
This is an example. It has no usage.
## Development
For developer info, see the top-level repository.

@ -0,0 +1 @@
This should be ignored by the loader, but may be included in a chart.

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

Loading…
Cancel
Save