Merge branch 'helm:main' into main

pull/31847/head
Julian Siebert 3 months ago committed by GitHub
commit 52680abdc2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

4
.github/env vendored

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

@ -18,11 +18,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout source code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # pin@v6.0.2
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # pin@v7.0.0
- name: Add variables to environment file
run: cat ".github/env" >> "$GITHUB_ENV"
- name: Setup Go
uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # pin@6.2.0
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # pin@6.5.0
with:
go-version: '${{ env.GOLANG_VERSION }}'
check-latest: true

@ -24,14 +24,15 @@ on:
schedule:
- cron: '29 6 * * 6'
permissions:
contents: read
security-events: write
permissions: {}
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
contents: read
security-events: write
strategy:
fail-fast: false
@ -43,11 +44,11 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # pin@v6.0.2
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # pin@v7.0.0
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@e296a935590eb16afc0c0108289f68c87e2a89a5 # pinv4.30.7
uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # pinv4.36.2
with:
languages: ${{ matrix.language }}
# 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).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@e296a935590eb16afc0c0108289f68c87e2a89a5 # pinv4.30.7
uses: github/codeql-action/autobuild@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # pinv4.36.2
# ℹ️ Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
@ -72,4 +73,4 @@ jobs:
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@e296a935590eb16afc0c0108289f68c87e2a89a5 # pinv4.30.7
uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # pinv4.36.2

@ -13,15 +13,15 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # pin@v6.0.2
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # pin@v7.0.0
- name: Add variables to environment file
run: cat ".github/env" >> "$GITHUB_ENV"
- name: Setup Go
uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # pin@6.2.0
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # pin@6.5.0
with:
go-version: '${{ env.GOLANG_VERSION }}'
check-latest: true
- name: golangci-lint
uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20 #pin@9.2.0
uses: golangci/golangci-lint-action@ba0d7d2ec06a0ea1cb5fa41b2e4a3ab91d21278a #pin@9.3.0
with:
version: ${{ env.GOLANGCI_LINT_VERSION }}

@ -19,13 +19,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # pin@v6.0.2
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # pin@v7.0.0
with:
persist-credentials: false
- name: Add variables to environment file
run: cat ".github/env" >> "$GITHUB_ENV"
- name: Setup Go
uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # pin@6.2.0
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # pin@6.5.0
with:
go-version: '${{ env.GOLANG_VERSION }}'
check-latest: true

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

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

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

1
.gitignore vendored

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

@ -17,22 +17,34 @@ formatters:
linters:
default: none
# Keep sorted alphabetically
enable:
- bidichk
- contextcheck
- depguard
- dupl
- errorlint
- exhaustive
- fatcontext
- gocritic
- gomodguard
- govet
- ineffassign
- misspell
- modernize
- nakedret
- noctx
- nolintlint
- perfsprint
- revive
- sloglint
- staticcheck
- testifylint
- thelper
- unused
- usestdlibvars
- usetesting
- exhaustive
- whitespace
exclusions:
@ -44,16 +56,9 @@ linters:
- legacy
- 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
# Keep sorted alphabetically
settings:
depguard:
rules:
@ -67,6 +72,57 @@ linters:
dupl:
threshold: 400
errorlint:
# Check for plain type assertions and type switches.
asserts: false
# Check for plain error comparisons.
comparison: false
exhaustive:
default-signifies-exhaustive: true
gocritic:
disabled-checks:
- appendAssign
- appendCombine
- assignOp
- badCall
- commentedOutCode
- commentFormatting
- deferInLoop
- elseif
- emptyStringTest
- exposedSyncMutex
- filepathJoin
- httpNoBody
- hugeParam
- ifElseChain
- importShadow
- nestingReduce
- nilValReturn
- octalLiteral
- paramTypeCombine
- preferStringWriter
- ptrToRefParam
- rangeValCopy
- regexpSimplify
- singleCaseSwitch
- sloppyReassign
- sloppyLen
- sprintfQuotedString
- stringConcatSimplify
- stringsCompare
- stringXbytes
- todoCommentWithoutDetail
- tooManyResultsChecker
- typeAssertChain
- uncheckedInlineErr
- unnamedResult
- unnecessaryDefer
- unslice
- whyNoLint
enable-all: true
gomodguard:
blocked:
modules:
@ -74,8 +130,40 @@ linters:
recommendations:
- github.com/evanphx/json-patch/v5
exhaustive:
default-signifies-exhaustive: true
nolintlint:
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:
- encoded-compare
- equal-values
- expected-actual
- float-compare
- go-require
- 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:
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
## 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
make build # Build binary
make test # Run all tests (style + unit)
make test-unit # Unit tests only
make test-coverage # With coverage
make test-style # Linting
golangci-lint run # Direct linting
make test-style # Linting (wraps golangci-lint)
go test -run TestName # Specific test
```
## Code Structure
- `/cmd/helm/` - CLI entry point (Cobra-based)
- `/pkg/` - Public API
## Code structure
Major packages:
- `cmd/helm/` - CLI entry point, wires CLI flags to `pkg/cmd/` commands
- `pkg/` - Public API
- `action/` - Core operations (install, upgrade, rollback)
- `cmd/` - Cobra command implementations bridging CLI flags to `pkg/action/`
- `chart/v2/` - Stable chart format
- `engine/` - Template rendering (Go templates + Sprig)
- `kube/` - Kubernetes client abstraction layer
- `registry/` - OCI support
- `release/` - Release types and interfaces (`v1/`, `common/`)
- `repo/` - Chart repository indexing and interaction
- `storage/` - Release backends (Secrets/ConfigMaps/SQL)
- `/internal/` - Private implementation
- `internal/` - Private implementations
- `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
- Golden files in `testdata/` for complex output
- Mock Kubernetes clients for action tests
- All commits must include DCO sign-off: `git commit -s`
### Branching
- `main` - Helm v4 development
- `dev-v3` - Helm v3 stable (backport from main)
### Dependencies
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
- `github.com/spf13/cobra` - CLI framework
- `github.com/Masterminds/sprig` - Template functions
### Key Patterns
- **Actions**: Operations in `/pkg/action/` use shared Configuration
- **Dual Chart Support**: v2 (stable) in `/pkg/`, v3 (dev) in `/internal/`
- **Storage Abstraction**: Pluggable release storage backends
### Key patterns
- **Actions**: High-level operations live in `pkg/action/`, typically using a shared Configuration
- **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

