Merge remote-tracking branch 'origin/main' into em/check-ownership-before-delete

pull/31584/head
Evans Mungai 5 months ago
commit ed5789cbda
No known key found for this signature in database
GPG Key ID: BBEB812143DD14E1

4
.github/env vendored

@ -1,2 +1,2 @@
GOLANG_VERSION=1.25 GOLANG_VERSION=1.26
GOLANGCI_LINT_VERSION=v2.5.0 GOLANGCI_LINT_VERSION=v2.11.3

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

@ -24,14 +24,15 @@ on:
schedule: schedule:
- cron: '29 6 * * 6' - cron: '29 6 * * 6'
permissions: permissions: {}
contents: read
security-events: write
jobs: jobs:
analyze: analyze:
name: Analyze name: Analyze
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: read
security-events: write
strategy: strategy:
fail-fast: false fail-fast: false
@ -43,11 +44,11 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # pin@v6.0.1 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # pin@v6.0.2
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@e296a935590eb16afc0c0108289f68c87e2a89a5 # pinv4.30.7 uses: github/codeql-action/init@e46ed2cbd01164d986452f91f178727624ae40d7 # pinv4.35.3
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file. # If you wish to specify custom queries, you can do so here or in a config file.
@ -58,7 +59,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below) # If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild - name: Autobuild
uses: github/codeql-action/autobuild@e296a935590eb16afc0c0108289f68c87e2a89a5 # pinv4.30.7 uses: github/codeql-action/autobuild@e46ed2cbd01164d986452f91f178727624ae40d7 # pinv4.35.3
# ℹ️ Command-line programs to run using the OS shell. # ℹ️ Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl # 📚 https://git.io/JvXDl
@ -72,4 +73,4 @@ jobs:
# make release # make release
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@e296a935590eb16afc0c0108289f68c87e2a89a5 # pinv4.30.7 uses: github/codeql-action/analyze@e46ed2cbd01164d986452f91f178727624ae40d7 # pinv4.35.3

@ -13,11 +13,11 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # pin@v6.0.1 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # pin@v6.0.2
- name: Add variables to environment file - name: Add variables to environment file
run: cat ".github/env" >> "$GITHUB_ENV" run: cat ".github/env" >> "$GITHUB_ENV"
- name: Setup Go - name: Setup Go
uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # pin@6.1.0 uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # pin@6.4.0
with: with:
go-version: '${{ env.GOLANG_VERSION }}' go-version: '${{ env.GOLANG_VERSION }}'
check-latest: true check-latest: true

@ -19,13 +19,13 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # pin@v6.0.1 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # pin@v6.0.2
with: with:
persist-credentials: false persist-credentials: false
- name: Add variables to environment file - name: Add variables to environment file
run: cat ".github/env" >> "$GITHUB_ENV" run: cat ".github/env" >> "$GITHUB_ENV"
- name: Setup Go - name: Setup Go
uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # pin@6.1.0 uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # pin@6.4.0
with: with:
go-version: '${{ env.GOLANG_VERSION }}' go-version: '${{ env.GOLANG_VERSION }}'
check-latest: true check-latest: true

@ -20,7 +20,7 @@ jobs:
runs-on: ubuntu-latest-16-cores runs-on: ubuntu-latest-16-cores
steps: steps:
- name: Checkout source code - name: Checkout source code
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # pin@v6.0.1 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # pin@v6.0.2
with: with:
fetch-depth: 0 fetch-depth: 0
@ -28,7 +28,7 @@ jobs:
run: cat ".github/env" >> "$GITHUB_ENV" run: cat ".github/env" >> "$GITHUB_ENV"
- name: Setup Go - name: Setup Go
uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # pin@6.1.0 uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # pin@6.4.0
with: with:
go-version: '${{ env.GOLANG_VERSION }}' go-version: '${{ env.GOLANG_VERSION }}'
check-latest: true check-latest: true
@ -85,13 +85,15 @@ jobs:
if: github.ref == 'refs/heads/main' && github.repository == 'helm/helm' if: github.ref == 'refs/heads/main' && github.repository == 'helm/helm'
steps: steps:
- name: Checkout source code - name: Checkout source code
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # pin@v6.0.1 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # pin@v6.0.2
with:
fetch-depth: 0
- name: Add variables to environment file - name: Add variables to environment file
run: cat ".github/env" >> "$GITHUB_ENV" run: cat ".github/env" >> "$GITHUB_ENV"
- name: Setup Go - name: Setup Go
uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # pin@6.1.0 uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # pin@6.4.0
with: with:
go-version: '${{ env.GOLANG_VERSION }}' go-version: '${{ env.GOLANG_VERSION }}'
check-latest: true check-latest: true

@ -28,7 +28,7 @@ jobs:
steps: steps:
- name: "Checkout code" - name: "Checkout code"
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with: with:
persist-credentials: false persist-credentials: false
@ -55,7 +55,7 @@ jobs:
# Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
# format to the repository Actions tab. # format to the repository Actions tab.
- name: "Upload artifact" - name: "Upload artifact"
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with: with:
name: SARIF file name: SARIF file
path: results.sarif path: results.sarif
@ -64,6 +64,6 @@ jobs:
# Upload the results to GitHub's code scanning dashboard (optional). # Upload the results to GitHub's code scanning dashboard (optional).
# Commenting out will disable upload of results to your repo's Code Scanning dashboard # Commenting out will disable upload of results to your repo's Code Scanning dashboard
- name: "Upload to code-scanning" - name: "Upload to code-scanning"
uses: github/codeql-action/upload-sarif@v4 uses: github/codeql-action/upload-sarif@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
with: with:
sarif_file: results.sarif sarif_file: results.sarif

@ -3,11 +3,16 @@ on:
schedule: schedule:
- cron: "0 0 * * *" - cron: "0 0 * * *"
permissions: {}
jobs: jobs:
stale: stale:
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps: steps:
- uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1 - uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0
with: with:
repo-token: ${{ secrets.GITHUB_TOKEN }} repo-token: ${{ secrets.GITHUB_TOKEN }}
stale-issue-message: 'This issue has been marked as stale because it has been open for 90 days with no activity. This thread will be automatically closed in 30 days if no further activity occurs.' stale-issue-message: 'This issue has been marked as stale because it has been open for 90 days with no activity. This thread will be automatically closed in 30 days if no further activity occurs.'

1
.gitignore vendored

@ -3,6 +3,7 @@
.DS_Store .DS_Store
.coverage/ .coverage/
.idea .idea
.claude
.vimrc .vimrc
.vscode/ .vscode/
.devcontainer/ .devcontainer/

@ -17,22 +17,27 @@ formatters:
linters: linters:
default: none default: none
# Keep sorted alphabetically
enable: enable:
- depguard - depguard
- dupl - dupl
- exhaustive
- gomodguard - gomodguard
- govet - govet
- ineffassign - ineffassign
- misspell - misspell
- modernize
- nakedret - nakedret
- nolintlint
- perfsprint
- revive - revive
- sloglint - sloglint
- staticcheck - staticcheck
- testifylint
- thelper - thelper
- unused - unused
- usestdlibvars - usestdlibvars
- usetesting - usetesting
- exhaustive
exclusions: exclusions:
@ -44,16 +49,9 @@ linters:
- legacy - legacy
- std-error-handling - std-error-handling
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 warn-unused: true
# Keep sorted alphabetically
settings: settings:
depguard: depguard:
rules: rules:
@ -67,6 +65,9 @@ linters:
dupl: dupl:
threshold: 400 threshold: 400
exhaustive:
default-signifies-exhaustive: true
gomodguard: gomodguard:
blocked: blocked:
modules: modules:
@ -74,8 +75,44 @@ linters:
recommendations: recommendations:
- github.com/evanphx/json-patch/v5 - github.com/evanphx/json-patch/v5
exhaustive: nolintlint:
default-signifies-exhaustive: true require-specific: true
revive:
enable-all-rules: false
enable-default-rules: true
max-open-files: 2048
rules:
- name: unnecessary-format
- name: var-naming
arguments:
- ["ID"] # AllowList
- ["VM"] # DenyList
- - skip-initialism-name-checks: true
upper-case-const: true
skip-package-name-checks: true
skip-package-name-collision-with-go-std: true
extra-bad-package-names:
- helpers
- models
testifylint:
disable:
- empty
- encoded-compare
- equal-values
- error-is-as
- error-nil
- expected-actual
- float-compare
- go-require
- len
- nil-compare
- require-error
- suite-dont-use-pkg
- suite-extra-assert-call
# Intentionally enable all testifylint rules so new checks are adopted automatically.
enable-all: true
run: run:
timeout: 10m timeout: 10m

@ -0,0 +1,74 @@
version: 2
project_name: helm
dist: _dist
builds:
- env:
- CGO_ENABLED=0
goos:
- linux
- windows
- darwin
goarch:
- amd64
- arm64
- arm
- "386"
- ppc64le
- s390x
- riscv64
- loong64
goamd64:
- v1
goarm:
- "7"
goarm64:
- v8.0
go386:
- sse2
goriscv64:
- rva20u64
ignore:
- goos: darwin
goarch: "386"
- goos: darwin
goarch: arm
- goos: darwin
goarch: ppc64le
- goos: darwin
goarch: s390x
- goos: darwin
goarch: riscv64
- goos: darwin
goarch: loong64
- goos: windows
goarch: "386"
- goos: windows
goarch: arm
- goos: windows
goarch: ppc64le
- goos: windows
goarch: s390x
- goos: windows
goarch: riscv64
- goos: windows
goarch: loong64
main: ./cmd/helm
no_unique_dist_dir: true
binary: "{{ .Os }}-{{ .Arch }}/helm"
ldflags:
- "{{ .Env.LDFLAGS }}"
flags:
- -trimpath
dir: .
snapshot:
version_template: "{{ if index .Env \"GORELEASER_CURRENT_TAG\" }}{{ .Env.GORELEASER_CURRENT_TAG }}{{ else }}{{ incpatch .Version }}-next{{ end }}"
changelog:
sort: asc
filters:
exclude:
- '^docs:'
- '^test:'