@ -1,7 +1,7 @@
# Contributing Guidelines
The Helm project accepts contributions via GitHub pull requests. This document outlines the process
to help get your contribution accepted.
to help you get your contribution accepted.
## Reporting a Security Issue
@ -15,7 +15,7 @@ chance to try to fix the issue before it is exploited in the wild.
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 enhancement will still be applied until November 11th 2026. See the blog <https://helm.sh/blog/helm-4-released#helm-v3-support> for more details.
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.
@ -110,9 +110,9 @@ Whether you are a user or contributor, official support channels include:
- User: [#helm-users](https://kubernetes.slack.com/messages/C0NH30761/details/)
- Contributor: [#helm-dev](https://kubernetes.slack.com/messages/C51E88VDG/)
Before opening a new issue or submitting a new pull request, it's helpful to search the project -
it's likely that another user has already reported the issue you're facing, or it's a known issue
that we're already aware of. It is also worth asking on the Slack channels.
Before opening a new issue or submitting a new pull request, it helps to search the project first.
Another user may have already reported the issue you are facing, or it may already be a known issue.
It is also worth asking on the Slack channels.
## Milestones
@ -137,7 +137,7 @@ are removed or substantially modified (unless we need to fix a security issue).
We also remain committed to not changing publicly accessible Go library definitions inside of the `pkg/` directory of our source code in a non-backwards-compatible way.
For more details on Helm’s minor and patch release backwards-compatibility rules, please read [HIP-0004](https://github.com/helm/community/blob/main/hips/hip-0004.md)
For more details on Helm’s minor and patch release backwards-compatibility rules, please read [HIP-0004](https://github.com/helm/community/blob/main/hips/hip-0004.md).
For a quick summary of our backward compatibility guidelines for releases between 3.0 and 4.0:
@ -162,9 +162,9 @@ 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
Slack channel or have particular interest to the community as a whole. Depending on the
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
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
both have a label and an issue title of "Proposal: [the rest of the title]." A proposal can become
a `feature` and does not require a milestone.

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

@ -32,8 +32,8 @@ Think of it like apt/yum/homebrew for Kubernetes.
## Helm Development and Stable Versions
Helm v4 is currently under development on the `main` branch. This is unstable and the APIs within the Go SDK and at the command line are changing.
Helm v3 (current stable) is maintained on the `dev-v3` branch. APIs there follow semantic versioning.
Helm v4 is the current stable release, developed on the `main` branch.
Helm v3 is in support mode on the `dev-v3` branch: bug fixes until July 8th 2026, security fixes until November 11th 2026.
## Install
@ -64,7 +64,7 @@ Get started with the [Quick Start guide](https://helm.sh/docs/intro/quickstart/)
The [Helm roadmap uses GitHub milestones](https://github.com/helm/helm/milestones) to track the progress of the project.
The development of Helm v4 is currently happening on the `main` branch while the development of Helm v3, the stable branch, is happening on the `dev-v3` branch. Changes should be made to the `main` branch prior to being added to the `dev-v3` branch so that all changes are carried along to Helm v4.
Helm v4 development happens on the `main` branch. Helm v3 is in support mode on the `dev-v3` branch and receives only bug and security fixes.
## Community, discussion, contribution, and support

@ -18,13 +18,13 @@ package main
import (
"bytes"
"errors"
"os"
"os/exec"
"runtime"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestCliPluginExitCode(t *testing.T) {
@ -46,7 +46,7 @@ func TestCliPluginExitCode(t *testing.T) {
// So that the second run is able to run main() and this first run can verify the exit status returned by that.
//
// This technique originates from https://talks.golang.org/2014/testing.slide#23.
cmd := exec.Command(os.Args[0], "-test.run=TestCliPluginExitCode")
cmd := exec.CommandContext(t.Context(), os.Args[0], "-test.run=TestCliPluginExitCode")
cmd.Env = append(
os.Environ(),
"RUN_MAIN_FOR_TESTING=1",
@ -62,20 +62,13 @@ func TestCliPluginExitCode(t *testing.T) {
err := cmd.Run()
exiterr := &exec.ExitError{}
ok := errors.As(err, &exiterr)
if !ok {
t.Fatalf("Unexpected error type returned by os.Exit: %T", err)
}
require.ErrorAs(t, err, &exiterr)
assert.Empty(t, stdout.String())
expectedStderr := "Error: plugin \"exitwith\" exited with error\n"
if stderr.String() != expectedStderr {
t.Errorf("Expected %q written to stderr: Got %q", expectedStderr, stderr.String())
}
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"
assert.Equal(t, expectedStderr, stderr.String())
if exiterr.ExitCode() != 43 {
t.Errorf("Expected exit code 43: Got %d", exiterr.ExitCode())
}
assert.Equal(t, 43, exiterr.ExitCode())
}
}

154
go.mod

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

330
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/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
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.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE=
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/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0=
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/vcs v1.13.3 h1:IIA2aBdXvfbIM+yl/eTnL4hb1XwdpvuQLglAix1gweE=
github.com/Masterminds/vcs v1.13.3/go.mod h1:TiE7xuEjl1N4j016moRd6vezp6e6Lz23gypeXfzXeW8=
github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw=
github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
github.com/ProtonMail/go-crypto v1.4.1 h1:9RfcZHqEQUvP8RzecWEUafnZVtEvrBVL9BiF67IQOfM=
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/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/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
@ -34,65 +36,65 @@ 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/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
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.0.0/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk=
github.com/bshuster-repo/logrus-logstash-hook v1.1.0 h1:o2FzZifLg+z/DN1OFmzTWzZZx/roaqt8IPZCIVco8r4=
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.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
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.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
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/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
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.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chai2010/gettext-go v1.0.2 h1:1Lwwip6Q2QGsAdl/ZKPCwTe9fe0CjlUbqj5bFNSjIRk=
github.com/chai2010/gettext-go v1.0.2/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA=
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
github.com/coreos/go-systemd/v22 v22.7.0 h1:LAEzFkke61DFROc7zNLX/WA2i5J8gYqe0rSj9KI28KA=
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/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE=
github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc=
github.com/cyphar/filepath-securejoin v0.7.0 h1:s0Y3ITPy6sQn5xt54DuYvTF8hu134ooYLUb58DX/HjE=
github.com/cyphar/filepath-securejoin v0.7.0/go.mod h1:ymLGms/u3BYaviIiuKFnUx8EkQEZeK6cInNoAPJA3o4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
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/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/distribution/distribution/v3 v3.0.0 h1:q4R8wemdRQDClzoNNStftB2ZAfqOiN6UX90KJc4HjyM=
github.com/distribution/distribution/v3 v3.0.0/go.mod h1:tRNuFoZsUdyRVegq8xGNeds4KLjwLCRin/tTo6i1DhU=
github.com/distribution/distribution/v3 v3.1.1 h1:KUbk7C8CfaLXy8kbf/hGq9cad/wCoLB6dbWH6DMbmX0=
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/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/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo=
github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M=
github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c h1:+pKlWGMw7gf6bQ+oDZB4KHQFypsfjYlq/C4rfL7D3g8=
github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA=
github.com/docker/docker-credential-helpers v0.9.5 h1:EFNN8DHvaiK8zVqFA2DT6BjXE0GzfLOZ38ggPTKePkY=
github.com/docker/docker-credential-helpers v0.9.5/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c=
github.com/docker/go-events v0.0.0-20250808211157-605354379745 h1:yOn6Ze6IbYI/KAw2lw/83ELYvZh6hvsygTVkD0dzMC4=
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/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw=
github.com/dylibso/observe-sdk/go v0.0.0-20240819160327-2d926c5d788a h1:UwSIFv5g5lIvbGgtf3tVwC7Ky9rmMFBp0RMs+6f6YqE=
github.com/dylibso/observe-sdk/go v0.0.0-20240819160327-2d926c5d788a/go.mod h1:C8DzXehI4zAbrdlbtOByKX6pfivJTBiV9Jjqv56Yd9Q=
github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU=
github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes=
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/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM=
github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f h1:Wl78ApPPB2Wvf/TIe2xdyJxTlb6obmF18d8QdkxNDu4=
github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f/go.mod h1:OSYXu++VVOHnXeitef/D8n/6y4QV8uLHSFXX4NeXMGc=
github.com/extism/go-sdk v1.7.1 h1:lWJos6uY+tRFdlIHR+SJjwFDApY7OypS/2nMhiVQ9Sw=
github.com/extism/go-sdk v1.7.1/go.mod h1:IT+Xdg5AZM9hVtpFUA+uZCJMge/hbvshl8bwzLtFyKA=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
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/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fluxcd/cli-utils v0.37.1-flux.1 h1:WnG2mHxCPZMj/soIq/S/1zvbrGCJN3GJGbNfG06X55M=
github.com/fluxcd/cli-utils v0.37.1-flux.1/go.mod h1:aND5wX3LuTFtB7eUT7vsWr8mmxRVSPR2Wkvbn0SqPfw=
github.com/fluxcd/cli-utils v1.2.2 h1:adDOmwE+LSwTzmYUaoEFPblruOuaQEKAg1ZNTmPJObE=
github.com/fluxcd/cli-utils v1.2.2/go.mod h1:FsghNGY+3Sr70c0FOB7I5So0kzoYVdvQ8GTid3XXVWM=
github.com/foxcpp/go-mockdns v1.2.0 h1:omK3OrHRD1IWJz1FuFBCFquhXslXoF17OvBS6JPzZF0=
github.com/foxcpp/go-mockdns v1.2.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
@ -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/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=
github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
@ -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/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
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-20250630185457-6e76a2b096b5/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA=
github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 h1:z2ogiKUYzX5Is6zr/vP9vJGqPwcdqsWjOt+V8J7+bTc=
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/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
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/gosuri/uitable v0.0.4 h1:IG2xLKRvErL3uhY6e1BylFzG+aJiwQviDDTfOKeKTpY=
github.com/gosuri/uitable v0.0.4/go.mod h1:tKR86bXuXPZazfOTG1FIzvjIdXzd0mo4Vtn16vt0PJo=
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA=
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c=
github.com/hashicorp/golang-lru/arc/v2 v2.0.5 h1:l2zaLDubNhW4XO3LnliVj0GXO3+/CGNJAg1dcN2Fpfw=
github.com/hashicorp/golang-lru/arc/v2 v2.0.5/go.mod h1:ny6zBSQZi2JxIeYcv7kt2sH2PXJtirBN7RDhRpxPkxU=
github.com/hashicorp/golang-lru/v2 v2.0.5 h1:wW7h1TG88eUIJ2i69gaE3uNVtEPIagzhGvHgwfx2Vm4=
@ -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/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/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
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/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
@ -192,21 +191,20 @@ github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtB
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/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lib/pq v1.11.2 h1:x6gxUeu39V0BHZiugWe8LXZYZ+Utk7hSJGThs8sdzfs=
github.com/lib/pq v1.11.2/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
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/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE=
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk=
github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
github.com/mattn/go-shellwords v1.0.13 h1:DC0OMEpGjm6LfNFU4ckYcvbQKyp2vE8atyFGXNtDcf4=
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/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
@ -233,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/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
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.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo=
github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A=
github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k=
github.com/onsi/ginkgo/v2 v2.28.1 h1:S4hj+HbZp40fNKuLUQOYLDgZLwNUVn19N3Atb98NCyI=
github.com/onsi/ginkgo/v2 v2.28.1/go.mod h1:CLtbVInNckU3/+gC8LzkGUb9oF+e8W8TdUsxPwvdOgE=
github.com/onsi/gomega v1.42.1 h1:iN1rCUX+44NZ1Dc97MPoeFYbFR0vh8zxoxMFwKdyZ6I=
github.com/onsi/gomega v1.42.1/go.mod h1:REff/hsDsodHoKlWsP2mAPhu1+5/6hVYNf9rIEBpeSg=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
@ -260,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/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.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
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.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
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.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw=
github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc=
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/go.mod h1:fyalQWdtzDBECAQFBJuQe5bzQ02jGd5Qcbgb97Flm7U=
github.com/redis/go-redis/extra/redisotel/v9 v9.0.5 h1:EfpWLLCyXw8PSM2/XNJLjI3Pb27yVE+gIAfeqp8LUCc=
@ -287,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/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
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.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
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/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
@ -308,65 +309,69 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834 h1:ZF+QBjOI+tILZjBaFj3HgFonKXUcwgJ4djLb6i42S3Q=
github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834/go.mod h1:m9ymHTgNSEjuxvw8E7WWe4Pl4hZQHXONY8wE6dMLaRk=
github.com/tetratelabs/wazero v1.11.0 h1:+gKemEuKCTevU4d7ZTzlsvgd1uaToIDtlQlmNbwqYhA=
github.com/tetratelabs/wazero v1.11.0/go.mod h1:eV28rsN8Q+xwjogd7f4/Pp4xFxO7uOGbLcD/LzB1wiU=
github.com/tetratelabs/wazero v1.12.0 h1:DuWcpNu/FzgEXgGBDp8J1Spc+CWOvvtvVyjKlaZopYU=
github.com/tetratelabs/wazero v1.12.0/go.mod h1:LvKtzl2RqO4gyF27BiXU+nKAjcV8f38U+kP/q2vgxh0=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ=
github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/contrib/bridges/prometheus v0.57.0 h1:UW0+QyeyBVhn+COBec3nGhfnFe5lwB0ic1JBVjzhk0w=
go.opentelemetry.io/contrib/bridges/prometheus v0.57.0/go.mod h1:ppciCHRLsyCio54qbzQv0E4Jyth/fLWDTJYfvWpcSVk=
go.opentelemetry.io/contrib/exporters/autoexport v0.57.0 h1:jmTVJ86dP60C01K3slFQa2NQ/Aoi7zA+wy7vMOKD9H4=
go.opentelemetry.io/contrib/exporters/autoexport v0.57.0/go.mod h1:EJBheUMttD/lABFyLXhce47Wr6DPWYReCzaZiXadH7g=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.8.0 h1:WzNab7hOOLzdDF/EoWCt4glhrbMPVMOO5JYTmpz36Ls=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.8.0/go.mod h1:hKvJwTzJdp90Vh7p6q/9PAOd55dI6WA6sWj62a/JvSs=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.8.0 h1:S+LdBGiQXtJdowoJoQPEtI52syEP/JYBUpjO49EQhV8=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.8.0/go.mod h1:5KXybFvPGds3QinJWQT7pmXf+TN5YIa7CNYObWRkj50=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.32.0 h1:j7ZSD+5yn+lo3sGV69nW04rRR0jhYnBwjuX3r0HvnK0=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.32.0/go.mod h1:WXbYJTUaZXAbYd8lbgGuvih0yuCfOFC5RJoYnoLcGz8=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.32.0 h1:t/Qur3vKSkUCcDVaSumWF2PKHt85pc7fRvFuoVT8qFU=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.32.0/go.mod h1:Rl61tySSdcOJWoEgYZVtmnKdA0GeKrSqkHC1t+91CH8=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0/go.mod h1:7Bept48yIeqxP2OZ9/AqIpYS94h2or0aB4FypJTc8ZM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 h1:tgJ0uaNS4c98WRNUEx5U3aDlrDOI5Rs+1Vifcw4DJ8U=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0/go.mod h1:U7HYyW0zt/a9x5J1Kjs+r1f/d4ZHnYFclhYY2+YbeoE=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0 h1:cMyu9O88joYEaI47CnQkxO1XZdpoTF9fEnW2duIddhw=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0/go.mod h1:6Am3rn7P9TVVeXYG+wtcGE7IE1tsQ+bP3AuWcKt/gOI=
go.opentelemetry.io/otel/exporters/prometheus v0.54.0 h1:rFwzp68QMgtzu9PgP3jm9XaMICI6TsofWWPcBDKwlsU=
go.opentelemetry.io/otel/exporters/prometheus v0.54.0/go.mod h1:QyjcV9qDP6VeK5qPyKETvNjmaaEc7+gqjh4SS0ZYzDU=
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.8.0 h1:CHXNXwfKWfzS65yrlB2PVds1IBZcdsX8Vepy9of0iRU=
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.8.0/go.mod h1:zKU4zUgKiaRxrdovSS2amdM5gOc59slmo/zJwGX+YBg=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.32.0 h1:SZmDnHcgp3zwlPBS2JX2urGYe/jBKEIT6ZedHRUyCz8=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.32.0/go.mod h1:fdWW0HtZJ7+jNpTKUR0GpMEDP69nR8YBJQxNiVCE3jk=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.32.0 h1:cC2yDI3IQd0Udsux7Qmq8ToKAx1XCilTQECZ0KDZyTw=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.32.0/go.mod h1:2PD5Ex6z8CFzDbTdOlwyNIUywRr1DN0ospafJM1wJ+s=
go.opentelemetry.io/otel/log v0.8.0 h1:egZ8vV5atrUWUbnSsHn6vB8R21G2wrKqNiDt3iWertk=
go.opentelemetry.io/otel/log v0.8.0/go.mod h1:M9qvDdUTRCopJcGRKg57+JSQ9LgLBrwwfC32epk5NX8=
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs=
go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY=
go.opentelemetry.io/otel/sdk/log v0.8.0 h1:zg7GUYXqxk1jnGF/dTdLPrK06xJdrXgqgFLnI4Crxvs=
go.opentelemetry.io/otel/sdk/log v0.8.0/go.mod h1:50iXr0UVwQrYS45KbruFrEt4LvAdCaWWgIrsN3ZQggo=
go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis=
go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4=
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4=
go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4=
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/bridges/prometheus v0.67.0 h1:dkBzNEAIKADEaFnuESzcXvpd09vxvDZsOjx11gjUqLk=
go.opentelemetry.io/contrib/bridges/prometheus v0.67.0/go.mod h1:Z5RIwRkZgauOIfnG5IpidvLpERjhTninpP1dTG2jTl4=
go.opentelemetry.io/contrib/exporters/autoexport v0.67.0 h1:4fnRcNpc6YFtG3zsFw9achKn3XgmxPxuMuqIL5rE8e8=
go.opentelemetry.io/contrib/exporters/autoexport v0.67.0/go.mod h1:qTvIHMFKoxW7HXg02gm6/Wofhq5p3Ib/A/NNt1EoBSQ=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg=
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.18.0 h1:deI9UQMoGFgrg5iLPgzueqFPHevDl+28YKfSpPTI6rY=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.18.0/go.mod h1:PFx9NgpNUKXdf7J4Q3agRxMs3Y07QhTCVipKmLsMKnU=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0 h1:HIBTQ3VO5aupLKjC90JgMqpezVXwFuq6Ryjn0/izoag=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0/go.mod h1:ji9vId85hMxqfvICA0Jt8JqEdrXaAkcpkI9HPXya0ro=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.42.0 h1:MdKucPl/HbzckWWEisiNqMPhRrAOQX8r4jTuGr636gk=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.42.0/go.mod h1:RolT8tWtfHcjajEH5wFIZ4Dgh5jpPdFXYV9pTAk/qjc=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0 h1:w1K+pCJoPpQifuVpsKamUdn9U0zM3xUziVOqsGksUrY=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0/go.mod h1:HBy4BjzgVE8139ieRI75oXm3EcDN+6GhD88JT1Kjvxg=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0 h1:zWWrB1U6nqhS/k6zYB74CjRpuiitRtLLi68VcgmOEto=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0/go.mod h1:2qXPNBX1OVRC0IwOnfo1ljoid+RD0QK3443EaqVlsOU=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 h1:3iZJKlCZufyRzPzlQhUIWVmfltrXuGyfjREgGP3UUjc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0/go.mod h1:/G+nUPfhq2e+qiXMGxMwumDrP5jtzU+mWN7/sjT2rak=
go.opentelemetry.io/otel/exporters/prometheus v0.64.0 h1:g0LRDXMX/G1SEZtK8zl8Chm4K6GBwRkjPKE36LxiTYs=
go.opentelemetry.io/otel/exporters/prometheus v0.64.0/go.mod h1:UrgcjnarfdlBDP3GjDIJWe6HTprwSazNjwsI+Ru6hro=
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.18.0 h1:KJVjPD3rcPb98rIs3HznyJlrfx9ge5oJvxxlGR+P/7s=
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.18.0/go.mod h1:K3kRa2ckmHWQaTWQdPRHc7qGXASuVuoEQXzrvlA98Ws=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.42.0 h1:lSZHgNHfbmQTPfuTmWVkEu8J8qXaQwuV30pjCcAUvP8=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.42.0/go.mod h1:so9ounLcuoRDu033MW/E0AD4hhUjVqswrMF5FoZlBcw=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.42.0 h1:s/1iRkCKDfhlh1JF26knRneorus8aOwVIDhvYx9WoDw=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.42.0/go.mod h1:UI3wi0FXg1Pofb8ZBiBLhtMzgoTm1TYkMvn71fAqDzs=
go.opentelemetry.io/otel/log v0.19.0 h1:KUZs/GOsw79TBBMfDWsXS+KZ4g2Ckzksd1ymzsIEbo4=
go.opentelemetry.io/otel/log v0.19.0/go.mod h1:5DQYeGmxVIr4n0/BcJvF4upsraHjg6vudJJpnkL6Ipk=
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
go.opentelemetry.io/otel/sdk/log v0.19.0 h1:scYVLqT22D2gqXItnWiocLUKGH9yvkkeql5dBDiXyko=
go.opentelemetry.io/otel/sdk/log v0.19.0/go.mod h1:vFBowwXGLlW9AvpuF7bMgnNI95LiW10szrOdvzBHlAg=
go.opentelemetry.io/otel/sdk/log/logtest v0.19.0 h1:BEbF7ZBB6qQloV/Ub1+3NQoOUnVtcGkU3XX4Ws3GQfk=
go.opentelemetry.io/otel/sdk/log/logtest v0.19.0/go.mod h1:Lua81/3yM0wOmoHTokLj9y9ADeA02v1naRrVrkAZuKk=
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
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/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
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/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
@ -377,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.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
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-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@ -395,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.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ=
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
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-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -407,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.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.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
golang.org/x/sync v0.21.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-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-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-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-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
golang.org/x/sys v0.46.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-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@ -435,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.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
golang.org/x/term v0.14.0/go.mod h1:TySc+nGkYR6qt8km8wUhuFRTVSMIX3XPR58y2lC8vww=
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc=
golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
@ -444,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.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.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
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-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.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.15.0/go.mod h1:hpksKq4dtpQWS1uQ61JkdqWM3LscIS6Slf+VVkm+wQk=
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
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=
google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:jbe3Bkdp+Dh2IrslsFCklNhweNTBgSYanP1UXhJDhKg=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a h1:v2PbRU4K3llS09c7zodFpNePeamkAwG3mPrAery9VeE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/grpc v1.72.2 h1:TdbGzwb82ty4OusHWepvFWGLgIbNo1/SUynEN0ssqv8=
google.golang.org/grpc v1.72.2/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA=
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 h1:m8qni9SQFH0tJc1X0vmnpw/0t+AImlSvp30sEupozUg=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=
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/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
@ -479,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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
k8s.io/api v0.35.1 h1:0PO/1FhlK/EQNVK5+txc4FuhQibV25VLSdLMmGpDE/Q=
k8s.io/api v0.35.1/go.mod h1:28uR9xlXWml9eT0uaGo6y71xK86JBELShLy4wR1XtxM=
k8s.io/apiextensions-apiserver v0.35.1 h1:p5vvALkknlOcAqARwjS20kJffgzHqwyQRM8vHLwgU7w=
k8s.io/apiextensions-apiserver v0.35.1/go.mod h1:2CN4fe1GZ3HMe4wBr25qXyJnJyZaquy4nNlNmb3R7AQ=
k8s.io/apimachinery v0.35.1 h1:yxO6gV555P1YV0SANtnTjXYfiivaTPvCTKX6w6qdDsU=
k8s.io/apimachinery v0.35.1/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns=
k8s.io/apiserver v0.35.1 h1:potxdhhTL4i6AYAa2QCwtlhtB1eCdWQFvJV6fXgJzxs=
k8s.io/apiserver v0.35.1/go.mod h1:BiL6Dd3A2I/0lBnteXfWmCFobHM39vt5+hJQd7Lbpi4=
k8s.io/cli-runtime v0.35.1 h1:uKcXFe8J7AMAM4Gm2JDK4mp198dBEq2nyeYtO+JfGJE=
k8s.io/cli-runtime v0.35.1/go.mod h1:55/hiXIq1C8qIJ3WBrWxEwDLdHQYhBNRdZOz9f7yvTw=
k8s.io/client-go v0.35.1 h1:+eSfZHwuo/I19PaSxqumjqZ9l5XiTEKbIaJ+j1wLcLM=
k8s.io/client-go v0.35.1/go.mod h1:1p1KxDt3a0ruRfc/pG4qT/3oHmUj1AhSHEcxNSGg+OA=
k8s.io/component-base v0.35.1 h1:XgvpRf4srp037QWfGBLFsYMUQJkE5yMa94UsJU7pmcE=
k8s.io/component-base v0.35.1/go.mod h1:HI/6jXlwkiOL5zL9bqA3en1Ygv60F03oEpnuU1G56Bs=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE=
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ=
k8s.io/kubectl v0.35.1 h1:zP3Er8C5i1dcAFUMh9Eva0kVvZHptXIn/+8NtRWMxwg=
k8s.io/kubectl v0.35.1/go.mod h1:cQ2uAPs5IO/kx8R5s5J3Ihv3VCYwrx0obCXum0CvnXo=
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck=
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
oras.land/oras-go/v2 v2.6.0 h1:X4ELRsiGkrbeox69+9tzTu492FMUu7zJQW6eJU+I2oc=
oras.land/oras-go/v2 v2.6.0/go.mod h1:magiQDfG6H1O9APp+rOsvCPcW1GD2MM7vgnKY0Y+u1o=
sigs.k8s.io/controller-runtime v0.23.1 h1:TjJSM80Nf43Mg21+RCy3J70aj/W6KyvDtOlpKf+PupE=
sigs.k8s.io/controller-runtime v0.23.1/go.mod h1:B6COOxKptp+YaUT5q4l6LqUJTRpizbgf9KSRNdQGns0=
k8s.io/api v0.36.2 h1:TF6YDLIzKfccK7cq9YpTcGX8TJmEkHVRv78DM51fRYY=
k8s.io/api v0.36.2/go.mod h1:F4LbMO4brjZYh7yFkXWhynSvtB7YauxV4c+HHkNRGNg=
k8s.io/apiextensions-apiserver v0.36.2 h1:3O5gqOj/dt2XWWbpMe+TXWpE9yU6pjM/tXxtHHJT/K4=
k8s.io/apiextensions-apiserver v0.36.2/go.mod h1:cL1tBWe8XSaP1H30iWKGo7hf6iAUUUJPEU70dskmAnA=
k8s.io/apimachinery v0.36.2 h1:0PE/W/WNy1UX61NLbXY5TMbJ6UwLL6E6lAPkYrKFxbQ=
k8s.io/apimachinery v0.36.2/go.mod h1:fvf/HOLXq9RId0rnDIbN1OEBvHXdQbLMM8nu0LcBUf4=
k8s.io/apiserver v0.36.2 h1:6vMnkmHZPeBloNkHUhmZYq7Ylv8WIB8xjyEl+eSt26E=
k8s.io/apiserver v0.36.2/go.mod h1:9PoQ2ikCytrZyZg11mGhLEF5m8Rgsb5FJmYJ4Wvnl1k=
k8s.io/cli-runtime v0.36.2 h1:CconTvEeV4DJs4ZX3HQKCFbFRGsm6OtuBM9yjmMP2VM=
k8s.io/cli-runtime v0.36.2/go.mod h1:LddcjiMf4YlnHO7c1Y7rEtDqL84FyiYVLco7V679GUU=
k8s.io/client-go v0.36.2 h1:bfgxmFKc9CgqsgX4xKLAAdmTQlWee7Ob/HlDOrJ5TBI=
k8s.io/client-go v0.36.2/go.mod h1:1vgO4OAlfPnoLcb+Rze2GF5rAr14w8qjrYMoyXJzQj0=
k8s.io/component-base v0.36.2 h1:Z0VH80O7Ng0HDZnZj3WRR3urEGa0kTwmO8CwEwjVK1w=
k8s.io/component-base v0.36.2/go.mod h1:mGfFOA7Gwpdm1VW2cwSQYbiDIlz8GD2WGwH88QSeCyA=
k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc=
k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0=
k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a h1:xCeOEAOoGYl2jnJoHkC3hkbPJgdATINPMAxaynU2Ovg=
k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a/go.mod h1:uGBT7iTA6c6MvqUvSXIaYZo9ukscABYi2btjhvgKGZ0=
k8s.io/kubectl v0.36.2 h1:rpUGGpeL09XVOLep2yle5jrtk//JA1L6ZHfkQQtVEwk=
k8s.io/kubectl v0.36.2/go.mod h1:gVbQ3B/yb4bSR2ggQ7rd0W6icUSWs7sduH4e16Vii+0=
k8s.io/utils v0.0.0-20260507154919-ff6756f316d2 h1:wU4tMEhLGgIbLvXQb1cfN+EcM0wf7zC6CPF+C79jroc=
k8s.io/utils v0.0.0-20260507154919-ff6756f316d2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk=
oras.land/oras-go/v2 v2.6.1 h1:bonOEkjLfp8tt6qXWRRWP6p1F+9octchOf2EqnWB4Zs=
oras.land/oras-go/v2 v2.6.1/go.mod h1:dhtFrFOuZuDtAVeZ9FUnaa5zfzplG3ZnFX9/uH1J/Yk=
sigs.k8s.io/controller-runtime v0.24.1 h1:miPEwrmirImAvgME1L9qebGHrOnGJoVmVdtOU9fRfo4=
sigs.k8s.io/controller-runtime v0.24.1/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/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
sigs.k8s.io/kustomize/api v0.20.1 h1:iWP1Ydh3/lmldBnH/S5RXgT98vWYMaTUL1ADcr+Sv7I=
sigs.k8s.io/kustomize/api v0.20.1/go.mod h1:t6hUFxO+Ph0VxIk1sKp1WS0dOjbPCtLJ4p8aADLwqjM=
sigs.k8s.io/kustomize/api v0.21.1 h1:lzqbzvz2CSvsjIUZUBNFKtIMsEw7hVLJp0JeSIVmuJs=
sigs.k8s.io/kustomize/api v0.21.1/go.mod h1:f3wkKByTrgpgltLgySCntrYoq5d3q7aaxveSagwTlwI=
sigs.k8s.io/kustomize/kyaml v0.21.1 h1:IVlbmhC076nf6foyL6Taw4BkrLuEsXUXNpsE+ScX7fI=
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/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 h1:2WOzJpHUBVrrkDjU4KBT8n5LDcj824eX0I5UKcgeRUs=
sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
sigs.k8s.io/structured-merge-diff/v6 v6.3.2 h1:kwVWMx5yS1CrnFWA/2QHyRVJ8jM6dBA80uLmm0wJkk8=
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/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=

@ -49,7 +49,7 @@ type Chart struct {
// Schema is an optional JSON schema for imposing structure on Values
Schema []byte `json:"schema"`
// 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,
// e.g. README, LICENSE, etc.
Files []*common.File `json:"files"`
@ -140,6 +140,7 @@ func (ch *Chart) AppVersion() string {
}
// CRDs returns a list of File objects in the 'crds/' directory of a Helm chart.
//
// Deprecated: use CRDObjects()
func (ch *Chart) CRDs() []*common.File {
files := []*common.File{}

@ -59,7 +59,7 @@ func TestCRDs(t *testing.T) {
is := assert.New(t)
crds := chrt.CRDs()
is.Equal(2, len(crds))
is.Len(crds, 2)
is.Equal("crds/foo.yaml", crds[0].Name)
is.Equal("crds/foo/bar/baz.yaml", crds[1].Name)
}
@ -104,7 +104,7 @@ func TestMetadata(t *testing.T) {
is.Equal("foo.yaml", chrt.Name())
is.Equal("1.0.0", chrt.AppVersion())
is.Equal(nil, chrt.Validate())
is.NoError(chrt.Validate())
}
func TestIsRoot(t *testing.T) {
@ -124,8 +124,8 @@ func TestIsRoot(t *testing.T) {
is := assert.New(t)
is.Equal(false, chrt1.IsRoot())
is.Equal(true, chrt2.IsRoot())
is.False(chrt1.IsRoot())
is.True(chrt2.IsRoot())
}
func TestChartPath(t *testing.T) {

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

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

@ -152,7 +152,7 @@ func validateChartVersion(cf *chart.Metadata) error {
valid, msg := c.Validate(version)
if !valid && len(msg) > 0 {
return fmt.Errorf("version %v", msg[0])
return fmt.Errorf("version %w", msg[0])
}
return nil

@ -50,26 +50,26 @@ func TestValidateChartYamlNotDirectory(t *testing.T) {
err := validateChartYamlNotDirectory(nonExistingChartFilePath)
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) {
err := validateChartYamlFormat(errors.New("Read error"))
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)
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) {
err := validateChartName(badChart)
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)
@ -149,7 +149,7 @@ func TestValidateChartMaintainer(t *testing.T) {
badChart.Maintainers = []*chart.Maintainer{nil}
err := validateChartMaintainer(badChart)
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" {
t.Errorf("validateChartMaintainer returned unexpected error for nil maintainer: %s", err.Error())
@ -185,7 +185,7 @@ func TestValidateChartIconPresence(t *testing.T) {
err := validateChartIconPresence(testChart)
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") {
t.Errorf("expected %q, got %q", "icon is recommended", err.Error())
}

@ -19,7 +19,6 @@ package rules
import (
"bytes"
"errors"
"fmt"
"io"
"io/fs"
"os"
@ -102,14 +101,14 @@ func validateCrdsDir(crdsPath string) error {
func validateCrdAPIVersion(obj *k8sYamlStruct) error {
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
}
func validateCrdKind(obj *k8sYamlStruct) error {
if obj.Kind != "CustomResourceDefinition" {
return fmt.Errorf("object kind is not 'CustomResourceDefinition'")
return errors.New("object kind is not 'CustomResourceDefinition'")
}
return nil
}

@ -62,5 +62,5 @@ metadata:
linter := support.Linter{ChartDir: chartDir}
Crds(&linter)
assert.Len(t, linter.Messages, 0)
assert.Empty(t, linter.Messages)
}

@ -72,7 +72,7 @@ func TestValidateDependencyInMetadata(t *testing.T) {
c := chartWithBadDependencies()
if err := validateDependencyInMetadata(&c); err == nil {
t.Errorf("chart should have been flagged for missing deps in chart metadata")
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 {
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")
}
}
}

@ -28,7 +28,7 @@ import (
kscheme "k8s.io/client-go/kubernetes/scheme"
)
// deprecatedAPIError indicates than an API is deprecated in Kubernetes
// deprecatedAPIError indicates that an API is deprecated in Kubernetes
type deprecatedAPIError struct {
Deprecated string
Message string

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

@ -19,6 +19,7 @@ package rules
import (
"bufio"
"bytes"
"context"
"errors"
"fmt"
"io"
@ -28,8 +29,8 @@ import (
"slices"
"strings"
"k8s.io/apimachinery/pkg/api/validate/content"
"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/yaml"
@ -104,7 +105,7 @@ func TemplatesWithSkipSchemaValidation(linter *support.Linter, values map[string
}
var e engine.Engine
e.LintMode = true
renderedContentMap, err := e.Render(chart, valuesToRender)
renderedContentMap, err := e.RenderWithContext(context.Background(), chart, valuesToRender)
renderOk := linter.RunLinterRule(support.ErrorSev, fpath, err)
@ -292,7 +293,7 @@ func validateMetadataNameFunc(obj *k8sYamlStruct) validation.ValidateNameFunc {
case "role", "clusterrole", "rolebinding", "clusterrolebinding":
// https://github.com/kubernetes/kubernetes/blob/v1.20.0/pkg/apis/rbac/validation/validation.go#L32-L34
return func(name string, _ bool) []string {
return apipath.IsValidPathSegmentName(name)
return content.IsPathSegmentName(name)
}
default:
return validation.NameIsDNSSubdomain

@ -242,7 +242,6 @@ data:
//
// See https://github.com/helm/helm/issues/7483
func TestStrictTemplateParsingMapError(t *testing.T) {
ch := chart.Chart{
Metadata: &chart.Metadata{
Name: "regression7483",
@ -371,7 +370,6 @@ func TestValidateTopIndentLevel(t *testing.T) {
t.Errorf("Expected %t for %q", shouldFail, doc)
}
}
}
// TestEmptyWithCommentsManifests checks the lint is not failing against empty manifests that contains only comments
@ -463,5 +461,4 @@ func TestIsYamlFileExtension(t *testing.T) {
t.Errorf("isYamlFileExtension(%s) = %v; want %v", test.filename, result, test.expected)
}
}
}

@ -17,6 +17,7 @@ limitations under the License.
package rules
import (
"errors"
"fmt"
"os"
"path/filepath"
@ -47,7 +48,7 @@ func ValuesWithOverrides(linter *support.Linter, valueOverrides map[string]any,
func validateValuesFileExistence(valuesPath string) error {
_, err := os.Stat(valuesPath)
if err != nil {
return fmt.Errorf("file does not exist")
return errors.New("file does not exist")
}
return nil
}

@ -57,7 +57,7 @@ func TestValidateValuesYamlNotDirectory(t *testing.T) {
err := validateValuesFileExistence(nonExistingValuesFilePath)
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")
}
}

@ -57,7 +57,7 @@ func LoadFile(name string) (*chart.Chart, error) {
c, err := LoadArchive(raw)
if err != nil {
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

@ -61,7 +61,7 @@ func TestLoadDirWithDevNull(t *testing.T) {
t.Fatalf("Failed to load testdata: %s", err)
}
if _, err := l.Load(); err == nil {
t.Errorf("packages with an irregular file (/dev/null) should not load")
t.Error("packages with an irregular file (/dev/null) should not load")
}
}
@ -337,7 +337,6 @@ icon: https://example.com/64x64.png
if text.String() != "" {
t.Errorf("Expected no message to Stderr, got %s", text.String())
}
}
// Packaging the chart on a Windows machine will produce an
@ -607,7 +606,6 @@ func verifyChart(t *testing.T, c *chart.Chart) {
t.Errorf("Expected %s version %s, got %s", dep.Name(), exp["version"], dep.Metadata.Version)
}
}
}
func verifyDependencies(t *testing.T, c *chart.Chart) {

@ -112,6 +112,9 @@ func (md *Metadata) Validate() error {
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) {
return ValidationErrorf("chart.metadata.name %q is invalid", md.Name)
}

@ -16,6 +16,7 @@ limitations under the License.
package v3
import (
"errors"
"testing"
)
@ -40,6 +41,16 @@ func TestValidate(t *testing.T) {
&Metadata{APIVersion: "v3", Version: "1.0"},
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",
&Metadata{Name: "../../test", APIVersion: "v3", Version: "1.0"},
@ -181,7 +192,7 @@ func TestValidate(t *testing.T) {
for _, tt := range tests {
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)
}
}

@ -35,7 +35,7 @@ func TestLoadChartfile(t *testing.T) {
func verifyChartfile(t *testing.T, f *chart.Metadata, name string) {
t.Helper()
if f == nil { //nolint:staticcheck
if f == 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" {
t.Errorf("Unexpected maintainer name.")
t.Error("Unexpected maintainer name.")
}
if f.Maintainers[1].Email != "nobody@example.com" {
t.Errorf("Unexpected maintainer email.")
t.Error("Unexpected maintainer email.")
}
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" {
@ -84,7 +84,7 @@ func verifyChartfile(t *testing.T, f *chart.Metadata, name string) {
}
if len(f.Annotations) != 2 {
t.Fatalf("Unexpected annotations")
t.Fatal("Unexpected annotations")
}
if want, got := "extravalue", f.Annotations["extrakey"]; want != got {
@ -111,7 +111,7 @@ func TestIsChartDir(t *testing.T) {
}
validChartDir, err = IsChartDir("testdata")
if validChartDir || err == nil {
t.Errorf("expected error but did not get any")
t.Error("expected error but did not get any")
return
}
}

@ -702,7 +702,6 @@ func CreateFrom(chartfile *chart.Metadata, dest, src string) error {
// error. In such a case, this will attempt to clean up by removing the
// new chart directory.
func Create(name, dir string) (string, error) {
// Sanity-check the name of a chart so user doesn't create one that causes problems.
if err := validateChartName(name); err != nil {
return "", err

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

@ -19,6 +19,7 @@ import (
"errors"
"fmt"
"log/slog"
"slices"
"strings"
chart "helm.sh/helm/v4/internal/chart/v3"
@ -260,8 +261,8 @@ func set(path []string, data map[string]any) map[string]any {
return nil
}
cur := data
for i := len(path) - 1; i >= 0; i-- {
cur = map[string]any{path[i]: cur}
for _, v := range slices.Backward(path) {
cur = map[string]any{v: cur}
}
return cur
}

@ -63,7 +63,7 @@ func TestLoadDependency(t *testing.T) {
}
func TestDependencyEnabled(t *testing.T) {
type M = map[string]interface{}
type M = map[string]any
tests := []struct {
name string
v M
@ -385,7 +385,7 @@ func TestGetAliasDependency(t *testing.T) {
req := c.Metadata.Dependencies
if len(req) == 0 {
t.Fatalf("there are no dependencies to test")
t.Fatal("there are no dependencies to test")
}
// Success case
@ -403,7 +403,7 @@ func TestGetAliasDependency(t *testing.T) {
if req[0].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"
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")
}
}
@ -459,7 +459,6 @@ func TestDependentChartAliases(t *testing.T) {
if aliasChart := getAliasDependency(c.Dependencies(), req[2]); aliasChart != nil {
t.Fatalf("expected no chart but got %s", aliasChart.Name())
}
}
func TestDependentChartWithSubChartsAbsentInDependency(t *testing.T) {

@ -42,4 +42,4 @@ into a Chart.
When creating charts in memory, use the 'helm.sh/helm/pkg/chart'
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")
}
// 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
// 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.
@ -61,6 +72,12 @@ func Expand(dir string, r io.Reader) error {
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
// comments.
for _, file := range files {

@ -17,11 +17,73 @@ limitations under the License.
package util
import (
"archive/tar"
"bytes"
"compress/gzip"
"io/fs"
"os"
"path/filepath"
"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) {
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) {
dest := t.TempDir()

@ -21,8 +21,8 @@ import (
"bytes"
"compress/gzip"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"io"
"os"
"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)
}
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
@ -153,7 +153,7 @@ func TestSavePreservesTimestamps(t *testing.T) {
Version: "1.2.3",
},
ModTime: initialCreateTime,
Values: map[string]interface{}{
Values: map[string]any{
"imageName": "testimage",
"imageId": 42,
},
@ -353,5 +353,5 @@ func sha256Sum(filePath string) (string, error) {
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

@ -79,7 +79,6 @@ func ValidateReleaseName(name string) error {
// This case is preserved for backwards compatibility
if name == "" {
return errMissingName
}
if len(name) > maxReleaseNameLen || !validName.MatchString(name) {
return errInvalidName

@ -20,11 +20,12 @@ import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
"helm.sh/helm/v4/pkg/release/common"
)
func TestColorizeStatus(t *testing.T) {
tests := []struct {
name string
status common.Status
@ -94,20 +95,17 @@ func TestColorizeStatus(t *testing.T) {
// In test environment, term.IsTerminal will be false, so we won't get color
// unless we're testing the logic without terminal detection
if hasColor && !tt.wantColor {
t.Errorf("ColorizeStatus() returned color when none expected: %q", result)
if hasColor {
assert.True(t, tt.wantColor, "ColorizeStatus() returned color when none expected: %q", result)
}
// Always check the status text is present
if !strings.Contains(result, tt.status.String()) {
t.Errorf("ColorizeStatus() = %q, want to contain %q", result, tt.status.String())
}
assert.Contains(t, result, tt.status.String())
})
}
}
func TestColorizeHeader(t *testing.T) {
tests := []struct {
name string
header string
@ -141,15 +139,12 @@ func TestColorizeHeader(t *testing.T) {
result := ColorizeHeader(tt.header, tt.noColor)
// Always check the header text is present
if !strings.Contains(result, tt.header) {
t.Errorf("ColorizeHeader() = %q, want to contain %q", result, tt.header)
}
assert.Contains(t, result, tt.header)
})
}
}
func TestColorizeNamespace(t *testing.T) {
tests := []struct {
name string
namespace string
@ -183,9 +178,7 @@ func TestColorizeNamespace(t *testing.T) {
result := ColorizeNamespace(tt.namespace, tt.noColor)
// Always check the namespace text is present
if !strings.Contains(result, tt.namespace) {
t.Errorf("ColorizeNamespace() = %q, want to contain %q", result, tt.namespace)
}
assert.Contains(t, result, tt.namespace)
})
}
}

@ -22,6 +22,9 @@ import (
"path/filepath"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TestAtomicWriteFile tests the happy path of AtomicWriteFile function.
@ -35,28 +38,17 @@ func TestAtomicWriteFile(t *testing.T) {
mode := os.FileMode(0644)
err := AtomicWriteFile(testpath, reader, mode)
if err != nil {
t.Errorf("AtomicWriteFile error: %s", err)
}
assert.NoError(t, err)
got, err := os.ReadFile(testpath)
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
if stringContent != string(got) {
t.Fatalf("expected: %s, got: %s", stringContent, string(got))
}
require.Equal(t, stringContent, string(got))
gotinfo, err := os.Stat(testpath)
if err != nil {
t.Fatal(err)
}
if mode != gotinfo.Mode() {
t.Fatalf("expected %s: to be the same mode as %s",
mode, gotinfo.Mode())
}
require.NoError(t, err)
require.Equal(t, mode, gotinfo.Mode())
}
// TestAtomicWriteFile_CreateTempError tests the error path when os.CreateTemp fails
@ -67,9 +59,7 @@ func TestAtomicWriteFile_CreateTempError(t *testing.T) {
mode := os.FileMode(0644)
err := AtomicWriteFile(invalidPath, reader, mode)
if err == nil {
t.Error("Expected error when CreateTemp fails, but got nil")
}
assert.Error(t, err, "Expected error when CreateTemp fails")
}
// TestAtomicWriteFile_EmptyContent tests with empty content
@ -81,18 +71,12 @@ func TestAtomicWriteFile_EmptyContent(t *testing.T) {
mode := os.FileMode(0644)
err := AtomicWriteFile(testpath, reader, mode)
if err != nil {
t.Errorf("AtomicWriteFile error with empty content: %s", err)
}
assert.NoError(t, err, "AtomicWriteFile error with empty content")
got, err := os.ReadFile(testpath)
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
if len(got) != 0 {
t.Fatalf("expected empty content, got: %s", string(got))
}
require.Empty(t, got)
}
// TestAtomicWriteFile_LargeContent tests with large content
@ -106,18 +90,12 @@ func TestAtomicWriteFile_LargeContent(t *testing.T) {
mode := os.FileMode(0644)
err := AtomicWriteFile(testpath, reader, mode)
if err != nil {
t.Errorf("AtomicWriteFile error with large content: %s", err)
}
assert.NoError(t, err, "AtomicWriteFile error with large content")
got, err := os.ReadFile(testpath)
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
if largeContent != string(got) {
t.Fatalf("expected large content to match, got different length: %d vs %d", len(largeContent), len(got))
}
require.Equal(t, largeContent, string(got))
}
// TestPlatformAtomicWriteFile_OverwritesExisting verifies that the platform
@ -127,21 +105,13 @@ func TestPlatformAtomicWriteFile_OverwritesExisting(t *testing.T) {
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)
}
require.NoError(t, PlatformAtomicWriteFile(path, first, 0644), "first write failed")
second := bytes.NewReader([]byte("second"))
if err := PlatformAtomicWriteFile(path, second, 0644); err != nil {
t.Fatalf("second write failed: %v", err)
}
require.NoError(t, PlatformAtomicWriteFile(path, second, 0644), "second write failed")
contents, err := os.ReadFile(path)
if err != nil {
t.Fatalf("failed reading result: %v", err)
}
require.NoError(t, err, "failed reading result")
if string(contents) != "second" {
t.Fatalf("expected file to be overwritten, got %q", string(contents))
}
require.Equal(t, "second", string(contents))
}

@ -33,7 +33,6 @@ type Client struct {
// New creates a new client
func New(u string) (*Client, error) {
// Validate we have a URL
if err := validate(u); err != nil {
return nil, err
@ -46,7 +45,6 @@ func New(u string) (*Client, error) {
// Validate if the base URL for monocular is valid.
func validate(u string) error {
// Check if it is parsable
p, err := url.Parse(u)
if err != nil {

@ -18,14 +18,13 @@ package monocular
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestNew(t *testing.T) {
c, err := New("https://hub.helm.sh")
if err != nil {
t.Errorf("error creating client: %s", err)
}
if c.BaseURL != "https://hub.helm.sh" {
t.Errorf("incorrect BaseURL. Expected \"https://hub.helm.sh\" but got %q", c.BaseURL)
}
require.NoError(t, err)
assert.Equal(t, "https://hub.helm.sh", c.BaseURL)
}

@ -17,6 +17,7 @@ limitations under the License.
package monocular
import (
"context"
"encoding/json"
"fmt"
"net/http"
@ -98,8 +99,14 @@ type ChartVersion struct {
}
// Search performs a search against the monocular search API
//
// Deprecated: Use SearchWithContext instead.
func (c *Client) Search(term string) ([]SearchResult, error) {
return c.SearchWithContext(context.Background(), term)
}
// SearchWithContext performs a search against the monocular search API
func (c *Client) SearchWithContext(ctx context.Context, term string) ([]SearchResult, error) {
// Create the URL to the search endpoint
// Note, this is currently an internal API for the Hub. This should be
// formatted without showing how monocular operates.
@ -114,7 +121,7 @@ func (c *Client) Search(term string) ([]SearchResult, error) {
p.RawQuery = "q=" + url.QueryEscape(term)
// Create request
req, err := http.NewRequest(http.MethodGet, p.String(), nil)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, p.String(), http.NoBody)
if err != nil {
return nil, err
}

@ -21,29 +21,25 @@ import (
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// A search response for phpmyadmin containing 2 results
var searchResult = `{"data":[{"id":"stable/phpmyadmin","type":"chart","attributes":{"name":"phpmyadmin","repo":{"name":"stable","url":"https://charts.helm.sh/stable"},"description":"phpMyAdmin is an mysql administration frontend","home":"https://www.phpmyadmin.net/","keywords":["mariadb","mysql","phpmyadmin"],"maintainers":[{"name":"Bitnami","email":"containers@bitnami.com"}],"sources":["https://github.com/bitnami/bitnami-docker-phpmyadmin"],"icon":""},"links":{"self":"/v1/charts/stable/phpmyadmin"},"relationships":{"latestChartVersion":{"data":{"version":"3.0.0","app_version":"4.9.0-1","created":"2019-08-08T17:57:31.38Z","digest":"119c499251bffd4b06ff0cd5ac98c2ce32231f84899fb4825be6c2d90971c742","urls":["https://charts.helm.sh/stable/phpmyadmin-3.0.0.tgz"],"readme":"/v1/assets/stable/phpmyadmin/versions/3.0.0/README.md","values":"/v1/assets/stable/phpmyadmin/versions/3.0.0/values.yaml"},"links":{"self":"/v1/charts/stable/phpmyadmin/versions/3.0.0"}}}},{"id":"bitnami/phpmyadmin","type":"chart","attributes":{"name":"phpmyadmin","repo":{"name":"bitnami","url":"https://charts.bitnami.com"},"description":"phpMyAdmin is an mysql administration frontend","home":"https://www.phpmyadmin.net/","keywords":["mariadb","mysql","phpmyadmin"],"maintainers":[{"name":"Bitnami","email":"containers@bitnami.com"}],"sources":["https://github.com/bitnami/bitnami-docker-phpmyadmin"],"icon":""},"links":{"self":"/v1/charts/bitnami/phpmyadmin"},"relationships":{"latestChartVersion":{"data":{"version":"3.0.0","app_version":"4.9.0-1","created":"2019-08-08T18:34:13.341Z","digest":"66d77cf6d8c2b52c488d0a294cd4996bd5bad8dc41d3829c394498fb401c008a","urls":["https://charts.bitnami.com/bitnami/phpmyadmin-3.0.0.tgz"],"readme":"/v1/assets/bitnami/phpmyadmin/versions/3.0.0/README.md","values":"/v1/assets/bitnami/phpmyadmin/versions/3.0.0/values.yaml"},"links":{"self":"/v1/charts/bitnami/phpmyadmin/versions/3.0.0"}}}}]}`
func TestSearch(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
fmt.Fprintln(w, searchResult)
}))
defer ts.Close()
c, err := New(ts.URL)
if err != nil {
t.Errorf("unable to create monocular client: %s", err)
}
results, err := c.Search("phpmyadmin")
if err != nil {
t.Errorf("unable to search monocular: %s", err)
}
if len(results) != 2 {
t.Error("Did not receive the expected number of results")
}
require.NoError(t, err, "unable to create monocular client")
results, err := c.SearchWithContext(t.Context(), "phpmyadmin")
require.NoError(t, err, "unable to search monocular")
assert.Len(t, results, 2)
}

@ -35,7 +35,6 @@ func TestPath(t *testing.T) {
}
for _, tt := range tests {
t.Setenv("HELM_PLUGINS", tt.helmPluginsDir)
baseIns := newBase(tt.source)
baseInsPath := baseIns.Path()

@ -90,7 +90,6 @@ func NewExtractor(source string) (Extractor, error) {
// - Beginning a path with a path separator is illegal
// - Rudimentary symlink protections are offered by SecureJoin.
func cleanJoin(root, dest string) (string, error) {
// On Windows, this is a drive separator. On UNIX-like, this is the path list separator.
// In neither case do we want to trust a TAR that contains these.
if strings.Contains(dest, ":") {

@ -17,6 +17,7 @@ package installer // import "helm.sh/helm/v4/internal/plugin/installer"
import (
"bytes"
"errors"
"fmt"
"log/slog"
"os"
@ -143,7 +144,7 @@ func (i *HTTPInstaller) Install() error {
// Update updates a local repository
// Not implemented for now since tarball most likely will be packaged by version
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
@ -163,7 +164,7 @@ func (i *HTTPInstaller) SupportsVerification() bool {
// GetVerificationData returns cached plugin and provenance data for verification
func (i *HTTPInstaller) GetVerificationData() (archiveData, provData []byte, filename string, err error) {
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

@ -53,16 +53,16 @@ var fakePluginB64 = "H4sIAAAAAAAAA+3SQUvDMBgG4Jz7K0LwapdvSxrwJig6mCKC5xHabBaXdDS
func TestStripName(t *testing.T) {
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" {
t.Errorf("name does not match expected value")
t.Error("name does not match expected value")
}
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" {
t.Errorf("name does not match expected value")
t.Error("name does not match expected value")
}
}
@ -124,7 +124,6 @@ func TestHTTPInstaller(t *testing.T) {
} else if err.Error() != "plugin already exists" {
t.Fatalf("expected error for plugin exists, got (%v)", err)
}
}
func TestHTTPInstallerNonExistentVersion(t *testing.T) {
@ -150,14 +149,13 @@ func TestHTTPInstallerNonExistentVersion(t *testing.T) {
// inject fake http client responding with error
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
if err := Install(i); err == nil {
t.Fatal("expected error from http client")
}
}
func TestHTTPInstallerUpdate(t *testing.T) {
@ -297,7 +295,6 @@ func TestExtract(t *testing.T) {
t.Fatalf("Expected %s to have %o mode but has %o (umask: %o)",
readmeFullPath, expectedReadmePerm, info.Mode().Perm(), currentUmask)
}
}
func TestCleanJoin(t *testing.T) {
@ -327,11 +324,9 @@ func TestCleanJoin(t *testing.T) {
t.Errorf("Test %d: Expected %q but got %q", i, fixture.expect, out)
}
}
}
func TestMediaTypeToExtension(t *testing.T) {
for mt, shouldPass := range map[string]bool{
"": false,
"application/gzip": true,
@ -345,7 +340,7 @@ func TestMediaTypeToExtension(t *testing.T) {
t.Errorf("Media type %q failed test", mt)
}
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 {
t.Error("Expected extension to be empty for unrecognized type")

@ -16,6 +16,7 @@ limitations under the License.
package installer
import (
"context"
"errors"
"fmt"
"log/slog"
@ -72,7 +73,6 @@ type VerificationResult struct {
// InstallWithOptions installs a plugin with options.
func InstallWithOptions(i Installer, opts Options) (*VerificationResult, error) {
if err := os.MkdirAll(filepath.Dir(i.Path()), 0755); err != nil {
return nil, err
}
@ -87,7 +87,7 @@ func InstallWithOptions(i Installer, opts Options) (*VerificationResult, error)
if opts.Verify {
verifier, ok := i.(Verifier)
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)
@ -98,24 +98,23 @@ func InstallWithOptions(i Installer, opts Options) (*VerificationResult, error)
// Check if provenance data exists
if len(provData) == 0 {
// No .prov file found - emit warning but continue installation
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
verification, err := plugin.VerifyPlugin(archiveData, provData, filename, opts.Keyring)
if err != nil {
return nil, fmt.Errorf("plugin verification failed: %w", err)
}
return nil, errors.New("plugin verification failed: no provenance file (.prov) found")
}
// Collect verification info
result = &VerificationResult{
SignedBy: make([]string, 0),
Fingerprint: fmt.Sprintf("%X", verification.SignedBy.PrimaryKey.Fingerprint),
FileHash: verification.FileHash,
}
for name := range verification.SignedBy.Identities {
result.SignedBy = append(result.SignedBy, name)
}
// Provenance data exists - verify the plugin
verification, err := plugin.VerifyPlugin(archiveData, provData, filename, opts.Keyring)
if err != nil {
return nil, fmt.Errorf("plugin verification failed: %w", err)
}
// Collect verification info
result = &VerificationResult{
SignedBy: make([]string, 0),
Fingerprint: fmt.Sprintf("%X", verification.SignedBy.PrimaryKey.Fingerprint),
FileHash: verification.FileHash,
}
for name := range verification.SignedBy.Identities {
result.SignedBy = append(result.SignedBy, name)
}
}
@ -137,7 +136,7 @@ func Update(i Installer) error {
// NewForSource determines the correct Installer for the given source.
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
installer, err = NewOCIInstaller(source)
} else if isLocalReference(source) {
@ -191,12 +190,17 @@ func isRemoteHTTPArchive(source string) bool {
}
// If no suffix match, try HEAD request to check content type
res, err := http.Head(source)
req, err := http.NewRequestWithContext(context.Background(), http.MethodHead, source, http.NoBody)
if err != nil {
// If we get an error at the network layer, we can't install it. So
// we return false.
return false
}
res, err := http.DefaultClient.Do(req)
if err != nil {
return false
}
defer res.Body.Close()
// Next, we look for the content type or content disposition headers to see
// if they have matching extractors.

@ -23,18 +23,18 @@ func TestIsRemoteHTTPArchive(t *testing.T) {
source := srv.URL + "/plugins/fake-plugin-0.0.1.tar.gz"
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
// 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") {
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
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) {

@ -188,7 +188,7 @@ func (i *LocalInstaller) SupportsVerification() bool {
// GetVerificationData loads plugin and provenance data from local files for verification
func (i *LocalInstaller) GetVerificationData() (archiveData, provData []byte, filename string, err error) {
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

@ -19,6 +19,7 @@ import (
"archive/tar"
"bytes"
"compress/gzip"
"errors"
"os"
"path/filepath"
"testing"
@ -64,7 +65,7 @@ func TestLocalInstallerNotAFolder(t *testing.T) {
if err == nil {
t.Fatal("expected error")
}
if err != ErrPluginNotADirectory {
if !errors.Is(err, ErrPluginNotADirectory) {
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
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

@ -82,7 +82,7 @@ command: "$HELM_PLUGIN_DIR/bin/%s"
// Add executable
execContent := fmt.Sprintf("#!/bin/sh\necho '%s test plugin'", pluginName)
execHeader := &tar.Header{
Name: fmt.Sprintf("bin/%s", pluginName),
Name: "bin/" + pluginName,
Mode: 0755,
Size: int64(len(execContent)),
Typeflag: tar.TypeReg,
@ -239,7 +239,7 @@ func TestNewOCIInstaller(t *testing.T) {
if tt.expectError {
if err == nil {
t.Errorf("expected error but got none")
t.Error("expected error but got none")
}
return
}
@ -416,7 +416,7 @@ func TestOCIInstaller_Install_WithGetterOptions(t *testing.T) {
err = Install(installer)
if tc.wantErr {
if err == nil {
t.Errorf("Expected installation to fail, but it succeeded")
t.Error("Expected installation to fail, but it succeeded")
}
} else {
if err != nil {
@ -526,13 +526,13 @@ func TestOCIInstaller_Install_ComponentExtraction(t *testing.T) {
// Verify plugin.yaml exists
pluginYAMLPath := filepath.Join(tempDir, "plugin.yaml")
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
binPath := filepath.Join(tempDir, "bin")
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
@ -540,12 +540,12 @@ func TestOCIInstaller_Install_ComponentExtraction(t *testing.T) {
if info, err := os.Stat(execPath); err != nil {
t.Errorf("executable not found: %v", err)
} 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
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")
}
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
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" {
t.Fatalf("expected error for plugin exists, got (%v)", err)
}
// Testing FindSource method, expect error because plugin code is not a cloned repository
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" {
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 {
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") {
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) {
@ -181,9 +181,8 @@ func TestVCSInstallerUpdate(t *testing.T) {
}
// Testing update for error
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" {
t.Fatalf("expected error for plugin modified, got (%v)", err)
}
}

@ -16,10 +16,8 @@ limitations under the License.
package installer
import (
"bytes"
"crypto/sha256"
"fmt"
"io"
"os"
"path/filepath"
"strings"
@ -44,33 +42,49 @@ func TestInstallWithOptions_VerifyMissingProvenance(t *testing.T) {
}
defer os.RemoveAll(installer.Path())
// Capture stderr to check warning message
oldStderr := os.Stderr
r, w, _ := os.Pipe()
os.Stderr = w
// Install with verification enabled (should warn but succeed)
// Install with verification enabled should fail when .prov is missing
result, err := InstallWithOptions(installer, Options{Verify: true, Keyring: "dummy"})
// Restore stderr and read captured output
w.Close()
os.Stderr = oldStderr
var buf bytes.Buffer
io.Copy(&buf, r)
output := buf.String()
// 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)
// Should fail with a missing provenance error
if err == nil {
t.Fatal("Expected installation to fail when .prov file is missing and verification is enabled")
}
if !strings.Contains(err.Error(), "no provenance file") {
t.Errorf("Expected 'no provenance file' in error message, got: %v", err)
}
if result != nil {
t.Errorf("Expected nil verification result when .prov file is missing, got: %+v", result)
}
// Should contain warning message
expectedWarning := "WARNING: No provenance file found for plugin"
if !strings.Contains(output, expectedWarning) {
t.Errorf("Expected warning message '%s' in output, got: %s", expectedWarning, output)
// Plugin should NOT be installed
if _, err := os.Stat(installer.Path()); !os.IsNotExist(err) {
t.Error("Plugin should not be installed when verification fails due to missing .prov")
}
}
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
@ -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
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") {
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
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
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 {
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
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
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") {
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
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") {
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)
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"
// depending on when the permission error occurs

@ -19,6 +19,7 @@ import (
"bytes"
"fmt"
"io"
"log/slog"
"os"
"path/filepath"
@ -44,7 +45,6 @@ func peekAPIVersion(r io.Reader) (string, error) {
}
func loadMetadataLegacy(metadataData []byte) (*Metadata, error) {
var ml MetadataLegacy
d := yaml.NewDecoder(bytes.NewReader(metadataData))
// NOTE: No strict unmarshalling for legacy plugins - maintain backwards compatibility
@ -64,7 +64,6 @@ func loadMetadataLegacy(metadataData []byte) (*Metadata, error) {
}
func loadMetadataV1(metadataData []byte) (*Metadata, error) {
var mv1 MetadataV1
d := yaml.NewDecoder(bytes.NewReader(metadataData))
d.KnownFields(true)
@ -108,7 +107,6 @@ type prototypePluginManager struct {
}
func newPrototypePluginManager() (*prototypePluginManager, error) {
cc, err := wazero.NewCompilationCacheWithDir(helmpath.CachePath("wazero-build"))
if err != nil {
return nil, fmt.Errorf("failed to create wazero compilation cache: %w", err)
@ -158,18 +156,27 @@ func LoadDir(dirname string) (Plugin, error) {
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.
func LoadAll(basedir string) ([]Plugin, error) {
var plugins []Plugin
// We want basedir/*/plugin.yaml
func LoadAllDir(basedir string, errorFilter ErrorFilterFunc) ([]Plugin, error) {
// We want <basedir>/*/plugin.yaml
scanpath := filepath.Join(basedir, "*", PluginFileName)
matches, err := filepath.Glob(scanpath)
if err != nil {
return nil, fmt.Errorf("failed to search for plugins in %q: %w", scanpath, err)
}
plugins := make([]Plugin, 0, len(matches))
// empty dir should load
if len(matches) == 0 {
return plugins, nil
@ -179,9 +186,12 @@ func LoadAll(basedir string) ([]Plugin, error) {
dir := filepath.Dir(yamlFile)
p, err := LoadDir(dir)
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)
}
@ -193,8 +203,12 @@ type findFunc func(pluginsDir string) ([]Plugin, error)
type filterFunc func(Plugin) bool
// 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) {
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
@ -212,7 +226,6 @@ func findPlugins(pluginsDirs []string, findFn findFunc, filterFn filterFunc) ([]
found = append(found, p)
}
}
}
return found, nil
@ -225,7 +238,6 @@ func makeDescriptorFilter(descriptor Descriptor) filterFunc {
// If name is specified, it must match
if descriptor.Name != "" && p.Metadata().Name != descriptor.Name {
return false
}
// If type is specified, it must match
if descriptor.Type != "" && p.Metadata().Type != descriptor.Type {
@ -237,7 +249,11 @@ func makeDescriptorFilter(descriptor Descriptor) filterFunc {
// FindPlugin returns a single plugin that matches the descriptor
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 {
return nil, err
}

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

@ -19,9 +19,17 @@ import (
"errors"
"fmt"
"github.com/Masterminds/semver/v3"
"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
// Specifically, Config and RuntimeConfig are converted to their respective types based on the plugin type and runtime
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))
}
// 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 == "" {
errs = append(errs, fmt.Errorf("empty APIVersion"))
errs = append(errs, errors.New("empty APIVersion"))
}
if m.Type == "" {
errs = append(errs, fmt.Errorf("empty type field"))
errs = append(errs, errors.New("empty type field"))
}
if m.Runtime == "" {
errs = append(errs, fmt.Errorf("empty runtime field"))
errs = append(errs, errors.New("empty runtime field"))
}
if m.Config == nil {
errs = append(errs, fmt.Errorf("missing config field"))
errs = append(errs, errors.New("missing config field"))
}
if m.RuntimeConfig == nil {
errs = append(errs, fmt.Errorf("missing runtimeConfig field"))
errs = append(errs, errors.New("missing runtimeConfig field"))
}
// Validate the config itself

@ -16,6 +16,7 @@ limitations under the License.
package plugin
import (
"errors"
"fmt"
"strings"
"unicode"
@ -49,7 +50,8 @@ type MetadataLegacy struct {
PlatformCommand []PlatformCommand `yaml:"platformCommand"`
// Command is the plugin command, as a single string.
// DEPRECATED: Use PlatformCommand instead. Removed in subprocess/v1 plugins.
//
// Deprecated: Use PlatformCommand instead. Removed in subprocess/v1 plugins.
Command string `yaml:"command"`
// IgnoreFlags ignores any flags passed in from Helm
@ -59,7 +61,8 @@ type MetadataLegacy struct {
PlatformHooks PlatformHooks `yaml:"platformHooks"`
// Hooks are commands that will run on plugin events, as a single string.
// DEPRECATED: Use PlatformHooks instead. Removed in subprocess/v1 plugins.
//
// Deprecated: Use PlatformHooks instead. Removed in subprocess/v1 plugins.
Hooks Hooks `yaml:"hooks"`
// Downloaders field is used if the plugin supply downloader mechanism
@ -71,14 +74,19 @@ func (m *MetadataLegacy) Validate() error {
if !validPluginName.MatchString(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)
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 {
return fmt.Errorf("both platformHooks and hooks are set")
return errors.New("both platformHooks and hooks are set")
}
// Validate downloader plugins

@ -26,6 +26,10 @@ func TestMetadataLegacyValidate(t *testing.T) {
"valid metadata": {
Name: "myplugin",
},
"valid metadata (empty version)": {
Name: "myplugin",
Version: "",
},
"valid with command": {
Name: "myplugin",
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 {
@ -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 {

@ -18,10 +18,11 @@ package plugin
import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
func TestValidatePluginData(t *testing.T) {
// A mock plugin with no commands
mockNoCommand := mockSubprocessCLIPlugin(t, "foo")
mockNoCommand.metadata.RuntimeConfig = &RuntimeConfigSubprocess{
@ -72,6 +73,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) {
// Create metadata with multiple validation issues
metadata := Metadata{

@ -16,6 +16,7 @@ limitations under the License.
package plugin
import (
"errors"
"fmt"
)
@ -48,7 +49,14 @@ type MetadataV1 struct {
func (m *MetadataV1) Validate() error {
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" {
@ -56,11 +64,11 @@ func (m *MetadataV1) Validate() error {
}
if m.Type == "" {
return fmt.Errorf("`type` missing")
return errors.New("`type` missing")
}
if m.Runtime == "" {
return fmt.Errorf("`runtime` missing")
return errors.New("`runtime` missing")
}
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)
})
}
}

@ -77,5 +77,5 @@ type Output struct {
// validPluginName is a regular expression that validates plugin names.
//
// Plugin names can only contain the ASCII characters a-z, A-Z, 0-9, ​_​ and ​-.
// Plugin names can only contain the ASCII characters a-z, A-Z, 0-9, _ and -.
var validPluginName = regexp.MustCompile("^[A-Za-z0-9_-]+$")

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

@ -28,7 +28,6 @@ func TestMakeOutputMessage(t *testing.T) {
ptm := pluginTypesIndex["getter/v1"]
outputType := reflect.Zero(ptm.outputType).Interface()
assert.IsType(t, schema.OutputMessageGetterV1{}, outputType)
}
func TestMakeConfig(t *testing.T) {

@ -99,7 +99,6 @@ type RuntimeExtismV1 struct {
var _ Runtime = (*RuntimeExtismV1)(nil)
func (r *RuntimeExtismV1) CreatePlugin(pluginDir string, metadata *Metadata) (Plugin, error) {
rc, ok := metadata.RuntimeConfig.(*RuntimeConfigExtismV1)
if !ok {
return nil, fmt.Errorf("invalid extism/v1 plugin runtime config type: %T", metadata.RuntimeConfig)
@ -139,7 +138,6 @@ func (p *ExtismV1PluginRuntime) Dir() string {
}
func (p *ExtismV1PluginRuntime) Invoke(ctx context.Context, input *Input) (*Output, error) {
var tmpDir string
if p.rc.FileSystem.CreateTempDir {
tmpDirInner, err := os.MkdirTemp(os.TempDir(), "helm-plugin-*")

@ -46,7 +46,7 @@ func buildLoadExtismPlugin(t *testing.T, dir string) pluginRaw {
require.NoError(t, err)
require.Equal(t, "extism/v1", m.Runtime, "expected plugin runtime to be extism/v1")
cmd := exec.Command("make", "-C", dir)
cmd := exec.CommandContext(t.Context(), "make", "-C", dir)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
require.NoError(t, cmd.Run(), "failed to build plugin in %q", dir)
@ -79,7 +79,7 @@ func TestRuntimeExtismV1InvokePlugin(t *testing.T) {
Name: "Phippy",
},
})
require.Nil(t, err)
require.NoError(t, err)
msg := output.Message.(schema.OutputMessageTestV1)
assert.Equal(t, "Hello, Phippy! (6)", msg.Greeting)

@ -96,14 +96,14 @@ func (r *SubprocessPluginRuntime) Metadata() Metadata {
return r.metadata
}
func (r *SubprocessPluginRuntime) Invoke(_ context.Context, input *Input) (*Output, error) {
func (r *SubprocessPluginRuntime) Invoke(ctx context.Context, input *Input) (*Output, error) {
switch input.Message.(type) {
case schema.InputMessageCLIV1:
return r.runCLI(input)
return r.runCLI(ctx, input)
case schema.InputMessageGetterV1:
return r.runGetter(input)
return r.runGetter(ctx, input)
case schema.InputMessagePostRendererV1:
return r.runPostrenderer(input)
return r.runPostrenderer(ctx, input)
default:
return nil, fmt.Errorf("unsupported subprocess plugin type %q", r.metadata.Type)
}
@ -113,12 +113,12 @@ func (r *SubprocessPluginRuntime) Invoke(_ context.Context, input *Input) (*Outp
// This method allows execution with different command/args than the plugin's default
func (r *SubprocessPluginRuntime) InvokeWithEnv(main string, argv []string, env []string, stdin io.Reader, stdout, stderr io.Writer) error {
mainCmdExp := os.ExpandEnv(main)
cmd := exec.Command(mainCmdExp, argv...)
cmd := exec.CommandContext(context.Background(), mainCmdExp, argv...)
cmd.Env = slices.Clone(os.Environ())
cmd.Env = append(
cmd.Env,
fmt.Sprintf("HELM_PLUGIN_NAME=%s", r.metadata.Name),
fmt.Sprintf("HELM_PLUGIN_DIR=%s", r.pluginDir))
"HELM_PLUGIN_NAME="+r.metadata.Name,
"HELM_PLUGIN_DIR="+r.pluginDir)
cmd.Env = append(cmd.Env, env...)
cmd.Stdin = stdin
@ -149,7 +149,7 @@ func (r *SubprocessPluginRuntime) InvokeHook(event string) error {
return err
}
cmd := exec.Command(main, argv...)
cmd := exec.CommandContext(context.Background(), main, argv...)
cmd.Env = FormatEnv(env)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
@ -189,7 +189,7 @@ func executeCmd(prog *exec.Cmd, pluginName string) error {
return nil
}
func (r *SubprocessPluginRuntime) runCLI(input *Input) (*Output, error) {
func (r *SubprocessPluginRuntime) runCLI(ctx context.Context, input *Input) (*Output, error) {
if _, ok := input.Message.(schema.InputMessageCLIV1); !ok {
return nil, fmt.Errorf("plugin %q input message does not implement InputMessageCLIV1", r.metadata.Name)
}
@ -209,7 +209,7 @@ func (r *SubprocessPluginRuntime) runCLI(input *Input) (*Output, error) {
return nil, fmt.Errorf("failed to prepare plugin command: %w", err)
}
cmd := exec.Command(command, args...)
cmd := exec.CommandContext(ctx, command, args...)
cmd.Env = FormatEnv(env)
cmd.Stdin = input.Stdin
@ -226,7 +226,7 @@ func (r *SubprocessPluginRuntime) runCLI(input *Input) (*Output, error) {
}, nil
}
func (r *SubprocessPluginRuntime) runPostrenderer(input *Input) (*Output, error) {
func (r *SubprocessPluginRuntime) runPostrenderer(ctx context.Context, input *Input) (*Output, error) {
if _, ok := input.Message.(schema.InputMessagePostRendererV1); !ok {
return nil, fmt.Errorf("plugin %q input message does not implement InputMessagePostRendererV1", r.metadata.Name)
}
@ -244,7 +244,8 @@ func (r *SubprocessPluginRuntime) runPostrenderer(input *Input) (*Output, error)
return nil, fmt.Errorf("failed to prepare plugin command: %w", err)
}
cmd := exec.Command(
cmd := exec.CommandContext(
ctx,
command,
args...)

@ -17,6 +17,7 @@ package plugin
import (
"bytes"
"context"
"fmt"
"log/slog"
"maps"
@ -24,6 +25,7 @@ import (
"os/exec"
"path/filepath"
"slices"
"strconv"
"helm.sh/helm/v4/internal/plugin/schema"
)
@ -39,7 +41,7 @@ func getProtocolCommand(commands []SubprocessProtocolCommand, protocol string) *
}
// TODO can we replace a lot of this func with RuntimeSubprocess.invokeWithEnv?
func (r *SubprocessPluginRuntime) runGetter(input *Input) (*Output, error) {
func (r *SubprocessPluginRuntime) runGetter(ctx context.Context, input *Input) (*Output, error) {
msg, ok := (input.Message).(schema.InputMessageGetterV1)
if !ok {
return nil, fmt.Errorf("expected input type schema.InputMessageGetterV1, got %T", input)
@ -63,7 +65,7 @@ func (r *SubprocessPluginRuntime) runGetter(input *Input) (*Output, error) {
env["HELM_PLUGIN_DIR"] = r.pluginDir
env["HELM_PLUGIN_USERNAME"] = msg.Options.Username
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)
if err != nil {
@ -80,7 +82,8 @@ func (r *SubprocessPluginRuntime) runGetter(input *Input) (*Output, error) {
buf := bytes.Buffer{} // subprocess getters are expected to write content to stdout
pluginCommand := filepath.Join(r.pluginDir, command)
cmd := exec.Command(
cmd := exec.CommandContext(
ctx,
pluginCommand,
args...)
cmd.Env = FormatEnv(env)

@ -42,7 +42,7 @@ func mockSubprocessCLIPluginErrorExit(t *testing.T, pluginName string, exitCode
md := Metadata{
Name: pluginName,
Version: "v0.1.2",
Version: "0.1.2",
Type: "cli/v1",
APIVersion: "v1",
Runtime: "subprocess",

@ -14,6 +14,7 @@
package schema
import (
"errors"
"fmt"
"time"
)
@ -55,7 +56,7 @@ type ConfigGetterV1 struct {
func (c *ConfigGetterV1) Validate() error {
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 {
if protocol == "" {

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

@ -16,7 +16,7 @@ limitations under the License.
package plugin
import (
"fmt"
"errors"
"os"
"runtime"
"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) {
cmdParts, args := getPlatformCommand(cmds)
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 {
return env[key]

@ -48,7 +48,6 @@ func TestPrepareCommand(t *testing.T) {
}
func TestPrepareCommandExtraArgs(t *testing.T) {
cmdMain := "sh"
cmdArgs := []string{"-c", "echo \"test\""}
platformCommand := []PlatformCommand{
@ -210,7 +209,7 @@ func TestPrepareCommandsNoMatch(t *testing.T) {
env := map[string]string{}
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 +218,7 @@ func TestPrepareCommandsNoCommands(t *testing.T) {
env := map[string]string{}
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)
}
// isTarball checks if a file has a tarball extension
// IsTarball checks if a file has a tarball extension
func IsTarball(filename string) bool {
return filepath.Ext(filename) == ".gz" || filepath.Ext(filename) == ".tgz"
}

@ -86,7 +86,7 @@ type Hook struct {
// 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,omitempty"`
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
@ -133,7 +133,7 @@ type hookExecutionJSON struct {
// 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]interface{}
var raw map[string]any
if err := json.Unmarshal(data, &raw); err != nil {
return err
}

@ -220,7 +220,7 @@ func TestHookExecutionEmptyStringRoundTrip(t *testing.T) {
data, err := json.Marshal(&exec)
require.NoError(t, err)
var result map[string]interface{}
var result map[string]any
err = json.Unmarshal(data, &result)
require.NoError(t, err)

@ -36,6 +36,8 @@ type Info struct {
Description string `json:"description,omitempty"`
// Status is the current state of the release
Status common.Status `json:"status,omitempty"`
// RollbackRevision is the revision that was rolled back to. Zero means not a rollback.
RollbackRevision int `json:"rollback_revision,omitempty"`
// Contains the rendered templates/NOTES.txt if available
Notes string `json:"notes,omitempty"`
// Contains the deployed resources information
@ -44,20 +46,21 @@ type Info struct {
// 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"`
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"`
RollbackRevision int `json:"rollback_revision,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]interface{}
var raw map[string]any
if err := json.Unmarshal(data, &raw); err != nil {
return err
}
@ -95,6 +98,7 @@ func (i *Info) UnmarshalJSON(data []byte) error {
}
i.Description = tmp.Description
i.Status = tmp.Status
i.RollbackRevision = tmp.RollbackRevision
i.Notes = tmp.Notes
i.Resources = tmp.Resources
@ -105,10 +109,11 @@ func (i *Info) UnmarshalJSON(data []byte) error {
// 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,
Description: i.Description,
Status: i.Status,
RollbackRevision: i.RollbackRevision,
Notes: i.Notes,
Resources: i.Resources,
}
if !i.FirstDeployed.IsZero() {

@ -87,6 +87,27 @@ func TestInfoMarshalJSON(t *testing.T) {
},
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"}`,
},
{
name: "with rollback revision",
info: Info{
FirstDeployed: now,
LastDeployed: later,
Status: common.StatusDeployed,
RollbackRevision: 2,
Description: "Rollback to 2",
},
expected: `{"first_deployed":"2025-10-08T12:00:00Z","last_deployed":"2025-10-08T13:00:00Z","status":"deployed","rollback_revision":2,"description":"Rollback to 2"}`,
},
{
name: "zero rollback revision omitted",
info: Info{
FirstDeployed: now,
LastDeployed: later,
Status: common.StatusDeployed,
Description: "Normal install",
},
expected: `{"first_deployed":"2025-10-08T12:00:00Z","last_deployed":"2025-10-08T13:00:00Z","status":"deployed","description":"Normal install"}`,
},
}
for _, tt := range tests {
@ -203,6 +224,27 @@ func TestInfoUnmarshalJSON(t *testing.T) {
Status: "",
},
},
{
name: "with rollback revision",
input: `{"first_deployed":"2025-10-08T12:00:00Z","last_deployed":"2025-10-08T13:00:00Z","status":"deployed","rollback_revision":2,"description":"Rollback to 2"}`,
expected: Info{
FirstDeployed: now,
LastDeployed: later,
Status: common.StatusDeployed,
RollbackRevision: 2,
Description: "Rollback to 2",
},
},
{
name: "zero rollback revision omitted",
input: `{"first_deployed":"2025-10-08T12:00:00Z","last_deployed":"2025-10-08T13:00:00Z","status":"deployed","description":"Normal install"}`,
expected: Info{
FirstDeployed: now,
LastDeployed: later,
Status: common.StatusDeployed,
Description: "Normal install",
},
},
}
for _, tt := range tests {
@ -219,6 +261,7 @@ func TestInfoUnmarshalJSON(t *testing.T) {
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.RollbackRevision, info.RollbackRevision)
assert.Equal(t, tt.expected.Notes, info.Notes)
assert.Equal(t, tt.expected.Resources, info.Resources)
})
@ -252,6 +295,61 @@ func TestInfoRoundTrip(t *testing.T) {
assert.Equal(t, original.Notes, decoded.Notes)
}
func TestInfoRollbackRevisionRoundTrip(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)
tests := []struct {
name string
info Info
}{
{
name: "with rollback revision",
info: Info{
FirstDeployed: now,
LastDeployed: later,
Description: "Rollback to 2",
Status: common.StatusDeployed,
RollbackRevision: 2,
},
},
{
name: "zero rollback revision",
info: Info{
FirstDeployed: now,
LastDeployed: later,
Description: "Normal install",
Status: common.StatusDeployed,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
data, err := json.Marshal(&tt.info)
require.NoError(t, err)
var decoded Info
err = json.Unmarshal(data, &decoded)
require.NoError(t, err)
assert.Equal(t, tt.info.RollbackRevision, decoded.RollbackRevision)
assert.Equal(t, tt.info.FirstDeployed.Unix(), decoded.FirstDeployed.Unix())
assert.Equal(t, tt.info.LastDeployed.Unix(), decoded.LastDeployed.Unix())
assert.Equal(t, tt.info.Status, decoded.Status)
assert.Equal(t, tt.info.Description, decoded.Description)
// Verify omitempty behavior: zero rollback_revision should not appear in JSON
if tt.info.RollbackRevision == 0 {
var raw map[string]any
err = json.Unmarshal(data, &raw)
require.NoError(t, err)
assert.NotContains(t, raw, "rollback_revision")
}
})
}
}
func TestInfoEmptyStringRoundTrip(t *testing.T) {
// This test specifically verifies that empty string time fields
// are handled correctly during parsing
@ -272,7 +370,7 @@ func TestInfoEmptyStringRoundTrip(t *testing.T) {
data, err := json.Marshal(&info)
require.NoError(t, err)
var result map[string]interface{}
var result map[string]any
err = json.Unmarshal(data, &result)
require.NoError(t, err)

@ -17,8 +17,8 @@ limitations under the License.
package v2
import (
"fmt"
"math/rand"
"strconv"
"time"
v3 "helm.sh/helm/v4/internal/chart/v3"
@ -57,7 +57,7 @@ func Mock(opts *MockReleaseOptions) *Release {
name := opts.Name
if name == "" {
name = "testrelease-" + fmt.Sprint(rand.Intn(100))
name = "testrelease-" + strconv.Itoa(rand.Intn(100))
}
version := 1
@ -124,7 +124,7 @@ func Mock(opts *MockReleaseOptions) *Release {
Name: name,
Info: info,
Chart: ch,
Config: map[string]interface{}{"name": "value"},
Config: map[string]any{"name": "value"},
Version: version,
Namespace: namespace,
Hooks: []*Hook{

@ -36,7 +36,7 @@ type Release struct {
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]interface{} `json:"config,omitempty"`
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.

@ -19,23 +19,19 @@ package util // import "helm.sh/helm/v4/internal/release/v2/util"
import (
"testing"
"github.com/stretchr/testify/require"
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))
}
require.Len(t, ls, 2)
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())
}
require.Equal(t, common.StatusUninstalled, r0.Info.Status)
require.Equal(t, common.StatusUninstalled, r1.Info.Status)
}
func TestFilterAll(t *testing.T) {
@ -47,14 +43,9 @@ func TestFilterAll(t *testing.T) {
})
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")
}
require.Len(t, ls, 1)
r0 := ls[0]
require.NotEqual(t, 4, r0.Version, "got release with status revision 4")
require.NotEqual(t, common.StatusUninstalled, r0.Info.Status, "got release with status UNINSTALLED")
}

@ -137,7 +137,7 @@ func sortHooksByKind(hooks []*release.Hook, ordering KindSortOrder) []*release.H
return h
}
func lessByKind(_ interface{}, _ interface{}, kindA string, kindB string, o KindSortOrder) bool {
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

@ -20,6 +20,9 @@ import (
"bytes"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
release "helm.sh/helm/v4/internal/release/v2"
)
@ -193,21 +196,15 @@ func TestKindSorter(t *testing.T) {
} {
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)
}
require.Len(t, manifests, len(test.expected), "Expected %d names in order", len(manifests))
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)
}
assert.Equal(t, test.expected, buf.String())
for i, manifest := range orig {
if manifest != manifests[i] {
t.Fatal("Expected input to sortManifestsByKind to stay the same")
}
require.Equal(t, manifest, manifests[i], "Expected input to sortManifestsByKind to stay the same")
}
})
}
@ -267,9 +264,7 @@ func TestKindSorterKeepOriginalOrder(t *testing.T) {
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)
}
assert.Equal(t, test.expected, buf.String())
})
}
}
@ -289,9 +284,7 @@ func TestKindSorterNamespaceAgainstUnknown(t *testing.T) {
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)
}
assert.Equal(t, expectedOrder[i].Name, manifest.Name)
}
}
@ -326,22 +319,16 @@ func TestKindSorterForHooks(t *testing.T) {
} {
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)
}
require.Len(t, hooks, len(test.expected), "Expected %d names in order", len(hooks))
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)
require.Equal(t, hook, hooks[i], "Expected input to sortHooksByKind to stay the same")
}
assert.Equal(t, test.expected, buf.String())
})
}
}

@ -21,6 +21,7 @@ import (
"regexp"
"strconv"
"strings"
"unicode"
)
// SimpleHead defines what the structure of the head of a manifest file
@ -35,7 +36,16 @@ type SimpleHead struct {
var sep = regexp.MustCompile("(?:^|\\s*\n)---\\s*")
// SplitManifests takes a string of manifest and returns a map contains individual manifests
// 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
@ -44,15 +54,15 @@ func SplitManifests(bigFile string) map[string]string {
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.TrimSpace(bigFile)
bigFileTmp := strings.TrimLeftFunc(bigFile, unicode.IsSpace)
docs := sep.Split(bigFileTmp, -1)
var count int
for _, d := range docs {
if d == "" {
if strings.TrimSpace(d) == "" {
continue
}
d = strings.TrimSpace(d)
d = strings.TrimLeftFunc(d, unicode.IsSpace)
res[fmt.Sprintf(tpl, count)] = d
count = count + 1
}

@ -17,16 +17,16 @@ limitations under the License.
package util // import "helm.sh/helm/v4/internal/release/v2/util"
import (
"reflect"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"sigs.k8s.io/yaml"
release "helm.sh/helm/v4/internal/release/v2"
)
func TestSortManifests(t *testing.T) {
data := []struct {
name []string
path string
@ -139,56 +139,39 @@ metadata:
}
hs, generic, err := SortManifests(manifests, nil, InstallOrder)
if err != nil {
t.Fatalf("Unexpected error: %s", err)
}
require.NoError(t, err)
// This test will fail if 'six' or 'seven' was added.
if len(generic) != 2 {
t.Errorf("Expected 2 generic manifests, got %d", len(generic))
}
assert.Len(t, generic, 2)
if len(hs) != 4 {
t.Errorf("Expected 4 hooks, got %d", len(hs))
}
assert.Len(t, hs, 4)
for _, out := range hs {
found := false
for _, expect := range data {
if out.Path == expect.path {
found = true
if out.Path != expect.path {
t.Errorf("Expected path %s, got %s", expect.path, out.Path)
}
assert.Equal(t, expect.path, out.Path)
nameFound := false
for _, expectedName := range expect.name {
if out.Name == expectedName {
nameFound = true
}
}
if !nameFound {
t.Errorf("Got unexpected name %s", out.Name)
}
assert.True(t, nameFound, "Got unexpected name %s", out.Name)
kindFound := false
for _, expectedKind := range expect.kind {
if out.Kind == expectedKind {
kindFound = true
}
}
if !kindFound {
t.Errorf("Got unexpected kind %s", out.Kind)
}
assert.True(t, kindFound, "Got unexpected kind %s", out.Kind)
expectedHooks := expect.hooks[out.Name]
if !reflect.DeepEqual(expectedHooks, out.Events) {
t.Errorf("expected events: %v but got: %v", expectedHooks, out.Events)
}
assert.Equal(t, expectedHooks, out.Events, "expected events: %v but got: %v", expectedHooks, out.Events)
}
}
if !found {
t.Errorf("Result not found: %v", out)
}
assert.True(t, found, "Result not found: %v", out)
}
// Verify the sort order
@ -220,8 +203,6 @@ metadata:
sorted = sortManifestsByKind(sorted, InstallOrder)
for i, m := range generic {
if m.Content != sorted[i].Content {
t.Errorf("Expected %q, got %q", m.Content, sorted[i].Content)
}
assert.Equal(t, m.Content, sorted[i].Content)
}
}

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

Loading…
Cancel
Save