@ -1,48 +1,88 @@
# AGENTS.md # AGENTS.md
## Overview ## Overview
Helm is a package manager for Kubernetes written in Go, supporting v3 (stable) and v4 (unstable) APIs.
## Build & Test Helm is a package manager for Kubernetes written in Go. It enables users to define, install, and upgrade complex Kubernetes applications using charts.
This document provides an overview of the codebase structure, development guidelines, and key patterns for contributors.
The codebase supports both an SDK for advanced users, and a CLI for direct end user usage.
The project currently supports Helm v3 and Helm v4 versions, based on the `dev-v3` and `main` branches respectively.
## Build and test
```bash ```bash
make build # Build binary make build # Build binary
make test # Run all tests (style + unit) make test # Run all tests (style + unit)
make test-unit # Unit tests only make test-unit # Unit tests only
make test-coverage # With coverage make test-coverage # With coverage
make test-style # Linting make test-style # Linting (wraps golangci-lint)
golangci-lint run # Direct linting
go test -run TestName # Specific test go test -run TestName # Specific test
``` ```
## Code Structure ## Code structure
- `/cmd/helm/` - CLI entry point (Cobra-based)
- `/pkg/` - Public API Major packages:
- `cmd/helm/` - CLI entry point, wires CLI flags to `pkg/cmd/` commands
- `pkg/` - Public API
- `action/` - Core operations (install, upgrade, rollback) - `action/` - Core operations (install, upgrade, rollback)
- `cmd/` - Cobra command implementations bridging CLI flags to `pkg/action/`
- `chart/v2/` - Stable chart format - `chart/v2/` - Stable chart format
- `engine/` - Template rendering (Go templates + Sprig) - `engine/` - Template rendering (Go templates + Sprig)
- `kube/` - Kubernetes client abstraction layer
- `registry/` - OCI support - `registry/` - OCI support
- `release/` - Release types and interfaces (`v1/`, `common/`)
- `repo/` - Chart repository indexing and interaction
- `storage/` - Release backends (Secrets/ConfigMaps/SQL) - `storage/` - Release backends (Secrets/ConfigMaps/SQL)
- `/internal/` - Private implementation - `internal/` - Private implementations
- `chart/v3/` - Next-gen chart format - `chart/v3/` - Next-gen chart format
- `release/v2/` - Release package for chart v3 support
## Development
### Compatibility
Changes are required to maintain backward compatibility as described in [HIP-0004: Document backwards-compatibility rules](https://github.com/helm/community/blob/main/hips/hip-0004.md).
Typically this means that:
## Development Guidelines - the signatures of public APIs, i.e., those in the `pkg/` directory should not change
- CLI commands and parameters should not be removed or changed in a way that would break existing scripts or workflows
- functional behaviour (as implied or documented) must not be modified in a way that would break existing users' expectations
An exception to the above is where incompatible changes are needed to fix a security vulnerability, where minimal breaking changes may be made to address the issue.
### Code standards
### Code Standards
- Use table-driven tests with testify - Use table-driven tests with testify
- Golden files in `testdata/` for complex output - Golden files in `testdata/` for complex output
- Mock Kubernetes clients for action tests - Mock Kubernetes clients for action tests
- All commits must include DCO sign-off: `git commit -s` - All commits must include DCO sign-off: `git commit -s`
### Branching ### Branching
- `main` - Helm v4 development
- `dev-v3` - Helm v3 stable (backport from main)
### Dependencies Standard workflow is for PR development changes to the `main` branch. Minor release branches are cut from `main`, then maintained for critical fixes via patch releases.
Bug and security fixes are also backported to `dev-v3` where applicable.
Development branches:
- `main` - Helm v4
- `dev-v3` - Helm v3 (backport security and bugfixes from main)
Release branches:
- `release-v3.X` - Release branches for v3.X versions
- `release-v4.X` - Release branches for v4.X versions
### Major dependencies
- `k8s.io/client-go` - Kubernetes interaction - `k8s.io/client-go` - Kubernetes interaction
- `github.com/spf13/cobra` - CLI framework - `github.com/spf13/cobra` - CLI framework
- `github.com/Masterminds/sprig` - Template functions - `github.com/Masterminds/sprig` - Template functions
### Key Patterns ### Key patterns
- **Actions**: Operations in `/pkg/action/` use shared Configuration
- **Dual Chart Support**: v2 (stable) in `/pkg/`, v3 (dev) in `/internal/` - **Actions**: High-level operations live in `pkg/action/`, typically using a shared Configuration
- **Storage Abstraction**: Pluggable release storage backends - **Chart versions**: Charts v2 (stable) in `pkg/chart/v2`, v3 (under development) in `internal/chart/v3`
- **Plugins and extensibility**: Enabling additional functionality via plugins and extension points, such as custom template functions or storage backends is preferred over incorporating into Helm's codebase

@ -11,9 +11,13 @@ vulnerability_, please email a report to
[cncf-helm-security@lists.cncf.io](mailto:cncf-helm-security@lists.cncf.io). This will give us a [cncf-helm-security@lists.cncf.io](mailto:cncf-helm-security@lists.cncf.io). This will give us a
chance to try to fix the issue before it is exploited in the wild. chance to try to fix the issue before it is exploited in the wild.
## Helm v3 and v4 ## Helm v3
Helm v4 is currently under development on the `main` branch. During the development of Helm v4 and for some time after its released, Helm v3 will continue to be supported and developed on the `dev-v3` branch. Helm v3 will continue to get bug fixes and updates for new Kubernetes releases. Helm v4 is where new features and major changes will happen. For features to be backported to Helm v3, an exception will be needed. Bugs should first be fixed on Helm v4 and then backported to Helm v3. Helm v4 development takes place on the `main` branch while Helm v3 is on the `dev-v3` branch.
Helm v3 will continue to receive bug fixes and updates for new Kubernetes releases until July 8th 2026. Security enhancements will still be applied until November 11th 2026. See the blog <https://helm.sh/blog/helm-4-released#helm-v3-support> for more details.
Bugs should first be fixed on Helm v4 and then backported to Helm v3. Helm v3 (and the `dev-v3` branch) is no longer accepting new features.
## Sign Your Work ## Sign Your Work
@ -158,7 +162,7 @@ There are 5 types of issues (each with their own corresponding [label](#labels))
for future reference. Generally these are questions that are too complex or large to store in the for future reference. Generally these are questions that are too complex or large to store in the
Slack channel or have particular interest to the community as a whole. Depending on the Slack channel or have particular interest to the community as a whole. Depending on the
discussion, these can turn into `feature` or `bug` issues. discussion, these can turn into `feature` or `bug` issues.
- `proposal`: Used for items (like this one) that propose a new ideas or functionality that require - `proposal`: Used for items (like this one) that propose new ideas or functionality that require
a larger community discussion. This allows for feedback from others in the community before a a larger community discussion. This allows for feedback from others in the community before a
feature is actually developed. This is not needed for small additions. Final word on whether feature is actually developed. This is not needed for small additions. Final word on whether
a feature needs a proposal is up to the core maintainers. All issues that are proposals should a feature needs a proposal is up to the core maintainers. All issues that are proposals should

20
KEYS

@ -1058,3 +1058,23 @@ K6V08VpFmniENmCDHshXYq0gGiTDAP9FsXl2UtmFU5xuYxH4fRKIxgmxJRAFMWI8
u3Rdu/s+DQ== u3Rdu/s+DQ==
=smBO =smBO
-----END PGP PUBLIC KEY BLOCK----- -----END PGP PUBLIC KEY BLOCK-----
pub ed25519 2026-02-08 [SC]
BF888333D96A1C18E2682AAED79D67C9EC016739
uid [ultimate] George Jenkins <gvjenkins@gmail.com>
sig 3 D79D67C9EC016739 2026-02-08 [self-signature]
sub cv25519 2026-02-08 [E]
sig D79D67C9EC016739 2026-02-08 [self-signature]
-----BEGIN PGP PUBLIC KEY BLOCK-----
mDMEaYgDUBYJKwYBBAHaRw8BAQdAWKYkFrwgmfaY/hUq5Z2YpEy8WACKclo2eV/n
1ausaEy0JEdlb3JnZSBKZW5raW5zIDxndmplbmtpbnNAZ21haWwuY29tPoiTBBMW
CgA7FiEEv4iDM9lqHBjiaCqu151nyewBZzkFAmmIA1ACGwMFCwkIBwICIgIGFQoJ
CAsCBBYCAwECHgcCF4AACgkQ151nyewBZzlP0gD/ZFhm9FikdlZO5pW7xWR4YnP4
yFAuY32G9dNdFn1x1p4BALR8Rtpp68eC9R8bq3/r1dK8gwig8DMWirdYaf2ePKoL
uDgEaYgDUBIKKwYBBAGXVQEFAQEHQJwM3R9CTypooHz/4w1waXAct8K2wA1bwi1r
yfb6uMMKAwEIB4h4BBgWCgAgFiEEv4iDM9lqHBjiaCqu151nyewBZzkFAmmIA1AC
GwwACgkQ151nyewBZzlgYAEAoVwYdoO6f3VwGukpv7RtKwF7PQC9AnBUx98TZZ6t
IaoA/RR14NXYYcd0fCwN6sFPq58/NbNkRHBrfw1CntxiJcYD
=duOC
-----END PGP PUBLIC KEY BLOCK-----

@ -1,7 +1,6 @@
BINDIR := $(CURDIR)/bin BINDIR := $(CURDIR)/bin
INSTALL_PATH ?= /usr/local/bin INSTALL_PATH ?= /usr/local/bin
DIST_DIRS := find * -type d -exec DIST_DIRS := find * -type d -exec
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 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 BINNAME ?= helm
@ -9,7 +8,7 @@ GOBIN = $(shell go env GOBIN)
ifeq ($(GOBIN),) ifeq ($(GOBIN),)
GOBIN = $(shell go env GOPATH)/bin GOBIN = $(shell go env GOPATH)/bin
endif endif
GOX = $(GOBIN)/gox GORELEASER = $(GOBIN)/goreleaser
GOIMPORTS = $(GOBIN)/goimports GOIMPORTS = $(GOBIN)/goimports
ARCH = $(shell go env GOARCH) ARCH = $(shell go env GOARCH)
@ -130,8 +129,7 @@ test-source-headers:
@scripts/validate-license.sh @scripts/validate-license.sh
.PHONY: test-acceptance .PHONY: test-acceptance
test-acceptance: TARGETS = linux/amd64 test-acceptance: build
test-acceptance: build build-cross
@if [ -d "${ACCEPTANCE_DIR}" ]; then \ @if [ -d "${ACCEPTANCE_DIR}" ]; then \
cd ${ACCEPTANCE_DIR} && \ cd ${ACCEPTANCE_DIR} && \
ROBOT_RUN_TESTS=$(ACCEPTANCE_RUN_TESTS) ROBOT_HELM_PATH='$(BINDIR)' make acceptance; \ ROBOT_RUN_TESTS=$(ACCEPTANCE_RUN_TESTS) ROBOT_HELM_PATH='$(BINDIR)' make acceptance; \
@ -162,8 +160,8 @@ gen-test-golden: test-unit
# dependencies to the go.mod file. To avoid that we change to a directory # dependencies to the go.mod file. To avoid that we change to a directory
# without a go.mod file when downloading the following dependencies # without a go.mod file when downloading the following dependencies
$(GOX): $(GORELEASER):
(cd /; go install github.com/mitchellh/gox@v1.0.2-0.20220701044238-9f712387e2d2) (cd /; go install github.com/goreleaser/goreleaser/v2@latest)
$(GOIMPORTS): $(GOIMPORTS):
(cd /; go install golang.org/x/tools/cmd/goimports@latest) (cd /; go install golang.org/x/tools/cmd/goimports@latest)
@ -173,8 +171,8 @@ $(GOIMPORTS):
.PHONY: build-cross .PHONY: build-cross
build-cross: LDFLAGS += -extldflags "-static" build-cross: LDFLAGS += -extldflags "-static"
build-cross: $(GOX) build-cross: $(GORELEASER)
GOFLAGS="-trimpath" CGO_ENABLED=0 $(GOX) -parallel=3 -output="_dist/{{.OS}}-{{.Arch}}/$(BINNAME)" -osarch='$(TARGETS)' $(GOFLAGS) -tags '$(TAGS)' -ldflags '$(LDFLAGS)' ./cmd/helm LDFLAGS='$(LDFLAGS)' $(GORELEASER) build --snapshot --clean
.PHONY: dist .PHONY: dist
dist: dist:

@ -17,6 +17,7 @@ limitations under the License.
package main // import "helm.sh/helm/v4/cmd/helm" package main // import "helm.sh/helm/v4/cmd/helm"
import ( import (
"errors"
"log/slog" "log/slog"
"os" "os"
@ -41,7 +42,8 @@ func main() {
} }
if err := cmd.Execute(); err != nil { if err := cmd.Execute(); err != nil {
if cerr, ok := err.(helmcmd.CommandError); ok { var cerr helmcmd.CommandError
if errors.As(err, &cerr) {
os.Exit(cerr.ExitCode) os.Exit(cerr.ExitCode)
} }
os.Exit(1) os.Exit(1)

@ -18,6 +18,7 @@ package main
import ( import (
"bytes" "bytes"
"errors"
"os" "os"
"os/exec" "os/exec"
"runtime" "runtime"
@ -60,14 +61,15 @@ func TestCliPluginExitCode(t *testing.T) {
cmd.Stderr = stderr cmd.Stderr = stderr
err := cmd.Run() err := cmd.Run()
exiterr, ok := err.(*exec.ExitError) exiterr := &exec.ExitError{}
ok := errors.As(err, &exiterr)
if !ok { if !ok {
t.Fatalf("Unexpected error type returned by os.Exit: %T", err) t.Fatalf("Unexpected error type returned by os.Exit: %T", err)
} }
assert.Empty(t, stdout.String()) assert.Empty(t, stdout.String())
expectedStderr := "Error: plugin \"exitwith\" exited with error\n" expectedStderr := "level=WARN msg=\"failed to load plugin (ignoring)\" plugin_yaml=../../pkg/cmd/testdata/helmhome/helm/plugins/noversion/plugin.yaml error=\"failed to load plugin \\\"../../pkg/cmd/testdata/helmhome/helm/plugins/noversion\\\": plugin `version` is required\"\nError: plugin \"exitwith\" exited with error\n"
if stderr.String() != expectedStderr { if stderr.String() != expectedStderr {
t.Errorf("Expected %q written to stderr: Got %q", expectedStderr, stderr.String()) t.Errorf("Expected %q written to stderr: Got %q", expectedStderr, stderr.String())
} }

150
go.mod

@ -1,30 +1,30 @@
module helm.sh/helm/v4 module helm.sh/helm/v4
go 1.25.0 go 1.26.0
require ( require (
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24
github.com/BurntSushi/toml v1.6.0 github.com/BurntSushi/toml v1.6.0
github.com/DATA-DOG/go-sqlmock v1.5.2 github.com/DATA-DOG/go-sqlmock v1.5.2
github.com/Masterminds/semver/v3 v3.4.0 github.com/Masterminds/semver/v3 v3.5.0
github.com/Masterminds/sprig/v3 v3.3.0 github.com/Masterminds/sprig/v3 v3.3.0
github.com/Masterminds/squirrel v1.5.4 github.com/Masterminds/squirrel v1.5.4
github.com/Masterminds/vcs v1.13.3 github.com/Masterminds/vcs v1.13.3
github.com/ProtonMail/go-crypto v1.3.0 github.com/ProtonMail/go-crypto v1.4.1
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2
github.com/cyphar/filepath-securejoin v0.6.1 github.com/cyphar/filepath-securejoin v0.6.1
github.com/distribution/distribution/v3 v3.0.0 github.com/distribution/distribution/v3 v3.1.1
github.com/evanphx/json-patch/v5 v5.9.11 github.com/evanphx/json-patch/v5 v5.9.11
github.com/extism/go-sdk v1.7.1 github.com/extism/go-sdk v1.7.1
github.com/fatih/color v1.18.0 github.com/fatih/color v1.19.0
github.com/fluxcd/cli-utils v0.37.0-flux.1 github.com/fluxcd/cli-utils v1.2.0
github.com/foxcpp/go-mockdns v1.2.0 github.com/foxcpp/go-mockdns v1.2.0
github.com/gobwas/glob v0.2.3 github.com/gobwas/glob v0.2.3
github.com/gofrs/flock v0.13.0 github.com/gofrs/flock v0.13.0
github.com/gosuri/uitable v0.0.4 github.com/gosuri/uitable v0.0.4
github.com/jmoiron/sqlx v1.4.0 github.com/jmoiron/sqlx v1.4.0
github.com/lib/pq v1.10.9 github.com/lib/pq v1.12.3
github.com/mattn/go-shellwords v1.0.12 github.com/mattn/go-shellwords v1.0.13
github.com/moby/term v0.5.2 github.com/moby/term v0.5.2
github.com/opencontainers/go-digest v1.0.0 github.com/opencontainers/go-digest v1.0.0
github.com/opencontainers/image-spec v1.1.1 github.com/opencontainers/image-spec v1.1.1
@ -35,21 +35,21 @@ require (
github.com/stretchr/testify v1.11.1 github.com/stretchr/testify v1.11.1
github.com/tetratelabs/wazero v1.11.0 github.com/tetratelabs/wazero v1.11.0
go.yaml.in/yaml/v3 v3.0.4 go.yaml.in/yaml/v3 v3.0.4
golang.org/x/crypto v0.46.0 golang.org/x/crypto v0.50.0
golang.org/x/term v0.38.0 golang.org/x/term v0.42.0
golang.org/x/text v0.32.0 golang.org/x/text v0.36.0
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/api v0.35.0 k8s.io/api v0.36.0
k8s.io/apiextensions-apiserver v0.35.0 k8s.io/apiextensions-apiserver v0.36.0
k8s.io/apimachinery v0.35.0 k8s.io/apimachinery v0.36.0
k8s.io/apiserver v0.35.0 k8s.io/apiserver v0.36.0
k8s.io/cli-runtime v0.35.0 k8s.io/cli-runtime v0.36.0
k8s.io/client-go v0.35.0 k8s.io/client-go v0.36.0
k8s.io/klog/v2 v2.130.1 k8s.io/klog/v2 v2.140.0
k8s.io/kubectl v0.35.0 k8s.io/kubectl v0.36.0
oras.land/oras-go/v2 v2.6.0 oras.land/oras-go/v2 v2.6.0
sigs.k8s.io/controller-runtime v0.22.4 sigs.k8s.io/controller-runtime v0.24.0
sigs.k8s.io/kustomize/kyaml v0.21.0 sigs.k8s.io/kustomize/kyaml v0.21.1
sigs.k8s.io/yaml v1.6.0 sigs.k8s.io/yaml v1.6.0
) )
@ -60,21 +60,21 @@ require (
github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/goutils v1.1.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect
github.com/blang/semver/v4 v4.0.0 // indirect github.com/blang/semver/v4 v4.0.0 // indirect
github.com/bshuster-repo/logrus-logstash-hook v1.0.0 // indirect github.com/bshuster-repo/logrus-logstash-hook v1.1.0 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/chai2010/gettext-go v1.0.2 // indirect github.com/chai2010/gettext-go v1.0.2 // indirect
github.com/cloudflare/circl v1.6.1 // indirect github.com/cloudflare/circl v1.6.3 // indirect
github.com/coreos/go-systemd/v22 v22.5.0 // indirect github.com/coreos/go-systemd/v22 v22.7.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/distribution/reference v0.6.0 // indirect github.com/distribution/reference v0.6.0 // indirect
github.com/docker/docker-credential-helpers v0.8.2 // indirect github.com/docker/docker-credential-helpers v0.9.5 // indirect
github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c // indirect github.com/docker/go-events v0.0.0-20250808211157-605354379745 // indirect
github.com/docker/go-metrics v0.0.1 // indirect github.com/docker/go-metrics v0.0.1 // indirect
github.com/dylibso/observe-sdk/go v0.0.0-20240819160327-2d926c5d788a // 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/emicklei/go-restful/v3 v3.13.0 // indirect
github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect
@ -91,8 +91,7 @@ require (
github.com/google/uuid v1.6.0 // indirect github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/handlers v1.5.2 // indirect github.com/gorilla/handlers v1.5.2 // indirect
github.com/gorilla/mux v1.8.1 // indirect github.com/gorilla/mux v1.8.1 // indirect
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.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/arc/v2 v2.0.5 // indirect
github.com/hashicorp/golang-lru/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/huandu/xstrings v1.5.0 // indirect
@ -100,12 +99,12 @@ require (
github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/josharian/intern v1.0.0 // indirect github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.18.0 // indirect github.com/klauspost/compress v1.18.4 // indirect
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect
github.com/mailru/easyjson v0.9.0 // indirect github.com/mailru/easyjson v0.9.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.9 // indirect github.com/mattn/go-runewidth v0.0.9 // indirect
github.com/miekg/dns v1.1.57 // indirect github.com/miekg/dns v1.1.57 // indirect
@ -116,66 +115,67 @@ require (
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // 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/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/onsi/gomega v1.38.2 // indirect github.com/onsi/gomega v1.39.1 // indirect
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_golang v1.23.2 // indirect github.com/prometheus/client_golang v1.23.2 // indirect
github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.66.1 // indirect github.com/prometheus/common v0.67.5 // indirect
github.com/prometheus/procfs v0.17.0 // indirect github.com/prometheus/otlptranslator v1.0.0 // indirect
github.com/prometheus/procfs v0.20.1 // indirect
github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5 // indirect github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5 // indirect
github.com/redis/go-redis/extra/redisotel/v9 v9.0.5 // indirect github.com/redis/go-redis/extra/redisotel/v9 v9.0.5 // indirect
github.com/redis/go-redis/v9 v9.7.3 // indirect github.com/redis/go-redis/v9 v9.7.3 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/shopspring/decimal v1.4.0 // indirect github.com/shopspring/decimal v1.4.0 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect github.com/sirupsen/logrus v1.9.4 // indirect
github.com/spf13/cast v1.7.0 // indirect github.com/spf13/cast v1.7.0 // indirect
github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834 // indirect github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834 // indirect
github.com/x448/float16 v0.8.4 // indirect github.com/x448/float16 v0.8.4 // indirect
github.com/xlab/treeprint v1.2.0 // indirect github.com/xlab/treeprint v1.2.0 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/bridges/prometheus v0.57.0 // indirect go.opentelemetry.io/contrib/bridges/prometheus v0.67.0 // indirect
go.opentelemetry.io/contrib/exporters/autoexport v0.57.0 // indirect go.opentelemetry.io/contrib/exporters/autoexport v0.67.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 // indirect
go.opentelemetry.io/otel v1.37.0 // indirect go.opentelemetry.io/otel v1.43.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.8.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.18.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.8.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.32.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.42.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.32.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 // indirect
go.opentelemetry.io/otel/exporters/prometheus v0.54.0 // indirect go.opentelemetry.io/otel/exporters/prometheus v0.64.0 // indirect
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.8.0 // indirect go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.18.0 // indirect
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.32.0 // indirect go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.42.0 // indirect
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.32.0 // indirect go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.42.0 // indirect
go.opentelemetry.io/otel/log v0.8.0 // indirect go.opentelemetry.io/otel/log v0.19.0 // indirect
go.opentelemetry.io/otel/metric v1.37.0 // indirect go.opentelemetry.io/otel/metric v1.43.0 // indirect
go.opentelemetry.io/otel/sdk v1.36.0 // indirect go.opentelemetry.io/otel/sdk v1.43.0 // indirect
go.opentelemetry.io/otel/sdk/log v0.8.0 // indirect go.opentelemetry.io/otel/sdk/log v0.19.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.36.0 // indirect go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect
go.opentelemetry.io/otel/trace v1.37.0 // indirect go.opentelemetry.io/otel/trace v1.43.0 // indirect
go.opentelemetry.io/proto/otlp v1.5.0 // indirect go.opentelemetry.io/proto/otlp v1.10.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect
golang.org/x/mod v0.30.0 // indirect golang.org/x/mod v0.34.0 // indirect
golang.org/x/net v0.47.0 // indirect golang.org/x/net v0.52.0 // indirect
golang.org/x/oauth2 v0.30.0 // indirect golang.org/x/oauth2 v0.36.0 // indirect
golang.org/x/sync v0.19.0 // indirect golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.39.0 // indirect golang.org/x/sys v0.43.0 // indirect
golang.org/x/time v0.12.0 // indirect golang.org/x/time v0.15.0 // indirect
golang.org/x/tools v0.39.0 // indirect golang.org/x/tools v0.43.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect
google.golang.org/grpc v1.72.2 // indirect google.golang.org/grpc v1.80.0 // indirect
google.golang.org/protobuf v1.36.8 // indirect google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect
k8s.io/component-base v0.35.0 // indirect k8s.io/component-base v0.36.0 // indirect
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a // indirect
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 // indirect
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
sigs.k8s.io/kustomize/api v0.20.1 // indirect sigs.k8s.io/kustomize/api v0.21.1 // indirect
sigs.k8s.io/randfill v1.0.0 // indirect sigs.k8s.io/randfill v1.0.0 // indirect
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect sigs.k8s.io/structured-merge-diff/v6 v6.3.2 // indirect
) )

321
go.sum

@ -14,18 +14,20 @@ github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE=
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Masterminds/semver/v3 v3.5.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs=
github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0=
github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM= github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM=
github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= 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 h1:IIA2aBdXvfbIM+yl/eTnL4hb1XwdpvuQLglAix1gweE=
github.com/Masterminds/vcs v1.13.3/go.mod h1:TiE7xuEjl1N4j016moRd6vezp6e6Lz23gypeXfzXeW8= 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.4.1 h1:9RfcZHqEQUvP8RzecWEUafnZVtEvrBVL9BiF67IQOfM=
github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE= github.com/ProtonMail/go-crypto v1.4.1/go.mod h1:e1OaTyu5SYVrO9gKOEhTc+5UcXtTUa+P3uLudwcgPqo=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 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/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alicebob/miniredis/v2 v2.35.0 h1:QwLphYqCEAo1eu1TqPRN2jgVMPBweeQcR21jeqDCONI=
github.com/alicebob/miniredis/v2 v2.35.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
@ -34,25 +36,25 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
github.com/bshuster-repo/logrus-logstash-hook v1.0.0 h1:e+C0SB5R1pu//O4MQ3f9cFuPGoOVeF2fE4Og9otCc70= github.com/bshuster-repo/logrus-logstash-hook v1.1.0 h1:o2FzZifLg+z/DN1OFmzTWzZZx/roaqt8IPZCIVco8r4=
github.com/bshuster-repo/logrus-logstash-hook v1.0.0/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk= github.com/bshuster-repo/logrus-logstash-hook v1.1.0/go.mod h1:Q2aXOe7rNuPgbBtPCOzYyWDvKX7+FpxE5sRdvcPoui0=
github.com/bsm/ginkgo/v2 v2.7.0/go.mod h1:AiKlXPm7ItEHNc/2+OkrNG4E0ITzojb9/xWzvQ9XZ9w= github.com/bsm/ginkgo/v2 v2.7.0/go.mod h1:AiKlXPm7ItEHNc/2+OkrNG4E0ITzojb9/xWzvQ9XZ9w=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.26.0/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= 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 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 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 h1:1Lwwip6Q2QGsAdl/ZKPCwTe9fe0CjlUbqj5bFNSjIRk=
github.com/chai2010/gettext-go v1.0.2/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA= 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.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= github.com/coreos/go-systemd/v22 v22.7.0 h1:LAEzFkke61DFROc7zNLX/WA2i5J8gYqe0rSj9KI28KA=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/go-systemd/v22 v22.7.0/go.mod h1:xNUYtjHu2EDXbsxz1i41wouACIwT7Ybq9o0BQhMwD0w=
github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0= 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/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
@ -65,34 +67,34 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/distribution/distribution/v3 v3.0.0 h1:q4R8wemdRQDClzoNNStftB2ZAfqOiN6UX90KJc4HjyM= github.com/distribution/distribution/v3 v3.1.1 h1:KUbk7C8CfaLXy8kbf/hGq9cad/wCoLB6dbWH6DMbmX0=
github.com/distribution/distribution/v3 v3.0.0/go.mod h1:tRNuFoZsUdyRVegq8xGNeds4KLjwLCRin/tTo6i1DhU= github.com/distribution/distribution/v3 v3.1.1/go.mod h1:d7lXwZpph0bVcOj4Aqn0nMrWHIwRQGdiV5TLeI+/w6Y=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo= github.com/docker/docker-credential-helpers v0.9.5 h1:EFNN8DHvaiK8zVqFA2DT6BjXE0GzfLOZ38ggPTKePkY=
github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M= github.com/docker/docker-credential-helpers v0.9.5/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c=
github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c h1:+pKlWGMw7gf6bQ+oDZB4KHQFypsfjYlq/C4rfL7D3g8= github.com/docker/go-events v0.0.0-20250808211157-605354379745 h1:yOn6Ze6IbYI/KAw2lw/83ELYvZh6hvsygTVkD0dzMC4=
github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= github.com/docker/go-events v0.0.0-20250808211157-605354379745/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA=
github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8= 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/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 h1:UwSIFv5g5lIvbGgtf3tVwC7Ky9rmMFBp0RMs+6f6YqE=
github.com/dylibso/observe-sdk/go v0.0.0-20240819160327-2d926c5d788a/go.mod h1:C8DzXehI4zAbrdlbtOByKX6pfivJTBiV9Jjqv56Yd9Q= 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.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes=
github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/emicklei/go-restful/v3 v3.13.0/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 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU=
github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= 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 h1:Wl78ApPPB2Wvf/TIe2xdyJxTlb6obmF18d8QdkxNDu4=
github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f/go.mod h1:OSYXu++VVOHnXeitef/D8n/6y4QV8uLHSFXX4NeXMGc= github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f/go.mod h1:OSYXu++VVOHnXeitef/D8n/6y4QV8uLHSFXX4NeXMGc=
github.com/extism/go-sdk v1.7.1 h1:lWJos6uY+tRFdlIHR+SJjwFDApY7OypS/2nMhiVQ9Sw= 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/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.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fluxcd/cli-utils v0.37.0-flux.1 h1:k/VvPNT3tGa/l2N+qzHduaQr3GVbgoWS6nw7tGZz16w= github.com/fluxcd/cli-utils v1.2.0 h1:1o07pXTMxJ/XJ1GpAbLtjdXwfCUMq4Ku1OcnvJHLohI=
github.com/fluxcd/cli-utils v0.37.0-flux.1/go.mod h1:aND5wX3LuTFtB7eUT7vsWr8mmxRVSPR2Wkvbn0SqPfw= github.com/fluxcd/cli-utils v1.2.0/go.mod h1:d5HdTDdR5sCbsIbgtOQ7x7srKYwYeZORU6CD2yn4j/M=
github.com/foxcpp/go-mockdns v1.2.0 h1:omK3OrHRD1IWJz1FuFBCFquhXslXoF17OvBS6JPzZF0= github.com/foxcpp/go-mockdns v1.2.0 h1:omK3OrHRD1IWJz1FuFBCFquhXslXoF17OvBS6JPzZF0=
github.com/foxcpp/go-mockdns v1.2.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk= github.com/foxcpp/go-mockdns v1.2.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
@ -126,7 +128,6 @@ github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1v
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= 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/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.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw= 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/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.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
@ -143,8 +144,8 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 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/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5 h1:xhMrHhTJ6zxu3gA4enFM9MLn9AY7613teCdFnlUVbSQ= github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 h1:z2ogiKUYzX5Is6zr/vP9vJGqPwcdqsWjOt+V8J7+bTc=
github.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA= github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE= github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE=
@ -153,10 +154,8 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/gosuri/uitable v0.0.4 h1:IG2xLKRvErL3uhY6e1BylFzG+aJiwQviDDTfOKeKTpY= github.com/gosuri/uitable v0.0.4 h1:IG2xLKRvErL3uhY6e1BylFzG+aJiwQviDDTfOKeKTpY=
github.com/gosuri/uitable v0.0.4/go.mod h1:tKR86bXuXPZazfOTG1FIzvjIdXzd0mo4Vtn16vt0PJo= 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/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs=
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c=
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 h1:l2zaLDubNhW4XO3LnliVj0GXO3+/CGNJAg1dcN2Fpfw=
github.com/hashicorp/golang-lru/arc/v2 v2.0.5/go.mod h1:ny6zBSQZi2JxIeYcv7kt2sH2PXJtirBN7RDhRpxPkxU= 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 h1:wW7h1TG88eUIJ2i69gaE3uNVtEPIagzhGvHgwfx2Vm4=
@ -177,8 +176,8 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE= github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
@ -191,21 +190,21 @@ github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o=
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk= github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk=
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ=
github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0=
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= 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 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.16/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 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 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 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk= github.com/mattn/go-shellwords v1.0.13 h1:DC0OMEpGjm6LfNFU4ckYcvbQKyp2vE8atyFGXNtDcf4=
github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= github.com/mattn/go-shellwords v1.0.13/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
@ -232,10 +231,10 @@ github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns= github.com/onsi/ginkgo/v2 v2.28.1 h1:S4hj+HbZp40fNKuLUQOYLDgZLwNUVn19N3Atb98NCyI=
github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo= github.com/onsi/ginkgo/v2 v2.28.1/go.mod h1:CLtbVInNckU3/+gC8LzkGUb9oF+e8W8TdUsxPwvdOgE=
github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28=
github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k= github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
@ -259,13 +258,15 @@ github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNw
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc=
github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
github.com/prometheus/otlptranslator v1.0.0 h1:s0LJW/iN9dkIH+EnhiD3BlkkP5QVIUVEoIwkU+A6qos=
github.com/prometheus/otlptranslator v1.0.0/go.mod h1:vRYWnXvI6aWGpsdY/mOT/cbeVRBlPWtBNDb7kGR3uKM=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0= github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc=
github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw= github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5 h1:EaDatTxkdHG+U3Bk4EUr+DZ7fOGwTfezUiUJMaIcaho= github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5 h1:EaDatTxkdHG+U3Bk4EUr+DZ7fOGwTfezUiUJMaIcaho=
github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5/go.mod h1:fyalQWdtzDBECAQFBJuQe5bzQ02jGd5Qcbgb97Flm7U= github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5/go.mod h1:fyalQWdtzDBECAQFBJuQe5bzQ02jGd5Qcbgb97Flm7U=
github.com/redis/go-redis/extra/redisotel/v9 v9.0.5 h1:EfpWLLCyXw8PSM2/XNJLjI3Pb27yVE+gIAfeqp8LUCc= github.com/redis/go-redis/extra/redisotel/v9 v9.0.5 h1:EfpWLLCyXw8PSM2/XNJLjI3Pb27yVE+gIAfeqp8LUCc=
@ -286,8 +287,9 @@ github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepq
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w=
github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
@ -314,58 +316,62 @@ github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcY
github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ=
github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
go.opentelemetry.io/contrib/bridges/prometheus v0.57.0 h1:UW0+QyeyBVhn+COBec3nGhfnFe5lwB0ic1JBVjzhk0w= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/contrib/bridges/prometheus v0.57.0/go.mod h1:ppciCHRLsyCio54qbzQv0E4Jyth/fLWDTJYfvWpcSVk= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/exporters/autoexport v0.57.0 h1:jmTVJ86dP60C01K3slFQa2NQ/Aoi7zA+wy7vMOKD9H4= go.opentelemetry.io/contrib/bridges/prometheus v0.67.0 h1:dkBzNEAIKADEaFnuESzcXvpd09vxvDZsOjx11gjUqLk=
go.opentelemetry.io/contrib/exporters/autoexport v0.57.0/go.mod h1:EJBheUMttD/lABFyLXhce47Wr6DPWYReCzaZiXadH7g= go.opentelemetry.io/contrib/bridges/prometheus v0.67.0/go.mod h1:Z5RIwRkZgauOIfnG5IpidvLpERjhTninpP1dTG2jTl4=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= go.opentelemetry.io/contrib/exporters/autoexport v0.67.0 h1:4fnRcNpc6YFtG3zsFw9achKn3XgmxPxuMuqIL5rE8e8=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= go.opentelemetry.io/contrib/exporters/autoexport v0.67.0/go.mod h1:qTvIHMFKoxW7HXg02gm6/Wofhq5p3Ib/A/NNt1EoBSQ=
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o=
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.8.0 h1:WzNab7hOOLzdDF/EoWCt4glhrbMPVMOO5JYTmpz36Ls= go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.8.0/go.mod h1:hKvJwTzJdp90Vh7p6q/9PAOd55dI6WA6sWj62a/JvSs= go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.8.0 h1:S+LdBGiQXtJdowoJoQPEtI52syEP/JYBUpjO49EQhV8= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.18.0 h1:deI9UQMoGFgrg5iLPgzueqFPHevDl+28YKfSpPTI6rY=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.8.0/go.mod h1:5KXybFvPGds3QinJWQT7pmXf+TN5YIa7CNYObWRkj50= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.18.0/go.mod h1:PFx9NgpNUKXdf7J4Q3agRxMs3Y07QhTCVipKmLsMKnU=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.32.0 h1:j7ZSD+5yn+lo3sGV69nW04rRR0jhYnBwjuX3r0HvnK0= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0 h1:HIBTQ3VO5aupLKjC90JgMqpezVXwFuq6Ryjn0/izoag=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.32.0/go.mod h1:WXbYJTUaZXAbYd8lbgGuvih0yuCfOFC5RJoYnoLcGz8= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0/go.mod h1:ji9vId85hMxqfvICA0Jt8JqEdrXaAkcpkI9HPXya0ro=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.32.0 h1:t/Qur3vKSkUCcDVaSumWF2PKHt85pc7fRvFuoVT8qFU= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.42.0 h1:MdKucPl/HbzckWWEisiNqMPhRrAOQX8r4jTuGr636gk=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.32.0/go.mod h1:Rl61tySSdcOJWoEgYZVtmnKdA0GeKrSqkHC1t+91CH8= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.42.0/go.mod h1:RolT8tWtfHcjajEH5wFIZ4Dgh5jpPdFXYV9pTAk/qjc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0 h1:w1K+pCJoPpQifuVpsKamUdn9U0zM3xUziVOqsGksUrY=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0/go.mod h1:7Bept48yIeqxP2OZ9/AqIpYS94h2or0aB4FypJTc8ZM= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0/go.mod h1:HBy4BjzgVE8139ieRI75oXm3EcDN+6GhD88JT1Kjvxg=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 h1:tgJ0uaNS4c98WRNUEx5U3aDlrDOI5Rs+1Vifcw4DJ8U= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k=
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 v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0 h1:cMyu9O88joYEaI47CnQkxO1XZdpoTF9fEnW2duIddhw= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0 h1:zWWrB1U6nqhS/k6zYB74CjRpuiitRtLLi68VcgmOEto=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0/go.mod h1:6Am3rn7P9TVVeXYG+wtcGE7IE1tsQ+bP3AuWcKt/gOI= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0/go.mod h1:2qXPNBX1OVRC0IwOnfo1ljoid+RD0QK3443EaqVlsOU=
go.opentelemetry.io/otel/exporters/prometheus v0.54.0 h1:rFwzp68QMgtzu9PgP3jm9XaMICI6TsofWWPcBDKwlsU= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 h1:3iZJKlCZufyRzPzlQhUIWVmfltrXuGyfjREgGP3UUjc=
go.opentelemetry.io/otel/exporters/prometheus v0.54.0/go.mod h1:QyjcV9qDP6VeK5qPyKETvNjmaaEc7+gqjh4SS0ZYzDU= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0/go.mod h1:/G+nUPfhq2e+qiXMGxMwumDrP5jtzU+mWN7/sjT2rak=
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.8.0 h1:CHXNXwfKWfzS65yrlB2PVds1IBZcdsX8Vepy9of0iRU= go.opentelemetry.io/otel/exporters/prometheus v0.64.0 h1:g0LRDXMX/G1SEZtK8zl8Chm4K6GBwRkjPKE36LxiTYs=
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.8.0/go.mod h1:zKU4zUgKiaRxrdovSS2amdM5gOc59slmo/zJwGX+YBg= go.opentelemetry.io/otel/exporters/prometheus v0.64.0/go.mod h1:UrgcjnarfdlBDP3GjDIJWe6HTprwSazNjwsI+Ru6hro=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.32.0 h1:SZmDnHcgp3zwlPBS2JX2urGYe/jBKEIT6ZedHRUyCz8= go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.18.0 h1:KJVjPD3rcPb98rIs3HznyJlrfx9ge5oJvxxlGR+P/7s=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.32.0/go.mod h1:fdWW0HtZJ7+jNpTKUR0GpMEDP69nR8YBJQxNiVCE3jk= go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.18.0/go.mod h1:K3kRa2ckmHWQaTWQdPRHc7qGXASuVuoEQXzrvlA98Ws=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.32.0 h1:cC2yDI3IQd0Udsux7Qmq8ToKAx1XCilTQECZ0KDZyTw= go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.42.0 h1:lSZHgNHfbmQTPfuTmWVkEu8J8qXaQwuV30pjCcAUvP8=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.32.0/go.mod h1:2PD5Ex6z8CFzDbTdOlwyNIUywRr1DN0ospafJM1wJ+s= go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.42.0/go.mod h1:so9ounLcuoRDu033MW/E0AD4hhUjVqswrMF5FoZlBcw=
go.opentelemetry.io/otel/log v0.8.0 h1:egZ8vV5atrUWUbnSsHn6vB8R21G2wrKqNiDt3iWertk= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.42.0 h1:s/1iRkCKDfhlh1JF26knRneorus8aOwVIDhvYx9WoDw=
go.opentelemetry.io/otel/log v0.8.0/go.mod h1:M9qvDdUTRCopJcGRKg57+JSQ9LgLBrwwfC32epk5NX8= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.42.0/go.mod h1:UI3wi0FXg1Pofb8ZBiBLhtMzgoTm1TYkMvn71fAqDzs=
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= go.opentelemetry.io/otel/log v0.19.0 h1:KUZs/GOsw79TBBMfDWsXS+KZ4g2Ckzksd1ymzsIEbo4=
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= go.opentelemetry.io/otel/log v0.19.0/go.mod h1:5DQYeGmxVIr4n0/BcJvF4upsraHjg6vudJJpnkL6Ipk=
go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs= go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY= go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
go.opentelemetry.io/otel/sdk/log v0.8.0 h1:zg7GUYXqxk1jnGF/dTdLPrK06xJdrXgqgFLnI4Crxvs= go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
go.opentelemetry.io/otel/sdk/log v0.8.0/go.mod h1:50iXr0UVwQrYS45KbruFrEt4LvAdCaWWgIrsN3ZQggo= go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis= go.opentelemetry.io/otel/sdk/log v0.19.0 h1:scYVLqT22D2gqXItnWiocLUKGH9yvkkeql5dBDiXyko=
go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4= go.opentelemetry.io/otel/sdk/log v0.19.0/go.mod h1:vFBowwXGLlW9AvpuF7bMgnNI95LiW10szrOdvzBHlAg=
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= go.opentelemetry.io/otel/sdk/log/logtest v0.19.0 h1:BEbF7ZBB6qQloV/Ub1+3NQoOUnVtcGkU3XX4Ws3GQfk=
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= go.opentelemetry.io/otel/sdk/log/logtest v0.19.0/go.mod h1:Lua81/3yM0wOmoHTokLj9y9ADeA02v1naRrVrkAZuKk=
go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
@ -376,14 +382,14 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g= golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@ -394,10 +400,10 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ= golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -406,27 +412,26 @@ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/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-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-20210616094352-59db8d763f22/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-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-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.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.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.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@ -434,8 +439,8 @@ golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
golang.org/x/term v0.14.0/go.mod h1:TySc+nGkYR6qt8km8wUhuFRTVSMIX3XPR58y2lC8vww= golang.org/x/term v0.14.0/go.mod h1:TySc+nGkYR6qt8km8wUhuFRTVSMIX3XPR58y2lC8vww=
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY=
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
@ -443,27 +448,29 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.15.0/go.mod h1:hpksKq4dtpQWS1uQ61JkdqWM3LscIS6Slf+VVkm+wQk= golang.org/x/tools v0.15.0/go.mod h1:hpksKq4dtpQWS1uQ61JkdqWM3LscIS6Slf+VVkm+wQk=
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb h1:p31xT4yrYrSM/G4Sn2+TNUkVhFCbG9y8itM2S6Th950= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:jbe3Bkdp+Dh2IrslsFCklNhweNTBgSYanP1UXhJDhKg= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a h1:v2PbRU4K3llS09c7zodFpNePeamkAwG3mPrAery9VeE= google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M=
google.golang.org/grpc v1.72.2 h1:TdbGzwb82ty4OusHWepvFWGLgIbNo1/SUynEN0ssqv8= google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 h1:m8qni9SQFH0tJc1X0vmnpw/0t+AImlSvp30sEupozUg=
google.golang.org/grpc v1.72.2/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af h1:+5/Sw3GsDNlEmu7TfklWKPdQ0Ykja5VEmq2i817+jbI=
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
@ -478,41 +485,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.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
k8s.io/api v0.35.0 h1:iBAU5LTyBI9vw3L5glmat1njFK34srdLmktWwLTprlY= k8s.io/api v0.36.0 h1:SgqDhZzHdOtMk40xVSvCXkP9ME0H05hPM3p9AB1kL80=
k8s.io/api v0.35.0/go.mod h1:AQ0SNTzm4ZAczM03QH42c7l3bih1TbAXYo0DkF8ktnA= k8s.io/api v0.36.0/go.mod h1:m1LVrGPNYax5NBHdO+QuAedXyuzTt4RryI/qnmNvs34=
k8s.io/apiextensions-apiserver v0.35.0 h1:3xHk2rTOdWXXJM+RDQZJvdx0yEOgC0FgQ1PlJatA5T4= k8s.io/apiextensions-apiserver v0.36.0 h1:Wt7E8J+VBCbj4FjiBfDTK/neXDDjyJVJc7xfuOHImZ0=
k8s.io/apiextensions-apiserver v0.35.0/go.mod h1:E1Ahk9SADaLQ4qtzYFkwUqusXTcaV2uw3l14aqpL2LU= k8s.io/apiextensions-apiserver v0.36.0/go.mod h1:kGDjH0msuiIB3tgsYRV0kS9GqpMYMUsQ3GHv7TApyug=
k8s.io/apimachinery v0.35.0 h1:Z2L3IHvPVv/MJ7xRxHEtk6GoJElaAqDCCU0S6ncYok8= k8s.io/apimachinery v0.36.0 h1:jZyPzhd5Z+3h9vJLt0z9XdzW9VzNzWAUw+P1xZ9PXtQ=
k8s.io/apimachinery v0.35.0/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns= k8s.io/apimachinery v0.36.0/go.mod h1:FklypaRJt6n5wUIwWXIP6GJlIpUizTgfo1T/As+Tyxc=
k8s.io/apiserver v0.35.0 h1:CUGo5o+7hW9GcAEF3x3usT3fX4f9r8xmgQeCBDaOgX4= k8s.io/apiserver v0.36.0 h1:Jg5OFAENUACByUCg15CmhZAYrr5ZyJ+jodyA1mHl3YE=
k8s.io/apiserver v0.35.0/go.mod h1:QUy1U4+PrzbJaM3XGu2tQ7U9A4udRRo5cyxkFX0GEds= k8s.io/apiserver v0.36.0/go.mod h1:mHvwdHf+qKEm+1/hYm756SV+oREOKSPnsjagOpx6Vho=
k8s.io/cli-runtime v0.35.0 h1:PEJtYS/Zr4p20PfZSLCbY6YvaoLrfByd6THQzPworUE= k8s.io/cli-runtime v0.36.0 h1:HNxciQpQMMOKS0/GiUXcKDyA6J2FDILJj9NmP2BZrTg=
k8s.io/cli-runtime v0.35.0/go.mod h1:VBRvHzosVAoVdP3XwUQn1Oqkvaa8facnokNkD7jOTMY= k8s.io/cli-runtime v0.36.0/go.mod h1:KObkknK9Ro5LYX+1RdiKc7C8CvGg4aX+V/Zv+E8WPHA=
k8s.io/client-go v0.35.0 h1:IAW0ifFbfQQwQmga0UdoH0yvdqrbwMdq9vIFEhRpxBE= k8s.io/client-go v0.36.0 h1:pOYi7C4RHChYjMiHpZSpSbIM6ZxVbRXBy7CuiIwqA3c=
k8s.io/client-go v0.35.0/go.mod h1:q2E5AAyqcbeLGPdoRB+Nxe3KYTfPce1Dnu1myQdqz9o= k8s.io/client-go v0.36.0/go.mod h1:ZKKcpwF0aLYfkHFCjillCKaTK/yBkEDHTDXCFY6AS9Y=
k8s.io/component-base v0.35.0 h1:+yBrOhzri2S1BVqyVSvcM3PtPyx5GUxCK2tinZz1G94= k8s.io/component-base v0.36.0 h1:hFjEktssxiJhrK1zfybkH4kJOi8iZuF+mIDCqS5+jRo=
k8s.io/component-base v0.35.0/go.mod h1:85SCX4UCa6SCFt6p3IKAPej7jSnF3L8EbfSyMZayJR0= k8s.io/component-base v0.36.0/go.mod h1:JZvIfcNHk+uck+8LhJzhSBtydWXaZNQwX2OdL+Mnwsk=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0=
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE= k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a h1:xCeOEAOoGYl2jnJoHkC3hkbPJgdATINPMAxaynU2Ovg=
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a/go.mod h1:uGBT7iTA6c6MvqUvSXIaYZo9ukscABYi2btjhvgKGZ0=
k8s.io/kubectl v0.35.0 h1:cL/wJKHDe8E8+rP3G7avnymcMg6bH6JEcR5w5uo06wc= k8s.io/kubectl v0.36.0 h1:hEGr8NvIm2Wjqs2Xy48Uzmvo6lpHdGKlLyMvau2gTms=
k8s.io/kubectl v0.35.0/go.mod h1:VR5/TSkYyxZwrRwY5I5dDq6l5KXmiCb+9w8IKplk3Qo= k8s.io/kubectl v0.36.0/go.mod h1:iDe8aV5BEi45W8k+5n71I2pJ/nwE0PHDu+/2cejzYoo=
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 h1:AZYQSJemyQB5eRxqcPky+/7EdBj0xi3g0ZcxxJ7vbWU=
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk=
oras.land/oras-go/v2 v2.6.0 h1:X4ELRsiGkrbeox69+9tzTu492FMUu7zJQW6eJU+I2oc= 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= oras.land/oras-go/v2 v2.6.0/go.mod h1:magiQDfG6H1O9APp+rOsvCPcW1GD2MM7vgnKY0Y+u1o=
sigs.k8s.io/controller-runtime v0.22.4 h1:GEjV7KV3TY8e+tJ2LCTxUTanW4z/FmNB7l327UfMq9A= sigs.k8s.io/controller-runtime v0.24.0 h1:Ck6N2LdS8Lovy1o25BB4r1xjvLEKUl1s2o9kU+KWDE4=
sigs.k8s.io/controller-runtime v0.22.4/go.mod h1:+QX1XUpTXN4mLoblf4tqr5CQcyHPAki2HLXqQMY6vh8= sigs.k8s.io/controller-runtime v0.24.0/go.mod h1:vFkfY5fGt5xAC/sKb8IBFKgWPNKG9OUG29dR8Y2wImw=
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
sigs.k8s.io/kustomize/api v0.20.1 h1:iWP1Ydh3/lmldBnH/S5RXgT98vWYMaTUL1ADcr+Sv7I= sigs.k8s.io/kustomize/api v0.21.1 h1:lzqbzvz2CSvsjIUZUBNFKtIMsEw7hVLJp0JeSIVmuJs=
sigs.k8s.io/kustomize/api v0.20.1/go.mod h1:t6hUFxO+Ph0VxIk1sKp1WS0dOjbPCtLJ4p8aADLwqjM= sigs.k8s.io/kustomize/api v0.21.1/go.mod h1:f3wkKByTrgpgltLgySCntrYoq5d3q7aaxveSagwTlwI=
sigs.k8s.io/kustomize/kyaml v0.21.0 h1:7mQAf3dUwf0wBerWJd8rXhVcnkk5Tvn/q91cGkaP6HQ= sigs.k8s.io/kustomize/kyaml v0.21.1 h1:IVlbmhC076nf6foyL6Taw4BkrLuEsXUXNpsE+ScX7fI=
sigs.k8s.io/kustomize/kyaml v0.21.0/go.mod h1:hmxADesM3yUN2vbA5z1/YTBnzLJ1dajdqpQonwBL1FQ= sigs.k8s.io/kustomize/kyaml v0.21.1/go.mod h1:hmxADesM3yUN2vbA5z1/YTBnzLJ1dajdqpQonwBL1FQ=
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= sigs.k8s.io/structured-merge-diff/v6 v6.3.2 h1:kwVWMx5yS1CrnFWA/2QHyRVJ8jM6dBA80uLmm0wJkk8=
sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/structured-merge-diff/v6 v6.3.2/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=

@ -45,11 +45,11 @@ type Chart struct {
// Templates for this chart. // Templates for this chart.
Templates []*common.File `json:"templates"` Templates []*common.File `json:"templates"`
// Values are default config for this chart. // Values are default config for this chart.
Values map[string]interface{} `json:"values"` Values map[string]any `json:"values"`
// Schema is an optional JSON schema for imposing structure on Values // Schema is an optional JSON schema for imposing structure on Values
Schema []byte `json:"schema"` Schema []byte `json:"schema"`
// SchemaModTime the schema was last modified // SchemaModTime the schema was last modified
SchemaModTime time.Time `json:"schemamodtime,omitempty"` SchemaModTime time.Time `json:"schemamodtime"`
// Files are miscellaneous files in a chart archive, // Files are miscellaneous files in a chart archive,
// e.g. README, LICENSE, etc. // e.g. README, LICENSE, etc.
Files []*common.File `json:"files"` Files []*common.File `json:"files"`

@ -124,8 +124,8 @@ func TestIsRoot(t *testing.T) {
is := assert.New(t) is := assert.New(t)
is.Equal(false, chrt1.IsRoot()) is.False(chrt1.IsRoot())
is.Equal(true, chrt2.IsRoot()) is.True(chrt2.IsRoot())
} }
func TestChartPath(t *testing.T) { func TestChartPath(t *testing.T) {

@ -44,7 +44,7 @@ type Dependency struct {
Enabled bool `json:"enabled,omitempty" yaml:"enabled,omitempty"` 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 // 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. // string or pair of child/parent sublist items.
ImportValues []interface{} `json:"import-values,omitempty" yaml:"import-values,omitempty"` ImportValues []any `json:"import-values,omitempty" yaml:"import-values,omitempty"`
// Alias usable alias to be used for the chart // Alias usable alias to be used for the chart
Alias string `json:"alias,omitempty" yaml:"alias,omitempty"` Alias string `json:"alias,omitempty" yaml:"alias,omitempty"`
} }

@ -25,6 +25,6 @@ func (v ValidationError) Error() string {
} }
// ValidationErrorf takes a message and formatting options and creates a ValidationError // ValidationErrorf takes a message and formatting options and creates a ValidationError
func ValidationErrorf(msg string, args ...interface{}) ValidationError { func ValidationErrorf(msg string, args ...any) ValidationError {
return ValidationError(fmt.Sprintf(msg, args...)) return ValidationError(fmt.Sprintf(msg, args...))
} }

@ -43,7 +43,7 @@ func WithSkipSchemaValidation(skipSchemaValidation bool) LinterOption {
} }
} }
func RunAll(baseDir string, values map[string]interface{}, namespace string, options ...LinterOption) support.Linter { func RunAll(baseDir string, values map[string]any, namespace string, options ...LinterOption) support.Linter {
chartDir, _ := filepath.Abs(baseDir) chartDir, _ := filepath.Abs(baseDir)

@ -95,7 +95,7 @@ func TestInvalidYaml(t *testing.T) {
t.Fatalf("All didn't fail with expected errors, got %#v", m) t.Fatalf("All didn't fail with expected errors, got %#v", m)
} }
if !strings.Contains(m[0].Err.Error(), "deliberateSyntaxError") { if !strings.Contains(m[0].Err.Error(), "deliberateSyntaxError") {
t.Errorf("All didn't have the error for deliberateSyntaxError") t.Error("All didn't have the error for deliberateSyntaxError")
} }
} }
@ -107,7 +107,7 @@ func TestInvalidChartYamlV3(t *testing.T) {
t.Fatalf("All didn't fail with expected errors, got %#v", m) 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") { 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") t.Error("All didn't have the error for duplicate YAML keys")
} }
} }
@ -231,13 +231,13 @@ func TestMalformedTemplate(t *testing.T) {
}() }()
select { select {
case <-c: case <-c:
t.Fatalf("lint malformed template timeout") t.Fatal("lint malformed template timeout")
case <-ch: case <-ch:
if len(m) != 1 { if len(m) != 1 {
t.Fatalf("All didn't fail with expected errors, got %#v", m) t.Fatalf("All didn't fail with expected errors, got %#v", m)
} }
if !strings.Contains(m[0].Err.Error(), "invalid character '{'") { if !strings.Contains(m[0].Err.Error(), "invalid character '{'") {
t.Errorf("All didn't have the error for invalid character '{'") t.Error("All didn't have the error for invalid character '{'")
} }
} }
} }

@ -69,15 +69,15 @@ func Chartfile(linter *support.Linter) {
linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartDependencies(chartFile)) linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartDependencies(chartFile))
} }
func validateChartVersionType(data map[string]interface{}) error { func validateChartVersionType(data map[string]any) error {
return isStringValue(data, "version") return isStringValue(data, "version")
} }
func validateChartAppVersionType(data map[string]interface{}) error { func validateChartAppVersionType(data map[string]any) error {
return isStringValue(data, "appVersion") return isStringValue(data, "appVersion")
} }
func isStringValue(data map[string]interface{}, key string) error { func isStringValue(data map[string]any, key string) error {
value, ok := data[key] value, ok := data[key]
if !ok { if !ok {
return nil return nil
@ -152,7 +152,7 @@ func validateChartVersion(cf *chart.Metadata) error {
valid, msg := c.Validate(version) valid, msg := c.Validate(version)
if !valid && len(msg) > 0 { if !valid && len(msg) > 0 {
return fmt.Errorf("version %v", msg[0]) return fmt.Errorf("version %w", msg[0])
} }
return nil return nil
@ -214,12 +214,12 @@ func validateChartType(cf *chart.Metadata) error {
// loadChartFileForTypeCheck loads the Chart.yaml // loadChartFileForTypeCheck loads the Chart.yaml
// in a generic form of a map[string]interface{}, so that the type // in a generic form of a map[string]interface{}, so that the type
// of the values can be checked // of the values can be checked
func loadChartFileForTypeCheck(filename string) (map[string]interface{}, error) { func loadChartFileForTypeCheck(filename string) (map[string]any, error) {
b, err := os.ReadFile(filename) b, err := os.ReadFile(filename)
if err != nil { if err != nil {
return nil, err return nil, err
} }
y := make(map[string]interface{}) y := make(map[string]any)
err = yaml.Unmarshal(b, &y) err = yaml.Unmarshal(b, &y)
return y, err return y, err
} }

@ -50,26 +50,26 @@ func TestValidateChartYamlNotDirectory(t *testing.T) {
err := validateChartYamlNotDirectory(nonExistingChartFilePath) err := validateChartYamlNotDirectory(nonExistingChartFilePath)
if err == nil { if err == nil {
t.Errorf("validateChartYamlNotDirectory to return a linter error, got no error") t.Error("validateChartYamlNotDirectory to return a linter error, got no error")
} }
} }
func TestValidateChartYamlFormat(t *testing.T) { func TestValidateChartYamlFormat(t *testing.T) {
err := validateChartYamlFormat(errors.New("Read error")) err := validateChartYamlFormat(errors.New("Read error"))
if err == nil { if err == nil {
t.Errorf("validateChartYamlFormat to return a linter error, got no error") t.Error("validateChartYamlFormat to return a linter error, got no error")
} }
err = validateChartYamlFormat(nil) err = validateChartYamlFormat(nil)
if err != nil { if err != nil {
t.Errorf("validateChartYamlFormat to return no error, got a linter error") t.Error("validateChartYamlFormat to return no error, got a linter error")
} }
} }
func TestValidateChartName(t *testing.T) { func TestValidateChartName(t *testing.T) {
err := validateChartName(badChart) err := validateChartName(badChart)
if err == nil { if err == nil {
t.Errorf("validateChartName to return a linter error, got no error") t.Error("validateChartName to return a linter error, got no error")
} }
err = validateChartName(badChartName) err = validateChartName(badChartName)
@ -149,7 +149,7 @@ func TestValidateChartMaintainer(t *testing.T) {
badChart.Maintainers = []*chart.Maintainer{nil} badChart.Maintainers = []*chart.Maintainer{nil}
err := validateChartMaintainer(badChart) err := validateChartMaintainer(badChart)
if err == nil { if err == nil {
t.Errorf("validateChartMaintainer did not return error for nil maintainer as expected") t.Error("validateChartMaintainer did not return error for nil maintainer as expected")
} }
if err.Error() != "a maintainer entry is empty" { if err.Error() != "a maintainer entry is empty" {
t.Errorf("validateChartMaintainer returned unexpected error for nil maintainer: %s", err.Error()) t.Errorf("validateChartMaintainer returned unexpected error for nil maintainer: %s", err.Error())
@ -185,7 +185,7 @@ func TestValidateChartIconPresence(t *testing.T) {
err := validateChartIconPresence(testChart) err := validateChartIconPresence(testChart)
if err == nil { if err == nil {
t.Errorf("validateChartIconPresence to return a linter error, got no error") t.Error("validateChartIconPresence to return a linter error, got no error")
} else if !strings.Contains(err.Error(), "icon is recommended") { } else if !strings.Contains(err.Error(), "icon is recommended") {
t.Errorf("expected %q, got %q", "icon is recommended", err.Error()) t.Errorf("expected %q, got %q", "icon is recommended", err.Error())
} }

@ -19,7 +19,6 @@ package rules
import ( import (
"bytes" "bytes"
"errors" "errors"
"fmt"
"io" "io"
"io/fs" "io/fs"
"os" "os"
@ -102,14 +101,14 @@ func validateCrdsDir(crdsPath string) error {
func validateCrdAPIVersion(obj *k8sYamlStruct) error { func validateCrdAPIVersion(obj *k8sYamlStruct) error {
if !strings.HasPrefix(obj.APIVersion, "apiextensions.k8s.io") { if !strings.HasPrefix(obj.APIVersion, "apiextensions.k8s.io") {
return fmt.Errorf("apiVersion is not in 'apiextensions.k8s.io'") return errors.New("apiVersion is not in 'apiextensions.k8s.io'")
} }
return nil return nil
} }
func validateCrdKind(obj *k8sYamlStruct) error { func validateCrdKind(obj *k8sYamlStruct) error {
if obj.Kind != "CustomResourceDefinition" { if obj.Kind != "CustomResourceDefinition" {
return fmt.Errorf("object kind is not 'CustomResourceDefinition'") return errors.New("object kind is not 'CustomResourceDefinition'")
} }
return nil return nil
} }

@ -72,7 +72,7 @@ func TestValidateDependencyInMetadata(t *testing.T) {
c := chartWithBadDependencies() c := chartWithBadDependencies()
if err := validateDependencyInMetadata(&c); err == nil { if err := validateDependencyInMetadata(&c); err == nil {
t.Errorf("chart should have been flagged for missing deps in chart metadata") t.Error("chart should have been flagged for missing deps in chart metadata")
} }
} }
@ -132,7 +132,7 @@ func TestValidateDependenciesUnique(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
if err := validateDependenciesUnique(&tt.chart); err == nil { if err := validateDependenciesUnique(&tt.chart); err == nil {
t.Errorf("chart should have been flagged for dependency shadowing") t.Error("chart should have been flagged for dependency shadowing")
} }
} }
} }

@ -36,6 +36,6 @@ func TestValidateNoDeprecations(t *testing.T) {
APIVersion: "v1", APIVersion: "v1",
Kind: "Pod", Kind: "Pod",
}, nil); err != nil { }, nil); err != nil {
t.Errorf("Expected a v1 Pod to not be deprecated") t.Error("Expected a v1 Pod to not be deprecated")
} }
} }

@ -28,8 +28,8 @@ import (
"slices" "slices"
"strings" "strings"
"k8s.io/apimachinery/pkg/api/validate/content"
"k8s.io/apimachinery/pkg/api/validation" "k8s.io/apimachinery/pkg/api/validation"
apipath "k8s.io/apimachinery/pkg/api/validation/path"
"k8s.io/apimachinery/pkg/util/validation/field" "k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/apimachinery/pkg/util/yaml" "k8s.io/apimachinery/pkg/util/yaml"
@ -42,17 +42,17 @@ import (
) )
// Templates lints the templates in the Linter. // Templates lints the templates in the Linter.
func Templates(linter *support.Linter, values map[string]interface{}, namespace string, _ bool) { func Templates(linter *support.Linter, values map[string]any, namespace string, _ bool) {
TemplatesWithKubeVersion(linter, values, namespace, nil) TemplatesWithKubeVersion(linter, values, namespace, nil)
} }
// TemplatesWithKubeVersion lints the templates in the Linter, allowing to specify the kubernetes version. // 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 *common.KubeVersion) { func TemplatesWithKubeVersion(linter *support.Linter, values map[string]any, namespace string, kubeVersion *common.KubeVersion) {
TemplatesWithSkipSchemaValidation(linter, values, namespace, kubeVersion, false) 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. // 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 *common.KubeVersion, skipSchemaValidation bool) { func TemplatesWithSkipSchemaValidation(linter *support.Linter, values map[string]any, namespace string, kubeVersion *common.KubeVersion, skipSchemaValidation bool) {
fpath := "templates/" fpath := "templates/"
templatesPath := filepath.Join(linter.ChartDir, fpath) templatesPath := filepath.Join(linter.ChartDir, fpath)
@ -292,7 +292,7 @@ func validateMetadataNameFunc(obj *k8sYamlStruct) validation.ValidateNameFunc {
case "role", "clusterrole", "rolebinding", "clusterrolebinding": case "role", "clusterrole", "rolebinding", "clusterrolebinding":
// https://github.com/kubernetes/kubernetes/blob/v1.20.0/pkg/apis/rbac/validation/validation.go#L32-L34 // https://github.com/kubernetes/kubernetes/blob/v1.20.0/pkg/apis/rbac/validation/validation.go#L32-L34
return func(name string, _ bool) []string { return func(name string, _ bool) []string {
return apipath.IsValidPathSegmentName(name) return content.IsPathSegmentName(name)
} }
default: default:
return validation.NameIsDNSSubdomain return validation.NameIsDNSSubdomain

@ -49,7 +49,7 @@ func TestValidateAllowedExtension(t *testing.T) {
} }
} }
var values = map[string]interface{}{"nameOverride": "", "httpPort": 80} var values = map[string]any{"nameOverride": "", "httpPort": 80}
const namespace = "testNamespace" const namespace = "testNamespace"
const strict = false const strict = false
@ -249,7 +249,7 @@ func TestStrictTemplateParsingMapError(t *testing.T) {
APIVersion: "v2", APIVersion: "v2",
Version: "0.1.0", Version: "0.1.0",
}, },
Values: map[string]interface{}{ Values: map[string]any{
"mymap": map[string]string{ "mymap": map[string]string{
"key1": "val1", "key1": "val1",
}, },

@ -17,6 +17,7 @@ limitations under the License.
package rules package rules
import ( import (
"errors"
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
@ -32,7 +33,7 @@ import (
// they are only tested for well-formedness. // they are only tested for well-formedness.
// //
// If additional values are supplied, they are coalesced into the values in values.yaml. // 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) { func ValuesWithOverrides(linter *support.Linter, valueOverrides map[string]any, skipSchemaValidation bool) {
file := "values.yaml" file := "values.yaml"
vf := filepath.Join(linter.ChartDir, file) vf := filepath.Join(linter.ChartDir, file)
fileExists := linter.RunLinterRule(support.InfoSev, file, validateValuesFileExistence(vf)) fileExists := linter.RunLinterRule(support.InfoSev, file, validateValuesFileExistence(vf))
@ -47,12 +48,12 @@ func ValuesWithOverrides(linter *support.Linter, valueOverrides map[string]inter
func validateValuesFileExistence(valuesPath string) error { func validateValuesFileExistence(valuesPath string) error {
_, err := os.Stat(valuesPath) _, err := os.Stat(valuesPath)
if err != nil { if err != nil {
return fmt.Errorf("file does not exist") return errors.New("file does not exist")
} }
return nil return nil
} }
func validateValuesFile(valuesPath string, overrides map[string]interface{}, skipSchemaValidation bool) error { func validateValuesFile(valuesPath string, overrides map[string]any, skipSchemaValidation bool) error {
values, err := common.ReadValuesFile(valuesPath) values, err := common.ReadValuesFile(valuesPath)
if err != nil { if err != nil {
return fmt.Errorf("unable to parse YAML: %w", err) return fmt.Errorf("unable to parse YAML: %w", err)
@ -63,7 +64,7 @@ func validateValuesFile(valuesPath string, overrides map[string]interface{}, ski
// We could change that. For now, though, we retain that strategy, and thus can // 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 // coalesce tables (like reuse-values does) instead of doing the full chart
// CoalesceValues // CoalesceValues
coalescedValues := util.CoalesceTables(make(map[string]interface{}, len(overrides)), overrides) coalescedValues := util.CoalesceTables(make(map[string]any, len(overrides)), overrides)
coalescedValues = util.CoalesceTables(coalescedValues, values) coalescedValues = util.CoalesceTables(coalescedValues, values)
ext := filepath.Ext(valuesPath) ext := filepath.Ext(valuesPath)

@ -57,7 +57,7 @@ func TestValidateValuesYamlNotDirectory(t *testing.T) {
err := validateValuesFileExistence(nonExistingValuesFilePath) err := validateValuesFileExistence(nonExistingValuesFilePath)
if err == nil { if err == nil {
t.Errorf("validateValuesFileExistence to return a linter error, got no error") t.Error("validateValuesFileExistence to return a linter error, got no error")
} }
} }
@ -67,7 +67,7 @@ func TestValidateValuesFileWellFormed(t *testing.T) {
` `
tmpdir := ensure.TempFile(t, "values.yaml", []byte(badYaml)) tmpdir := ensure.TempFile(t, "values.yaml", []byte(badYaml))
valfile := filepath.Join(tmpdir, "values.yaml") valfile := filepath.Join(tmpdir, "values.yaml")
if err := validateValuesFile(valfile, map[string]interface{}{}, false); err == nil { if err := validateValuesFile(valfile, map[string]any{}, false); err == nil {
t.Fatal("expected values file to fail parsing") t.Fatal("expected values file to fail parsing")
} }
} }
@ -78,7 +78,7 @@ func TestValidateValuesFileSchema(t *testing.T) {
createTestingSchema(t, tmpdir) createTestingSchema(t, tmpdir)
valfile := filepath.Join(tmpdir, "values.yaml") valfile := filepath.Join(tmpdir, "values.yaml")
if err := validateValuesFile(valfile, map[string]interface{}{}, false); err != nil { if err := validateValuesFile(valfile, map[string]any{}, false); err != nil {
t.Fatalf("Failed validation with %s", err) t.Fatalf("Failed validation with %s", err)
} }
} }
@ -91,7 +91,7 @@ func TestValidateValuesFileSchemaFailure(t *testing.T) {
valfile := filepath.Join(tmpdir, "values.yaml") valfile := filepath.Join(tmpdir, "values.yaml")
err := validateValuesFile(valfile, map[string]interface{}{}, false) err := validateValuesFile(valfile, map[string]any{}, false)
if err == nil { if err == nil {
t.Fatal("expected values file to fail parsing") t.Fatal("expected values file to fail parsing")
} }
@ -107,7 +107,7 @@ func TestValidateValuesFileSchemaFailureButWithSkipSchemaValidation(t *testing.T
valfile := filepath.Join(tmpdir, "values.yaml") valfile := filepath.Join(tmpdir, "values.yaml")
err := validateValuesFile(valfile, map[string]interface{}{}, true) err := validateValuesFile(valfile, map[string]any{}, true)
if err != nil { if err != nil {
t.Fatal("expected values file to pass parsing because of skipSchemaValidation") t.Fatal("expected values file to pass parsing because of skipSchemaValidation")
} }
@ -115,7 +115,7 @@ func TestValidateValuesFileSchemaFailureButWithSkipSchemaValidation(t *testing.T
func TestValidateValuesFileSchemaOverrides(t *testing.T) { func TestValidateValuesFileSchemaOverrides(t *testing.T) {
yaml := "username: admin" yaml := "username: admin"
overrides := map[string]interface{}{ overrides := map[string]any{
"password": "swordfish", "password": "swordfish",
} }
tmpdir := ensure.TempFile(t, "values.yaml", []byte(yaml)) tmpdir := ensure.TempFile(t, "values.yaml", []byte(yaml))
@ -131,24 +131,24 @@ func TestValidateValuesFile(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
yaml string yaml string
overrides map[string]interface{} overrides map[string]any
errorMessage string errorMessage string
}{ }{
{ {
name: "value added", name: "value added",
yaml: "username: admin", yaml: "username: admin",
overrides: map[string]interface{}{"password": "swordfish"}, overrides: map[string]any{"password": "swordfish"},
}, },
{ {
name: "value not overridden", name: "value not overridden",
yaml: "username: admin\npassword:", yaml: "username: admin\npassword:",
overrides: map[string]interface{}{"username": "anotherUser"}, overrides: map[string]any{"username": "anotherUser"},
errorMessage: "- at '/password': got null, want string", errorMessage: "- at '/password': got null, want string",
}, },
{ {
name: "value overridden", name: "value overridden",
yaml: "username: admin\npassword:", yaml: "username: admin\npassword:",
overrides: map[string]interface{}{"username": "anotherUser", "password": "swordfish"}, overrides: map[string]any{"username": "anotherUser", "password": "swordfish"},
}, },
} }

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

@ -182,11 +182,11 @@ func LoadFiles(files []*archive.BufferedFile) (*chart.Chart, error) {
// //
// The reader is expected to contain one or more YAML documents, the values of which are merged. // The reader is expected to contain one or more YAML documents, the values of which are merged.
// And the values can be either a chart's default values or user-supplied values. // And the values can be either a chart's default values or user-supplied values.
func LoadValues(data io.Reader) (map[string]interface{}, error) { func LoadValues(data io.Reader) (map[string]any, error) {
values := map[string]interface{}{} values := map[string]any{}
reader := utilyaml.NewYAMLReader(bufio.NewReader(data)) reader := utilyaml.NewYAMLReader(bufio.NewReader(data))
for { for {
currentMap := map[string]interface{}{} currentMap := map[string]any{}
raw, err := reader.Read() raw, err := reader.Read()
if err != nil { if err != nil {
if errors.Is(err, io.EOF) { if errors.Is(err, io.EOF) {
@ -204,13 +204,13 @@ func LoadValues(data io.Reader) (map[string]interface{}, error) {
// MergeMaps merges two maps. If a key exists in both maps, the value from b will be used. // 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. // If the value is a map, the maps will be merged recursively.
func MergeMaps(a, b map[string]interface{}) map[string]interface{} { func MergeMaps(a, b map[string]any) map[string]any {
out := make(map[string]interface{}, len(a)) out := make(map[string]any, len(a))
maps.Copy(out, a) maps.Copy(out, a)
for k, v := range b { for k, v := range b {
if v, ok := v.(map[string]interface{}); ok { if v, ok := v.(map[string]any); ok {
if bv, ok := out[k]; ok { if bv, ok := out[k]; ok {
if bv, ok := bv.(map[string]interface{}); ok { if bv, ok := bv.(map[string]any); ok {
out[k] = MergeMaps(bv, v) out[k] = MergeMaps(bv, v)
continue continue
} }

@ -61,7 +61,7 @@ func TestLoadDirWithDevNull(t *testing.T) {
t.Fatalf("Failed to load testdata: %s", err) t.Fatalf("Failed to load testdata: %s", err)
} }
if _, err := l.Load(); err == nil { if _, err := l.Load(); err == nil {
t.Errorf("packages with an irregular file (/dev/null) should not load") t.Error("packages with an irregular file (/dev/null) should not load")
} }
} }
@ -455,7 +455,7 @@ func TestLoadInvalidArchive(t *testing.T) {
func TestLoadValues(t *testing.T) { func TestLoadValues(t *testing.T) {
testCases := map[string]struct { testCases := map[string]struct {
data []byte data []byte
expctedValues map[string]interface{} expctedValues map[string]any
}{ }{
"It should load values correctly": { "It should load values correctly": {
data: []byte(` data: []byte(`
@ -464,11 +464,11 @@ foo:
bar: bar:
version: v2 version: v2
`), `),
expctedValues: map[string]interface{}{ expctedValues: map[string]any{
"foo": map[string]interface{}{ "foo": map[string]any{
"image": "foo:v1", "image": "foo:v1",
}, },
"bar": map[string]interface{}{ "bar": map[string]any{
"version": "v2", "version": "v2",
}, },
}, },
@ -483,11 +483,11 @@ bar:
foo: foo:
image: foo:v2 image: foo:v2
`), `),
expctedValues: map[string]interface{}{ expctedValues: map[string]any{
"foo": map[string]interface{}{ "foo": map[string]any{
"image": "foo:v2", "image": "foo:v2",
}, },
"bar": map[string]interface{}{ "bar": map[string]any{
"version": "v2", "version": "v2",
}, },
}, },
@ -507,24 +507,24 @@ foo:
} }
func TestMergeValuesV3(t *testing.T) { func TestMergeValuesV3(t *testing.T) {
nestedMap := map[string]interface{}{ nestedMap := map[string]any{
"foo": "bar", "foo": "bar",
"baz": map[string]string{ "baz": map[string]string{
"cool": "stuff", "cool": "stuff",
}, },
} }
anotherNestedMap := map[string]interface{}{ anotherNestedMap := map[string]any{
"foo": "bar", "foo": "bar",
"baz": map[string]string{ "baz": map[string]string{
"cool": "things", "cool": "things",
"awesome": "stuff", "awesome": "stuff",
}, },
} }
flatMap := map[string]interface{}{ flatMap := map[string]any{
"foo": "bar", "foo": "bar",
"baz": "stuff", "baz": "stuff",
} }
anotherFlatMap := map[string]interface{}{ anotherFlatMap := map[string]any{
"testing": "fun", "testing": "fun",
} }
@ -547,7 +547,7 @@ func TestMergeValuesV3(t *testing.T) {
} }
testMap = MergeMaps(anotherFlatMap, anotherNestedMap) testMap = MergeMaps(anotherFlatMap, anotherNestedMap)
expectedMap := map[string]interface{}{ expectedMap := map[string]any{
"testing": "fun", "testing": "fun",
"foo": "bar", "foo": "bar",
"baz": map[string]string{ "baz": map[string]string{

@ -112,6 +112,9 @@ func (md *Metadata) Validate() error {
return ValidationError("chart.metadata.name is required") return ValidationError("chart.metadata.name is required")
} }
if md.Name == "." || md.Name == ".." {
return ValidationErrorf("chart.metadata.name %q is not allowed", md.Name)
}
if md.Name != filepath.Base(md.Name) { if md.Name != filepath.Base(md.Name) {
return ValidationErrorf("chart.metadata.name %q is invalid", md.Name) return ValidationErrorf("chart.metadata.name %q is invalid", md.Name)
} }

@ -16,6 +16,7 @@ limitations under the License.
package v3 package v3
import ( import (
"errors"
"testing" "testing"
) )
@ -40,6 +41,16 @@ func TestValidate(t *testing.T) {
&Metadata{APIVersion: "v3", Version: "1.0"}, &Metadata{APIVersion: "v3", Version: "1.0"},
ValidationError("chart.metadata.name is required"), ValidationError("chart.metadata.name is required"),
}, },
{
"chart with dot name",
&Metadata{Name: ".", APIVersion: "v3", Version: "1.0"},
ValidationError("chart.metadata.name \".\" is not allowed"),
},
{
"chart with dotdot name",
&Metadata{Name: "..", APIVersion: "v3", Version: "1.0"},
ValidationError("chart.metadata.name \"..\" is not allowed"),
},
{ {
"chart without name", "chart without name",
&Metadata{Name: "../../test", APIVersion: "v3", Version: "1.0"}, &Metadata{Name: "../../test", APIVersion: "v3", Version: "1.0"},
@ -181,7 +192,7 @@ func TestValidate(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
result := tt.md.Validate() result := tt.md.Validate()
if result != tt.err { if !errors.Is(result, tt.err) {
t.Errorf("expected %q, got %q in test %q", tt.err, result, tt.name) t.Errorf("expected %q, got %q in test %q", tt.err, result, tt.name)
} }
} }

@ -35,7 +35,7 @@ func TestLoadChartfile(t *testing.T) {
func verifyChartfile(t *testing.T, f *chart.Metadata, name string) { func verifyChartfile(t *testing.T, f *chart.Metadata, name string) {
t.Helper() t.Helper()
if f == nil { //nolint:staticcheck if f == nil {
t.Fatal("Failed verifyChartfile because f is nil") t.Fatal("Failed verifyChartfile because f is nil")
} }
@ -56,15 +56,15 @@ func verifyChartfile(t *testing.T, f *chart.Metadata, name string) {
} }
if f.Maintainers[0].Name != "The Helm Team" { if f.Maintainers[0].Name != "The Helm Team" {
t.Errorf("Unexpected maintainer name.") t.Error("Unexpected maintainer name.")
} }
if f.Maintainers[1].Email != "nobody@example.com" { if f.Maintainers[1].Email != "nobody@example.com" {
t.Errorf("Unexpected maintainer email.") t.Error("Unexpected maintainer email.")
} }
if len(f.Sources) != 1 { if len(f.Sources) != 1 {
t.Fatalf("Unexpected number of sources") t.Fatal("Unexpected number of sources")
} }
if f.Sources[0] != "https://example.com/foo/bar" { if f.Sources[0] != "https://example.com/foo/bar" {
@ -84,7 +84,7 @@ func verifyChartfile(t *testing.T, f *chart.Metadata, name string) {
} }
if len(f.Annotations) != 2 { if len(f.Annotations) != 2 {
t.Fatalf("Unexpected annotations") t.Fatal("Unexpected annotations")
} }
if want, got := "extravalue", f.Annotations["extrakey"]; want != got { if want, got := "extravalue", f.Annotations["extrakey"]; want != got {
@ -111,7 +111,7 @@ func TestIsChartDir(t *testing.T) {
} }
validChartDir, err = IsChartDir("testdata") validChartDir, err = IsChartDir("testdata")
if validChartDir || err == nil { if validChartDir || err == nil {
t.Errorf("expected error but did not get any") t.Error("expected error but did not get any")
return return
} }
} }

@ -670,7 +670,7 @@ func CreateFrom(chartfile *chart.Metadata, dest, src string) error {
return fmt.Errorf("reading values file: %w", err) return fmt.Errorf("reading values file: %w", err)
} }
var m map[string]interface{} var m map[string]any
if err := yaml.Unmarshal(transform(string(b), schart.Name()), &m); err != nil { if err := yaml.Unmarshal(transform(string(b), schart.Name()), &m); err != nil {
return fmt.Errorf("transforming values file: %w", err) return fmt.Errorf("transforming values file: %w", err)
} }

@ -140,7 +140,7 @@ func TestCreate_Overwrite(t *testing.T) {
} }
if errlog.Len() == 0 { if errlog.Len() == 0 {
t.Errorf("Expected warnings about overwriting files.") t.Error("Expected warnings about overwriting files.")
} }
} }

@ -16,6 +16,7 @@ limitations under the License.
package util package util
import ( import (
"errors"
"fmt" "fmt"
"log/slog" "log/slog"
"strings" "strings"
@ -44,6 +45,7 @@ func processDependencyConditions(reqs []*chart.Dependency, cvals common.Values,
if len(c) > 0 { if len(c) > 0 {
// retrieve value // retrieve value
vv, err := cvals.PathValue(cpath + c) vv, err := cvals.PathValue(cpath + c)
var errNoValue common.ErrNoValue
if err == nil { if err == nil {
// if not bool, warn // if not bool, warn
if bv, ok := vv.(bool); ok { if bv, ok := vv.(bool); ok {
@ -51,7 +53,7 @@ func processDependencyConditions(reqs []*chart.Dependency, cvals common.Values,
break break
} }
slog.Warn("returned non-bool value", "path", c, "chart", r.Name) slog.Warn("returned non-bool value", "path", c, "chart", r.Name)
} else if _, ok := err.(common.ErrNoValue); !ok { } else if errors.As(err, &errNoValue) {
// this is a real error // this is a real error
slog.Warn("the method PathValue returned error", slog.Any("error", err)) slog.Warn("the method PathValue returned error", slog.Any("error", err))
} }
@ -140,7 +142,7 @@ func copyMetadata(metadata *chart.Metadata) *chart.Metadata {
} }
// processDependencyEnabled removes disabled charts from dependencies // processDependencyEnabled removes disabled charts from dependencies
func processDependencyEnabled(c *chart.Chart, v map[string]interface{}, path string) error { func processDependencyEnabled(c *chart.Chart, v map[string]any, path string) error {
if c.Metadata.Dependencies == nil { if c.Metadata.Dependencies == nil {
return nil return nil
} }
@ -226,7 +228,7 @@ Loop:
} }
// pathToMap creates a nested map given a YAML path in dot notation. // pathToMap creates a nested map given a YAML path in dot notation.
func pathToMap(path string, data map[string]interface{}) map[string]interface{} { func pathToMap(path string, data map[string]any) map[string]any {
if path == "." { if path == "." {
return data return data
} }
@ -235,13 +237,13 @@ func pathToMap(path string, data map[string]interface{}) map[string]interface{}
func parsePath(key string) []string { return strings.Split(key, ".") } func parsePath(key string) []string { return strings.Split(key, ".") }
func set(path []string, data map[string]interface{}) map[string]interface{} { func set(path []string, data map[string]any) map[string]any {
if len(path) == 0 { if len(path) == 0 {
return nil return nil
} }
cur := data cur := data
for i := len(path) - 1; i >= 0; i-- { for i := len(path) - 1; i >= 0; i-- {
cur = map[string]interface{}{path[i]: cur} cur = map[string]any{path[i]: cur}
} }
return cur return cur
} }
@ -262,13 +264,13 @@ func processImportValues(c *chart.Chart, merge bool) error {
if err != nil { if err != nil {
return err return err
} }
b := make(map[string]interface{}) b := make(map[string]any)
// import values from each dependency if specified in import-values // import values from each dependency if specified in import-values
for _, r := range c.Metadata.Dependencies { for _, r := range c.Metadata.Dependencies {
var outiv []interface{} var outiv []any
for _, riv := range r.ImportValues { for _, riv := range r.ImportValues {
switch iv := riv.(type) { switch iv := riv.(type) {
case map[string]interface{}: case map[string]any:
child := fmt.Sprintf("%v", iv["child"]) child := fmt.Sprintf("%v", iv["child"])
parent := fmt.Sprintf("%v", iv["parent"]) parent := fmt.Sprintf("%v", iv["parent"])
@ -336,27 +338,27 @@ func processImportValues(c *chart.Chart, merge bool) error {
return nil return nil
} }
func deepCopyMap(vals map[string]interface{}) map[string]interface{} { func deepCopyMap(vals map[string]any) map[string]any {
valsCopy, err := copystructure.Copy(vals) valsCopy, err := copystructure.Copy(vals)
if err != nil { if err != nil {
return vals return vals
} }
return valsCopy.(map[string]interface{}) return valsCopy.(map[string]any)
} }
func trimNilValues(vals map[string]interface{}) map[string]interface{} { func trimNilValues(vals map[string]any) map[string]any {
valsCopy, err := copystructure.Copy(vals) valsCopy, err := copystructure.Copy(vals)
if err != nil { if err != nil {
return vals return vals
} }
valsCopyMap := valsCopy.(map[string]interface{}) valsCopyMap := valsCopy.(map[string]any)
for key, val := range valsCopyMap { for key, val := range valsCopyMap {
if val == nil { if val == nil {
// Iterate over the values and remove nil keys // Iterate over the values and remove nil keys
delete(valsCopyMap, key) delete(valsCopyMap, key)
} else if istable(val) { } else if istable(val) {
// Recursively call into ourselves to remove keys from inner tables // Recursively call into ourselves to remove keys from inner tables
valsCopyMap[key] = trimNilValues(val.(map[string]interface{})) valsCopyMap[key] = trimNilValues(val.(map[string]any))
} }
} }
@ -364,8 +366,8 @@ func trimNilValues(vals map[string]interface{}) map[string]interface{} {
} }
// istable is a special-purpose function to see if the present thing matches the definition of a YAML table. // istable is a special-purpose function to see if the present thing matches the definition of a YAML table.
func istable(v interface{}) bool { func istable(v any) bool {
_, ok := v.(map[string]interface{}) _, ok := v.(map[string]any)
return ok return ok
} }

@ -63,7 +63,7 @@ func TestLoadDependency(t *testing.T) {
} }
func TestDependencyEnabled(t *testing.T) { func TestDependencyEnabled(t *testing.T) {
type M = map[string]interface{} type M = map[string]any
tests := []struct { tests := []struct {
name string name string
v M v M
@ -385,7 +385,7 @@ func TestGetAliasDependency(t *testing.T) {
req := c.Metadata.Dependencies req := c.Metadata.Dependencies
if len(req) == 0 { if len(req) == 0 {
t.Fatalf("there are no dependencies to test") t.Fatal("there are no dependencies to test")
} }
// Success case // Success case
@ -403,7 +403,7 @@ func TestGetAliasDependency(t *testing.T) {
if req[0].Version != "" { if req[0].Version != "" {
if !IsCompatibleRange(req[0].Version, aliasChart.Metadata.Version) { if !IsCompatibleRange(req[0].Version, aliasChart.Metadata.Version) {
t.Fatalf("dependency chart version is not in the compatible range") t.Fatal("dependency chart version is not in the compatible range")
} }
} }
@ -415,7 +415,7 @@ func TestGetAliasDependency(t *testing.T) {
req[0].Version = "something else which is not in the compatible range" req[0].Version = "something else which is not in the compatible range"
if IsCompatibleRange(req[0].Version, aliasChart.Metadata.Version) { if IsCompatibleRange(req[0].Version, aliasChart.Metadata.Version) {
t.Fatalf("dependency chart version which is not in the compatible range should cause a failure other than a success ") t.Fatal("dependency chart version outside the compatible range should not be considered compatible")
} }
} }

@ -42,4 +42,4 @@ into a Chart.
When creating charts in memory, use the 'helm.sh/helm/pkg/chart' When creating charts in memory, use the 'helm.sh/helm/pkg/chart'
package directly. package directly.
*/ */
package util // import chartutil "helm.sh/helm/v4/internal/chart/v3/util" package util // import "helm.sh/helm/v4/internal/chart/v3/util"

@ -52,6 +52,17 @@ func Expand(dir string, r io.Reader) error {
return errors.New("chart name not specified") return errors.New("chart name not specified")
} }
// Reject chart names that are POSIX path dot-segments or dot-dot segments or contain path separators.
// A dot-segment name (e.g. ".") causes SecureJoin to resolve to the root
// directory and extraction then to write files directly into that extraction root
// instead of a per-chart subdirectory.
if chartName == "." || chartName == ".." {
return fmt.Errorf("chart name %q is not allowed", chartName)
}
if chartName != filepath.Base(chartName) {
return fmt.Errorf("chart name %q must not contain path separators", chartName)
}
// Find the base directory // Find the base directory
// The directory needs to be cleaned prior to passing to SecureJoin or the location may end up // The directory needs to be cleaned prior to passing to SecureJoin or the location may end up
// being wrong or returning an error. This was introduced in v0.4.0. // being wrong or returning an error. This was introduced in v0.4.0.
@ -61,6 +72,12 @@ func Expand(dir string, r io.Reader) error {
return err return err
} }
// Defense-in-depth: the chart directory must be a subdirectory of dir,
// never dir itself.
if chartdir == dir {
return fmt.Errorf("chart name %q resolves to the extraction root", chartName)
}
// Copy all files verbatim. We don't parse these files because parsing can remove // Copy all files verbatim. We don't parse these files because parsing can remove
// comments. // comments.
for _, file := range files { for _, file := range files {

@ -17,11 +17,73 @@ limitations under the License.
package util package util
import ( import (
"archive/tar"
"bytes"
"compress/gzip"
"io/fs"
"os" "os"
"path/filepath" "path/filepath"
"testing" "testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
// makeTestChartArchive builds a gzipped tar archive from the given sourceDir directory, file entries are prefixed with the given chartName
func makeTestChartArchive(t *testing.T, chartName, sourceDir string) *bytes.Buffer {
t.Helper()
var result bytes.Buffer
gw := gzip.NewWriter(&result)
tw := tar.NewWriter(gw)
dir := os.DirFS(sourceDir)
writeFile := func(relPath string) {
t.Helper()
f, err := dir.Open(relPath)
require.NoError(t, err)
fStat, err := f.Stat()
require.NoError(t, err)
err = tw.WriteHeader(&tar.Header{
Name: filepath.Join(chartName, relPath),
Mode: int64(fStat.Mode()),
Size: fStat.Size(),
})
require.NoError(t, err)
data, err := fs.ReadFile(dir, relPath)
require.NoError(t, err)
tw.Write(data)
}
err := fs.WalkDir(dir, ".", func(path string, d os.DirEntry, walkErr error) error {
if walkErr != nil {
return walkErr
}
if d.IsDir() {
return nil
}
writeFile(path)
return nil
})
if err != nil {
t.Fatal(err)
}
err = tw.Close()
require.NoError(t, err)
err = gw.Close()
require.NoError(t, err)
return &result
}
func TestExpand(t *testing.T) { func TestExpand(t *testing.T) {
dest := t.TempDir() dest := t.TempDir()
@ -75,6 +137,28 @@ func TestExpand(t *testing.T) {
} }
} }
func TestExpandError(t *testing.T) {
tests := map[string]struct {
chartName string
chartDir string
wantErr string
}{
"dot name": {"dotname", "testdata/dotname", "not allowed"},
"dotdot name": {"dotdotname", "testdata/dotdotname", "not allowed"},
"slash in name": {"slashinname", "testdata/slashinname", "must not contain path separators"},
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
archive := makeTestChartArchive(t, tt.chartName, tt.chartDir)
dest := t.TempDir()
err := Expand(dest, archive)
assert.ErrorContains(t, err, tt.wantErr)
})
}
}
func TestExpandFile(t *testing.T) { func TestExpandFile(t *testing.T) {
dest := t.TempDir() dest := t.TempDir()

@ -21,8 +21,8 @@ import (
"bytes" "bytes"
"compress/gzip" "compress/gzip"
"crypto/sha256" "crypto/sha256"
"encoding/hex"
"errors" "errors"
"fmt"
"io" "io"
"os" "os"
"path" "path"
@ -87,7 +87,7 @@ func TestSave(t *testing.T) {
t.Fatalf("Schema data did not match.\nExpected:\n%s\nActual:\n%s", formattedExpected, formattedActual) t.Fatalf("Schema data did not match.\nExpected:\n%s\nActual:\n%s", formattedExpected, formattedActual)
} }
if _, err := Save(&chartWithInvalidJSON, dest); err == nil { if _, err := Save(&chartWithInvalidJSON, dest); err == nil {
t.Fatalf("Invalid JSON was not caught while saving chart") t.Fatal("Invalid JSON was not caught while saving chart")
} }
c.Metadata.APIVersion = chart.APIVersionV3 c.Metadata.APIVersion = chart.APIVersionV3
@ -153,7 +153,7 @@ func TestSavePreservesTimestamps(t *testing.T) {
Version: "1.2.3", Version: "1.2.3",
}, },
ModTime: initialCreateTime, ModTime: initialCreateTime,
Values: map[string]interface{}{ Values: map[string]any{
"imageName": "testimage", "imageName": "testimage",
"imageId": 42, "imageId": 42,
}, },
@ -353,5 +353,5 @@ func sha256Sum(filePath string) (string, error) {
return "", err return "", err
} }
return fmt.Sprintf("%x", h.Sum(nil)), nil return hex.EncodeToString(h.Sum(nil)), nil
} }

@ -0,0 +1,4 @@
apiVersion: v3
name: ..
description: A Helm chart for Kubernetes
version: 0.1.0

@ -0,0 +1,4 @@
apiVersion: v3
name: .
description: A Helm chart for Kubernetes
version: 0.1.0

@ -0,0 +1,4 @@
apiVersion: v3
name: a/../b
description: A Helm chart for Kubernetes
version: 0.1.0

@ -89,7 +89,15 @@ func copyValue(original reflect.Value) (any, error) {
} }
copied := reflect.MakeSlice(original.Type(), original.Len(), original.Cap()) copied := reflect.MakeSlice(original.Type(), original.Len(), original.Cap())
for i := 0; i < original.Len(); i++ { for i := 0; i < original.Len(); i++ {
val, err := copyValue(original.Index(i)) elem := original.Index(i)
// Handle nil values in slices (e.g., interface{} elements that are nil)
if elem.Kind() == reflect.Interface && elem.IsNil() {
copied.Index(i).Set(elem)
continue
}
val, err := copyValue(elem)
if err != nil { if err != nil {
return nil, err return nil, err
} }

@ -113,6 +113,21 @@ func TestCopy_Slice(t *testing.T) {
input[0]["key1"] = "modified" input[0]["key1"] = "modified"
assert.Equal(t, "value1", resultSlice[0]["key1"]) assert.Equal(t, "value1", resultSlice[0]["key1"])
}) })
t.Run("slice with nil elements", func(t *testing.T) {
input := []any{
"value1",
nil,
"value2",
}
result, err := Copy(input)
require.NoError(t, err)
resultSlice, ok := result.([]any)
require.True(t, ok)
assert.Equal(t, input, resultSlice)
assert.Nil(t, resultSlice[1])
})
} }
func TestCopy_Map(t *testing.T) { func TestCopy_Map(t *testing.T) {

@ -119,3 +119,29 @@ func TestAtomicWriteFile_LargeContent(t *testing.T) {
t.Fatalf("expected large content to match, got different length: %d vs %d", len(largeContent), len(got)) t.Fatalf("expected large content to match, got different length: %d vs %d", len(largeContent), len(got))
} }
} }
// TestPlatformAtomicWriteFile_OverwritesExisting verifies that the platform
// helper replaces existing files instead of silently skipping them.
func TestPlatformAtomicWriteFile_OverwritesExisting(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "overwrite_test")
first := bytes.NewReader([]byte("first"))
if err := PlatformAtomicWriteFile(path, first, 0644); err != nil {
t.Fatalf("first write failed: %v", err)
}
second := bytes.NewReader([]byte("second"))
if err := PlatformAtomicWriteFile(path, second, 0644); err != nil {
t.Fatalf("second write failed: %v", err)
}
contents, err := os.ReadFile(path)
if err != nil {
t.Fatalf("failed reading result: %v", err)
}
if string(contents) != "second" {
t.Fatalf("expected file to be overwritten, got %q", string(contents))
}
}

@ -0,0 +1,32 @@
//go:build !windows
/*
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 fileutil
import (
"io"
"os"
)
// PlatformAtomicWriteFile atomically writes a file to disk.
//
// On non-Windows platforms we don't need extra coordination, so this simply
// delegates to AtomicWriteFile to preserve the existing overwrite behaviour.
func PlatformAtomicWriteFile(filename string, reader io.Reader, mode os.FileMode) error {
return AtomicWriteFile(filename, reader, mode)
}

@ -0,0 +1,54 @@
//go:build windows
/*
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 fileutil
import (
"io"
"os"
"github.com/gofrs/flock"
)
// PlatformAtomicWriteFile atomically writes a file to disk with file locking to
// prevent concurrent writes. This is particularly useful on Windows where
// concurrent writes to the same file can cause "Access Denied" errors.
//
// The function acquires a lock on the target file and performs an atomic write,
// preserving the existing behaviour of overwriting any previous content once
// the lock is obtained.
func PlatformAtomicWriteFile(filename string, reader io.Reader, mode os.FileMode) error {
// Use a separate lock file to coordinate access between processes
// We cannot lock the target file directly as it would prevent the atomic rename
lockFileName := filename + ".lock"
fileLock := flock.New(lockFileName)
// Lock() ensures serialized access - if another process is writing, this will wait
if err := fileLock.Lock(); err != nil {
return err
}
defer func() {
fileLock.Unlock()
// Clean up the lock file
// Ignore errors as the file might not exist or be in use by another process
os.Remove(lockFileName)
}()
// Perform the atomic write while holding the lock
return AtomicWriteFile(filename, reader, mode)
}

@ -0,0 +1,19 @@
/*
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 gates contains internal feature gates that can be used to enable or disable experimental features.
// This is a separate internal package instead of using the pkg/gates package to avoid circular dependencies.
package gates

@ -0,0 +1,21 @@
/*
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 gates
import "helm.sh/helm/v4/pkg/gates"
// ChartV3 is the feature gate for chart API version v3.
const ChartV3 gates.Gate = "HELM_EXPERIMENTAL_CHART_V3"

@ -17,6 +17,7 @@ package installer // import "helm.sh/helm/v4/internal/plugin/installer"
import ( import (
"bytes" "bytes"
"errors"
"fmt" "fmt"
"log/slog" "log/slog"
"os" "os"
@ -143,7 +144,7 @@ func (i *HTTPInstaller) Install() error {
// Update updates a local repository // Update updates a local repository
// Not implemented for now since tarball most likely will be packaged by version // Not implemented for now since tarball most likely will be packaged by version
func (i *HTTPInstaller) Update() error { func (i *HTTPInstaller) Update() error {
return fmt.Errorf("method Update() not implemented for HttpInstaller") return errors.New("method Update() not implemented for HttpInstaller")
} }
// Path is overridden because we want to join on the plugin name not the file name // Path is overridden because we want to join on the plugin name not the file name
@ -163,7 +164,7 @@ func (i *HTTPInstaller) SupportsVerification() bool {
// GetVerificationData returns cached plugin and provenance data for verification // GetVerificationData returns cached plugin and provenance data for verification
func (i *HTTPInstaller) GetVerificationData() (archiveData, provData []byte, filename string, err error) { func (i *HTTPInstaller) GetVerificationData() (archiveData, provData []byte, filename string, err error) {
if !i.SupportsVerification() { if !i.SupportsVerification() {
return nil, nil, "", fmt.Errorf("verification not supported for this source") return nil, nil, "", errors.New("verification not supported for this source")
} }
// Download plugin data once and cache it // Download plugin data once and cache it

@ -53,16 +53,16 @@ var fakePluginB64 = "H4sIAAAAAAAAA+3SQUvDMBgG4Jz7K0LwapdvSxrwJig6mCKC5xHabBaXdDS
func TestStripName(t *testing.T) { func TestStripName(t *testing.T) {
if stripPluginName("fake-plugin-0.0.1.tar.gz") != "fake-plugin" { if stripPluginName("fake-plugin-0.0.1.tar.gz") != "fake-plugin" {
t.Errorf("name does not match expected value") t.Error("name does not match expected value")
} }
if stripPluginName("fake-plugin-0.0.1.tgz") != "fake-plugin" { if stripPluginName("fake-plugin-0.0.1.tgz") != "fake-plugin" {
t.Errorf("name does not match expected value") t.Error("name does not match expected value")
} }
if stripPluginName("fake-plugin.tgz") != "fake-plugin" { if stripPluginName("fake-plugin.tgz") != "fake-plugin" {
t.Errorf("name does not match expected value") t.Error("name does not match expected value")
} }
if stripPluginName("fake-plugin.tar.gz") != "fake-plugin" { if stripPluginName("fake-plugin.tar.gz") != "fake-plugin" {
t.Errorf("name does not match expected value") t.Error("name does not match expected value")
} }
} }
@ -150,7 +150,7 @@ func TestHTTPInstallerNonExistentVersion(t *testing.T) {
// inject fake http client responding with error // inject fake http client responding with error
httpInstaller.getter = &TestHTTPGetter{ httpInstaller.getter = &TestHTTPGetter{
MockError: fmt.Errorf("failed to download plugin for some reason"), MockError: errors.New("failed to download plugin for some reason"),
} }
// attempt to install the plugin // attempt to install the plugin
@ -345,7 +345,7 @@ func TestMediaTypeToExtension(t *testing.T) {
t.Errorf("Media type %q failed test", mt) t.Errorf("Media type %q failed test", mt)
} }
if shouldPass && ext == "" { if shouldPass && ext == "" {
t.Errorf("Expected an extension but got empty string") t.Error("Expected an extension but got empty string")
} }
if !shouldPass && len(ext) != 0 { if !shouldPass && len(ext) != 0 {
t.Error("Expected extension to be empty for unrecognized type") t.Error("Expected extension to be empty for unrecognized type")

@ -87,7 +87,7 @@ func InstallWithOptions(i Installer, opts Options) (*VerificationResult, error)
if opts.Verify { if opts.Verify {
verifier, ok := i.(Verifier) verifier, ok := i.(Verifier)
if !ok || !verifier.SupportsVerification() { if !ok || !verifier.SupportsVerification() {
return nil, fmt.Errorf("--verify is only supported for plugin tarballs (.tgz files)") return nil, errors.New("--verify is only supported for plugin tarballs (.tgz files)")
} }
// Get verification data (works for both memory and file-based installers) // Get verification data (works for both memory and file-based installers)
@ -98,9 +98,9 @@ func InstallWithOptions(i Installer, opts Options) (*VerificationResult, error)
// Check if provenance data exists // Check if provenance data exists
if len(provData) == 0 { if len(provData) == 0 {
// No .prov file found - emit warning but continue installation return nil, errors.New("plugin verification failed: no provenance file (.prov) found")
fmt.Fprintf(os.Stderr, "WARNING: No provenance file found for plugin. Plugin is not signed and cannot be verified.\n") }
} else {
// Provenance data exists - verify the plugin // Provenance data exists - verify the plugin
verification, err := plugin.VerifyPlugin(archiveData, provData, filename, opts.Keyring) verification, err := plugin.VerifyPlugin(archiveData, provData, filename, opts.Keyring)
if err != nil { if err != nil {
@ -117,7 +117,6 @@ func InstallWithOptions(i Installer, opts Options) (*VerificationResult, error)
result.SignedBy = append(result.SignedBy, name) result.SignedBy = append(result.SignedBy, name)
} }
} }
}
if err := i.Install(); err != nil { if err := i.Install(); err != nil {
return nil, err return nil, err
@ -137,7 +136,7 @@ func Update(i Installer) error {
// NewForSource determines the correct Installer for the given source. // NewForSource determines the correct Installer for the given source.
func NewForSource(source, version string) (installer Installer, err error) { func NewForSource(source, version string) (installer Installer, err error) {
if strings.HasPrefix(source, fmt.Sprintf("%s://", registry.OCIScheme)) { if strings.HasPrefix(source, registry.OCIScheme+"://") {
// Source is an OCI registry reference // Source is an OCI registry reference
installer, err = NewOCIInstaller(source) installer, err = NewOCIInstaller(source)
} else if isLocalReference(source) { } else if isLocalReference(source) {

@ -23,18 +23,18 @@ func TestIsRemoteHTTPArchive(t *testing.T) {
source := srv.URL + "/plugins/fake-plugin-0.0.1.tar.gz" source := srv.URL + "/plugins/fake-plugin-0.0.1.tar.gz"
if isRemoteHTTPArchive("/not/a/URL") { if isRemoteHTTPArchive("/not/a/URL") {
t.Errorf("Expected non-URL to return false") t.Error("Expected non-URL to return false")
} }
// URLs with valid archive extensions are considered valid archives // URLs with valid archive extensions are considered valid archives
// even if the server is unreachable (optimization to avoid unnecessary HTTP requests) // even if the server is unreachable (optimization to avoid unnecessary HTTP requests)
if !isRemoteHTTPArchive("https://127.0.0.1:123/fake/plugin-1.2.3.tgz") { if !isRemoteHTTPArchive("https://127.0.0.1:123/fake/plugin-1.2.3.tgz") {
t.Errorf("URL with .tgz extension should be considered a valid archive") t.Error("URL with .tgz extension should be considered a valid archive")
} }
// Test with invalid extension and unreachable server // Test with invalid extension and unreachable server
if isRemoteHTTPArchive("https://127.0.0.1:123/fake/plugin-1.2.3.notanarchive") { if isRemoteHTTPArchive("https://127.0.0.1:123/fake/plugin-1.2.3.notanarchive") {
t.Errorf("Bad URL without valid extension should not succeed") t.Error("Bad URL without valid extension should not succeed")
} }
if !isRemoteHTTPArchive(source) { if !isRemoteHTTPArchive(source) {

@ -188,7 +188,7 @@ func (i *LocalInstaller) SupportsVerification() bool {
// GetVerificationData loads plugin and provenance data from local files for verification // GetVerificationData loads plugin and provenance data from local files for verification
func (i *LocalInstaller) GetVerificationData() (archiveData, provData []byte, filename string, err error) { func (i *LocalInstaller) GetVerificationData() (archiveData, provData []byte, filename string, err error) {
if !i.SupportsVerification() { if !i.SupportsVerification() {
return nil, nil, "", fmt.Errorf("verification not supported for directories") return nil, nil, "", errors.New("verification not supported for directories")
} }
// Read and cache the plugin archive file // Read and cache the plugin archive file

@ -19,6 +19,7 @@ import (
"archive/tar" "archive/tar"
"bytes" "bytes"
"compress/gzip" "compress/gzip"
"errors"
"os" "os"
"path/filepath" "path/filepath"
"testing" "testing"
@ -64,7 +65,7 @@ func TestLocalInstallerNotAFolder(t *testing.T) {
if err == nil { if err == nil {
t.Fatal("expected error") t.Fatal("expected error")
} }
if err != ErrPluginNotADirectory { if !errors.Is(err, ErrPluginNotADirectory) {
t.Fatalf("expected error to equal: %q", err) t.Fatalf("expected error to equal: %q", err)
} }
} }

@ -130,7 +130,7 @@ func (i *OCIInstaller) Install() error {
// Check if this is a gzip compressed file // Check if this is a gzip compressed file
if len(i.pluginData) < 2 || i.pluginData[0] != 0x1f || i.pluginData[1] != 0x8b { if len(i.pluginData) < 2 || i.pluginData[0] != 0x1f || i.pluginData[1] != 0x8b {
return fmt.Errorf("plugin data is not a gzip compressed archive") return errors.New("plugin data is not a gzip compressed archive")
} }
// Create cache directory // Create cache directory

@ -82,7 +82,7 @@ command: "$HELM_PLUGIN_DIR/bin/%s"
// Add executable // Add executable
execContent := fmt.Sprintf("#!/bin/sh\necho '%s test plugin'", pluginName) execContent := fmt.Sprintf("#!/bin/sh\necho '%s test plugin'", pluginName)
execHeader := &tar.Header{ execHeader := &tar.Header{
Name: fmt.Sprintf("bin/%s", pluginName), Name: "bin/" + pluginName,
Mode: 0755, Mode: 0755,
Size: int64(len(execContent)), Size: int64(len(execContent)),
Typeflag: tar.TypeReg, Typeflag: tar.TypeReg,
@ -239,7 +239,7 @@ func TestNewOCIInstaller(t *testing.T) {
if tt.expectError { if tt.expectError {
if err == nil { if err == nil {
t.Errorf("expected error but got none") t.Error("expected error but got none")
} }
return return
} }
@ -416,7 +416,7 @@ func TestOCIInstaller_Install_WithGetterOptions(t *testing.T) {
err = Install(installer) err = Install(installer)
if tc.wantErr { if tc.wantErr {
if err == nil { if err == nil {
t.Errorf("Expected installation to fail, but it succeeded") t.Error("Expected installation to fail, but it succeeded")
} }
} else { } else {
if err != nil { if err != nil {
@ -526,13 +526,13 @@ func TestOCIInstaller_Install_ComponentExtraction(t *testing.T) {
// Verify plugin.yaml exists // Verify plugin.yaml exists
pluginYAMLPath := filepath.Join(tempDir, "plugin.yaml") pluginYAMLPath := filepath.Join(tempDir, "plugin.yaml")
if _, err := os.Stat(pluginYAMLPath); os.IsNotExist(err) { if _, err := os.Stat(pluginYAMLPath); os.IsNotExist(err) {
t.Errorf("plugin.yaml not found after extraction") t.Error("plugin.yaml not found after extraction")
} }
// Verify bin directory exists // Verify bin directory exists
binPath := filepath.Join(tempDir, "bin") binPath := filepath.Join(tempDir, "bin")
if _, err := os.Stat(binPath); os.IsNotExist(err) { if _, err := os.Stat(binPath); os.IsNotExist(err) {
t.Errorf("bin directory not found after extraction") t.Error("bin directory not found after extraction")
} }
// Verify executable exists and has correct permissions // Verify executable exists and has correct permissions
@ -540,12 +540,12 @@ func TestOCIInstaller_Install_ComponentExtraction(t *testing.T) {
if info, err := os.Stat(execPath); err != nil { if info, err := os.Stat(execPath); err != nil {
t.Errorf("executable not found: %v", err) t.Errorf("executable not found: %v", err)
} else if info.Mode()&0111 == 0 { } else if info.Mode()&0111 == 0 {
t.Errorf("file is not executable") t.Error("file is not executable")
} }
// Verify this would be recognized as a plugin // Verify this would be recognized as a plugin
if !isPlugin(tempDir) { if !isPlugin(tempDir) {
t.Errorf("extracted directory is not a valid plugin") t.Error("extracted directory is not a valid plugin")
} }
} }
@ -798,7 +798,7 @@ func TestOCIInstaller_Install_ValidationErrors(t *testing.T) {
t.Error("expected valid gzip data") t.Error("expected valid gzip data")
} }
if !strings.Contains(tt.errorMsg, "is not a gzip compressed archive") { if !strings.Contains(tt.errorMsg, "is not a gzip compressed archive") {
t.Errorf("expected error message to contain 'is not a gzip compressed archive'") t.Error("expected error message to contain 'is not a gzip compressed archive'")
} }
} }
}) })

@ -90,14 +90,14 @@ func TestVCSInstaller(t *testing.T) {
// Install again to test plugin exists error // Install again to test plugin exists error
if err := Install(i); err == nil { if err := Install(i); err == nil {
t.Fatalf("expected error for plugin exists, got none") t.Fatal("expected error for plugin exists, got none")
} else if err.Error() != "plugin already exists" { } else if err.Error() != "plugin already exists" {
t.Fatalf("expected error for plugin exists, got (%v)", err) t.Fatalf("expected error for plugin exists, got (%v)", err)
} }
// Testing FindSource method, expect error because plugin code is not a cloned repository // Testing FindSource method, expect error because plugin code is not a cloned repository
if _, err := FindSource(i.Path()); err == nil { if _, err := FindSource(i.Path()); err == nil {
t.Fatalf("expected error for inability to find plugin source, got none") t.Fatal("expected error for inability to find plugin source, got none")
} else if err.Error() != "cannot get information about plugin source" { } else if err.Error() != "cannot get information about plugin source" {
t.Fatalf("expected error for inability to find plugin source, got (%v)", err) t.Fatalf("expected error for inability to find plugin source, got (%v)", err)
} }
@ -120,7 +120,7 @@ func TestVCSInstallerNonExistentVersion(t *testing.T) {
} }
if err := Install(i); err == nil { if err := Install(i); err == nil {
t.Fatalf("expected error for version does not exists, got none") t.Fatal("expected error for version does not exists, got none")
} else if strings.Contains(err.Error(), "Could not resolve host: github.com") { } else if strings.Contains(err.Error(), "Could not resolve host: github.com") {
t.Skip("Unable to run test without Internet access") t.Skip("Unable to run test without Internet access")
} else if err.Error() != fmt.Sprintf("requested version %q does not exist for plugin %q", version, source) { } else if err.Error() != fmt.Sprintf("requested version %q does not exist for plugin %q", version, source) {
@ -181,7 +181,7 @@ func TestVCSInstallerUpdate(t *testing.T) {
} }
// Testing update for error // Testing update for error
if err := Update(vcsInstaller); err == nil { if err := Update(vcsInstaller); err == nil {
t.Fatalf("expected error for plugin modified, got none") t.Fatal("expected error for plugin modified, got none")
} else if err.Error() != "plugin repo was modified" { } else if err.Error() != "plugin repo was modified" {
t.Fatalf("expected error for plugin modified, got (%v)", err) t.Fatalf("expected error for plugin modified, got (%v)", err)
} }

@ -16,10 +16,8 @@ limitations under the License.
package installer package installer
import ( import (
"bytes"
"crypto/sha256" "crypto/sha256"
"fmt" "fmt"
"io"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
@ -44,33 +42,49 @@ func TestInstallWithOptions_VerifyMissingProvenance(t *testing.T) {
} }
defer os.RemoveAll(installer.Path()) defer os.RemoveAll(installer.Path())
// Capture stderr to check warning message // Install with verification enabled should fail when .prov is missing
oldStderr := os.Stderr
r, w, _ := os.Pipe()
os.Stderr = w
// Install with verification enabled (should warn but succeed)
result, err := InstallWithOptions(installer, Options{Verify: true, Keyring: "dummy"}) result, err := InstallWithOptions(installer, Options{Verify: true, Keyring: "dummy"})
// Restore stderr and read captured output // Should fail with a missing provenance error
w.Close() if err == nil {
os.Stderr = oldStderr t.Fatal("Expected installation to fail when .prov file is missing and verification is enabled")
var buf bytes.Buffer }
io.Copy(&buf, r) if !strings.Contains(err.Error(), "no provenance file") {
output := buf.String() t.Errorf("Expected 'no provenance file' in error message, got: %v", err)
// Should succeed with nil result (no verification performed)
if err != nil {
t.Fatalf("Expected installation to succeed despite missing .prov file, got error: %v", err)
} }
if result != nil { if result != nil {
t.Errorf("Expected nil verification result when .prov file is missing, got: %+v", result) t.Errorf("Expected nil verification result when .prov file is missing, got: %+v", result)
} }
// Should contain warning message // Plugin should NOT be installed
expectedWarning := "WARNING: No provenance file found for plugin" if _, err := os.Stat(installer.Path()); !os.IsNotExist(err) {
if !strings.Contains(output, expectedWarning) { t.Error("Plugin should not be installed when verification fails due to missing .prov")
t.Errorf("Expected warning message '%s' in output, got: %s", expectedWarning, output) }
}
func TestInstallWithOptions_NoVerifyMissingProvenance(t *testing.T) {
ensure.HelmHome(t)
// Create a temporary plugin tarball without .prov file
pluginDir := createTestPluginDir(t)
pluginTgz := createTarballFromPluginDir(t, pluginDir)
defer os.Remove(pluginTgz)
// Create local installer
installer, err := NewLocalInstaller(pluginTgz)
if err != nil {
t.Fatalf("Failed to create installer: %v", err)
}
defer os.RemoveAll(installer.Path())
// Install with verification explicitly disabled should succeed without .prov
result, err := InstallWithOptions(installer, Options{Verify: false})
if err != nil {
t.Fatalf("Expected installation to succeed with --verify=false, got error: %v", err)
}
if result != nil {
t.Errorf("Expected nil verification result when verification is disabled, got: %+v", result)
} }
// Plugin should be installed // Plugin should be installed
@ -107,7 +121,7 @@ func TestInstallWithOptions_VerifyWithValidProvenance(t *testing.T) {
// Should fail due to invalid signature (empty keyring) but we test that it gets past the hash check // Should fail due to invalid signature (empty keyring) but we test that it gets past the hash check
if err == nil { if err == nil {
t.Fatalf("Expected installation to fail with empty keyring") t.Fatal("Expected installation to fail with empty keyring")
} }
if !strings.Contains(err.Error(), "plugin verification failed") { if !strings.Contains(err.Error(), "plugin verification failed") {
t.Errorf("Expected plugin verification failed error, got: %v", err) t.Errorf("Expected plugin verification failed error, got: %v", err)
@ -118,7 +132,7 @@ func TestInstallWithOptions_VerifyWithValidProvenance(t *testing.T) {
// Plugin should not be installed due to verification failure // Plugin should not be installed due to verification failure
if _, err := os.Stat(installer.Path()); !os.IsNotExist(err) { if _, err := os.Stat(installer.Path()); !os.IsNotExist(err) {
t.Errorf("Plugin should not be installed when verification fails") t.Error("Plugin should not be installed when verification fails")
} }
} }
@ -150,7 +164,7 @@ func TestInstallWithOptions_VerifyWithInvalidProvenance(t *testing.T) {
// Should fail with verification error // Should fail with verification error
if err == nil { if err == nil {
t.Fatalf("Expected installation with invalid .prov file to fail") t.Fatal("Expected installation with invalid .prov file to fail")
} }
if result != nil { if result != nil {
t.Errorf("Expected nil verification result when verification fails, got: %+v", result) t.Errorf("Expected nil verification result when verification fails, got: %+v", result)
@ -164,7 +178,7 @@ func TestInstallWithOptions_VerifyWithInvalidProvenance(t *testing.T) {
// Plugin should not be installed // Plugin should not be installed
if _, err := os.Stat(installer.Path()); !os.IsNotExist(err) { if _, err := os.Stat(installer.Path()); !os.IsNotExist(err) {
t.Errorf("Plugin should not be installed when verification fails") t.Error("Plugin should not be installed when verification fails")
} }
} }
@ -218,7 +232,7 @@ func TestInstallWithOptions_VerifyDirectoryNotSupported(t *testing.T) {
// Should fail with verification not supported error // Should fail with verification not supported error
if err == nil { if err == nil {
t.Fatalf("Expected installation to fail with verification not supported error") t.Fatal("Expected installation to fail with verification not supported error")
} }
if !strings.Contains(err.Error(), "--verify is only supported for plugin tarballs") { if !strings.Contains(err.Error(), "--verify is only supported for plugin tarballs") {
t.Errorf("Expected verification not supported error, got: %v", err) t.Errorf("Expected verification not supported error, got: %v", err)
@ -257,7 +271,7 @@ func TestInstallWithOptions_VerifyMismatchedProvenance(t *testing.T) {
// Should fail with verification error // Should fail with verification error
if err == nil { if err == nil {
t.Fatalf("Expected installation to fail with hash mismatch") t.Fatal("Expected installation to fail with hash mismatch")
} }
if !strings.Contains(err.Error(), "plugin verification failed") { if !strings.Contains(err.Error(), "plugin verification failed") {
t.Errorf("Expected plugin verification failed error, got: %v", err) t.Errorf("Expected plugin verification failed error, got: %v", err)
@ -298,7 +312,7 @@ func TestInstallWithOptions_VerifyProvenanceAccessError(t *testing.T) {
// Should fail with access error (either at stat level or during verification) // Should fail with access error (either at stat level or during verification)
if err == nil { if err == nil {
t.Fatalf("Expected installation to fail with provenance file access error") t.Fatal("Expected installation to fail with provenance file access error")
} }
// The error could be either "failed to access provenance file" or "plugin verification failed" // The error could be either "failed to access provenance file" or "plugin verification failed"
// depending on when the permission error occurs // depending on when the permission error occurs

@ -19,6 +19,7 @@ import (
"bytes" "bytes"
"fmt" "fmt"
"io" "io"
"log/slog"
"os" "os"
"path/filepath" "path/filepath"
@ -158,18 +159,27 @@ func LoadDir(dirname string) (Plugin, error) {
return pm.CreatePlugin(dirname, m) return pm.CreatePlugin(dirname, m)
} }
// LoadAll loads all plugins found beneath the base directory. func LogIgnorePluginLoadErrorFilterFunc(pluginYAML string, err error) error {
slog.Warn("failed to load plugin (ignoring)", slog.String("plugin_yaml", pluginYAML), slog.Any("error", err))
return nil
}
// errorFilterFunc is a function that can filter errors during plugin loading
type ErrorFilterFunc func(string, error) error
// LoadAllDir load all plugins found beneath the base directory, using the provided error filter to determine whether to fail on individual plugin load errors.
// //
// This scans only one directory level. // This scans only one directory level.
func LoadAll(basedir string) ([]Plugin, error) { func LoadAllDir(basedir string, errorFilter ErrorFilterFunc) ([]Plugin, error) {
var plugins []Plugin // We want <basedir>/*/plugin.yaml
// We want basedir/*/plugin.yaml
scanpath := filepath.Join(basedir, "*", PluginFileName) scanpath := filepath.Join(basedir, "*", PluginFileName)
matches, err := filepath.Glob(scanpath) matches, err := filepath.Glob(scanpath)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to search for plugins in %q: %w", scanpath, err) return nil, fmt.Errorf("failed to search for plugins in %q: %w", scanpath, err)
} }
plugins := make([]Plugin, 0, len(matches))
// empty dir should load // empty dir should load
if len(matches) == 0 { if len(matches) == 0 {
return plugins, nil return plugins, nil
@ -179,10 +189,13 @@ func LoadAll(basedir string) ([]Plugin, error) {
dir := filepath.Dir(yamlFile) dir := filepath.Dir(yamlFile)
p, err := LoadDir(dir) p, err := LoadDir(dir)
if err != nil { if err != nil {
return plugins, err if errNew := errorFilter(yamlFile, err); errNew != nil {
return plugins, errNew
} }
} else {
plugins = append(plugins, p) plugins = append(plugins, p)
} }
}
return plugins, detectDuplicates(plugins) return plugins, detectDuplicates(plugins)
} }
@ -193,8 +206,12 @@ type findFunc func(pluginsDir string) ([]Plugin, error)
type filterFunc func(Plugin) bool type filterFunc func(Plugin) bool
// FindPlugins returns a list of plugins that match the descriptor // FindPlugins returns a list of plugins that match the descriptor
// Errors loading a plugin are ignored with a warning
func FindPlugins(pluginsDirs []string, descriptor Descriptor) ([]Plugin, error) { func FindPlugins(pluginsDirs []string, descriptor Descriptor) ([]Plugin, error) {
return findPlugins(pluginsDirs, LoadAll, makeDescriptorFilter(descriptor)) loadAllIgnoreErrors := func(pluginsDir string) ([]Plugin, error) {
return LoadAllDir(pluginsDir, LogIgnorePluginLoadErrorFilterFunc)
}
return findPlugins(pluginsDirs, loadAllIgnoreErrors, makeDescriptorFilter(descriptor))
} }
// findPlugins is the internal implementation that uses the find and filter functions // findPlugins is the internal implementation that uses the find and filter functions
@ -237,7 +254,11 @@ func makeDescriptorFilter(descriptor Descriptor) filterFunc {
// FindPlugin returns a single plugin that matches the descriptor // FindPlugin returns a single plugin that matches the descriptor
func FindPlugin(dirs []string, descriptor Descriptor) (Plugin, error) { func FindPlugin(dirs []string, descriptor Descriptor) (Plugin, error) {
plugins, err := FindPlugins(dirs, descriptor) loadAllIgnoreErrors := func(pluginsDir string) ([]Plugin, error) {
return LoadAllDir(pluginsDir, LogIgnorePluginLoadErrorFilterFunc)
}
plugins, err := findPlugins(dirs, loadAllIgnoreErrors, makeDescriptorFilter(descriptor))
if err != nil { if err != nil {
return nil, err return nil, err
} }

@ -17,7 +17,6 @@ package plugin
import ( import (
"bytes" "bytes"
"fmt"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -71,7 +70,7 @@ func TestLoadDir(t *testing.T) {
} }
return Metadata{ return Metadata{
APIVersion: apiVersion, APIVersion: apiVersion,
Name: fmt.Sprintf("hello-%s", apiVersion), Name: "hello-" + apiVersion,
Version: "0.1.0", Version: "0.1.0",
Type: "cli/v1", Type: "cli/v1",
Runtime: "subprocess", Runtime: "subprocess",
@ -205,16 +204,16 @@ func TestDetectDuplicates(t *testing.T) {
} }
} }
func TestLoadAll(t *testing.T) { func TestLoadAllDir_Empty(t *testing.T) {
// Verify that empty dir loads: emptyDir := t.TempDir()
{ plugs, err := LoadAllDir(emptyDir, func(_ string, err error) error { return err })
plugs, err := LoadAll("testdata")
require.NoError(t, err) require.NoError(t, err)
assert.Len(t, plugs, 0) assert.Len(t, plugs, 0)
} }
func TestLoadAllPluginsDir(t *testing.T) {
basedir := "testdata/plugdir/good" basedir := "testdata/plugdir/good"
plugs, err := LoadAll(basedir) plugs, err := LoadAllDir(basedir, func(_ string, err error) error { return err })
require.NoError(t, err) require.NoError(t, err)
require.NotEmpty(t, plugs, "expected plugins to be loaded from %s", basedir) require.NotEmpty(t, plugs, "expected plugins to be loaded from %s", basedir)
@ -233,7 +232,7 @@ func TestLoadAll(t *testing.T) {
assert.Contains(t, plugsMap, "postrenderer-v1") assert.Contains(t, plugsMap, "postrenderer-v1")
} }
func TestFindPlugins(t *testing.T) { func TestLoadAllPluginsDir_Zero(t *testing.T) {
cases := []struct { cases := []struct {
name string name string
plugdirs string plugdirs string
@ -241,28 +240,20 @@ func TestFindPlugins(t *testing.T) {
}{ }{
{ {
name: "plugdirs is empty", name: "plugdirs is empty",
plugdirs: "", plugdirs: t.TempDir(),
expected: 0,
}, },
{ {
name: "plugdirs isn't dir", name: "plugdirs isn't dir",
plugdirs: "./plugin_test.go", plugdirs: "./plugin_test.go",
expected: 0,
}, },
{ {
name: "plugdirs doesn't have plugin", name: "plugdirs doesn't have plugin",
plugdirs: ".", plugdirs: ".",
expected: 0,
},
{
name: "normal",
plugdirs: "./testdata/plugdir/good",
expected: 7,
}, },
} }
for _, c := range cases { for _, c := range cases {
t.Run(t.Name(), func(t *testing.T) { t.Run(t.Name(), func(t *testing.T) {
plugin, err := LoadAll(c.plugdirs) plugin, err := LoadAllDir(c.plugdirs, func(_ string, err error) error { return err })
require.NoError(t, err) require.NoError(t, err)
assert.Len(t, plugin, c.expected, "expected %d plugins, got %d", c.expected, len(plugin)) assert.Len(t, plugin, c.expected, "expected %d plugins, got %d", c.expected, len(plugin))
}) })
@ -338,6 +329,7 @@ runtime: subprocess
"correct name field": { "correct name field": {
yaml: `apiVersion: v1 yaml: `apiVersion: v1
name: my-plugin name: my-plugin
version: 1.0.0
type: cli/v1 type: cli/v1
runtime: subprocess runtime: subprocess
`, `,

@ -19,9 +19,17 @@ import (
"errors" "errors"
"fmt" "fmt"
"github.com/Masterminds/semver/v3"
"helm.sh/helm/v4/internal/plugin/schema" "helm.sh/helm/v4/internal/plugin/schema"
) )
// isValidSemver checks if the given string is a valid semantic version
func isValidSemver(v string) bool {
_, err := semver.StrictNewVersion(v)
return err == nil
}
// Metadata of a plugin, converted from the "on-disk" legacy or v1 plugin.yaml // Metadata of a plugin, converted from the "on-disk" legacy or v1 plugin.yaml
// Specifically, Config and RuntimeConfig are converted to their respective types based on the plugin type and runtime // Specifically, Config and RuntimeConfig are converted to their respective types based on the plugin type and runtime
type Metadata struct { type Metadata struct {
@ -57,24 +65,29 @@ func (m Metadata) Validate() error {
errs = append(errs, fmt.Errorf("invalid plugin name %q: must contain only a-z, A-Z, 0-9, _ and -", m.Name)) errs = append(errs, fmt.Errorf("invalid plugin name %q: must contain only a-z, A-Z, 0-9, _ and -", m.Name))
} }
// Require version to be valid semver if specified
if m.Version != "" && !isValidSemver(m.Version) {
errs = append(errs, fmt.Errorf("invalid plugin version %q: must be valid semver", m.Version))
}
if m.APIVersion == "" { if m.APIVersion == "" {
errs = append(errs, fmt.Errorf("empty APIVersion")) errs = append(errs, errors.New("empty APIVersion"))
} }
if m.Type == "" { if m.Type == "" {
errs = append(errs, fmt.Errorf("empty type field")) errs = append(errs, errors.New("empty type field"))
} }
if m.Runtime == "" { if m.Runtime == "" {
errs = append(errs, fmt.Errorf("empty runtime field")) errs = append(errs, errors.New("empty runtime field"))
} }
if m.Config == nil { if m.Config == nil {
errs = append(errs, fmt.Errorf("missing config field")) errs = append(errs, errors.New("missing config field"))
} }
if m.RuntimeConfig == nil { if m.RuntimeConfig == nil {
errs = append(errs, fmt.Errorf("missing runtimeConfig field")) errs = append(errs, errors.New("missing runtimeConfig field"))
} }
// Validate the config itself // Validate the config itself

@ -16,6 +16,7 @@ limitations under the License.
package plugin package plugin
import ( import (
"errors"
"fmt" "fmt"
"strings" "strings"
"unicode" "unicode"
@ -71,14 +72,19 @@ func (m *MetadataLegacy) Validate() error {
if !validPluginName.MatchString(m.Name) { if !validPluginName.MatchString(m.Name) {
return fmt.Errorf("invalid plugin name %q: must contain only a-z, A-Z, 0-9, _ and -", m.Name) return fmt.Errorf("invalid plugin name %q: must contain only a-z, A-Z, 0-9, _ and -", m.Name)
} }
if m.Version != "" && !isValidSemver(m.Version) {
return fmt.Errorf("invalid plugin version %q: must be valid semver", m.Version)
}
m.Usage = sanitizeString(m.Usage) m.Usage = sanitizeString(m.Usage)
if len(m.PlatformCommand) > 0 && len(m.Command) > 0 { if len(m.PlatformCommand) > 0 && len(m.Command) > 0 {
return fmt.Errorf("both platformCommand and command are set") return errors.New("both platformCommand and command are set")
} }
if len(m.PlatformHooks) > 0 && len(m.Hooks) > 0 { if len(m.PlatformHooks) > 0 && len(m.Hooks) > 0 {
return fmt.Errorf("both platformHooks and hooks are set") return errors.New("both platformHooks and hooks are set")
} }
// Validate downloader plugins // Validate downloader plugins

@ -26,6 +26,10 @@ func TestMetadataLegacyValidate(t *testing.T) {
"valid metadata": { "valid metadata": {
Name: "myplugin", Name: "myplugin",
}, },
"valid metadata (empty version)": {
Name: "myplugin",
Version: "",
},
"valid with command": { "valid with command": {
Name: "myplugin", Name: "myplugin",
Command: "echo hello", Command: "echo hello",
@ -59,6 +63,13 @@ func TestMetadataLegacyValidate(t *testing.T) {
}, },
}, },
}, },
"valid with version": {
Name: "myplugin",
Version: "1.0.0",
},
"valid with empty version": {
Name: "myplugin",
},
} }
for testName, metadata := range testsValid { for testName, metadata := range testsValid {
@ -116,6 +127,14 @@ func TestMetadataLegacyValidate(t *testing.T) {
}, },
}, },
}, },
"path traversal version": {
Name: "myplugin",
Version: "../../../../tmp/evil",
},
"invalid version": {
Name: "myplugin",
Version: "not-a-version",
},
} }
for testName, metadata := range testsInvalid { for testName, metadata := range testsInvalid {

@ -18,6 +18,8 @@ package plugin
import ( import (
"strings" "strings"
"testing" "testing"
"github.com/stretchr/testify/assert"
) )
func TestValidatePluginData(t *testing.T) { func TestValidatePluginData(t *testing.T) {
@ -72,6 +74,43 @@ func TestValidatePluginData(t *testing.T) {
} }
} }
func TestMetadataValidateVersion(t *testing.T) {
testValid := map[string]struct {
version string
}{
"valid semver": {version: "1.0.0"},
"valid semver with prerelease": {version: "1.2.3-alpha.1+build.123"},
"empty version": {version: ""},
}
testInvalid := map[string]struct {
version string
}{
"valid semver with v prefix": {version: "v1.0.0"},
"path traversal": {version: "../../../../tmp/evil"},
"path traversal in version": {version: "1.0.0/../../etc"},
"not a version": {version: "not-a-version"},
}
for name, tc := range testValid {
t.Run(name, func(t *testing.T) {
m := mockSubprocessCLIPlugin(t, "testplugin")
m.metadata.Version = tc.version
err := m.Metadata().Validate()
assert.NoError(t, err)
})
}
for name, tc := range testInvalid {
t.Run(name, func(t *testing.T) {
m := mockSubprocessCLIPlugin(t, "testplugin")
m.metadata.Version = tc.version
err := m.Metadata().Validate()
assert.ErrorContains(t, err, "invalid plugin version")
})
}
}
func TestMetadataValidateMultipleErrors(t *testing.T) { func TestMetadataValidateMultipleErrors(t *testing.T) {
// Create metadata with multiple validation issues // Create metadata with multiple validation issues
metadata := Metadata{ metadata := Metadata{

@ -16,6 +16,7 @@ limitations under the License.
package plugin package plugin
import ( import (
"errors"
"fmt" "fmt"
) )
@ -48,7 +49,14 @@ type MetadataV1 struct {
func (m *MetadataV1) Validate() error { func (m *MetadataV1) Validate() error {
if !validPluginName.MatchString(m.Name) { if !validPluginName.MatchString(m.Name) {
return fmt.Errorf("invalid plugin `name`") return errors.New("invalid plugin `name`")
}
if m.Version == "" {
return errors.New("plugin `version` is required")
}
if !isValidSemver(m.Version) {
return fmt.Errorf("invalid plugin `version` %q: must be valid semver", m.Version)
} }
if m.APIVersion != "v1" { if m.APIVersion != "v1" {
@ -56,11 +64,11 @@ func (m *MetadataV1) Validate() error {
} }
if m.Type == "" { if m.Type == "" {
return fmt.Errorf("`type` missing") return errors.New("`type` missing")
} }
if m.Runtime == "" { if m.Runtime == "" {
return fmt.Errorf("`runtime` missing") return errors.New("`runtime` missing")
} }
return nil return nil

@ -0,0 +1,85 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package plugin
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestMetadataV1ValidateVersion(t *testing.T) {
base := func() MetadataV1 {
return MetadataV1{
APIVersion: "v1",
Name: "myplugin",
Type: "cli/v1",
Runtime: "subprocess",
Version: "1.0.0",
}
}
testsValid := map[string]string{
"simple version": "1.0.0",
"with prerelease": "1.2.3-alpha.1",
"with build meta": "1.2.3+build.123",
"full prerelease": "1.2.3-alpha.1+build.123",
}
for name, version := range testsValid {
t.Run("valid/"+name, func(t *testing.T) {
m := base()
m.Version = version
assert.NoError(t, m.Validate())
})
}
testsInvalid := map[string]struct {
version string
errMsg string
}{
"empty version": {
version: "",
errMsg: "plugin `version` is required",
},
"v prefix": {
version: "v1.0.0",
errMsg: "invalid plugin `version` \"v1.0.0\": must be valid semver",
},
"path traversal": {
version: "../../../../tmp/evil",
errMsg: "invalid plugin `version`",
},
"path traversal etc": {
version: "../../../etc/passwd",
errMsg: "invalid plugin `version`",
},
"not a version": {
version: "not-a-version",
errMsg: "invalid plugin `version`",
},
}
for name, tc := range testsInvalid {
t.Run("invalid/"+name, func(t *testing.T) {
m := base()
m.Version = tc.version
err := m.Validate()
assert.Error(t, err)
assert.Contains(t, err.Error(), tc.errMsg)
})
}
}

@ -82,7 +82,7 @@ func mockSubprocessCLIPlugin(t *testing.T, pluginName string) *SubprocessPluginR
return &SubprocessPluginRuntime{ return &SubprocessPluginRuntime{
metadata: Metadata{ metadata: Metadata{
Name: pluginName, Name: pluginName,
Version: "v0.1.2", Version: "0.1.2",
Type: "cli/v1", Type: "cli/v1",
APIVersion: "v1", APIVersion: "v1",
Runtime: "subprocess", Runtime: "subprocess",

@ -53,13 +53,13 @@ func remarshalRuntimeConfig[T RuntimeConfig](runtimeData map[string]any) (Runtim
return config, nil return config, nil
} }
// parseEnv takes a list of "KEY=value" environment variable strings // ParseEnv takes a list of "KEY=value" environment variable strings
// and transforms the result into a map[KEY]=value // and transforms the result into a map[KEY]=value
// //
// - empty input strings are ignored // - empty input strings are ignored
// - input strings with no value are stored as empty strings // - input strings with no value are stored as empty strings
// - duplicate keys overwrite earlier values // - duplicate keys overwrite earlier values
func parseEnv(env []string) map[string]string { func ParseEnv(env []string) map[string]string {
result := make(map[string]string, len(env)) result := make(map[string]string, len(env))
for _, envVar := range env { for _, envVar := range env {
parts := strings.SplitN(envVar, "=", 2) parts := strings.SplitN(envVar, "=", 2)
@ -75,7 +75,9 @@ func parseEnv(env []string) map[string]string {
return result return result
} }
func formatEnv(env map[string]string) []string { // FormatEnv takes a map[KEY]=value and transforms it into
// a list of "KEY=value" environment variable strings
func FormatEnv(env map[string]string) []string {
result := make([]string, 0, len(env)) result := make([]string, 0, len(env))
for key, value := range env { for key, value := range env {
result = append(result, fmt.Sprintf("%s=%s", key, value)) result = append(result, fmt.Sprintf("%s=%s", key, value))

@ -259,7 +259,7 @@ func buildPluginConfig(input *Input, r *RuntimeExtismV1) extism.PluginConfig {
mc = mc.WithStderr(input.Stderr) mc = mc.WithStderr(input.Stderr)
} }
if len(input.Env) > 0 { if len(input.Env) > 0 {
env := parseEnv(input.Env) env := ParseEnv(input.Env)
for k, v := range env { for k, v := range env {
mc = mc.WithEnv(k, v) mc = mc.WithEnv(k, v)
} }

@ -117,8 +117,8 @@ func (r *SubprocessPluginRuntime) InvokeWithEnv(main string, argv []string, env
cmd.Env = slices.Clone(os.Environ()) cmd.Env = slices.Clone(os.Environ())
cmd.Env = append( cmd.Env = append(
cmd.Env, cmd.Env,
fmt.Sprintf("HELM_PLUGIN_NAME=%s", r.metadata.Name), "HELM_PLUGIN_NAME="+r.metadata.Name,
fmt.Sprintf("HELM_PLUGIN_DIR=%s", r.pluginDir)) "HELM_PLUGIN_DIR="+r.pluginDir)
cmd.Env = append(cmd.Env, env...) cmd.Env = append(cmd.Env, env...)
cmd.Stdin = stdin cmd.Stdin = stdin
@ -139,7 +139,7 @@ func (r *SubprocessPluginRuntime) InvokeHook(event string) error {
return nil return nil
} }
env := parseEnv(os.Environ()) env := ParseEnv(os.Environ())
maps.Insert(env, maps.All(r.EnvVars)) maps.Insert(env, maps.All(r.EnvVars))
env["HELM_PLUGIN_NAME"] = r.metadata.Name env["HELM_PLUGIN_NAME"] = r.metadata.Name
env["HELM_PLUGIN_DIR"] = r.pluginDir env["HELM_PLUGIN_DIR"] = r.pluginDir
@ -150,7 +150,7 @@ func (r *SubprocessPluginRuntime) InvokeHook(event string) error {
} }
cmd := exec.Command(main, argv...) cmd := exec.Command(main, argv...)
cmd.Env = formatEnv(env) cmd.Env = FormatEnv(env)
cmd.Stdout = os.Stdout cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr cmd.Stderr = os.Stderr
@ -198,9 +198,9 @@ func (r *SubprocessPluginRuntime) runCLI(input *Input) (*Output, error) {
cmds := r.RuntimeConfig.PlatformCommand cmds := r.RuntimeConfig.PlatformCommand
env := parseEnv(os.Environ()) env := ParseEnv(os.Environ())
maps.Insert(env, maps.All(r.EnvVars)) maps.Insert(env, maps.All(r.EnvVars))
maps.Insert(env, maps.All(parseEnv(input.Env))) maps.Insert(env, maps.All(ParseEnv(input.Env)))
env["HELM_PLUGIN_NAME"] = r.metadata.Name env["HELM_PLUGIN_NAME"] = r.metadata.Name
env["HELM_PLUGIN_DIR"] = r.pluginDir env["HELM_PLUGIN_DIR"] = r.pluginDir
@ -210,7 +210,7 @@ func (r *SubprocessPluginRuntime) runCLI(input *Input) (*Output, error) {
} }
cmd := exec.Command(command, args...) cmd := exec.Command(command, args...)
cmd.Env = formatEnv(env) cmd.Env = FormatEnv(env)
cmd.Stdin = input.Stdin cmd.Stdin = input.Stdin
cmd.Stdout = input.Stdout cmd.Stdout = input.Stdout
@ -231,9 +231,9 @@ func (r *SubprocessPluginRuntime) runPostrenderer(input *Input) (*Output, error)
return nil, fmt.Errorf("plugin %q input message does not implement InputMessagePostRendererV1", r.metadata.Name) return nil, fmt.Errorf("plugin %q input message does not implement InputMessagePostRendererV1", r.metadata.Name)
} }
env := parseEnv(os.Environ()) env := ParseEnv(os.Environ())
maps.Insert(env, maps.All(r.EnvVars)) maps.Insert(env, maps.All(r.EnvVars))
maps.Insert(env, maps.All(parseEnv(input.Env))) maps.Insert(env, maps.All(ParseEnv(input.Env)))
env["HELM_PLUGIN_NAME"] = r.metadata.Name env["HELM_PLUGIN_NAME"] = r.metadata.Name
env["HELM_PLUGIN_DIR"] = r.pluginDir env["HELM_PLUGIN_DIR"] = r.pluginDir
@ -261,7 +261,7 @@ func (r *SubprocessPluginRuntime) runPostrenderer(input *Input) (*Output, error)
postRendered := &bytes.Buffer{} postRendered := &bytes.Buffer{}
stderr := &bytes.Buffer{} stderr := &bytes.Buffer{}
cmd.Env = formatEnv(env) cmd.Env = FormatEnv(env)
cmd.Stdout = postRendered cmd.Stdout = postRendered
cmd.Stderr = stderr cmd.Stderr = stderr

@ -24,6 +24,7 @@ import (
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"slices" "slices"
"strconv"
"helm.sh/helm/v4/internal/plugin/schema" "helm.sh/helm/v4/internal/plugin/schema"
) )
@ -56,14 +57,14 @@ func (r *SubprocessPluginRuntime) runGetter(input *Input) (*Output, error) {
return nil, fmt.Errorf("no downloader found for protocol %q", msg.Protocol) return nil, fmt.Errorf("no downloader found for protocol %q", msg.Protocol)
} }
env := parseEnv(os.Environ()) env := ParseEnv(os.Environ())
maps.Insert(env, maps.All(r.EnvVars)) maps.Insert(env, maps.All(r.EnvVars))
maps.Insert(env, maps.All(parseEnv(input.Env))) maps.Insert(env, maps.All(ParseEnv(input.Env)))
env["HELM_PLUGIN_NAME"] = r.metadata.Name env["HELM_PLUGIN_NAME"] = r.metadata.Name
env["HELM_PLUGIN_DIR"] = r.pluginDir env["HELM_PLUGIN_DIR"] = r.pluginDir
env["HELM_PLUGIN_USERNAME"] = msg.Options.Username env["HELM_PLUGIN_USERNAME"] = msg.Options.Username
env["HELM_PLUGIN_PASSWORD"] = msg.Options.Password env["HELM_PLUGIN_PASSWORD"] = msg.Options.Password
env["HELM_PLUGIN_PASS_CREDENTIALS_ALL"] = fmt.Sprintf("%t", msg.Options.PassCredentialsAll) env["HELM_PLUGIN_PASS_CREDENTIALS_ALL"] = strconv.FormatBool(msg.Options.PassCredentialsAll)
command, args, err := PrepareCommands(d.PlatformCommand, false, []string{}, env) command, args, err := PrepareCommands(d.PlatformCommand, false, []string{}, env)
if err != nil { if err != nil {
@ -83,7 +84,7 @@ func (r *SubprocessPluginRuntime) runGetter(input *Input) (*Output, error) {
cmd := exec.Command( cmd := exec.Command(
pluginCommand, pluginCommand,
args...) args...)
cmd.Env = formatEnv(env) cmd.Env = FormatEnv(env)
cmd.Stdout = &buf cmd.Stdout = &buf
cmd.Stderr = os.Stderr cmd.Stderr = os.Stderr

@ -16,6 +16,7 @@ limitations under the License.
package plugin package plugin
import ( import (
"errors"
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
@ -41,7 +42,7 @@ func mockSubprocessCLIPluginErrorExit(t *testing.T, pluginName string, exitCode
md := Metadata{ md := Metadata{
Name: pluginName, Name: pluginName,
Version: "v0.1.2", Version: "0.1.2",
Type: "cli/v1", Type: "cli/v1",
APIVersion: "v1", APIVersion: "v1",
Runtime: "subprocess", Runtime: "subprocess",
@ -76,7 +77,8 @@ func TestSubprocessPluginRuntime(t *testing.T) {
}) })
require.Error(t, err) require.Error(t, err)
ieerr, ok := err.(*InvokeExecError) ieerr := &InvokeExecError{}
ok := errors.As(err, &ieerr)
require.True(t, ok, "expected InvokeExecError, got %T", err) require.True(t, ok, "expected InvokeExecError, got %T", err)
assert.Equal(t, 56, ieerr.ExitCode) assert.Equal(t, 56, ieerr.ExitCode)

@ -56,7 +56,7 @@ func TestParseEnv(t *testing.T) {
for name, tc := range testCases { for name, tc := range testCases {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
result := parseEnv(tc.env) result := ParseEnv(tc.env)
assert.Equal(t, tc.expected, result) assert.Equal(t, tc.expected, result)
}) })
} }
@ -93,7 +93,7 @@ func TestFormatEnv(t *testing.T) {
for name, tc := range testCases { for name, tc := range testCases {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
result := formatEnv(tc.env) result := FormatEnv(tc.env)
assert.ElementsMatch(t, tc.expected, result) assert.ElementsMatch(t, tc.expected, result)
}) })
} }

@ -14,6 +14,7 @@
package schema package schema
import ( import (
"errors"
"fmt" "fmt"
"time" "time"
) )
@ -55,7 +56,7 @@ type ConfigGetterV1 struct {
func (c *ConfigGetterV1) Validate() error { func (c *ConfigGetterV1) Validate() error {
if len(c.Protocols) == 0 { if len(c.Protocols) == 0 {
return fmt.Errorf("getter has no protocols") return errors.New("getter has no protocols")
} }
for i, protocol := range c.Protocols { for i, protocol := range c.Protocols {
if protocol == "" { if protocol == "" {

@ -23,7 +23,7 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/ProtonMail/go-crypto/openpgp/clearsign" //nolint "github.com/ProtonMail/go-crypto/openpgp/clearsign"
"helm.sh/helm/v4/pkg/helmpath" "helm.sh/helm/v4/pkg/helmpath"
) )

@ -16,7 +16,7 @@ limitations under the License.
package plugin package plugin
import ( import (
"fmt" "errors"
"os" "os"
"runtime" "runtime"
"strings" "strings"
@ -80,7 +80,7 @@ func getPlatformCommand(cmds []PlatformCommand) ([]string, []string) {
func PrepareCommands(cmds []PlatformCommand, expandArgs bool, extraArgs []string, env map[string]string) (string, []string, error) { func PrepareCommands(cmds []PlatformCommand, expandArgs bool, extraArgs []string, env map[string]string) (string, []string, error) {
cmdParts, args := getPlatformCommand(cmds) cmdParts, args := getPlatformCommand(cmds)
if len(cmdParts) == 0 || cmdParts[0] == "" { if len(cmdParts) == 0 || cmdParts[0] == "" {
return "", nil, fmt.Errorf("no plugin command is applicable") return "", nil, errors.New("no plugin command is applicable")
} }
envMappingFunc := func(key string) string { envMappingFunc := func(key string) string {
return env[key] return env[key]

@ -210,7 +210,7 @@ func TestPrepareCommandsNoMatch(t *testing.T) {
env := map[string]string{} env := map[string]string{}
if _, _, err := PrepareCommands(cmds, true, []string{}, env); err == nil { if _, _, err := PrepareCommands(cmds, true, []string{}, env); err == nil {
t.Fatalf("Expected error to be returned") t.Fatal("Expected error to be returned")
} }
} }
@ -219,7 +219,7 @@ func TestPrepareCommandsNoCommands(t *testing.T) {
env := map[string]string{} env := map[string]string{}
if _, _, err := PrepareCommands(cmds, true, []string{}, env); err == nil { if _, _, err := PrepareCommands(cmds, true, []string{}, env); err == nil {
t.Fatalf("Expected error to be returned") t.Fatal("Expected error to be returned")
} }
} }

@ -33,7 +33,7 @@ func VerifyPlugin(archiveData, provData []byte, filename, keyring string) (*prov
return sig.Verify(archiveData, provData, filename) return sig.Verify(archiveData, provData, filename)
} }
// isTarball checks if a file has a tarball extension // IsTarball checks if a file has a tarball extension
func IsTarball(filename string) bool { func IsTarball(filename string) bool {
return filepath.Ext(filename) == ".gz" || filepath.Ext(filename) == ".tgz" return filepath.Ext(filename) == ".gz" || filepath.Ext(filename) == ".tgz"
} }

@ -0,0 +1,17 @@
/*
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 v2 provides release handling for apiVersion v3 charts.
package v2

@ -0,0 +1,189 @@
/*
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 v2
import (
"encoding/json"
"time"
)
// HookEvent specifies the hook event
type HookEvent string
// Hook event types
const (
HookPreInstall HookEvent = "pre-install"
HookPostInstall HookEvent = "post-install"
HookPreDelete HookEvent = "pre-delete"
HookPostDelete HookEvent = "post-delete"
HookPreUpgrade HookEvent = "pre-upgrade"
HookPostUpgrade HookEvent = "post-upgrade"
HookPreRollback HookEvent = "pre-rollback"
HookPostRollback HookEvent = "post-rollback"
HookTest HookEvent = "test"
)
func (x HookEvent) String() string { return string(x) }
// HookDeletePolicy specifies the hook delete policy
type HookDeletePolicy string
// Hook delete policy types
const (
HookSucceeded HookDeletePolicy = "hook-succeeded"
HookFailed HookDeletePolicy = "hook-failed"
HookBeforeHookCreation HookDeletePolicy = "before-hook-creation"
)
func (x HookDeletePolicy) String() string { return string(x) }
// HookOutputLogPolicy specifies the hook output log policy
type HookOutputLogPolicy string
// Hook output log policy types
const (
HookOutputOnSucceeded HookOutputLogPolicy = "hook-succeeded"
HookOutputOnFailed HookOutputLogPolicy = "hook-failed"
)
func (x HookOutputLogPolicy) String() string { return string(x) }
// HookAnnotation is the label name for a hook
const HookAnnotation = "helm.sh/hook"
// HookWeightAnnotation is the label name for a hook weight
const HookWeightAnnotation = "helm.sh/hook-weight"
// HookDeleteAnnotation is the label name for the delete policy for a hook
const HookDeleteAnnotation = "helm.sh/hook-delete-policy"
// HookOutputLogAnnotation is the label name for the output log policy for a hook
const HookOutputLogAnnotation = "helm.sh/hook-output-log-policy"
// Hook defines a hook object.
type Hook struct {
Name string `json:"name,omitempty"`
// Kind is the Kubernetes kind.
Kind string `json:"kind,omitempty"`
// Path is the chart-relative path to the template.
Path string `json:"path,omitempty"`
// Manifest is the manifest contents.
Manifest string `json:"manifest,omitempty"`
// Events are the events that this hook fires on.
Events []HookEvent `json:"events,omitempty"`
// LastRun indicates the date/time this was last run.
LastRun HookExecution `json:"last_run"`
// Weight indicates the sort order for execution among similar Hook type
Weight int `json:"weight,omitempty"`
// DeletePolicies are the policies that indicate when to delete the hook
DeletePolicies []HookDeletePolicy `json:"delete_policies,omitempty"`
// OutputLogPolicies defines whether we should copy hook logs back to main process
OutputLogPolicies []HookOutputLogPolicy `json:"output_log_policies,omitempty"`
}
// A HookExecution records the result for the last execution of a hook for a given release.
type HookExecution struct {
// StartedAt indicates the date/time this hook was started
StartedAt time.Time `json:"started_at,omitzero"`
// CompletedAt indicates the date/time this hook was completed.
CompletedAt time.Time `json:"completed_at,omitzero"`
// Phase indicates whether the hook completed successfully
Phase HookPhase `json:"phase"`
}
// A HookPhase indicates the state of a hook execution
type HookPhase string
const (
// HookPhaseUnknown indicates that a hook is in an unknown state
HookPhaseUnknown HookPhase = "Unknown"
// HookPhaseRunning indicates that a hook is currently executing
HookPhaseRunning HookPhase = "Running"
// HookPhaseSucceeded indicates that hook execution succeeded
HookPhaseSucceeded HookPhase = "Succeeded"
// HookPhaseFailed indicates that hook execution failed
HookPhaseFailed HookPhase = "Failed"
)
// String converts a hook phase to a printable string
func (x HookPhase) String() string { return string(x) }
// hookExecutionJSON is used for custom JSON marshaling/unmarshaling
type hookExecutionJSON struct {
StartedAt *time.Time `json:"started_at,omitempty"`
CompletedAt *time.Time `json:"completed_at,omitempty"`
Phase HookPhase `json:"phase"`
}
// UnmarshalJSON implements the json.Unmarshaler interface.
// It handles empty string time fields by treating them as zero values.
func (h *HookExecution) UnmarshalJSON(data []byte) error {
// First try to unmarshal into a map to handle empty string time fields
var raw map[string]any
if err := json.Unmarshal(data, &raw); err != nil {
return err
}
// Replace empty string time fields with nil
for _, field := range []string{"started_at", "completed_at"} {
if val, ok := raw[field]; ok {
if str, ok := val.(string); ok && str == "" {
raw[field] = nil
}
}
}
// Re-marshal with cleaned data
cleaned, err := json.Marshal(raw)
if err != nil {
return err
}
// Unmarshal into temporary struct with pointer time fields
var tmp hookExecutionJSON
if err := json.Unmarshal(cleaned, &tmp); err != nil {
return err
}
// Copy values to HookExecution struct
if tmp.StartedAt != nil {
h.StartedAt = *tmp.StartedAt
}
if tmp.CompletedAt != nil {
h.CompletedAt = *tmp.CompletedAt
}
h.Phase = tmp.Phase
return nil
}
// MarshalJSON implements the json.Marshaler interface.
// It omits zero-value time fields from the JSON output.
func (h HookExecution) MarshalJSON() ([]byte, error) {
tmp := hookExecutionJSON{
Phase: h.Phase,
}
if !h.StartedAt.IsZero() {
tmp.StartedAt = &h.StartedAt
}
if !h.CompletedAt.IsZero() {
tmp.CompletedAt = &h.CompletedAt
}
return json.Marshal(tmp)
}

@ -0,0 +1,231 @@
/*
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 v2
import (
"encoding/json"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestHookExecutionMarshalJSON(t *testing.T) {
started := time.Date(2025, 10, 8, 12, 0, 0, 0, time.UTC)
completed := time.Date(2025, 10, 8, 12, 5, 0, 0, time.UTC)
tests := []struct {
name string
exec HookExecution
expected string
}{
{
name: "all fields populated",
exec: HookExecution{
StartedAt: started,
CompletedAt: completed,
Phase: HookPhaseSucceeded,
},
expected: `{"started_at":"2025-10-08T12:00:00Z","completed_at":"2025-10-08T12:05:00Z","phase":"Succeeded"}`,
},
{
name: "only phase",
exec: HookExecution{
Phase: HookPhaseRunning,
},
expected: `{"phase":"Running"}`,
},
{
name: "with started time only",
exec: HookExecution{
StartedAt: started,
Phase: HookPhaseRunning,
},
expected: `{"started_at":"2025-10-08T12:00:00Z","phase":"Running"}`,
},
{
name: "failed phase",
exec: HookExecution{
StartedAt: started,
CompletedAt: completed,
Phase: HookPhaseFailed,
},
expected: `{"started_at":"2025-10-08T12:00:00Z","completed_at":"2025-10-08T12:05:00Z","phase":"Failed"}`,
},
{
name: "unknown phase",
exec: HookExecution{
Phase: HookPhaseUnknown,
},
expected: `{"phase":"Unknown"}`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
data, err := json.Marshal(&tt.exec)
require.NoError(t, err)
assert.JSONEq(t, tt.expected, string(data))
})
}
}
func TestHookExecutionUnmarshalJSON(t *testing.T) {
started := time.Date(2025, 10, 8, 12, 0, 0, 0, time.UTC)
completed := time.Date(2025, 10, 8, 12, 5, 0, 0, time.UTC)
tests := []struct {
name string
input string
expected HookExecution
wantErr bool
}{
{
name: "all fields populated",
input: `{"started_at":"2025-10-08T12:00:00Z","completed_at":"2025-10-08T12:05:00Z","phase":"Succeeded"}`,
expected: HookExecution{
StartedAt: started,
CompletedAt: completed,
Phase: HookPhaseSucceeded,
},
},
{
name: "only phase",
input: `{"phase":"Running"}`,
expected: HookExecution{
Phase: HookPhaseRunning,
},
},
{
name: "empty string time fields",
input: `{"started_at":"","completed_at":"","phase":"Succeeded"}`,
expected: HookExecution{
Phase: HookPhaseSucceeded,
},
},
{
name: "missing time fields",
input: `{"phase":"Failed"}`,
expected: HookExecution{
Phase: HookPhaseFailed,
},
},
{
name: "null time fields",
input: `{"started_at":null,"completed_at":null,"phase":"Unknown"}`,
expected: HookExecution{
Phase: HookPhaseUnknown,
},
},
{
name: "mixed empty and valid time fields",
input: `{"started_at":"2025-10-08T12:00:00Z","completed_at":"","phase":"Running"}`,
expected: HookExecution{
StartedAt: started,
Phase: HookPhaseRunning,
},
},
{
name: "with started time only",
input: `{"started_at":"2025-10-08T12:00:00Z","phase":"Running"}`,
expected: HookExecution{
StartedAt: started,
Phase: HookPhaseRunning,
},
},
{
name: "failed phase with times",
input: `{"started_at":"2025-10-08T12:00:00Z","completed_at":"2025-10-08T12:05:00Z","phase":"Failed"}`,
expected: HookExecution{
StartedAt: started,
CompletedAt: completed,
Phase: HookPhaseFailed,
},
},
{
name: "invalid time format",
input: `{"started_at":"invalid-time","phase":"Running"}`,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var exec HookExecution
err := json.Unmarshal([]byte(tt.input), &exec)
if tt.wantErr {
assert.Error(t, err)
return
}
require.NoError(t, err)
assert.Equal(t, tt.expected.StartedAt.Unix(), exec.StartedAt.Unix())
assert.Equal(t, tt.expected.CompletedAt.Unix(), exec.CompletedAt.Unix())
assert.Equal(t, tt.expected.Phase, exec.Phase)
})
}
}
func TestHookExecutionRoundTrip(t *testing.T) {
started := time.Date(2025, 10, 8, 12, 0, 0, 0, time.UTC)
completed := time.Date(2025, 10, 8, 12, 5, 0, 0, time.UTC)
original := HookExecution{
StartedAt: started,
CompletedAt: completed,
Phase: HookPhaseSucceeded,
}
data, err := json.Marshal(&original)
require.NoError(t, err)
var decoded HookExecution
err = json.Unmarshal(data, &decoded)
require.NoError(t, err)
assert.Equal(t, original.StartedAt.Unix(), decoded.StartedAt.Unix())
assert.Equal(t, original.CompletedAt.Unix(), decoded.CompletedAt.Unix())
assert.Equal(t, original.Phase, decoded.Phase)
}
func TestHookExecutionEmptyStringRoundTrip(t *testing.T) {
// This test specifically verifies that empty string time fields
// are handled correctly during parsing
input := `{"started_at":"","completed_at":"","phase":"Succeeded"}`
var exec HookExecution
err := json.Unmarshal([]byte(input), &exec)
require.NoError(t, err)
// Verify time fields are zero values
assert.True(t, exec.StartedAt.IsZero())
assert.True(t, exec.CompletedAt.IsZero())
assert.Equal(t, HookPhaseSucceeded, exec.Phase)
// Marshal back and verify empty time fields are omitted
data, err := json.Marshal(&exec)
require.NoError(t, err)
var result map[string]any
err = json.Unmarshal(data, &result)
require.NoError(t, err)
// Zero time values should be omitted
assert.NotContains(t, result, "started_at")
assert.NotContains(t, result, "completed_at")
assert.Equal(t, "Succeeded", result["phase"])
}

@ -0,0 +1,125 @@
/*
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 v2
import (
"encoding/json"
"time"
"helm.sh/helm/v4/pkg/release/common"
"k8s.io/apimachinery/pkg/runtime"
)
// Info describes release information.
type Info struct {
// FirstDeployed is when the release was first deployed.
FirstDeployed time.Time `json:"first_deployed,omitzero"`
// LastDeployed is when the release was last deployed.
LastDeployed time.Time `json:"last_deployed,omitzero"`
// Deleted tracks when this object was deleted.
Deleted time.Time `json:"deleted,omitzero"`
// Description is human-friendly "log entry" about this release.
Description string `json:"description,omitempty"`
// Status is the current state of the release
Status common.Status `json:"status,omitempty"`
// Contains the rendered templates/NOTES.txt if available
Notes string `json:"notes,omitempty"`
// Contains the deployed resources information
Resources map[string][]runtime.Object `json:"resources,omitempty"`
}
// infoJSON is used for custom JSON marshaling/unmarshaling
type infoJSON struct {
FirstDeployed *time.Time `json:"first_deployed,omitempty"`
LastDeployed *time.Time `json:"last_deployed,omitempty"`
Deleted *time.Time `json:"deleted,omitempty"`
Description string `json:"description,omitempty"`
Status common.Status `json:"status,omitempty"`
Notes string `json:"notes,omitempty"`
Resources map[string][]runtime.Object `json:"resources,omitempty"`
}
// UnmarshalJSON implements the json.Unmarshaler interface.
// It handles empty string time fields by treating them as zero values.
func (i *Info) UnmarshalJSON(data []byte) error {
// First try to unmarshal into a map to handle empty string time fields
var raw map[string]any
if err := json.Unmarshal(data, &raw); err != nil {
return err
}
// Replace empty string time fields with nil
for _, field := range []string{"first_deployed", "last_deployed", "deleted"} {
if val, ok := raw[field]; ok {
if str, ok := val.(string); ok && str == "" {
raw[field] = nil
}
}
}
// Re-marshal with cleaned data
cleaned, err := json.Marshal(raw)
if err != nil {
return err
}
// Unmarshal into temporary struct with pointer time fields
var tmp infoJSON
if err := json.Unmarshal(cleaned, &tmp); err != nil {
return err
}
// Copy values to Info struct
if tmp.FirstDeployed != nil {
i.FirstDeployed = *tmp.FirstDeployed
}
if tmp.LastDeployed != nil {
i.LastDeployed = *tmp.LastDeployed
}
if tmp.Deleted != nil {
i.Deleted = *tmp.Deleted
}
i.Description = tmp.Description
i.Status = tmp.Status
i.Notes = tmp.Notes
i.Resources = tmp.Resources
return nil
}
// MarshalJSON implements the json.Marshaler interface.
// It omits zero-value time fields from the JSON output.
func (i Info) MarshalJSON() ([]byte, error) {
tmp := infoJSON{
Description: i.Description,
Status: i.Status,
Notes: i.Notes,
Resources: i.Resources,
}
if !i.FirstDeployed.IsZero() {
tmp.FirstDeployed = &i.FirstDeployed
}
if !i.LastDeployed.IsZero() {
tmp.LastDeployed = &i.LastDeployed
}
if !i.Deleted.IsZero() {
tmp.Deleted = &i.Deleted
}
return json.Marshal(tmp)
}

@ -0,0 +1,285 @@
/*
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 v2
import (
"encoding/json"
"testing"
"time"
"helm.sh/helm/v4/pkg/release/common"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestInfoMarshalJSON(t *testing.T) {
now := time.Date(2025, 10, 8, 12, 0, 0, 0, time.UTC)
later := time.Date(2025, 10, 8, 13, 0, 0, 0, time.UTC)
deleted := time.Date(2025, 10, 8, 14, 0, 0, 0, time.UTC)
tests := []struct {
name string
info Info
expected string
}{
{
name: "all fields populated",
info: Info{
FirstDeployed: now,
LastDeployed: later,
Deleted: deleted,
Description: "Test release",
Status: common.StatusDeployed,
Notes: "Test notes",
},
expected: `{"first_deployed":"2025-10-08T12:00:00Z","last_deployed":"2025-10-08T13:00:00Z","deleted":"2025-10-08T14:00:00Z","description":"Test release","status":"deployed","notes":"Test notes"}`,
},
{
name: "only required fields",
info: Info{
FirstDeployed: now,
LastDeployed: later,
Status: common.StatusDeployed,
},
expected: `{"first_deployed":"2025-10-08T12:00:00Z","last_deployed":"2025-10-08T13:00:00Z","status":"deployed"}`,
},
{
name: "zero time values omitted",
info: Info{
Description: "Test release",
Status: common.StatusDeployed,
},
expected: `{"description":"Test release","status":"deployed"}`,
},
{
name: "with pending status",
info: Info{
FirstDeployed: now,
LastDeployed: later,
Status: common.StatusPendingInstall,
Description: "Installing release",
},
expected: `{"first_deployed":"2025-10-08T12:00:00Z","last_deployed":"2025-10-08T13:00:00Z","description":"Installing release","status":"pending-install"}`,
},
{
name: "uninstalled with deleted time",
info: Info{
FirstDeployed: now,
LastDeployed: later,
Deleted: deleted,
Status: common.StatusUninstalled,
Description: "Uninstalled release",
},
expected: `{"first_deployed":"2025-10-08T12:00:00Z","last_deployed":"2025-10-08T13:00:00Z","deleted":"2025-10-08T14:00:00Z","description":"Uninstalled release","status":"uninstalled"}`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
data, err := json.Marshal(&tt.info)
require.NoError(t, err)
assert.JSONEq(t, tt.expected, string(data))
})
}
}
func TestInfoUnmarshalJSON(t *testing.T) {
now := time.Date(2025, 10, 8, 12, 0, 0, 0, time.UTC)
later := time.Date(2025, 10, 8, 13, 0, 0, 0, time.UTC)
deleted := time.Date(2025, 10, 8, 14, 0, 0, 0, time.UTC)
tests := []struct {
name string
input string
expected Info
wantErr bool
}{
{
name: "all fields populated",
input: `{"first_deployed":"2025-10-08T12:00:00Z","last_deployed":"2025-10-08T13:00:00Z","deleted":"2025-10-08T14:00:00Z","description":"Test release","status":"deployed","notes":"Test notes"}`,
expected: Info{
FirstDeployed: now,
LastDeployed: later,
Deleted: deleted,
Description: "Test release",
Status: common.StatusDeployed,
Notes: "Test notes",
},
},
{
name: "only required fields",
input: `{"first_deployed":"2025-10-08T12:00:00Z","last_deployed":"2025-10-08T13:00:00Z","status":"deployed"}`,
expected: Info{
FirstDeployed: now,
LastDeployed: later,
Status: common.StatusDeployed,
},
},
{
name: "empty string time fields",
input: `{"first_deployed":"","last_deployed":"","deleted":"","description":"Test release","status":"deployed"}`,
expected: Info{
Description: "Test release",
Status: common.StatusDeployed,
},
},
{
name: "missing time fields",
input: `{"description":"Test release","status":"deployed"}`,
expected: Info{
Description: "Test release",
Status: common.StatusDeployed,
},
},
{
name: "null time fields",
input: `{"first_deployed":null,"last_deployed":null,"deleted":null,"description":"Test release","status":"deployed"}`,
expected: Info{
Description: "Test release",
Status: common.StatusDeployed,
},
},
{
name: "mixed empty and valid time fields",
input: `{"first_deployed":"2025-10-08T12:00:00Z","last_deployed":"","deleted":"","status":"deployed"}`,
expected: Info{
FirstDeployed: now,
Status: common.StatusDeployed,
},
},
{
name: "pending install status",
input: `{"first_deployed":"2025-10-08T12:00:00Z","status":"pending-install","description":"Installing"}`,
expected: Info{
FirstDeployed: now,
Status: common.StatusPendingInstall,
Description: "Installing",
},
},
{
name: "uninstalled with deleted time",
input: `{"first_deployed":"2025-10-08T12:00:00Z","last_deployed":"2025-10-08T13:00:00Z","deleted":"2025-10-08T14:00:00Z","status":"uninstalled"}`,
expected: Info{
FirstDeployed: now,
LastDeployed: later,
Deleted: deleted,
Status: common.StatusUninstalled,
},
},
{
name: "failed status",
input: `{"first_deployed":"2025-10-08T12:00:00Z","last_deployed":"2025-10-08T13:00:00Z","status":"failed","description":"Deployment failed"}`,
expected: Info{
FirstDeployed: now,
LastDeployed: later,
Status: common.StatusFailed,
Description: "Deployment failed",
},
},
{
name: "invalid time format",
input: `{"first_deployed":"invalid-time","status":"deployed"}`,
wantErr: true,
},
{
name: "empty object",
input: `{}`,
expected: Info{
Status: "",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var info Info
err := json.Unmarshal([]byte(tt.input), &info)
if tt.wantErr {
assert.Error(t, err)
return
}
require.NoError(t, err)
assert.Equal(t, tt.expected.FirstDeployed.Unix(), info.FirstDeployed.Unix())
assert.Equal(t, tt.expected.LastDeployed.Unix(), info.LastDeployed.Unix())
assert.Equal(t, tt.expected.Deleted.Unix(), info.Deleted.Unix())
assert.Equal(t, tt.expected.Description, info.Description)
assert.Equal(t, tt.expected.Status, info.Status)
assert.Equal(t, tt.expected.Notes, info.Notes)
assert.Equal(t, tt.expected.Resources, info.Resources)
})
}
}
func TestInfoRoundTrip(t *testing.T) {
now := time.Date(2025, 10, 8, 12, 0, 0, 0, time.UTC)
later := time.Date(2025, 10, 8, 13, 0, 0, 0, time.UTC)
original := Info{
FirstDeployed: now,
LastDeployed: later,
Description: "Test release",
Status: common.StatusDeployed,
Notes: "Release notes",
}
data, err := json.Marshal(&original)
require.NoError(t, err)
var decoded Info
err = json.Unmarshal(data, &decoded)
require.NoError(t, err)
assert.Equal(t, original.FirstDeployed.Unix(), decoded.FirstDeployed.Unix())
assert.Equal(t, original.LastDeployed.Unix(), decoded.LastDeployed.Unix())
assert.Equal(t, original.Deleted.Unix(), decoded.Deleted.Unix())
assert.Equal(t, original.Description, decoded.Description)
assert.Equal(t, original.Status, decoded.Status)
assert.Equal(t, original.Notes, decoded.Notes)
}
func TestInfoEmptyStringRoundTrip(t *testing.T) {
// This test specifically verifies that empty string time fields
// are handled correctly during parsing
input := `{"first_deployed":"","last_deployed":"","deleted":"","status":"deployed","description":"test"}`
var info Info
err := json.Unmarshal([]byte(input), &info)
require.NoError(t, err)
// Verify time fields are zero values
assert.True(t, info.FirstDeployed.IsZero())
assert.True(t, info.LastDeployed.IsZero())
assert.True(t, info.Deleted.IsZero())
assert.Equal(t, common.StatusDeployed, info.Status)
assert.Equal(t, "test", info.Description)
// Marshal back and verify empty time fields are omitted
data, err := json.Marshal(&info)
require.NoError(t, err)
var result map[string]any
err = json.Unmarshal(data, &result)
require.NoError(t, err)
// Zero time values should be omitted due to omitzero tag
assert.NotContains(t, result, "first_deployed")
assert.NotContains(t, result, "last_deployed")
assert.NotContains(t, result, "deleted")
assert.Equal(t, "deployed", result["status"])
assert.Equal(t, "test", result["description"])
}

@ -0,0 +1,143 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package v2
import (
"math/rand"
"strconv"
"time"
v3 "helm.sh/helm/v4/internal/chart/v3"
"helm.sh/helm/v4/pkg/chart/common"
rcommon "helm.sh/helm/v4/pkg/release/common"
)
// MockHookTemplate is the hook template used for all mock release objects.
var MockHookTemplate = `apiVersion: v1
kind: Job
metadata:
annotations:
"helm.sh/hook": pre-install
`
// MockManifest is the manifest used for all mock release objects.
var MockManifest = `apiVersion: v1
kind: Secret
metadata:
name: fixture
`
// MockReleaseOptions allows for user-configurable options on mock release objects.
type MockReleaseOptions struct {
Name string
Version int
Chart *v3.Chart
Status rcommon.Status
Namespace string
Labels map[string]string
}
// Mock creates a mock release object based on options set by MockReleaseOptions. This function should typically not be used outside of testing.
func Mock(opts *MockReleaseOptions) *Release {
date := time.Unix(242085845, 0).UTC()
name := opts.Name
if name == "" {
name = "testrelease-" + strconv.Itoa(rand.Intn(100))
}
version := 1
if opts.Version != 0 {
version = opts.Version
}
namespace := opts.Namespace
if namespace == "" {
namespace = "default"
}
var labels map[string]string
if len(opts.Labels) > 0 {
labels = opts.Labels
}
ch := opts.Chart
if opts.Chart == nil {
ch = &v3.Chart{
Metadata: &v3.Metadata{
Name: "foo",
Version: "0.1.0-beta.1",
AppVersion: "1.0",
APIVersion: v3.APIVersionV3,
Annotations: map[string]string{
"category": "web-apps",
"supported": "true",
},
Dependencies: []*v3.Dependency{
{
Name: "cool-plugin",
Version: "1.0.0",
Repository: "https://coolplugin.io/charts",
Condition: "coolPlugin.enabled",
Enabled: true,
},
{
Name: "crds",
Version: "2.7.1",
Condition: "crds.enabled",
},
},
},
Templates: []*common.File{
{Name: "templates/foo.tpl", ModTime: time.Now(), Data: []byte(MockManifest)},
},
}
}
scode := rcommon.StatusDeployed
if len(opts.Status) > 0 {
scode = opts.Status
}
info := &Info{
FirstDeployed: date,
LastDeployed: date,
Status: scode,
Description: "Release mock",
Notes: "Some mock release notes!",
}
return &Release{
Name: name,
Info: info,
Chart: ch,
Config: map[string]any{"name": "value"},
Version: version,
Namespace: namespace,
Hooks: []*Hook{
{
Name: "pre-install-hook",
Kind: "Job",
Path: "pre-install-hook.yaml",
Manifest: MockHookTemplate,
LastRun: HookExecution{},
Events: []HookEvent{HookPreInstall},
},
},
Manifest: MockManifest,
Labels: labels,
}
}

@ -0,0 +1,60 @@
/*
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 v2
import (
chart "helm.sh/helm/v4/internal/chart/v3"
"helm.sh/helm/v4/pkg/release/common"
)
type ApplyMethod string
const ApplyMethodClientSideApply ApplyMethod = "csa"
const ApplyMethodServerSideApply ApplyMethod = "ssa"
// Release describes a deployment of a chart, together with the chart
// and the variables used to deploy that chart.
type Release struct {
// Name is the name of the release
Name string `json:"name,omitempty"`
// Info provides information about a release
Info *Info `json:"info,omitempty"`
// Chart is the chart that was released.
Chart *chart.Chart `json:"chart,omitempty"`
// Config is the set of extra Values added to the chart.
// These values override the default values inside of the chart.
Config map[string]any `json:"config,omitempty"`
// Manifest is the string representation of the rendered template.
Manifest string `json:"manifest,omitempty"`
// Hooks are all of the hooks declared for this release.
Hooks []*Hook `json:"hooks,omitempty"`
// Version is an int which represents the revision of the release.
Version int `json:"version,omitempty"`
// Namespace is the kubernetes namespace of the release.
Namespace string `json:"namespace,omitempty"`
// Labels of the release.
// Disabled encoding into Json cause labels are stored in storage driver metadata field.
Labels map[string]string `json:"-"`
// ApplyMethod stores whether server-side or client-side apply was used for the release
// Unset (empty string) should be treated as the default of client-side apply
ApplyMethod string `json:"apply_method,omitempty"` // "ssa" | "csa"
}
// SetStatus is a helper for setting the status on a release.
func (r *Release) SetStatus(status common.Status, msg string) {
r.Info.Status = status
r.Info.Description = msg
}

@ -0,0 +1,81 @@
/*
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 util // import "helm.sh/helm/v4/internal/release/v2/util"
import (
v2 "helm.sh/helm/v4/internal/release/v2"
"helm.sh/helm/v4/pkg/release/common"
)
// FilterFunc returns true if the release object satisfies
// the predicate of the underlying filter func.
type FilterFunc func(*v2.Release) bool
// Check applies the FilterFunc to the release object.
func (fn FilterFunc) Check(rls *v2.Release) bool {
if rls == nil {
return false
}
return fn(rls)
}
// Filter applies the filter(s) to the list of provided releases
// returning the list that satisfies the filtering predicate.
func (fn FilterFunc) Filter(rels []*v2.Release) (rets []*v2.Release) {
for _, rel := range rels {
if fn.Check(rel) {
rets = append(rets, rel)
}
}
return
}
// Any returns a FilterFunc that filters a list of releases
// determined by the predicate 'f0 || f1 || ... || fn'.
func Any(filters ...FilterFunc) FilterFunc {
return func(rls *v2.Release) bool {
for _, filter := range filters {
if filter(rls) {
return true
}
}
return false
}
}
// All returns a FilterFunc that filters a list of releases
// determined by the predicate 'f0 && f1 && ... && fn'.
func All(filters ...FilterFunc) FilterFunc {
return func(rls *v2.Release) bool {
for _, filter := range filters {
if !filter(rls) {
return false
}
}
return true
}
}
// StatusFilter filters a set of releases by status code.
func StatusFilter(status common.Status) FilterFunc {
return FilterFunc(func(rls *v2.Release) bool {
if rls == nil {
return true
}
return rls.Info.Status == status
})
}

@ -0,0 +1,60 @@
/*
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 util // import "helm.sh/helm/v4/internal/release/v2/util"
import (
"testing"
rspb "helm.sh/helm/v4/internal/release/v2"
"helm.sh/helm/v4/pkg/release/common"
)
func TestFilterAny(t *testing.T) {
ls := Any(StatusFilter(common.StatusUninstalled)).Filter(releases)
if len(ls) != 2 {
t.Fatalf("expected 2 results, got '%d'", len(ls))
}
r0, r1 := ls[0], ls[1]
switch {
case r0.Info.Status != common.StatusUninstalled:
t.Fatalf("expected UNINSTALLED result, got '%s'", r1.Info.Status.String())
case r1.Info.Status != common.StatusUninstalled:
t.Fatalf("expected UNINSTALLED result, got '%s'", r1.Info.Status.String())
}
}
func TestFilterAll(t *testing.T) {
fn := FilterFunc(func(rls *rspb.Release) bool {
// true if not uninstalled and version < 4
v0 := !StatusFilter(common.StatusUninstalled).Check(rls)
v1 := rls.Version < 4
return v0 && v1
})
ls := All(fn).Filter(releases)
if len(ls) != 1 {
t.Fatalf("expected 1 result, got '%d'", len(ls))
}
switch r0 := ls[0]; {
case r0.Version == 4:
t.Fatal("got release with status revision 4")
case r0.Info.Status == common.StatusUninstalled:
t.Fatal("got release with status UNINSTALLED")
}
}

@ -0,0 +1,165 @@
/*
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 util // import "helm.sh/helm/v4/internal/release/v2/util"
import (
"sort"
release "helm.sh/helm/v4/internal/release/v2"
)
// KindSortOrder is an ordering of Kinds.
type KindSortOrder []string
// InstallOrder is the order in which manifests should be installed (by Kind).
//
// Those occurring earlier in the list get installed before those occurring later in the list.
var InstallOrder KindSortOrder = []string{
"PriorityClass",
"Namespace",
"NetworkPolicy",
"ResourceQuota",
"LimitRange",
"PodSecurityPolicy",
"PodDisruptionBudget",
"ServiceAccount",
"Secret",
"SecretList",
"ConfigMap",
"StorageClass",
"PersistentVolume",
"PersistentVolumeClaim",
"CustomResourceDefinition",
"ClusterRole",
"ClusterRoleList",
"ClusterRoleBinding",
"ClusterRoleBindingList",
"Role",
"RoleList",
"RoleBinding",
"RoleBindingList",
"Service",
"DaemonSet",
"Pod",
"ReplicationController",
"ReplicaSet",
"Deployment",
"HorizontalPodAutoscaler",
"StatefulSet",
"Job",
"CronJob",
"IngressClass",
"Ingress",
"APIService",
"MutatingWebhookConfiguration",
"ValidatingWebhookConfiguration",
}
// UninstallOrder is the order in which manifests should be uninstalled (by Kind).
//
// Those occurring earlier in the list get uninstalled before those occurring later in the list.
var UninstallOrder KindSortOrder = []string{
// For uninstall, we remove validation before mutation to ensure webhooks don't block removal
"ValidatingWebhookConfiguration",
"MutatingWebhookConfiguration",
"APIService",
"Ingress",
"IngressClass",
"Service",
"CronJob",
"Job",
"StatefulSet",
"HorizontalPodAutoscaler",
"Deployment",
"ReplicaSet",
"ReplicationController",
"Pod",
"DaemonSet",
"RoleBindingList",
"RoleBinding",
"RoleList",
"Role",
"ClusterRoleBindingList",
"ClusterRoleBinding",
"ClusterRoleList",
"ClusterRole",
"CustomResourceDefinition",
"PersistentVolumeClaim",
"PersistentVolume",
"StorageClass",
"ConfigMap",
"SecretList",
"Secret",
"ServiceAccount",
"PodDisruptionBudget",
"PodSecurityPolicy",
"LimitRange",
"ResourceQuota",
"NetworkPolicy",
"Namespace",
"PriorityClass",
}
// sort manifests by kind.
//
// Results are sorted by 'ordering', keeping order of items with equal kind/priority
func sortManifestsByKind(manifests []Manifest, ordering KindSortOrder) []Manifest {
sort.SliceStable(manifests, func(i, j int) bool {
return lessByKind(manifests[i], manifests[j], manifests[i].Head.Kind, manifests[j].Head.Kind, ordering)
})
return manifests
}
// sort hooks by kind, using an out-of-place sort to preserve the input parameters.
//
// Results are sorted by 'ordering', keeping order of items with equal kind/priority
func sortHooksByKind(hooks []*release.Hook, ordering KindSortOrder) []*release.Hook {
h := hooks
sort.SliceStable(h, func(i, j int) bool {
return lessByKind(h[i], h[j], h[i].Kind, h[j].Kind, ordering)
})
return h
}
func lessByKind(_ any, _ any, kindA string, kindB string, o KindSortOrder) bool {
ordering := make(map[string]int, len(o))
for v, k := range o {
ordering[k] = v
}
first, aok := ordering[kindA]
second, bok := ordering[kindB]
if !aok && !bok {
// if both are unknown then sort alphabetically by kind, keep original order if same kind
if kindA != kindB {
return kindA < kindB
}
return first < second
}
// unknown kind is last
if !aok {
return false
}
if !bok {
return true
}
// sort different kinds, keep original order if same priority
return first < second
}

@ -0,0 +1,347 @@
/*
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 util // import "helm.sh/helm/v4/internal/release/v2/util"
import (
"bytes"
"testing"
release "helm.sh/helm/v4/internal/release/v2"
)
func TestKindSorter(t *testing.T) {
manifests := []Manifest{
{
Name: "U",
Head: &SimpleHead{Kind: "IngressClass"},
},
{
Name: "E",
Head: &SimpleHead{Kind: "SecretList"},
},
{
Name: "i",
Head: &SimpleHead{Kind: "ClusterRole"},
},
{
Name: "I",
Head: &SimpleHead{Kind: "ClusterRoleList"},
},
{
Name: "j",
Head: &SimpleHead{Kind: "ClusterRoleBinding"},
},
{
Name: "J",
Head: &SimpleHead{Kind: "ClusterRoleBindingList"},
},
{
Name: "f",
Head: &SimpleHead{Kind: "ConfigMap"},
},
{
Name: "u",
Head: &SimpleHead{Kind: "CronJob"},
},
{
Name: "2",
Head: &SimpleHead{Kind: "CustomResourceDefinition"},
},
{
Name: "n",
Head: &SimpleHead{Kind: "DaemonSet"},
},
{
Name: "r",
Head: &SimpleHead{Kind: "Deployment"},
},
{
Name: "!",
Head: &SimpleHead{Kind: "HonkyTonkSet"},
},
{
Name: "v",
Head: &SimpleHead{Kind: "Ingress"},
},
{
Name: "t",
Head: &SimpleHead{Kind: "Job"},
},
{
Name: "c",
Head: &SimpleHead{Kind: "LimitRange"},
},
{
Name: "a",
Head: &SimpleHead{Kind: "Namespace"},
},
{
Name: "A",
Head: &SimpleHead{Kind: "NetworkPolicy"},
},
{
Name: "g",
Head: &SimpleHead{Kind: "PersistentVolume"},
},
{
Name: "h",
Head: &SimpleHead{Kind: "PersistentVolumeClaim"},
},
{
Name: "o",
Head: &SimpleHead{Kind: "Pod"},
},
{
Name: "3",
Head: &SimpleHead{Kind: "PodDisruptionBudget"},
},
{
Name: "C",
Head: &SimpleHead{Kind: "PodSecurityPolicy"},
},
{
Name: "q",
Head: &SimpleHead{Kind: "ReplicaSet"},
},
{
Name: "p",
Head: &SimpleHead{Kind: "ReplicationController"},
},
{
Name: "b",
Head: &SimpleHead{Kind: "ResourceQuota"},
},
{
Name: "k",
Head: &SimpleHead{Kind: "Role"},
},
{
Name: "K",
Head: &SimpleHead{Kind: "RoleList"},
},
{
Name: "l",
Head: &SimpleHead{Kind: "RoleBinding"},
},
{
Name: "L",
Head: &SimpleHead{Kind: "RoleBindingList"},
},
{
Name: "e",
Head: &SimpleHead{Kind: "Secret"},
},
{
Name: "m",
Head: &SimpleHead{Kind: "Service"},
},
{
Name: "d",
Head: &SimpleHead{Kind: "ServiceAccount"},
},
{
Name: "s",
Head: &SimpleHead{Kind: "StatefulSet"},
},
{
Name: "1",
Head: &SimpleHead{Kind: "StorageClass"},
},
{
Name: "w",
Head: &SimpleHead{Kind: "APIService"},
},
{
Name: "x",
Head: &SimpleHead{Kind: "HorizontalPodAutoscaler"},
},
{
Name: "F",
Head: &SimpleHead{Kind: "PriorityClass"},
},
{
Name: "M",
Head: &SimpleHead{Kind: "MutatingWebhookConfiguration"},
},
{
Name: "V",
Head: &SimpleHead{Kind: "ValidatingWebhookConfiguration"},
},
}
for _, test := range []struct {
description string
order KindSortOrder
expected string
}{
{"install", InstallOrder, "FaAbcC3deEf1gh2iIjJkKlLmnopqrxstuUvwMV!"},
{"uninstall", UninstallOrder, "VMwvUmutsxrqponLlKkJjIi2hg1fEed3CcbAaF!"},
} {
var buf bytes.Buffer
t.Run(test.description, func(t *testing.T) {
if got, want := len(test.expected), len(manifests); got != want {
t.Fatalf("Expected %d names in order, got %d", want, got)
}
defer buf.Reset()
orig := manifests
for _, r := range sortManifestsByKind(manifests, test.order) {
buf.WriteString(r.Name)
}
if got := buf.String(); got != test.expected {
t.Errorf("Expected %q, got %q", test.expected, got)
}
for i, manifest := range orig {
if manifest != manifests[i] {
t.Fatal("Expected input to sortManifestsByKind to stay the same")
}
}
})
}
}
// TestKindSorterKeepOriginalOrder verifies manifests of same kind are kept in original order
func TestKindSorterKeepOriginalOrder(t *testing.T) {
manifests := []Manifest{
{
Name: "a",
Head: &SimpleHead{Kind: "ClusterRole"},
},
{
Name: "A",
Head: &SimpleHead{Kind: "ClusterRole"},
},
{
Name: "0",
Head: &SimpleHead{Kind: "ConfigMap"},
},
{
Name: "1",
Head: &SimpleHead{Kind: "ConfigMap"},
},
{
Name: "z",
Head: &SimpleHead{Kind: "ClusterRoleBinding"},
},
{
Name: "!",
Head: &SimpleHead{Kind: "ClusterRoleBinding"},
},
{
Name: "u2",
Head: &SimpleHead{Kind: "Unknown"},
},
{
Name: "u1",
Head: &SimpleHead{Kind: "Unknown"},
},
{
Name: "t3",
Head: &SimpleHead{Kind: "Unknown2"},
},
}
for _, test := range []struct {
description string
order KindSortOrder
expected string
}{
// expectation is sorted by kind (unknown is last) and within each group of same kind, the order is kept
{"cm,clusterRole,clusterRoleBinding,Unknown,Unknown2", InstallOrder, "01aAz!u2u1t3"},
} {
var buf bytes.Buffer
t.Run(test.description, func(t *testing.T) {
defer buf.Reset()
for _, r := range sortManifestsByKind(manifests, test.order) {
buf.WriteString(r.Name)
}
if got := buf.String(); got != test.expected {
t.Errorf("Expected %q, got %q", test.expected, got)
}
})
}
}
func TestKindSorterNamespaceAgainstUnknown(t *testing.T) {
unknown := Manifest{
Name: "a",
Head: &SimpleHead{Kind: "Unknown"},
}
namespace := Manifest{
Name: "b",
Head: &SimpleHead{Kind: "Namespace"},
}
manifests := []Manifest{unknown, namespace}
manifests = sortManifestsByKind(manifests, InstallOrder)
expectedOrder := []Manifest{namespace, unknown}
for i, manifest := range manifests {
if expectedOrder[i].Name != manifest.Name {
t.Errorf("Expected %s, got %s", expectedOrder[i].Name, manifest.Name)
}
}
}
// test hook sorting with a small subset of kinds, since it uses the same algorithm as sortManifestsByKind
func TestKindSorterForHooks(t *testing.T) {
hooks := []*release.Hook{
{
Name: "i",
Kind: "ClusterRole",
},
{
Name: "j",
Kind: "ClusterRoleBinding",
},
{
Name: "c",
Kind: "LimitRange",
},
{
Name: "a",
Kind: "Namespace",
},
}
for _, test := range []struct {
description string
order KindSortOrder
expected string
}{
{"install", InstallOrder, "acij"},
{"uninstall", UninstallOrder, "jica"},
} {
var buf bytes.Buffer
t.Run(test.description, func(t *testing.T) {
if got, want := len(test.expected), len(hooks); got != want {
t.Fatalf("Expected %d names in order, got %d", want, got)
}
defer buf.Reset()
orig := hooks
for _, r := range sortHooksByKind(hooks, test.order) {
buf.WriteString(r.Name)
}
for i, hook := range orig {
if hook != hooks[i] {
t.Fatal("Expected input to sortHooksByKind to stay the same")
}
}
if got := buf.String(); got != test.expected {
t.Errorf("Expected %q, got %q", test.expected, got)
}
})
}
}

@ -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 util // import "helm.sh/helm/v4/internal/release/v2/util"
import (
"fmt"
"regexp"
"strconv"
"strings"
"unicode"
)
// SimpleHead defines what the structure of the head of a manifest file
type SimpleHead struct {
Version string `json:"apiVersion"`
Kind string `json:"kind,omitempty"`
Metadata *struct {
Name string `json:"name"`
Annotations map[string]string `json:"annotations"`
} `json:"metadata,omitempty"`
}
var sep = regexp.MustCompile("(?:^|\\s*\n)---\\s*")
// SplitManifests takes a manifest string and returns a map containing individual manifests.
//
// **Note for Chart API v3**: This function (due to the regex above) has allowed _WRONG_
// Go templates to be defined inside charts across the years. The generated text from Go
// templates may contain `---apiVersion: v1`, and this function magically splits this back
// to `---\napiVersion: v1`. This has caused issues recently after Helm 4 introduced
// kio.ParseAll to inject annotations when post-renderers are used. In Chart API v3,
// we should kill this regex with fire (or change it) and expose charts doing the wrong
// thing Go template-wise. Helm should say a big _NO_ to charts doing the wrong thing,
// with or without post-renderers.
func SplitManifests(bigFile string) map[string]string {
// Basically, we're quickly splitting a stream of YAML documents into an
// array of YAML docs. The file name is just a place holder, but should be
// integer-sortable so that manifests get output in the same order as the
// input (see `BySplitManifestsOrder`).
tpl := "manifest-%d"
res := map[string]string{}
// Making sure that any extra whitespace in YAML stream doesn't interfere in splitting documents correctly.
bigFileTmp := strings.TrimLeftFunc(bigFile, unicode.IsSpace)
docs := sep.Split(bigFileTmp, -1)
var count int
for _, d := range docs {
if strings.TrimSpace(d) == "" {
continue
}
d = strings.TrimLeftFunc(d, unicode.IsSpace)
res[fmt.Sprintf(tpl, count)] = d
count = count + 1
}
return res
}
// BySplitManifestsOrder sorts by in-file manifest order, as provided in function `SplitManifests`
type BySplitManifestsOrder []string
func (a BySplitManifestsOrder) Len() int { return len(a) }
func (a BySplitManifestsOrder) Less(i, j int) bool {
// Split `manifest-%d`
anum, _ := strconv.ParseInt(a[i][len("manifest-"):], 10, 0)
bnum, _ := strconv.ParseInt(a[j][len("manifest-"):], 10, 0)
return anum < bnum
}
func (a BySplitManifestsOrder) Swap(i, j int) { a[i], a[j] = a[j], a[i] }

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

Loading…
Cancel
Save